Zeilenenden der 24 verbliebenen Dateien normalisieren

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
This commit is contained in:
Florian Wilde
2026-08-23 23:37:02 +02:00
parent 6a0db51e3b
commit 1ed19eb76e
24 changed files with 7448 additions and 7448 deletions
+385 -385
View File
@@ -1,385 +1,385 @@
'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.` }
}
'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.` }
}