Files
Webklar-Kundenbereich/server/services/employeeAdmin.js
T
ClaudeandClaude Opus 5.5 2659385205 Mitarbeiter nur noch per Label: Kunden-Konten bekommen keinen Team-Zugang
Kunden haben Konten im selben Appwrite-Projekt wie das Ticketsystem. Bisher
galt als Mitarbeiter, wer einen employees-Eintrag hatte - den legte das
Ticketsystem beim Login jedes Kontos automatisch an, und jedes angemeldete
Konto konnte ihn auch selbst anlegen. Damit kam ein Kunde ueber /api/team an
Chats, Kundenansicht, Angebote und Finanzen.

- staffLabels.js: Mitarbeiter = Label "mitarbeiter" oder "admin" (setzt nur
  der Server-Key)
- Team-Eingang (requireStaffJwt) und Portal-Login-Sperre nur noch per Label;
  kein Abgleich ueber employees-Eintrag oder Mail mehr
- Portal-Login: Konto-Abruf gestoert -> 503 fuer alle (fail closed)
- Mitarbeiter anlegen/bearbeiten setzt das Label "mitarbeiter"
- scripts/mitarbeiter-zugang.mjs: --status (markiert employees-Eintraege von
  Kunden), --label, --eintrag-loeschen (nur Eintrag, nie das Konto), --rechte
  (Probelauf, --anwenden mit Sicherung, --zurueck): any/guests/users ->
  label:mitarbeiter + label:admin in Collections, Buckets und bei Dokument-/
  Datei-Sicherheit auch einzelnen Dokumenten und Dateien

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:54:09 +00:00

163 lines
5.2 KiB
JavaScript

import { config } from '../config.js'
import {
createAppwriteUser,
createDocument,
deleteAppwriteUser,
deleteDocument,
getDocument,
getUserById,
listDocuments,
updateAppwriteUserEmail,
updateAppwriteUserLabels,
updateAppwriteUserName,
updateAppwriteUserPassword,
updateDocument,
Query,
} from './appwriteAdmin.js'
import { ADMIN_LABEL, STAFF_LABEL, isAdminAccount } from './staffLabels.js'
function httpError(status, message) {
const error = new Error(message)
error.status = status
return error
}
function normalizeEmail(value) {
return String(value || '').trim().toLowerCase()
}
function normalizeShortcode(value) {
return String(value || '').trim().toUpperCase().slice(0, 10)
}
function sanitizeEmployee(employee, user) {
return {
$id: employee.$id,
userId: employee.userId,
displayName: employee.displayName || user?.name || '',
email: employee.email || user?.email || '',
shortcode: employee.shortcode || '',
isAdmin: isAdminAccount(user),
}
}
async function employeeWithUser(employee) {
const user = await getUserById(employee.userId).catch(() => null)
return sanitizeEmployee(employee, user)
}
// Jeder Mitarbeiter traegt das Label "mitarbeiter" (Zugang zum Ticketsystem, siehe staffLabels.js);
// isAdmin undefined laesst "admin" unveraendert
async function setStaffLabels(userId, isAdmin) {
const user = await getUserById(userId)
const labels = new Set(user.labels || [])
labels.add(STAFF_LABEL)
if (isAdmin === true) labels.add(ADMIN_LABEL)
if (isAdmin === false) labels.delete(ADMIN_LABEL)
await updateAppwriteUserLabels(userId, [...labels])
}
export async function listEmployeesForAdmin() {
const employees = await listDocuments(config.collections.employees, [
Query.orderAsc('displayName'),
Query.limit(500),
])
return Promise.all(employees.map(employeeWithUser))
}
export async function createEmployeeWithLogin(payload) {
const displayName = String(payload?.displayName || '').trim()
const email = normalizeEmail(payload?.email)
const password = String(payload?.password || '')
const shortcode = normalizeShortcode(payload?.shortcode)
const isAdmin = Boolean(payload?.isAdmin)
if (!displayName) throw httpError(400, 'Name ist erforderlich')
if (!email) throw httpError(400, 'E-Mail ist erforderlich')
if (password.length < 8) {
throw httpError(400, 'Passwort muss mindestens 8 Zeichen haben')
}
const user = await createAppwriteUser({ email, password, name: displayName })
let employee
try {
await setStaffLabels(user.$id, isAdmin)
employee = await createDocument(config.collections.employees, {
userId: user.$id,
displayName,
email,
shortcode,
})
} catch (error) {
await deleteAppwriteUser(user.$id).catch(() => {})
throw error
}
return { employee: await employeeWithUser(employee) }
}
export async function updateEmployeeWithLogin(employeeId, payload, actorUserId) {
const employee = await getDocument(config.collections.employees, employeeId)
.catch(() => null)
if (!employee) throw httpError(404, 'Mitarbeiter nicht gefunden')
const displayName = payload?.displayName === undefined
? employee.displayName
: String(payload.displayName || '').trim()
const email = payload?.email === undefined
? normalizeEmail(employee.email)
: normalizeEmail(payload.email)
const shortcode = payload?.shortcode === undefined
? employee.shortcode || ''
: normalizeShortcode(payload.shortcode)
const password = typeof payload?.password === 'string' ? payload.password : ''
if (
employee.userId === actorUserId &&
payload?.isAdmin === false
) {
throw httpError(400, 'Die eigene Admin-Berechtigung kann nicht entfernt werden')
}
if (!displayName) throw httpError(400, 'Name ist erforderlich')
if (!email) throw httpError(400, 'E-Mail ist erforderlich')
if (password && password.length < 8) {
throw httpError(400, 'Passwort muss mindestens 8 Zeichen haben')
}
const user = await getUserById(employee.userId)
if (email !== normalizeEmail(user.email)) {
await updateAppwriteUserEmail(employee.userId, email)
}
if (displayName !== (user.name || '')) {
await updateAppwriteUserName(employee.userId, displayName)
}
if (password) {
await updateAppwriteUserPassword(employee.userId, password)
}
await setStaffLabels(employee.userId, payload?.isAdmin === undefined ? undefined : Boolean(payload.isAdmin))
const updated = await updateDocument(config.collections.employees, employeeId, {
displayName,
email,
shortcode,
})
return { employee: await employeeWithUser(updated) }
}
export async function deleteEmployeeWithLogin(employeeId, actorUserId) {
const employee = await getDocument(config.collections.employees, employeeId)
.catch(() => null)
if (!employee) throw httpError(404, 'Mitarbeiter nicht gefunden')
if (employee.userId === actorUserId) {
throw httpError(400, 'Der eigene angemeldete Account kann nicht gelöscht werden')
}
await deleteDocument(config.collections.employees, employeeId)
await deleteAppwriteUser(employee.userId).catch((error) => {
error.message = `Mitarbeiter entfernt, Login konnte nicht gelöscht werden: ${error.message}`
throw error
})
return { success: true }
}