From acc86c267b8c43e04c6cb1d341ac3e162cbc6530 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 10:32:34 +0000 Subject: [PATCH] =?UTF-8?q?Skript:=202FA=20f=C3=BCr=20Mitarbeiter=20anzeig?= =?UTF-8?q?en,=20einschalten,=20abschalten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/mitarbeiter-2fa.mjs | 73 ++++++++++++++++++++++++++++++++ server/services/appwriteAdmin.js | 11 +++++ server/services/staffMfaAdmin.js | 38 +++++++++++++++++ test/staffMfaAdmin.test.js | 39 +++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 scripts/mitarbeiter-2fa.mjs create mode 100644 server/services/staffMfaAdmin.js create mode 100644 test/staffMfaAdmin.test.js diff --git a/scripts/mitarbeiter-2fa.mjs b/scripts/mitarbeiter-2fa.mjs new file mode 100644 index 0000000..00cd4ad --- /dev/null +++ b/scripts/mitarbeiter-2fa.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Mitarbeiter-2FA (E-Mail-Code) anzeigen, einschalten, abschalten. + * + * Usage (im Container, mit dem Server-Key): + * docker exec -it -w /app webklar-kundenbereich-server node scripts/mitarbeiter-2fa.mjs --status + * ... --enable |alle [--yes] + * ... --disable (Notfall: bei einer Person kommt keine Mail an) + */ +import 'dotenv/config' +import { createInterface } from 'node:readline/promises' +import { disableStaffMfa, enableStaffMfa, listStaffMfaStatus } from '../server/services/staffMfaAdmin.js' + +const args = process.argv.slice(2) +const flag = (name) => { + const i = args.indexOf(name) + return i === -1 ? null : args[i + 1] || '' +} + +function printTable(rows) { + for (const r of rows) { + console.log( + `${r.userId.padEnd(22)} ${r.name.padEnd(20)} ${r.email.padEnd(32)} E-Mail bestätigt: ${r.emailVerification ? 'ja ' : 'nein'} 2FA: ${r.mfa ? 'an' : 'aus'}` + ) + } +} + +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()) +} + +async function main() { + const rows = await listStaffMfaStatus() + + if (args.includes('--status')) { + printTable(rows) + return + } + + const enable = flag('--enable') + if (enable !== null) { + const targets = enable === 'alle' ? rows.filter((r) => !r.mfa) : rows.filter((r) => r.userId === enable) + if (!targets.length) throw new Error(enable === 'alle' ? 'Alle Mitarbeiter haben 2FA schon an.' : `Kein Mitarbeiter mit userId ${enable}`) + console.log('Der Code geht an diese Adressen – sie müssen Post empfangen:') + printTable(targets) + if (!(await confirm('2FA für diese Konten einschalten?'))) return console.log('Abgebrochen.') + for (const r of targets) { + await enableStaffMfa(r.userId) + console.log(`2FA an: ${r.name} <${r.email}>`) + } + return + } + + const disable = flag('--disable') + if (disable) { + const target = rows.find((r) => r.userId === disable) + if (!target) throw new Error(`Kein Mitarbeiter mit userId ${disable}`) + await disableStaffMfa(disable) + console.log(`2FA aus: ${target.name} <${target.email}>`) + return + } + + console.log('Usage: --status | --enable |alle [--yes] | --disable ') +} + +main().catch((err) => { + console.error(err.message) + process.exit(1) +}) diff --git a/server/services/appwriteAdmin.js b/server/services/appwriteAdmin.js index 4b88c61..2ca1576 100644 --- a/server/services/appwriteAdmin.js +++ b/server/services/appwriteAdmin.js @@ -132,6 +132,17 @@ export async function updateAppwriteUserLabels(userId, labels) { }) } +export async function updateAppwriteUserMfa(userId, mfa) { + return adminFetch(`/users/${userId}/mfa`, { method: 'PATCH', body: { mfa: Boolean(mfa) } }) +} + +export async function updateAppwriteUserEmailVerification(userId, emailVerification) { + return adminFetch(`/users/${userId}/verification`, { + method: 'PATCH', + body: { emailVerification: Boolean(emailVerification) }, + }) +} + export async function deleteAppwriteUser(userId) { return adminFetch(`/users/${userId}`, { method: 'DELETE' }) } diff --git a/server/services/staffMfaAdmin.js b/server/services/staffMfaAdmin.js new file mode 100644 index 0000000..17dc819 --- /dev/null +++ b/server/services/staffMfaAdmin.js @@ -0,0 +1,38 @@ +import { config } from '../config.js' +import { + getUserById, + listDocuments, + Query, + updateAppwriteUserEmailVerification, + updateAppwriteUserMfa, +} from './appwriteAdmin.js' + +/** + * Mitarbeiter-2FA verwalten (scripts/mitarbeiter-2fa.mjs). Appwrite verlangt den E-Mail-Code nur, wenn + * die Adresse bestaetigt ist - deshalb beim Einschalten erst bestaetigen, dann 2FA an. + */ +export async function listStaffMfaStatus() { + const employees = await listDocuments(config.collections.employees, [Query.limit(100)]) + const rows = [] + for (const employee of employees) { + if (!employee.userId) continue + const user = await getUserById(employee.userId) + rows.push({ + userId: employee.userId, + name: employee.displayName || user.name || '', + email: user.email || employee.email || '', + emailVerification: Boolean(user.emailVerification), + mfa: Boolean(user.mfa), + }) + } + return rows +} + +export async function enableStaffMfa(userId) { + await updateAppwriteUserEmailVerification(userId, true) + await updateAppwriteUserMfa(userId, true) +} + +export async function disableStaffMfa(userId) { + await updateAppwriteUserMfa(userId, false) +} diff --git a/test/staffMfaAdmin.test.js b/test/staffMfaAdmin.test.js new file mode 100644 index 0000000..2491f49 --- /dev/null +++ b/test/staffMfaAdmin.test.js @@ -0,0 +1,39 @@ +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' + +// Einrichtungs-Skript mitarbeiter-2fa.mjs: Status, einschalten (erst E-Mail bestaetigen, dann 2FA), abschalten +const aw = await startAppwriteMock() +const st = await startFakeStripe() +useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) +const { listStaffMfaStatus, enableStaffMfa, disableStaffMfa } = await import('../server/services/staffMfaAdmin.js') +after(() => Promise.all([aw.close(), st.close()])) + +aw.addUser({ $id: 'u1', email: 'kenso@webklar.com', name: 'Kenso' }) +aw.addUser({ $id: 'u2', email: 'justin@webklar.com', name: 'Justin', emailVerification: true, mfa: true }) +aw.seed('employees', { $id: 'e1', userId: 'u1', displayName: 'Kenso', email: 'kenso@webklar.com' }) +aw.seed('employees', { $id: 'e2', userId: 'u2', displayName: 'Justin', email: 'justin@webklar.com' }) +aw.seed('employees', { $id: 'e3', userId: '', displayName: 'Ohne Login', email: 'alt@webklar.com' }) + +test('Status: nur Mitarbeiter mit Appwrite-Konto', async () => { + assert.deepEqual(await listStaffMfaStatus(), [ + { userId: 'u1', name: 'Kenso', email: 'kenso@webklar.com', emailVerification: false, mfa: false }, + { userId: 'u2', name: 'Justin', email: 'justin@webklar.com', emailVerification: true, mfa: true }, + ]) +}) + +test('Einschalten: erst E-Mail bestaetigen, dann 2FA', async () => { + await enableStaffMfa('u1') + assert.deepEqual(aw.userCalls(), ['verification:u1', 'mfa:u1']) + assert.equal(aw.user('u1').mfa, true) + assert.equal(aw.user('u1').emailVerification, true) +}) + +test('Abschalten: nur 2FA aus, E-Mail bleibt bestaetigt', async () => { + await disableStaffMfa('u2') + assert.equal(aw.user('u2').mfa, false) + assert.equal(aw.user('u2').emailVerification, true) + assert.equal(aw.userCalls().at(-1), 'mfa:u2') +})