221 lines
9.1 KiB
JavaScript
221 lines
9.1 KiB
JavaScript
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
|
|
}
|