Mitarbeiter-Zugang: Review-Punkte (fail closed, Kunden-Konto bleibt, Probelauf robuster)
- removeEmployeeEntry: nur 404 heisst "kein Konto", jeder andere Fehler bricht ab - Mitarbeiter loeschen (Admin-Panel): Konto ohne Mitarbeiter-Label (Kunde mit altem employees-Eintrag) bleibt bestehen, nur der Eintrag geht - Mitarbeiter bearbeiten gibt nie automatisch das Label (der Eintrag kann von einem Kunden stammen); nur Neuanlage bekommt "mitarbeiter" - --rechte: --offen-lassen <ids>, unlesbare Listen als Fehler statt Abbruch (--anwenden verweigert dann), --sicherung <datei> + docker-cp-Hinweis, Bucket-Einstellung transformations wird mitgeschickt - getEmployeeByEmail entfernt (unbenutzt) - Tests: write/users-unverified, zweiter Lauf idempotent, >100 Dokumente, Loeschen mit Kunden- und Mitarbeiter-Konto Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2659385205
commit
10b393687c
6 files changed
+139
-40
No files matched your search
@@ -223,15 +223,6 @@ export async function getEmployeeByUserId(userId) {
|
||||
return docs[0] || null
|
||||
}
|
||||
|
||||
export async function getEmployeeByEmail(email) {
|
||||
if (!email) return null
|
||||
const docs = await listDocuments(config.collections.employees, [
|
||||
Query.equal('email', email.trim().toLowerCase()),
|
||||
Query.limit(1),
|
||||
])
|
||||
return docs[0] || null
|
||||
}
|
||||
|
||||
export async function getPortalAccessByCustomerId(customerId) {
|
||||
const docs = await listDocuments(config.collections.customerPortalAccess, [
|
||||
Query.equal('customerId', customerId),
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
updateDocument,
|
||||
Query,
|
||||
} from './appwriteAdmin.js'
|
||||
import { ADMIN_LABEL, STAFF_LABEL, isAdminAccount } from './staffLabels.js'
|
||||
import { ADMIN_LABEL, STAFF_LABEL, isAdminAccount, isStaffAccount } from './staffLabels.js'
|
||||
|
||||
function httpError(status, message) {
|
||||
const error = new Error(message)
|
||||
@@ -46,12 +46,13 @@ async function employeeWithUser(employee) {
|
||||
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) {
|
||||
// Neue Mitarbeiter bekommen das Label "mitarbeiter" (Zugang zum Ticketsystem, siehe staffLabels.js). Beim
|
||||
// Bearbeiten nie automatisch: ein employees-Eintrag kann von einem Kunden stammen (frueher selbst anlegbar);
|
||||
// Zugang fuer Bestandskonten nur per scripts/mitarbeiter-zugang.mjs --label
|
||||
async function setStaffLabels(userId, { isAdmin, grantStaff = false }) {
|
||||
const user = await getUserById(userId)
|
||||
const labels = new Set(user.labels || [])
|
||||
labels.add(STAFF_LABEL)
|
||||
if (grantStaff) labels.add(STAFF_LABEL)
|
||||
if (isAdmin === true) labels.add(ADMIN_LABEL)
|
||||
if (isAdmin === false) labels.delete(ADMIN_LABEL)
|
||||
await updateAppwriteUserLabels(userId, [...labels])
|
||||
@@ -81,7 +82,7 @@ export async function createEmployeeWithLogin(payload) {
|
||||
const user = await createAppwriteUser({ email, password, name: displayName })
|
||||
let employee
|
||||
try {
|
||||
await setStaffLabels(user.$id, isAdmin)
|
||||
await setStaffLabels(user.$id, { isAdmin, grantStaff: true })
|
||||
employee = await createDocument(config.collections.employees, {
|
||||
userId: user.$id,
|
||||
displayName,
|
||||
@@ -135,7 +136,9 @@ export async function updateEmployeeWithLogin(employeeId, payload, actorUserId)
|
||||
if (password) {
|
||||
await updateAppwriteUserPassword(employee.userId, password)
|
||||
}
|
||||
await setStaffLabels(employee.userId, payload?.isAdmin === undefined ? undefined : Boolean(payload.isAdmin))
|
||||
if (payload?.isAdmin !== undefined) {
|
||||
await setStaffLabels(employee.userId, { isAdmin: Boolean(payload.isAdmin) })
|
||||
}
|
||||
|
||||
const updated = await updateDocument(config.collections.employees, employeeId, {
|
||||
displayName,
|
||||
@@ -153,7 +156,16 @@ export async function deleteEmployeeWithLogin(employeeId, actorUserId) {
|
||||
throw httpError(400, 'Der eigene angemeldete Account kann nicht gelöscht werden')
|
||||
}
|
||||
|
||||
// Konto ohne Mitarbeiter-Label (z. B. Kunde mit altem employees-Eintrag): nur den Eintrag loeschen, das Konto ist
|
||||
// sein Portal-Login. Konto nicht lesbar -> abbrechen statt zu raten
|
||||
const user = employee.userId
|
||||
? await getUserById(employee.userId).catch((err) => {
|
||||
if (err.status === 404) return null
|
||||
throw err
|
||||
})
|
||||
: null
|
||||
await deleteDocument(config.collections.employees, employeeId)
|
||||
if (!isStaffAccount(user)) return { success: true, accountKept: Boolean(user) }
|
||||
await deleteAppwriteUser(employee.userId).catch((error) => {
|
||||
error.message = `Mitarbeiter entfernt, Login konnte nicht gelöscht werden: ${error.message}`
|
||||
throw error
|
||||
|
||||
@@ -92,7 +92,13 @@ export async function grantStaffLabel(userId, request = appwriteAdminRequest) {
|
||||
export async function removeEmployeeEntry(employeeId, request = appwriteAdminRequest) {
|
||||
const path = `/databases/${config.appwrite.databaseId}/collections/${config.collections.employees}/documents/${employeeId}`
|
||||
const employee = await request(path)
|
||||
const user = employee.userId ? await request(`/users/${employee.userId}`).catch(() => null) : null
|
||||
// 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 || '' }
|
||||
@@ -100,18 +106,31 @@ export async function removeEmployeeEntry(employeeId, request = appwriteAdminReq
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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). 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) {
|
||||
export async function planPermissionChanges(request = appwriteAdminRequest, { keep = [] } = {}) {
|
||||
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 listAll(request, '/databases', 'databases')) {
|
||||
for (const database of await tryList('/databases', 'databases')) {
|
||||
const collectionsPath = `/databases/${database.$id}/collections`
|
||||
for (const collection of await listAll(request, collectionsPath, 'collections')) {
|
||||
for (const collection of await tryList(collectionsPath, 'collections')) {
|
||||
if (kept.has(collection.$id)) continue
|
||||
add(
|
||||
{
|
||||
kind: 'collection',
|
||||
@@ -123,13 +142,14 @@ export async function planPermissionChanges(request = appwriteAdminRequest) {
|
||||
collection.$permissions || []
|
||||
)
|
||||
if (!collection.documentSecurity) continue
|
||||
for (const document of await listAll(request, `${collectionsPath}/${collection.$id}/documents`, 'documents')) {
|
||||
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 || [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const bucket of await listAll(request, '/storage/buckets', 'buckets')) {
|
||||
for (const bucket of await tryList('/storage/buckets', 'buckets')) {
|
||||
if (kept.has(bucket.$id)) continue
|
||||
add(
|
||||
{
|
||||
kind: 'bucket',
|
||||
@@ -144,16 +164,18 @@ export async function planPermissionChanges(request = appwriteAdminRequest) {
|
||||
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 listAll(request, `/storage/buckets/${bucket.$id}/files`, 'files')) {
|
||||
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
|
||||
return { changes, errors }
|
||||
}
|
||||
|
||||
function writeChange(change, permissions, request) {
|
||||
|
||||
Reference in new issue
Block a user