Files
Webklar-Kundenbereich/scripts/mitarbeiter-zugang.mjs
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

128 lines
5.5 KiB
JavaScript

#!/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)
})