import { config } from '../config.js' import { appwriteAdminRequest, Query } from './appwriteAdmin.js' import { STAFF_LABEL, STAFF_LABELS, isStaffAccount } from './staffLabels.js' /** * Zugang zum Ticketsystem nur fuer Mitarbeiter (scripts/mitarbeiter-zugang.mjs). Kunden haben Konten im selben * Appwrite-Projekt; Rechte fuer "users"/"any"/"guests" galten deshalb auch fuer jeden Kunden. Danach gehoeren * diese Rechte nur noch den Labels "mitarbeiter" und "admin" (staffLabels.js). */ const OPEN_ROLES = new Set(['any', 'guests', 'users', 'users/verified', 'users/unverified']) const PERMISSION = /^(read|create|update|delete|write)\("([^"]+)"\)$/ const PAGE = 100 /** Neue Rechteliste: jede Freigabe fuer any/guests/users geht an die Mitarbeiter-Labels, andere Rollen bleiben. */ export function staffOnlyPermissions(permissions = []) { const result = [] for (const permission of permissions) { const match = String(permission).match(PERMISSION) if (match && OPEN_ROLES.has(match[2])) { for (const label of STAFF_LABELS) result.push(`${match[1]}("label:${label}")`) } else { result.push(permission) } } return [...new Set(result)] } const samePermissions = (a = [], b = []) => a.length === b.length && a.every((p, i) => p === b[i]) async function listAll(request, path, key) { const all = [] let cursor = null for (;;) { const queries = [Query.limit(PAGE), ...(cursor ? [Query.cursorAfter(cursor)] : [])] const page = (await request(path, { queries }))[key] || [] all.push(...page) if (page.length < PAGE) return all cursor = page.at(-1).$id } } /** * Mitarbeiter-Status: jeder employees-Eintrag mit seinem Konto, dazu Konten mit Mitarbeiter-Label ohne Eintrag. * customer = das Konto gehoert zu einem Kunden (customers.appwriteUserId) - ein solcher employees-Eintrag ist beim * Login eines Kunden ins Ticketsystem automatisch entstanden und muss weg. */ export async function staffAccessStatus(request = appwriteAdminRequest) { const db = `/databases/${config.appwrite.databaseId}/collections` const [users, employees, customers] = await Promise.all([ listAll(request, '/users', 'users'), listAll(request, `${db}/${config.collections.employees}/documents`, 'documents'), listAll(request, `${db}/${config.collections.customers}/documents`, 'documents'), ]) const userById = new Map(users.map((u) => [u.$id, u])) const customerByUser = new Map(customers.filter((c) => c.appwriteUserId).map((c) => [c.appwriteUserId, c])) const rows = employees.map((employee) => { const user = userById.get(employee.userId) || null const customer = customerByUser.get(employee.userId) || null return { employeeId: employee.$id, userId: employee.userId || '', name: employee.displayName || user?.name || '', email: user?.email || employee.email || '', labels: user?.labels || [], staff: isStaffAccount(user), customer: customer ? customer.name || customer.$id : '', login: Boolean(user), } }) const withEntry = new Set(employees.map((e) => e.userId)) const labelOnly = users .filter((u) => isStaffAccount(u) && !withEntry.has(u.$id)) .map((u) => ({ userId: u.$id, name: u.name || '', email: u.email || '', labels: u.labels || [] })) return { rows, labelOnly } } /** Label "mitarbeiter" an ein Konto haengen (vorhandene Labels bleiben). */ export async function grantStaffLabel(userId, request = appwriteAdminRequest) { const user = await request(`/users/${userId}`) const labels = new Set(user.labels || []) labels.add(STAFF_LABEL) await request(`/users/${userId}/labels`, { method: 'PUT', body: { labels: [...labels] } }) return { userId, name: user.name || '', email: user.email || '', labels: [...labels] } } /** * employees-Eintrag eines Kunden loeschen - nur den Eintrag, nie das Konto (das ist der Portal-Login des Kunden). * Verweigert bei einem Konto mit Mitarbeiter-Label. */ export async function removeEmployeeEntry(employeeId, request = appwriteAdminRequest) { const path = `/databases/${config.appwrite.databaseId}/collections/${config.collections.employees}/documents/${employeeId}` const employee = await request(path) // Nur "Konto gibt es nicht" (404) zaehlt als kein Mitarbeiter; jeder andere Fehler bricht ab (fail closed) const user = employee.userId ? await request(`/users/${employee.userId}`).catch((err) => { if (err.status === 404) return null throw err }) : null if (isStaffAccount(user)) throw new Error(`${employee.displayName || employeeId} hat das Mitarbeiter-Label - Eintrag bleibt.`) await request(path, { method: 'DELETE' }) return { employeeId, name: employee.displayName || '', userId: employee.userId || '' } } /** * Alle Stellen mit offenen Rechten: Collections und Buckets, bei Dokument- bzw. Datei-Sicherheit auch einzelne * Dokumente und Dateien. Jede Aenderung traegt before/after, damit sie sich zuruecknehmen laesst. keep = IDs von * Collections/Buckets, die bewusst offen bleiben (samt ihrer Dokumente/Dateien), buckets false laesst alle Buckets * aus. Eine Liste, die sich nicht lesen * laesst (z. B. fehlender Key-Scope), landet in errors statt den ganzen Lauf abzubrechen. */ export async function planPermissionChanges(request = appwriteAdminRequest, { keep = [], buckets = true } = {}) { const changes = [] const errors = [] const kept = new Set(keep) const add = (change, before) => { const after = staffOnlyPermissions(before) if (!samePermissions(before, after)) changes.push({ ...change, before, after }) } const tryList = async (path, key) => { try { return await listAll(request, path, key) } catch (err) { errors.push({ path, error: err.message }) return [] } } for (const database of await tryList('/databases', 'databases')) { const collectionsPath = `/databases/${database.$id}/collections` for (const collection of await tryList(collectionsPath, 'collections')) { if (kept.has(collection.$id)) continue add( { kind: 'collection', databaseId: database.$id, id: collection.$id, name: collection.name, settings: { name: collection.name, documentSecurity: collection.documentSecurity, enabled: collection.enabled }, }, collection.$permissions || [] ) if (!collection.documentSecurity) continue for (const document of await tryList(`${collectionsPath}/${collection.$id}/documents`, 'documents')) { add({ kind: 'document', databaseId: database.$id, collectionId: collection.$id, id: document.$id }, document.$permissions || []) } } } // buckets: false -> Buckets bleiben aussen vor (Key ohne buckets-Scope; dann von Hand in der Konsole) for (const bucket of buckets ? await tryList('/storage/buckets', 'buckets') : []) { if (kept.has(bucket.$id)) continue add( { kind: 'bucket', id: bucket.$id, name: bucket.name, settings: { name: bucket.name, fileSecurity: bucket.fileSecurity, enabled: bucket.enabled, maximumFileSize: bucket.maximumFileSize, allowedFileExtensions: bucket.allowedFileExtensions, compression: bucket.compression, encryption: bucket.encryption, antivirus: bucket.antivirus, // neuere Appwrite-Versionen: Bild-Transformationen; mitschicken, sonst setzt das PUT den Standard ...(bucket.transformations === undefined ? {} : { transformations: bucket.transformations }), }, }, bucket.$permissions || [] ) if (!bucket.fileSecurity) continue for (const file of await tryList(`/storage/buckets/${bucket.$id}/files`, 'files')) { add({ kind: 'file', bucketId: bucket.$id, id: file.$id }, file.$permissions || []) } } return { changes, errors } } function writeChange(change, permissions, request) { if (change.kind === 'collection') { return request(`/databases/${change.databaseId}/collections/${change.id}`, { method: 'PUT', body: { ...change.settings, permissions }, }) } if (change.kind === 'document') { return request(`/databases/${change.databaseId}/collections/${change.collectionId}/documents/${change.id}`, { method: 'PATCH', body: { permissions }, }) } if (change.kind === 'bucket') { return request(`/storage/buckets/${change.id}`, { method: 'PUT', body: { ...change.settings, permissions } }) } if (change.kind === 'file') { return request(`/storage/buckets/${change.bucketId}/files/${change.id}`, { method: 'PUT', body: { permissions } }) } throw new Error(`Unbekannte Art ${change.kind}`) } /** * Aenderungen schreiben (direction 'after') oder zuruecknehmen ('before'). Ein Fehler stoppt nichts: jede Stelle * meldet ok oder error (z. B. fehlender Key-Scope fuer Buckets). */ export async function applyPermissionChanges(changes, { direction = 'after', request = appwriteAdminRequest } = {}) { const results = [] for (const change of changes) { try { await writeChange(change, change[direction], request) results.push({ change, ok: true }) } catch (err) { results.push({ change, ok: false, error: err.message }) } } return results }