Files
betreuungs-cockpit/src/lib/auth/rollen.ts
T
Florian Wilde bd1c7b801f feat: Anmeldung mit Pflicht-2FA, Rollen und Audit-Log
Auth.js mit Zugangsdaten-Provider: E-Mail, Passwort und TOTP-Code werden
in einem Formular abgefragt. Ein zweistufiger Ablauf muesste zwischen den
Schritten einen Zwischenzustand halten - genau das ist die uebliche
Schwachstelle, und am Handy ist ein Formular ohnehin schneller.

Anmeldung:
- Sperre wird VOR dem Passwortvergleich geprueft, damit ein gesperrter
  Zugang nicht einmal Argon2-Rechenzeit kostet.
- Bei unbekannter Adresse wird gegen einen Dummy-Hash gerechnet und die
  Antwort auf eine Mindestdauer gestreckt. Sonst verraet die Antwortzeit,
  welche Adressen existieren.
- Wiederherstellungscodes werden nach Gebrauch entfernt.
- Veraltete Argon2-Parameter werden beim erfolgreichen Login nachgezogen.

Rate-Limiting in der Datenbank statt Redis - ein Server, wenige Benutzer,
kein Grund fuer eine weitere Komponente. Gezaehlt wird nach E-Mail und
nach Anschluss, damit weder viele Anschluesse noch viele Adressen helfen.

Zugriffsschutz (sitzung.ts):
- Die Sitzung allein entscheidet nichts. Bei jedem Aufruf wird der
  Benutzer frisch geladen und die Sitzungsgeneration verglichen - so
  lassen sich alle Anmeldungen auf einen Schlag beenden.
- erfordereZugang() prueft immer gegen die betreuteId, nie nur im UI.
- Erneute Bestaetigung vor sensiblen Aktionen ueber ein signiertes,
  kurzlebiges Cookie statt ueber Serverzustand.

Audit-Log: aenderungErmitteln() protokolliert nur die tatsaechlich
geaenderten Felder; Passwoerter und TOTP-Geheimnisse werden ersetzt, nie
gespeichert. Ein Fehler im Protokoll bricht die Fachaktion nicht ab -
ausser die Aktion laeuft in einer Transaktion, dann faellt beides.

CSP mit Nonce pro Anfrage in der Middleware: keine Fremdquellen, kein
CDN, keine Fonts von aussen. Fachseiten werden nicht zwischengespeichert.

Etappe: 1
Phase: Fundament
2026-08-22 01:46:20 +02:00

90 lines
2.5 KiB
TypeScript

import type { Modul, Rolle } from '@prisma/client'
/**
* Rollen und Modulfreigaben - ohne "server-only", weil die Beschriftungen
* auch im Browser gebraucht werden.
*
* Wichtig: Diese Datei entscheidet nichts allein. Der eigentliche
* Zugriffsschutz sitzt in src/lib/auth/sitzung.ts und prueft serverseitig
* gegen die betreuteId. Hier stehen nur die Regeln, damit sie an einem Ort
* nachlesbar sind.
*/
export const ROLLEN_BESCHRIFTUNG: Record<Rolle, string> = {
BETREUER: 'Betreuer',
MITLESER: 'Mitleser',
ADMIN: 'Administrator',
}
export const ROLLEN_ERKLAERUNG: Record<Rolle, string> = {
BETREUER: 'Darf alles sehen und ändern, was zu den zugeordneten Betreuten gehört.',
MITLESER: 'Darf nur lesen, und nur die ausdrücklich freigegebenen Bereiche.',
ADMIN: 'Verwaltet Benutzer und Systemeinstellungen.',
}
export const MODUL_BESCHRIFTUNG: Record<Modul, string> = {
STAMMDATEN: 'Stammdaten',
VORGAENGE: 'Vorgänge',
FRISTEN: 'Fristen',
DOKUMENTE: 'Dokumente',
FINANZEN: 'Vermögen & Kassenbuch',
SOZIALHILFE: 'Sozialhilfe',
IMMOBILIE: 'Immobilie',
VERTRAEGE: 'Versicherungen & Verträge',
GESUNDHEIT: 'Heim & Pflege',
KONTAKTE: 'Kontakte',
BERICHTE: 'Berichte',
}
export const ALLE_MODULE: Modul[] = [
'STAMMDATEN',
'VORGAENGE',
'FRISTEN',
'DOKUMENTE',
'FINANZEN',
'SOZIALHILFE',
'IMMOBILIE',
'VERTRAEGE',
'GESUNDHEIT',
'KONTAKTE',
'BERICHTE',
]
/**
* Module mit besonders sensiblen Inhalten. Fuer sie ist eine Freigabe an
* Mitleser eine bewusste Entscheidung - die Oberflaeche weist darauf hin.
*/
export const BESONDERS_SENSIBEL: Modul[] = ['GESUNDHEIT', 'FINANZEN', 'SOZIALHILFE']
export type Zugriffsart = 'lesen' | 'schreiben'
export type Zugang = {
rolle: Rolle
freigegebeneModule: Modul[]
}
/**
* Die eine Regel, nach der entschieden wird:
*
* - BETREUER darf alles, lesen wie schreiben.
* - ADMIN darf alles (er verwaltet die Anwendung).
* - MITLESER darf ausschliesslich lesen, und nur freigegebene Module.
*/
export function darf(zugang: Zugang | null, modul: Modul, art: Zugriffsart = 'lesen'): boolean {
if (!zugang) return false
if (zugang.rolle === 'BETREUER' || zugang.rolle === 'ADMIN') return true
if (zugang.rolle === 'MITLESER') {
if (art === 'schreiben') return false
return zugang.freigegebeneModule.includes(modul)
}
return false
}
/** Kurzform fuer die Oberflaeche. */
export function darfSchreiben(zugang: Zugang | null, modul: Modul): boolean {
return darf(zugang, modul, 'schreiben')
}