Ausschliesslich Zeilenenden. Nachgewiesen mit "git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich kein Zeichen Inhalt geaendert. Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit echten Aenderungen zu verstecken, macht die Historie an dieser Stelle unlesbar. So laesst er sich beim Durchsehen ueberspringen. Refs #26
386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
'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<string, string>
|
||
erfolg?: string
|
||
warnung?: string
|
||
}
|
||
|
||
function felderAusZod(fehler: { path: (string | number)[]; message: string }[]): Record<string, string> {
|
||
const felder: Record<string, string> = {}
|
||
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<number> {
|
||
return db.user.count({ where: { rolle: 'ADMIN', aktiv: true, ...nichtGeloescht } })
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// Benutzer
|
||
// -----------------------------------------------------------------------------
|
||
|
||
export async function benutzerAnlegen(
|
||
_zustand: Aktionszustand,
|
||
formular: FormData,
|
||
): Promise<Aktionszustand> {
|
||
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<Aktionszustand> {
|
||
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<Aktionszustand> {
|
||
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<Aktionszustand> {
|
||
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<Aktionszustand> {
|
||
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<Aktionszustand> {
|
||
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.` }
|
||
}
|