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>
128 lines
5.5 KiB
JavaScript
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)
|
|
})
|