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>
This commit is contained in:
1 parent
be7f9b1aa3
commit
2659385205
12 files changed
+602
-65
No files matched your search
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Zugang zum Ticketsystem nur fuer Mitarbeiter (Label "mitarbeiter" oder "admin").
|
||||
*
|
||||
* Usage (im Container, mit dem Server-Key):
|
||||
* docker exec -it -w /app webklar-kundenbereich-server node scripts/mitarbeiter-zugang.mjs --status
|
||||
* ... --label <userId>[,<userId>...] [--yes] Label "mitarbeiter" vergeben (nur ausdrueckliche Konten)
|
||||
* ... --eintrag-loeschen <employeeId> [--yes] employees-Eintrag eines Kunden loeschen (Konto bleibt)
|
||||
* ... --rechte Probelauf: welche Rechte sich aendern
|
||||
* ... --rechte --anwenden [--yes] Rechte umstellen, Sicherung als JSON (Pfad wird ausgegeben)
|
||||
* ... --rechte --zurueck <sicherung.json> [--yes] Rechte aus einer Sicherung wiederherstellen
|
||||
*/
|
||||
import 'dotenv/config'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createInterface } from 'node:readline/promises'
|
||||
import {
|
||||
applyPermissionChanges,
|
||||
grantStaffLabel,
|
||||
planPermissionChanges,
|
||||
removeEmployeeEntry,
|
||||
staffAccessStatus,
|
||||
} from '../server/services/staffAccessAdmin.js'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const flag = (name) => {
|
||||
const i = args.indexOf(name)
|
||||
return i === -1 ? null : args[i + 1] || ''
|
||||
}
|
||||
|
||||
async function confirm(question) {
|
||||
if (args.includes('--yes')) return true
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
||||
const answer = await rl.question(`${question} (Ja/Nein) `)
|
||||
rl.close()
|
||||
return /^j(a)?$/i.test(answer.trim())
|
||||
}
|
||||
|
||||
function where(change) {
|
||||
if (change.kind === 'collection') return `Collection ${change.databaseId}/${change.id} (${change.name})`
|
||||
if (change.kind === 'document') return `Dokument ${change.collectionId}/${change.id}`
|
||||
if (change.kind === 'bucket') return `Bucket ${change.id} (${change.name})`
|
||||
return `Datei ${change.bucketId}/${change.id}`
|
||||
}
|
||||
|
||||
function printChanges(changes) {
|
||||
for (const change of changes) {
|
||||
console.log(where(change))
|
||||
console.log(` vorher: ${change.before.join(' ') || '(keine)'}`)
|
||||
console.log(` nachher: ${change.after.join(' ') || '(keine)'}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function status() {
|
||||
const { rows, labelOnly } = await staffAccessStatus()
|
||||
console.log('employees-Einträge:')
|
||||
for (const r of rows) {
|
||||
const state = r.staff ? 'Zugang: ja ' : 'Zugang: NEIN'
|
||||
const note = r.customer ? ` ← KUNDE (${r.customer}), Eintrag löschen` : r.login ? '' : ' (kein Konto)'
|
||||
console.log(` ${r.employeeId.padEnd(22)} ${r.userId.padEnd(38)} ${r.name.padEnd(20)} ${r.email.padEnd(32)} ${state} [${r.labels.join(',')}]${note}`)
|
||||
}
|
||||
if (labelOnly.length) {
|
||||
console.log('Konten mit Mitarbeiter-Label ohne employees-Eintrag:')
|
||||
for (const u of labelOnly) console.log(` ${u.userId.padEnd(38)} ${u.name.padEnd(20)} ${u.email.padEnd(32)} [${u.labels.join(',')}]`)
|
||||
}
|
||||
}
|
||||
|
||||
async function label(ids) {
|
||||
const userIds = ids.split(',').map((id) => id.trim()).filter(Boolean)
|
||||
if (!userIds.length) throw new Error('Bitte userIds angeben (kommagetrennt).')
|
||||
console.log(`Label "mitarbeiter" für: ${userIds.join(', ')}`)
|
||||
if (!(await confirm('Diese Konten bekommen Zugang zum Ticketsystem. Fortfahren?'))) return console.log('Abgebrochen.')
|
||||
for (const userId of userIds) {
|
||||
const r = await grantStaffLabel(userId)
|
||||
console.log(`Zugang: ${r.name} <${r.email}> [${r.labels.join(',')}]`)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEntry(employeeId) {
|
||||
if (!(await confirm(`employees-Eintrag ${employeeId} löschen (das Konto bleibt)?`))) return console.log('Abgebrochen.')
|
||||
const r = await removeEmployeeEntry(employeeId)
|
||||
console.log(`Eintrag gelöscht: ${r.name} (Konto ${r.userId} bleibt)`)
|
||||
}
|
||||
|
||||
function report(results) {
|
||||
const failed = results.filter((r) => !r.ok)
|
||||
for (const r of failed) console.log(`FEHLER ${where(r.change)}: ${r.error}`)
|
||||
console.log(`${results.length - failed.length} von ${results.length} Stellen geschrieben.`)
|
||||
if (failed.length) process.exitCode = 1
|
||||
}
|
||||
|
||||
async function permissions() {
|
||||
const restore = flag('--zurueck')
|
||||
if (restore) {
|
||||
const changes = JSON.parse(readFileSync(restore, 'utf8'))
|
||||
printChanges(changes.map((c) => ({ ...c, before: c.after, after: c.before })))
|
||||
if (!(await confirm(`${changes.length} Stellen auf den Stand der Sicherung zurücksetzen?`))) return console.log('Abgebrochen.')
|
||||
return report(await applyPermissionChanges(changes, { direction: 'before' }))
|
||||
}
|
||||
|
||||
const changes = await planPermissionChanges()
|
||||
if (!changes.length) return console.log('Keine offenen Rechte (any/guests/users) gefunden.')
|
||||
printChanges(changes)
|
||||
if (!args.includes('--anwenden')) return console.log(`Probelauf: ${changes.length} Stellen. Mit --anwenden umstellen.`)
|
||||
|
||||
const backup = join(tmpdir(), `mitarbeiter-rechte-${new Date().toISOString().replace(/[:.]/g, '-')}.json`)
|
||||
writeFileSync(backup, JSON.stringify(changes, null, 2))
|
||||
console.log(`Sicherung: ${backup}`)
|
||||
if (!(await confirm(`${changes.length} Stellen nur noch für Mitarbeiter freigeben?`))) return console.log('Abgebrochen.')
|
||||
report(await applyPermissionChanges(changes))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.includes('--status')) return status()
|
||||
const ids = flag('--label')
|
||||
if (ids !== null) return label(ids)
|
||||
const employeeId = flag('--eintrag-loeschen')
|
||||
if (employeeId) return removeEntry(employeeId)
|
||||
if (args.includes('--rechte')) return permissions()
|
||||
console.log('Usage: --status | --label <userId>[,...] | --eintrag-loeschen <employeeId> | --rechte [--anwenden | --zurueck <datei>] [--yes]')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { config } from '../config.js'
|
||||
import { getEmployeeByEmail, getEmployeeByUserId } from '../services/appwriteAdmin.js'
|
||||
import { getEmployeeByUserId } from '../services/appwriteAdmin.js'
|
||||
import { isAdminAccount, isStaffAccount } from '../services/staffLabels.js'
|
||||
|
||||
/**
|
||||
* Appwrite-Konto zum Bearer-JWT aus dem Ticketsystem (GET /account). null ohne Header oder wenn Appwrite
|
||||
@@ -20,20 +21,15 @@ export async function appwriteAccountFromJwt(req) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mitarbeiter fuer den Team-Eingang: employees-Eintrag zur Konto-ID, Label "admin" oder - nur mit bestaetigter
|
||||
* Konto-Mail - ein employees-Eintrag zur Mail. Kunden haben Konten im selben Appwrite-Projekt und koennen ihre
|
||||
* Mail ohne Bestaetigung aendern; eine unbestaetigte Mail zaehlt deshalb nie.
|
||||
* Mitarbeiter fuer den Team-Eingang: nur ein Konto mit Label "mitarbeiter" oder "admin" (staffLabels.js). Der
|
||||
* employees-Eintrag liefert nur noch den Anzeigenamen - Kunden haben Konten im selben Appwrite-Projekt und
|
||||
* konnten sich einen solchen Eintrag selbst anlegen.
|
||||
*/
|
||||
export async function resolveTeamStaff(account) {
|
||||
if (!account?.$id) return null
|
||||
const isAdmin = Array.isArray(account.labels) && account.labels.includes('admin')
|
||||
let employee = await getEmployeeByUserId(account.$id).catch(() => null)
|
||||
if (!employee && !isAdmin && account.emailVerification === true && account.email) {
|
||||
employee = await getEmployeeByEmail(account.email).catch(() => null)
|
||||
}
|
||||
if (!employee && !isAdmin) return null
|
||||
if (!account?.$id || !isStaffAccount(account)) return null
|
||||
const employee = await getEmployeeByUserId(account.$id).catch(() => null)
|
||||
return {
|
||||
isAdmin,
|
||||
isAdmin: isAdminAccount(account),
|
||||
name: employee?.displayName || account.name || 'Mitarbeiter',
|
||||
email: account.email || employee?.email || '',
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
startKundenansicht,
|
||||
} from '../services/kundenansicht.js'
|
||||
import { logViewAction } from '../services/customerViewLog.js'
|
||||
import { resolveStaffAccount } from '../services/staffAccount.js'
|
||||
import { isStaffAccount } from '../services/staffLabels.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -162,13 +162,17 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
try {
|
||||
const user = await loginWithAppwrite(email.trim(), password)
|
||||
// Appwrite-Konto: Label "admin" und Passwortstand fuer die Kunden-Session (passwordVersion.js)
|
||||
// Appwrite-Konto: Labels und Passwortstand fuer die Kunden-Session (passwordVersion.js). Ohne Konto keine
|
||||
// Entscheidung, ob Mitarbeiter -> Anmeldung ablehnen statt raten (fail closed)
|
||||
const account = await getUserById(user.$id).catch(() => null)
|
||||
if (!account) {
|
||||
await appwriteSessionFetch('/account/sessions/current', { method: 'DELETE', sessionCookie: user.sessionCookie }).catch(() => {})
|
||||
return res.status(503).json({ error: 'Anmeldung gerade nicht möglich. Bitte in einer Minute erneut versuchen.' })
|
||||
}
|
||||
|
||||
// Mitarbeiter (employees-Collection oder Label admin, mit oder ohne 2FA) arbeiten im Ticketsystem:
|
||||
// Mitarbeiter (Label mitarbeiter oder admin, mit oder ohne 2FA) arbeiten im Ticketsystem:
|
||||
// keine Portal-Sitzung, die eben erzeugte Appwrite-Sitzung wieder beenden (best effort)
|
||||
const staff = await resolveStaffAccount(user.$id, email.trim(), account)
|
||||
if (staff) {
|
||||
if (isStaffAccount(account)) {
|
||||
await appwriteSessionFetch('/account/sessions/current', { method: 'DELETE', sessionCookie: user.sessionCookie }).catch((err) =>
|
||||
console.warn('[auth] Appwrite-Sitzung eines Mitarbeiters nicht beendet:', err.message)
|
||||
)
|
||||
|
||||
@@ -68,6 +68,11 @@ async function adminFetch(path, { method = 'GET', body, queries = [], raw = fals
|
||||
return data
|
||||
}
|
||||
|
||||
/** Roher Server-Key-Aufruf (Body unverpackt), z. B. fuer Collection- und Bucket-Rechte (staffAccessAdmin.js). */
|
||||
export async function appwriteAdminRequest(path, options = {}) {
|
||||
return adminFetch(path, { ...options, raw: true })
|
||||
}
|
||||
|
||||
/** Appwrite Query-Helper (kompatibel zu bisherigen Aufrufen) */
|
||||
export const Query = {
|
||||
equal: (attribute, value) => ({
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
updateDocument,
|
||||
Query,
|
||||
} from './appwriteAdmin.js'
|
||||
import { ADMIN_LABEL, STAFF_LABEL, isAdminAccount } from './staffLabels.js'
|
||||
|
||||
function httpError(status, message) {
|
||||
const error = new Error(message)
|
||||
@@ -36,7 +37,7 @@ function sanitizeEmployee(employee, user) {
|
||||
displayName: employee.displayName || user?.name || '',
|
||||
email: employee.email || user?.email || '',
|
||||
shortcode: employee.shortcode || '',
|
||||
isAdmin: Boolean(user?.labels?.includes('admin')),
|
||||
isAdmin: isAdminAccount(user),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +46,14 @@ async function employeeWithUser(employee) {
|
||||
return sanitizeEmployee(employee, user)
|
||||
}
|
||||
|
||||
async function setAdminLabel(userId, isAdmin) {
|
||||
// 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 || [])
|
||||
if (isAdmin) labels.add('admin')
|
||||
else labels.delete('admin')
|
||||
labels.add(STAFF_LABEL)
|
||||
if (isAdmin === true) labels.add(ADMIN_LABEL)
|
||||
if (isAdmin === false) labels.delete(ADMIN_LABEL)
|
||||
await updateAppwriteUserLabels(userId, [...labels])
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ export async function createEmployeeWithLogin(payload) {
|
||||
const user = await createAppwriteUser({ email, password, name: displayName })
|
||||
let employee
|
||||
try {
|
||||
await setAdminLabel(user.$id, isAdmin)
|
||||
await setStaffLabels(user.$id, isAdmin)
|
||||
employee = await createDocument(config.collections.employees, {
|
||||
userId: user.$id,
|
||||
displayName,
|
||||
@@ -131,9 +135,7 @@ export async function updateEmployeeWithLogin(employeeId, payload, actorUserId)
|
||||
if (password) {
|
||||
await updateAppwriteUserPassword(employee.userId, password)
|
||||
}
|
||||
if (payload?.isAdmin !== undefined) {
|
||||
await setAdminLabel(employee.userId, Boolean(payload.isAdmin))
|
||||
}
|
||||
await setStaffLabels(employee.userId, payload?.isAdmin === undefined ? undefined : Boolean(payload.isAdmin))
|
||||
|
||||
const updated = await updateDocument(config.collections.employees, employeeId, {
|
||||
displayName,
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
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)
|
||||
const user = employee.userId ? await request(`/users/${employee.userId}`).catch(() => null) : 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.
|
||||
*/
|
||||
export async function planPermissionChanges(request = appwriteAdminRequest) {
|
||||
const changes = []
|
||||
const add = (change, before) => {
|
||||
const after = staffOnlyPermissions(before)
|
||||
if (!samePermissions(before, after)) changes.push({ ...change, before, after })
|
||||
}
|
||||
|
||||
for (const database of await listAll(request, '/databases', 'databases')) {
|
||||
const collectionsPath = `/databases/${database.$id}/collections`
|
||||
for (const collection of await listAll(request, collectionsPath, 'collections')) {
|
||||
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 listAll(request, `${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')) {
|
||||
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,
|
||||
},
|
||||
},
|
||||
bucket.$permissions || []
|
||||
)
|
||||
if (!bucket.fileSecurity) continue
|
||||
for (const file of await listAll(request, `/storage/buckets/${bucket.$id}/files`, 'files')) {
|
||||
add({ kind: 'file', bucketId: bucket.$id, id: file.$id }, file.$permissions || [])
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { getEmployeeByEmail, getEmployeeByUserId } from './appwriteAdmin.js'
|
||||
|
||||
/**
|
||||
* Ticketsystem-Mitarbeiter (employees-Collection) oder Appwrite-Label "admin" -> Admin-Ansicht im Portal.
|
||||
* isAdmin = Label "admin"; null = kein Mitarbeiter.
|
||||
*/
|
||||
export async function resolveStaffAccount(appwriteUserId, email, account) {
|
||||
const isAdmin = Array.isArray(account?.labels) && account.labels.includes('admin')
|
||||
|
||||
let employee = await getEmployeeByUserId(appwriteUserId).catch(() => null)
|
||||
if (!employee) {
|
||||
employee = await getEmployeeByEmail(account?.email || email).catch(() => null)
|
||||
}
|
||||
|
||||
if (!employee && !isAdmin) return null
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
name: employee?.displayName || account?.name || 'Mitarbeiter',
|
||||
email: account?.email || employee?.email || email,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Wer ist Mitarbeiter? Nur das Appwrite-Label "mitarbeiter" (oder "admin") am Konto. Labels setzt allein der
|
||||
* Server-Key - anders als ein employees-Eintrag, den jedes angemeldete Konto (auch ein Kunde) selbst anlegen
|
||||
* konnte. Dieselben Labels stehen in den Appwrite-Rechten der Ticketsystem-Collections
|
||||
* (scripts/mitarbeiter-zugang.mjs) und im Login des Ticketsystems.
|
||||
*/
|
||||
export const STAFF_LABEL = 'mitarbeiter'
|
||||
export const ADMIN_LABEL = 'admin'
|
||||
export const STAFF_LABELS = [STAFF_LABEL, ADMIN_LABEL]
|
||||
|
||||
function labelsOf(account) {
|
||||
return Array.isArray(account?.labels) ? account.labels : []
|
||||
}
|
||||
|
||||
export function isStaffAccount(account) {
|
||||
const labels = labelsOf(account)
|
||||
return STAFF_LABELS.some((label) => labels.includes(label))
|
||||
}
|
||||
|
||||
export function isAdminAccount(account) {
|
||||
return labelsOf(account).includes(ADMIN_LABEL)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Zugang zum Ticketsystem nur mit Label (scripts/mitarbeiter-zugang.mjs, employeeAdmin.js)
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const {
|
||||
applyPermissionChanges,
|
||||
grantStaffLabel,
|
||||
planPermissionChanges,
|
||||
removeEmployeeEntry,
|
||||
staffAccessStatus,
|
||||
staffOnlyPermissions,
|
||||
} = await import('../server/services/staffAccessAdmin.js')
|
||||
const { isStaffAccount, isAdminAccount } = await import('../server/services/staffLabels.js')
|
||||
const { createEmployeeWithLogin, updateEmployeeWithLogin } = await import('../server/services/employeeAdmin.js')
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
|
||||
const STAFF = ['label:mitarbeiter', 'label:admin']
|
||||
|
||||
test('isStaffAccount: nur Label mitarbeiter oder admin, nie ohne Konto', () => {
|
||||
assert.equal(isStaffAccount({ labels: ['mitarbeiter'] }), true)
|
||||
assert.equal(isStaffAccount({ labels: ['admin'] }), true)
|
||||
assert.equal(isStaffAccount({ labels: [] }), false)
|
||||
assert.equal(isStaffAccount({ labels: ['kunde'] }), false)
|
||||
assert.equal(isStaffAccount(null), false)
|
||||
assert.equal(isAdminAccount({ labels: ['mitarbeiter'] }), false)
|
||||
})
|
||||
|
||||
test('staffOnlyPermissions: any/guests/users -> Mitarbeiter-Labels, andere Rollen bleiben, keine Doppel', () => {
|
||||
assert.deepEqual(
|
||||
staffOnlyPermissions(['read("users")', 'create("users")', 'read("any")', 'update("guests")', 'read("user:abc")', 'delete("users/verified")']),
|
||||
[
|
||||
...STAFF.map((r) => `read("${r}")`),
|
||||
...STAFF.map((r) => `create("${r}")`),
|
||||
...STAFF.map((r) => `update("${r}")`),
|
||||
'read("user:abc")',
|
||||
...STAFF.map((r) => `delete("${r}")`),
|
||||
]
|
||||
)
|
||||
assert.deepEqual(staffOnlyPermissions(['read("label:mitarbeiter")', 'read("team:x")']), ['read("label:mitarbeiter")', 'read("team:x")'])
|
||||
assert.deepEqual(staffOnlyPermissions([]), [])
|
||||
})
|
||||
|
||||
// Nachbau der Appwrite-Verwaltungs-API fuer Rechte (Datenbanken, Collections, Dokumente, Buckets, Dateien)
|
||||
function fakeAdminApi() {
|
||||
const collections = {
|
||||
tickets: { $id: 'workorders', name: 'Work Orders', documentSecurity: false, enabled: true, $permissions: ['read("users")', 'update("users")'] },
|
||||
portal: { $id: 'chatMessages', name: 'Chat', documentSecurity: false, enabled: true, $permissions: [] },
|
||||
docs: { $id: 'notes', name: 'Notes', documentSecurity: true, enabled: true, $permissions: ['read("label:mitarbeiter")'] },
|
||||
}
|
||||
const documents = [
|
||||
{ $id: 'n1', $permissions: ['read("user:u1")'] },
|
||||
{ $id: 'n2', $permissions: ['read("any")'] },
|
||||
]
|
||||
const bucket = {
|
||||
$id: 'legal-files', name: 'Legal', fileSecurity: false, enabled: true, maximumFileSize: 30000000,
|
||||
allowedFileExtensions: ['pdf'], compression: 'none', encryption: true, antivirus: true, $permissions: ['read("users")', 'create("users")'],
|
||||
}
|
||||
const writes = []
|
||||
const request = async (path, { method = 'GET', body } = {}) => {
|
||||
if (method !== 'GET') {
|
||||
if (path.endsWith('/collections/workorders') && body.name === undefined) throw new Error('name fehlt')
|
||||
if (path.startsWith('/storage/') && method === 'PUT' && path.endsWith('/broken')) throw new Error('missing scope (buckets.write)')
|
||||
writes.push({ method, path, body })
|
||||
return {}
|
||||
}
|
||||
if (path === '/databases') return { databases: [{ $id: 'woms-database' }] }
|
||||
if (path === '/databases/woms-database/collections') return { collections: Object.values(collections) }
|
||||
if (path === '/databases/woms-database/collections/notes/documents') return { documents }
|
||||
if (path === '/storage/buckets') return { buckets: [bucket, { ...bucket, $id: 'broken', name: 'Kaputt' }] }
|
||||
throw new Error(`unerwartet ${path}`)
|
||||
}
|
||||
return { request, writes }
|
||||
}
|
||||
|
||||
test('planPermissionChanges: offene Collections, Dokumente und Buckets, geschlossene nicht', async () => {
|
||||
const api = fakeAdminApi()
|
||||
const changes = await planPermissionChanges(api.request)
|
||||
assert.deepEqual(changes.map((c) => `${c.kind}:${c.id}`), ['collection:workorders', 'document:n2', 'bucket:legal-files', 'bucket:broken'])
|
||||
const tickets = changes[0]
|
||||
assert.deepEqual(tickets.before, ['read("users")', 'update("users")'])
|
||||
assert.deepEqual(tickets.after, [...STAFF.map((r) => `read("${r}")`), ...STAFF.map((r) => `update("${r}")`)])
|
||||
assert.deepEqual(tickets.settings, { name: 'Work Orders', documentSecurity: false, enabled: true })
|
||||
})
|
||||
|
||||
test('applyPermissionChanges: schreibt mit allen Einstellungen, meldet Fehler je Stelle, Ruecknahme schreibt before', async () => {
|
||||
const api = fakeAdminApi()
|
||||
const changes = await planPermissionChanges(api.request)
|
||||
const results = await applyPermissionChanges(changes, { request: api.request })
|
||||
assert.deepEqual(results.map((r) => r.ok), [true, true, true, false])
|
||||
assert.match(results[3].error, /buckets\.write/)
|
||||
|
||||
const [collection, document, bucket] = api.writes
|
||||
assert.deepEqual(collection, {
|
||||
method: 'PUT',
|
||||
path: '/databases/woms-database/collections/workorders',
|
||||
body: { name: 'Work Orders', documentSecurity: false, enabled: true, permissions: changes[0].after },
|
||||
})
|
||||
assert.deepEqual(document, { method: 'PATCH', path: '/databases/woms-database/collections/notes/documents/n2', body: { permissions: changes[1].after } })
|
||||
assert.equal(bucket.path, '/storage/buckets/legal-files')
|
||||
assert.equal(bucket.body.maximumFileSize, 30000000)
|
||||
assert.deepEqual(bucket.body.allowedFileExtensions, ['pdf'])
|
||||
assert.equal(bucket.body.antivirus, true)
|
||||
|
||||
api.writes.length = 0
|
||||
await applyPermissionChanges(changes.slice(0, 1), { direction: 'before', request: api.request })
|
||||
assert.deepEqual(api.writes[0].body.permissions, ['read("users")', 'update("users")'])
|
||||
})
|
||||
|
||||
test('Status: markiert Eintraege von Kunden und Eintraege ohne Label, listet Label-Konten ohne Eintrag', async () => {
|
||||
aw.addUser({ $id: 'u_kenso', email: 'kenso@webklar.com', name: 'Kenso', labels: ['admin', 'mitarbeiter'] })
|
||||
aw.addUser({ $id: 'u_andrej', email: 'andrej@webklar.com', name: 'Andrej' })
|
||||
aw.addUser({ $id: 'u_kunde', email: 'kunde@example.invalid', name: 'Kunde' })
|
||||
aw.addUser({ $id: 'u_nur_label', email: 'nico@webklar.com', name: 'Nico', labels: ['mitarbeiter'] })
|
||||
aw.seed('employees', { $id: 'e_kenso', userId: 'u_kenso', displayName: 'Kenso', email: 'kenso@webklar.com' })
|
||||
aw.seed('employees', { $id: 'e_andrej', userId: 'u_andrej', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
aw.seed('employees', { $id: 'e_kunde', userId: 'u_kunde', displayName: 'Kunde', email: 'kunde@example.invalid' })
|
||||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u_kunde', name: 'Kunde GmbH' })
|
||||
|
||||
const users = [...['u_kenso', 'u_andrej', 'u_kunde', 'u_nur_label']].map((id) => aw.user(id))
|
||||
const request = async (path, options) => (path === '/users' ? { users } : (await import('../server/services/appwriteAdmin.js')).appwriteAdminRequest(path, options))
|
||||
const { rows, labelOnly } = await staffAccessStatus(request)
|
||||
const byId = Object.fromEntries(rows.map((r) => [r.employeeId, r]))
|
||||
assert.equal(byId.e_kenso.staff, true)
|
||||
assert.equal(byId.e_andrej.staff, false)
|
||||
assert.equal(byId.e_andrej.customer, '')
|
||||
assert.equal(byId.e_kunde.staff, false)
|
||||
assert.equal(byId.e_kunde.customer, 'Kunde GmbH')
|
||||
assert.deepEqual(labelOnly.map((u) => u.userId), ['u_nur_label'])
|
||||
})
|
||||
|
||||
test('grantStaffLabel: haengt mitarbeiter an, vorhandene Labels bleiben', async () => {
|
||||
aw.addUser({ $id: 'u_justin', email: 'justin@webklar.com', name: 'Justin', labels: ['vip'] })
|
||||
const r = await grantStaffLabel('u_justin')
|
||||
assert.deepEqual(r.labels, ['vip', 'mitarbeiter'])
|
||||
assert.deepEqual(aw.user('u_justin').labels, ['vip', 'mitarbeiter'])
|
||||
})
|
||||
|
||||
test('removeEmployeeEntry: loescht nur den Eintrag eines Kunden, nie das Konto; Mitarbeiter bleiben', async () => {
|
||||
aw.addUser({ $id: 'u_k2', email: 'k2@example.invalid', name: 'K2' })
|
||||
aw.addUser({ $id: 'u_m2', email: 'm2@webklar.com', name: 'M2', labels: ['mitarbeiter'] })
|
||||
aw.seed('employees', { $id: 'e_k2', userId: 'u_k2', displayName: 'K2' })
|
||||
aw.seed('employees', { $id: 'e_m2', userId: 'u_m2', displayName: 'M2' })
|
||||
|
||||
await removeEmployeeEntry('e_k2')
|
||||
assert.equal(aw.docs('employees').has('e_k2'), false)
|
||||
assert.ok(aw.user('u_k2'), 'Konto des Kunden bleibt')
|
||||
|
||||
await assert.rejects(removeEmployeeEntry('e_m2'), /Mitarbeiter-Label/)
|
||||
assert.equal(aw.docs('employees').has('e_m2'), true)
|
||||
})
|
||||
|
||||
test('Mitarbeiter anlegen: Label mitarbeiter (+ admin), Bearbeiten ergaenzt es und laesst admin ohne Angabe stehen', async () => {
|
||||
const { employee } = await createEmployeeWithLogin({ displayName: 'Neu', email: 'neu@webklar.com', password: 'neu-neu-neu', isAdmin: false })
|
||||
assert.deepEqual(aw.user(employee.userId).labels, ['mitarbeiter'])
|
||||
|
||||
const { employee: chef } = await createEmployeeWithLogin({ displayName: 'Chef', email: 'chef@webklar.com', password: 'chef-chef-chef', isAdmin: true })
|
||||
assert.deepEqual(aw.user(chef.userId).labels.sort(), ['admin', 'mitarbeiter'])
|
||||
assert.equal(chef.isAdmin, true)
|
||||
|
||||
// Altbestand ohne Label: jede Bearbeitung holt es nach, admin bleibt ohne Angabe unveraendert
|
||||
aw.addUser({ $id: 'u_alt', email: 'alt@webklar.com', name: 'Alt', labels: ['admin'] })
|
||||
aw.seed('employees', { $id: 'e_alt', userId: 'u_alt', displayName: 'Alt', email: 'alt@webklar.com' })
|
||||
await updateEmployeeWithLogin('e_alt', { shortcode: 'ALT' }, 'u_kenso')
|
||||
assert.deepEqual(aw.user('u_alt').labels.sort(), ['admin', 'mitarbeiter'])
|
||||
await updateEmployeeWithLogin('e_alt', { isAdmin: false }, 'u_kenso')
|
||||
assert.deepEqual(aw.user('u_alt').labels, ['mitarbeiter'])
|
||||
})
|
||||
+22
-7
@@ -16,11 +16,16 @@ const app = await startTestApp({ '/api/auth': authRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
aw.addUser({ $id: 'u_kunde', email: 'kunde@example.invalid', password: 'kunde-kunde' })
|
||||
aw.addUser({ $id: 'u_ohne', email: 'justin@webklar.com', password: 'ohne-ohne-ohne' })
|
||||
aw.addUser({ $id: 'u_mfa', email: 'kenso@webklar.com', password: 'mit-mit-mit', mfa: true, emailVerification: true, labels: ['admin'] })
|
||||
aw.addUser({ $id: 'u_ohne', email: 'justin@webklar.com', password: 'ohne-ohne-ohne', labels: ['mitarbeiter'] })
|
||||
aw.addUser({ $id: 'u_mfa', email: 'kenso@webklar.com', password: 'mit-mit-mit', mfa: true, emailVerification: true, labels: ['admin', 'mitarbeiter'] })
|
||||
// Kunde, fuer den beim Login ins Ticketsystem automatisch ein employees-Eintrag entstand (kein Label)
|
||||
aw.addUser({ $id: 'u_kunde_e', email: 'kunde2@example.invalid', password: 'kunde2-kunde2' })
|
||||
aw.addUser({ $id: 'u_label', email: 'label@webklar.com', password: 'label-label', labels: ['admin'] })
|
||||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u_kunde', email: 'kunde@example.invalid', name: 'Kunde', customerStatus: 'active', portalAccessEnabled: true })
|
||||
aw.seed('customerPortalAccess', { $id: 'pa1', customerId: 'c1', enabled: true })
|
||||
aw.seed('customers', { $id: 'c2', appwriteUserId: 'u_kunde_e', email: 'kunde2@example.invalid', name: 'Kunde Zwei', customerStatus: 'active', portalAccessEnabled: true })
|
||||
aw.seed('customerPortalAccess', { $id: 'pa2', customerId: 'c2', enabled: true })
|
||||
aw.seed('employees', { $id: 'e3', userId: 'u_kunde_e', displayName: 'Kunde Zwei', email: 'kunde2@example.invalid' })
|
||||
aw.seed('employees', { $id: 'e1', userId: 'u_ohne', displayName: 'Justin', email: 'justin@webklar.com' })
|
||||
aw.seed('employees', { $id: 'e2', userId: 'u_mfa', displayName: 'Kenso', email: 'kenso@webklar.com' })
|
||||
|
||||
@@ -41,6 +46,13 @@ test('Kunde meldet sich weiter normal an', async () => {
|
||||
assert.equal(aw.sessionDeletes(), before)
|
||||
})
|
||||
|
||||
test('Kunde mit employees-Eintrag ohne Label meldet sich normal an (Eintrag macht niemanden zum Mitarbeiter)', async () => {
|
||||
const res = await login('kunde2@example.invalid', 'kunde2-kunde2')
|
||||
assert.equal(res.status, 200)
|
||||
assert.equal(res.body.role, 'customer')
|
||||
assert.equal(liveCookies(res, SESSION).length, 1)
|
||||
})
|
||||
|
||||
test('Mitarbeiter ohne 2FA: 403 mit Link ins Ticketsystem, keine Sitzung, Appwrite-Sitzung beendet', async () => {
|
||||
const before = aw.sessionDeletes()
|
||||
const res = await login('justin@webklar.com', 'ohne-ohne-ohne')
|
||||
@@ -68,13 +80,16 @@ test('Nur Label admin (ohne employees-Eintrag): ebenfalls gesperrt', async () =>
|
||||
assert.equal(liveCookies(res, SESSION).length, 0)
|
||||
})
|
||||
|
||||
test('Konto-Abruf gestoert: Mitarbeiter bleibt gesperrt (employees-Treffer)', async () => {
|
||||
test('Konto-Abruf gestoert: niemand bekommt eine Sitzung (503), Appwrite-Sitzung beendet', async () => {
|
||||
aw.failUserLookups(true)
|
||||
try {
|
||||
const res = await login('kenso@webklar.com', 'mit-mit-mit')
|
||||
assert.equal(res.status, 403)
|
||||
assert.deepEqual(res.body, STAFF_BLOCKED)
|
||||
assert.equal(liveCookies(res, SESSION).length, 0)
|
||||
for (const [email, password] of [['kenso@webklar.com', 'mit-mit-mit'], ['kunde@example.invalid', 'kunde-kunde']]) {
|
||||
const before = aw.sessionDeletes()
|
||||
const res = await login(email, password)
|
||||
assert.equal(res.status, 503, email)
|
||||
assert.equal(liveCookies(res, SESSION).length, 0)
|
||||
assert.equal(aw.sessionDeletes(), before + 1)
|
||||
}
|
||||
} finally {
|
||||
aw.failUserLookups(false)
|
||||
}
|
||||
|
||||
@@ -229,6 +229,21 @@ export async function startAppwriteMock({ schema = {}, datetimes = {}, defaultLi
|
||||
return [200, { $id: `s-${user.$id}`, userId: user.$id, factors: session.factors }]
|
||||
}
|
||||
}
|
||||
// Konto anlegen und Labels setzen (Mitarbeiter-Verwaltung, employeeAdmin.js)
|
||||
if (url.pathname === '/v1/users' && req.method === 'POST') {
|
||||
const { userId, email, password, name } = await readBody(req)
|
||||
users.set(userId, { $id: userId, email, password, name, labels: [], mfa: false, emailVerification: false })
|
||||
userCalls.push(`create:${userId}`)
|
||||
return [201, publicUser(users.get(userId))]
|
||||
}
|
||||
const labelsPath = url.pathname.match(/^\/v1\/users\/([^/]+)\/labels$/)
|
||||
if (labelsPath && req.method === 'PUT') {
|
||||
const user = users.get(labelsPath[1])
|
||||
if (!user) return [404, { message: 'User not found', code: 404, type: 'user_not_found' }]
|
||||
user.labels = (await readBody(req)).labels
|
||||
userCalls.push(`labels:${labelsPath[1]}`)
|
||||
return [200, publicUser(user)]
|
||||
}
|
||||
const mfaPath = url.pathname.match(/^\/v1\/users\/([^/]+)\/(mfa|verification)$/)
|
||||
if (mfaPath && req.method === 'PATCH') {
|
||||
const user = users.get(mfaPath[1])
|
||||
@@ -286,7 +301,9 @@ export async function startAppwriteMock({ schema = {}, datetimes = {}, defaultLi
|
||||
if (jwt === 'jwt-admin') return [200, { $id: 'u_admin', email: 'kenso@webklar.com', labels: ['admin'] }]
|
||||
if (jwt === 'jwt-kunde') return [200, { $id: 'u_admin', email: 'kenso@webklar.com', labels: [] }]
|
||||
// jwt-staff = Mitarbeiter ohne Label admin, jwt-halb = JWT aus einer Sitzung ohne zweiten Faktor
|
||||
if (jwt === 'jwt-staff') return [200, { $id: 'u_staff2', email: 'andrej@webklar.com', name: 'Andrej', labels: [] }]
|
||||
if (jwt === 'jwt-staff') return [200, { $id: 'u_staff2', email: 'andrej@webklar.com', name: 'Andrej', labels: ['mitarbeiter'] }]
|
||||
// jwt-kunde-eintrag = Kunde ohne Label, dessen Konto-ID ein (selbst angelegter) employees-Eintrag traegt
|
||||
if (jwt === 'jwt-kunde-eintrag') return [200, { $id: 'u_kunde_e', email: 'kunde@example.invalid', name: 'Kunde', labels: [] }]
|
||||
if (jwt === 'jwt-halb') return moreFactors
|
||||
// Team-Eingang: Konto-Mail gleicht einem Mitarbeiter, ist aber unbestaetigt bzw. bestaetigt (ohne userId-Treffer)
|
||||
if (jwt === 'jwt-mail-ungeprueft') return [200, { $id: 'u_fremd', email: 'andrej@webklar.com', emailVerification: false, labels: [] }]
|
||||
|
||||
+12
-10
@@ -5,7 +5,7 @@ import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Team-Eingang /api/team: Appwrite-JWT aus dem Ticketsystem, jeder Mitarbeiter (nicht nur Label admin)
|
||||
// Team-Eingang /api/team: Appwrite-JWT aus dem Ticketsystem, jedes Konto mit Label mitarbeiter oder admin
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
@@ -25,6 +25,8 @@ after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
aw.seed('employees', { $id: 'e_nico', userId: '', displayName: 'Nico', email: 'nico@webklar.com' })
|
||||
// Beim Login eines Kunden ins Ticketsystem entstand frueher automatisch ein employees-Eintrag
|
||||
aw.seed('employees', { $id: 'e_kunde', userId: 'u_kunde_e', displayName: 'Kunde', email: 'kunde@example.invalid' })
|
||||
|
||||
const probe = (jwt) => rawGet('/probe', jwt ? { headers: { authorization: `Bearer ${jwt}` } } : {})
|
||||
|
||||
@@ -38,18 +40,18 @@ test('Kunde (kein Mitarbeiter, kein Label) -> 403', async () => {
|
||||
assert.equal(res.body.error, 'Kein Mitarbeiterkonto.')
|
||||
})
|
||||
|
||||
test('Mail gleicht Mitarbeiter, ist aber unbestaetigt -> 403', async () => {
|
||||
test('Kunde mit employees-Eintrag, aber ohne Label -> 403', async () => {
|
||||
const res = await probe('jwt-kunde-eintrag')
|
||||
assert.equal(res.status, 403)
|
||||
assert.equal(res.body.error, 'Kein Mitarbeiterkonto.')
|
||||
})
|
||||
|
||||
test('Mail gleicht einem Mitarbeiter (bestaetigt oder nicht), kein Label -> 403', async () => {
|
||||
assert.equal((await probe('jwt-mail-ungeprueft')).status, 403)
|
||||
assert.equal((await probe('jwt-mail-geprueft')).status, 403)
|
||||
})
|
||||
|
||||
test('bestaetigte Mail eines Mitarbeiters ohne userId -> erlaubt', async () => {
|
||||
const res = await probe('jwt-mail-geprueft')
|
||||
assert.equal(res.status, 200)
|
||||
assert.equal(res.body.name, 'Nico')
|
||||
assert.equal(res.body.appwriteUserId, 'u_neu')
|
||||
})
|
||||
|
||||
test('Mitarbeiter ohne Admin-Label -> erlaubt, Sitzung in Admin-Form', async () => {
|
||||
test('Label mitarbeiter (ohne admin) -> erlaubt, Name aus employees, Sitzung in Admin-Form', async () => {
|
||||
const res = await probe('jwt-staff')
|
||||
assert.equal(res.status, 200)
|
||||
assert.deepEqual(res.body, { role: 'admin', isAdmin: false, appwriteUserId: 'u_staff2', name: 'Andrej', email: 'andrej@webklar.com' })
|
||||
|
||||
Reference in new issue
Block a user