'use server' import { revalidatePath } from 'next/cache' import type { Modul } from '@prisma/client' import { db, nichtGeloescht } from '@/lib/db' import { protokollieren } from '@/lib/audit' import { passwortHashen } from '@/lib/crypto/hash' import { erfordereBenutzer, erfordereFrischeAnmeldung, erfordereZugang } from '@/lib/auth/sitzung' import { MODUL_BESCHRIFTUNG, ROLLEN_BESCHRIFTUNG } from '@/lib/auth/rollen' import { benutzerAktivSchema, benutzerAnlegenSchema, darfBenutzerVerwalten, darfFreigabenVergeben, freigabeAenderung, freigabenNormalisieren, freigabenSchema, letzterAdminGeschuetzt, rolleAendernSchema, sitzungenBeendenSchema, } from '@/lib/validierung/freigaben' /** * Benutzerverwaltung und Modulfreigaben. * * Alles hier ist eine sensible Aktion: Wer Rollen aendert oder einem * Mitleser die Gesundheitsdaten freigibt, verschiebt die Grenze, wer was * ueber einen kranken Menschen erfahren darf. Deshalb gilt durchgehend: * frische Anmeldung, Administratorrecht bzw. Betreuerrecht fuer die * betroffene Person, und ein Audit-Eintrag, der spaeter beantwortet, wer * wem wann was freigegeben hat. */ export type Aktionszustand = { fehler?: string felder?: Record erfolg?: string warnung?: string } function felderAusZod(fehler: { path: (string | number)[]; message: string }[]): Record { const felder: Record = {} for (const problem of fehler) { const schluessel = String(problem.path[0] ?? 'formular') if (!felder[schluessel]) felder[schluessel] = problem.message } return felder } async function administratorSein(weiter: string) { const benutzer = await erfordereBenutzer(weiter) if (!darfBenutzerVerwalten(benutzer.rolle)) { throw new Error('Die Benutzerverwaltung ist Administratoren vorbehalten.') } await erfordereFrischeAnmeldung(benutzer, weiter) return benutzer } async function anzahlAktiverAdmins(): Promise { return db.user.count({ where: { rolle: 'ADMIN', aktiv: true, ...nichtGeloescht } }) } // ----------------------------------------------------------------------------- // Benutzer // ----------------------------------------------------------------------------- export async function benutzerAnlegen( _zustand: Aktionszustand, formular: FormData, ): Promise { const gelesen = benutzerAnlegenSchema.safeParse({ name: formular.get('name'), email: formular.get('email'), rolle: formular.get('rolle'), passwort: formular.get('passwort'), }) if (!gelesen.success) { return { fehler: 'Bitte prüfe deine Eingaben.', felder: felderAusZod(gelesen.error.issues) } } const admin = await administratorSein('/einstellungen/benutzer') const vorhanden = await db.user.findUnique({ where: { email: gelesen.data.email } }) if (vorhanden) { return { fehler: 'Unter dieser E-Mail-Adresse gibt es schon ein Konto.', felder: { email: 'Bereits vergeben.' }, } } const neu = await db.user.create({ data: { name: gelesen.data.name, email: gelesen.data.email, rolle: gelesen.data.rolle, passwortHash: await passwortHashen(gelesen.data.passwort), }, }) await protokollieren({ aktion: 'ERSTELLT', entitaet: 'User', entitaetId: neu.id, bezeichnung: `${neu.name} <${neu.email}> als ${ROLLEN_BESCHRIFTUNG[neu.rolle]}`, userId: admin.id, userEmail: admin.email, userName: admin.name, }) revalidatePath('/einstellungen/benutzer') return { erfolg: `${neu.name} ist angelegt. Beim ersten Anmelden muss der zweite Faktor eingerichtet ` + `werden – ohne ihn kommt niemand an Fachdaten.`, } } export async function rolleAendern( _zustand: Aktionszustand, formular: FormData, ): Promise { const gelesen = rolleAendernSchema.safeParse({ userId: formular.get('userId'), rolle: formular.get('rolle'), }) if (!gelesen.success) { return { fehler: 'Bitte prüfe deine Eingaben.', felder: felderAusZod(gelesen.error.issues) } } const admin = await administratorSein('/einstellungen/benutzer') const ziel = await db.user.findFirst({ where: { id: gelesen.data.userId, ...nichtGeloescht } }) if (!ziel) return { fehler: 'Diesen Benutzer gibt es nicht (mehr).' } if (ziel.rolle === gelesen.data.rolle) return { erfolg: 'Die Rolle war bereits so gesetzt.' } const schutz = letzterAdminGeschuetzt({ zielRolleVorher: ziel.rolle, zielRolleNachher: gelesen.data.rolle, zielBleibtAktiv: ziel.aktiv, anzahlAktiverAdmins: await anzahlAktiverAdmins(), }) if (schutz) return { fehler: schutz } // Eine Rollenaenderung beendet alle laufenden Sitzungen des Benutzers - // sonst behielte er bis zum Ablauf des Tokens seine alten Rechte. await db.user.update({ where: { id: ziel.id }, data: { rolle: gelesen.data.rolle, sitzungsGeneration: { increment: 1 } }, }) await protokollieren({ aktion: 'ROLLE_GEAENDERT', entitaet: 'User', entitaetId: ziel.id, bezeichnung: `${ziel.name}: ${ROLLEN_BESCHRIFTUNG[ziel.rolle]} → ${ROLLEN_BESCHRIFTUNG[gelesen.data.rolle]}`, userId: admin.id, userEmail: admin.email, userName: admin.name, aenderung: { rolle: { alt: ziel.rolle, neu: gelesen.data.rolle } }, }) revalidatePath('/einstellungen/benutzer') return { erfolg: `${ziel.name} ist jetzt ${ROLLEN_BESCHRIFTUNG[gelesen.data.rolle]}. Bestehende Anmeldungen wurden beendet.`, } } export async function benutzerAktivSetzen( _zustand: Aktionszustand, formular: FormData, ): Promise { const gelesen = benutzerAktivSchema.safeParse({ userId: formular.get('userId'), aktiv: formular.get('aktiv') === 'ja', }) if (!gelesen.success) return { fehler: 'Bitte prüfe deine Eingaben.' } const admin = await administratorSein('/einstellungen/benutzer') const ziel = await db.user.findFirst({ where: { id: gelesen.data.userId, ...nichtGeloescht } }) if (!ziel) return { fehler: 'Diesen Benutzer gibt es nicht (mehr).' } if (ziel.id === admin.id && !gelesen.data.aktiv) { return { fehler: 'Du kannst dich nicht selbst abschalten.' } } const schutz = letzterAdminGeschuetzt({ zielRolleVorher: ziel.rolle, zielRolleNachher: ziel.rolle, zielBleibtAktiv: gelesen.data.aktiv, anzahlAktiverAdmins: await anzahlAktiverAdmins(), }) if (schutz) return { fehler: schutz } await db.user.update({ where: { id: ziel.id }, data: { aktiv: gelesen.data.aktiv, // Wer abgeschaltet wird, ist sofort abgemeldet. sitzungsGeneration: gelesen.data.aktiv ? undefined : { increment: 1 }, }, }) await protokollieren({ aktion: 'GEAENDERT', entitaet: 'User', entitaetId: ziel.id, bezeichnung: `${ziel.name} ${gelesen.data.aktiv ? 'wieder aktiviert' : 'deaktiviert'}`, userId: admin.id, userEmail: admin.email, userName: admin.name, aenderung: { aktiv: { alt: ziel.aktiv, neu: gelesen.data.aktiv } }, }) revalidatePath('/einstellungen/benutzer') return { erfolg: gelesen.data.aktiv ? `${ziel.name} kann sich wieder anmelden.` : `${ziel.name} ist deaktiviert und sofort abgemeldet.`, } } export async function sitzungenBeenden( _zustand: Aktionszustand, formular: FormData, ): Promise { const gelesen = sitzungenBeendenSchema.safeParse({ userId: formular.get('userId') }) if (!gelesen.success) return { fehler: 'Bitte prüfe deine Eingaben.' } const admin = await administratorSein('/einstellungen/benutzer') const ziel = await db.user.findFirst({ where: { id: gelesen.data.userId, ...nichtGeloescht } }) if (!ziel) return { fehler: 'Diesen Benutzer gibt es nicht (mehr).' } await db.user.update({ where: { id: ziel.id }, data: { sitzungsGeneration: { increment: 1 } }, }) await protokollieren({ aktion: 'GEAENDERT', entitaet: 'User', entitaetId: ziel.id, bezeichnung: `Alle Sitzungen von ${ziel.name} beendet`, userId: admin.id, userEmail: admin.email, userName: admin.name, aenderung: { sitzungsGeneration: { alt: ziel.sitzungsGeneration, neu: ziel.sitzungsGeneration + 1 }, }, }) revalidatePath('/einstellungen/benutzer') return { erfolg: `Alle Geräte von ${ziel.name} sind abgemeldet.` } } // ----------------------------------------------------------------------------- // Freigaben je betreuter Person // ----------------------------------------------------------------------------- export async function freigabenSpeichern( _zustand: Aktionszustand, formular: FormData, ): Promise { const gelesen = freigabenSchema.safeParse({ userId: formular.get('userId'), betreuteId: formular.get('betreuteId'), rolle: formular.get('rolle'), freigegebeneModule: formular.getAll('freigegebeneModule').map(String), }) if (!gelesen.success) { return { fehler: 'Bitte prüfe deine Eingaben.', felder: felderAusZod(gelesen.error.issues) } } const { userId, betreuteId, rolle } = gelesen.data const { benutzer, zugang, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben') if (!darfFreigabenVergeben(zugang)) { return { fehler: 'Freigaben vergibt nur der Betreuer dieser Person oder ein Administrator.' } } await erfordereFrischeAnmeldung(benutzer, `/einstellungen/freigaben?betreute=${betreuteId}`) const ziel = await db.user.findFirst({ where: { id: userId, ...nichtGeloescht } }) if (!ziel) return { fehler: 'Diesen Benutzer gibt es nicht (mehr).' } const freigaben = freigabenNormalisieren(rolle, gelesen.data.freigegebeneModule as Modul[]) const vorher = await db.betreuungsZugang.findUnique({ where: { userId_betreuteId: { userId, betreuteId } }, select: { rolle: true, freigegebeneModule: true }, }) const aenderung = freigabeAenderung( vorher ?? { rolle: 'MITLESER', freigegebeneModule: [] }, { rolle, freigegebeneModule: freigaben }, ) await db.betreuungsZugang.upsert({ where: { userId_betreuteId: { userId, betreuteId } }, create: { userId, betreuteId, rolle, freigegebeneModule: freigaben }, update: { rolle, freigegebeneModule: freigaben }, }) // Wer Rechte verliert, soll sie nicht bis zum Ablauf seines Tokens // weiterbenutzen koennen. if (aenderung.entfernt.length > 0 || aenderung.rolleGeaendert) { await db.user.update({ where: { id: userId }, data: { sitzungsGeneration: { increment: 1 } } }) } const beschreibung = [ aenderung.rolleGeaendert ? `Rolle ${ROLLEN_BESCHRIFTUNG[aenderung.rolleVorher]} → ${ROLLEN_BESCHRIFTUNG[aenderung.rolleNachher]}` : null, aenderung.hinzugefuegt.length > 0 ? `freigegeben: ${aenderung.hinzugefuegt.map((m) => MODUL_BESCHRIFTUNG[m]).join(', ')}` : null, aenderung.entfernt.length > 0 ? `entzogen: ${aenderung.entfernt.map((m) => MODUL_BESCHRIFTUNG[m]).join(', ')}` : null, ] .filter(Boolean) .join('; ') await protokollieren({ aktion: aenderung.rolleGeaendert ? 'ROLLE_GEAENDERT' : 'FREIGABE_GEAENDERT', entitaet: 'BetreuungsZugang', entitaetId: userId, bezeichnung: `${ziel.name} für ${betreute.vorname} ${betreute.nachname}: ${beschreibung || 'unverändert'}`, betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, aenderung: { rolle: { alt: aenderung.rolleVorher, neu: aenderung.rolleNachher }, freigegebeneModule: { alt: vorher?.freigegebeneModule ?? [], neu: freigaben }, }, }) revalidatePath('/einstellungen/freigaben') revalidatePath('/', 'layout') const warnung = aenderung.sensibelHinzugefuegt.length > 0 ? `Achtung: ${aenderung.sensibelHinzugefuegt.map((m) => MODUL_BESCHRIFTUNG[m]).join(' und ')} ` + `enthalten besonders schützenswerte Daten. ${ziel.name} kann sie ab sofort lesen.` : undefined return { erfolg: beschreibung ? `Gespeichert: ${beschreibung}.` : 'Es gab nichts zu ändern.', warnung } } export async function zugangEntziehen( _zustand: Aktionszustand, formular: FormData, ): Promise { const userId = String(formular.get('userId') ?? '') const betreuteId = String(formular.get('betreuteId') ?? '') const { benutzer, zugang, betreute } = await erfordereZugang(betreuteId, 'STAMMDATEN', 'schreiben') if (!darfFreigabenVergeben(zugang)) { return { fehler: 'Zugänge entzieht nur der Betreuer dieser Person oder ein Administrator.' } } await erfordereFrischeAnmeldung(benutzer, `/einstellungen/freigaben?betreute=${betreuteId}`) if (userId === benutzer.id) { return { fehler: 'Du kannst dir den eigenen Zugang nicht entziehen.' } } const ziel = await db.user.findFirst({ where: { id: userId }, select: { name: true } }) await db.betreuungsZugang.deleteMany({ where: { userId, betreuteId } }) await db.user.update({ where: { id: userId }, data: { sitzungsGeneration: { increment: 1 } } }) await protokollieren({ aktion: 'FREIGABE_GEAENDERT', entitaet: 'BetreuungsZugang', entitaetId: userId, bezeichnung: `Zugang von ${ziel?.name ?? userId} zu ${betreute.vorname} ${betreute.nachname} entzogen`, betreuteId, userId: benutzer.id, userEmail: benutzer.email, userName: benutzer.name, }) revalidatePath('/einstellungen/freigaben') revalidatePath('/', 'layout') return { erfolg: `${ziel?.name ?? 'Der Benutzer'} hat keinen Zugriff mehr auf diese Person.` } }