diff --git a/CLAUDE.md b/CLAUDE.md index 9ef55fe..d8f84c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,11 @@ Lokaler Seed-Admin (AppHost-Parameter, siehe `src/FamilyPlanner.AppHost/appsetti * Commits: Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`, `refactor:`, `ci:`). * Alle Zeiten in UTC speichern (`DateTimeOffset`/UTC), Anzeige in Europe/Berlin. `TimeProvider` injizieren statt `DateTime.UtcNow`. * Neue Entitäten erben von `AuditableEntity`; löschbare implementieren `ISoftDeletable`; Einkauf/Aufgaben/Termine bekommen `RowVersion`. -* Berechtigungen immer serverseitig über Policies (`RequireAuthorization(Policies.X)`), nie nur im UI. Zu jeder Policy Unit-Tests. +* Berechtigungen immer serverseitig, nie nur im UI: fachliche Endpunkte mit `.RequirePermission(Permission.X)` (Matrix in + `Domain/Authorization/Permissions.cs`, ADR-0008). Bei `AccessLevel.Restricted` muss der Slice objektbezogen prüfen + (z. B. „nur eigene“) – dafür Tests schreiben. Matrix-Änderungen in `PermissionMatrixTests` nachziehen. +* Administrative Aktionen schreiben ins Audit-Log (`IAuditLogger`), ohne Passwörter/PINs. +* Request-DTOs mit Validatoren in `FamilyPlanner.Shared` (FluentValidation) – Api validiert per `.Validate()`, der Client nutzt dieselben Validatoren. * Schreibende Requests vom Client tragen `X-Requested-With: FamilyPlanner` (wird vom `HttpClient`-Handler im Client gesetzt, siehe ADR-0006). * Validierung mit FluentValidation im jeweiligen Slice. * Pakete nur über `Directory.Packages.props` (keine Versionen in `.csproj`), nur stabile Versionen. diff --git a/README.md b/README.md index 94e3577..5e74deb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Digitaler Familienplaner als Progressive Web App: Einkaufsliste, Essenplan, Aufgaben & Ämtchen, Familienkalender (inkl. Abo für iPhone/Outlook) und Chat – für Smartphone, Tablet und Desktop. -> **Status:** Phase 1 (Fundament) ist umgesetzt: Login, Rollen, Navigation, PWA-Grundgerüst, Aspire- und Docker-Betrieb. +> **Status:** Phase 1 (Fundament) und Phase 2 (Rollen & Admin) sind umgesetzt: Login, Rollenmatrix, Benutzerverwaltung, +> Familieneinstellungen, Audit-Log, Navigation, PWA-Grundgerüst, Aspire- und Docker-Betrieb. > Die Fachfunktionen folgen phasenweise, siehe [`docs/ROADMAP.md`](docs/ROADMAP.md). * Architektur: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) @@ -66,7 +67,10 @@ einem eigenen Reverse Proxy mit TLS. | `REGISTRY`, `TAG` | optionales Image-Präfix und Tag | Weitere Einstellungen (alle Dienste) folgen der ASP.NET-Core-Konvention `Abschnitt__Schlüssel`, z. B. -`RateLimiting__LoginPermitLimit=10` (Api). +`RateLimiting__LoginPermitLimit=10` (Api) oder `Features__PinLogin=true` (Api, schaltet den vorbereiteten PIN-Login für Kinder frei). + +Benutzer, Familieneinstellungen (Familienname, Zeitzone, Essenplan, Mahlzeiten, Aufgaben-Kategorien, Punktesystem, Gast-Rechte) +und das Protokoll administrativer Aktionen verwaltet der Administrator in der App unter **Verwaltung**. ### Update diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 31c55fc..c9ded83 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -79,38 +79,47 @@ Alle Zeitpunkte werden als UTC (`DateTimeOffset`/`DateTime` UTC) gespeichert und | Bereich | Entitäten | |---|---| -| Identity | `AppUser` (DisplayName, Color, AvatarUrl, MustChangePassword, IsActive, ExpiresAt, PinHash), `AppRole` (Admin, Familie, Kind, Gast) | -| Einstellungen | `FamilySettings` (Name, TimeZone, MealPlanDays, MealTypes, PointsEnabled, ChildTasksNeedApproval, GuestCanEditShopping, GuestChatEnabled) | +| Identity ✅ | `AppUser` (DisplayName, Color, AvatarUrl, MustChangePassword, IsActive, ExpiresAt, PinHash, LastLoginAt, Soft Delete), `AppRole` (Admin, Familie, Kind, Gast), `UserAvatar` (Bild ≤ 1 MB) | +| Einstellungen ✅ | `FamilySettings` (FamilyName, TimeZoneId, MealPlanDays, MealTypes, TaskCategories, ChorePointsEnabled, ChildTasksRequireApproval, GuestsCanEditShoppingList, GuestChatEnabled) | | Einkauf | `ShoppingList` (Name, IsDefault), `ShoppingItem` (Text, Quantity, Category, IsDone, DoneAt, SortOrder), `ShoppingSuggestion` (Text, UseCount) | | Essen | `MealPlanEntry` (Date, MealType, Dish, Note, RecipeUrl, Ingredients), `Dish` (Name, Ingredients – für Autovervollständigung), `MealWish` (von Kindern) | | Aufgaben | `TodoTask` (Title, Notes, DueAt, HasTime, Priority, Status, Category, IsPrivate, IsChore, Points, NeedsApproval, RecurrenceRule, SeriesId), `TaskAssignment`, `TaskChecklistItem`, `TaskCompletion` (wer/wann/genehmigt), `TaskCategory`, `PointsLedger` | | Kalender | `Calendar` (Name, Color), `CalendarPermission` (Rolle/Benutzer → Read/Write), `CalendarEvent` (Title, StartUtc, EndUtc, AllDay, Location, Notes, RRule, ExDates, Uid), `EventPersonTag`, `EventException` (Overrides), `IcsFeedToken` (Token-Hash, Scope, Owner, RevokedAt) | | Chat | `ChatChannel` (Family / Direct), `ChatMember` (LastReadAt), `ChatMessage` (Text, Attachment), `ChatReaction` | -| Betrieb | `AuditLogEntry` (Who, Action, Target, Details, At), `PushSubscription` (vorbereitet) | +| Betrieb | `AuditLogEntry` ✅ (ActorName, Action, TargetType/Id/Name, Details, CreatedAt), `PushSubscription` (vorbereitet) | ## 4. Rollen- und Berechtigungsmodell Rollen: **Admin, Familie, Kind, Gast** (Konstanten in `FamilyPlanner.Domain.Roles`). -Berechtigungen werden als **Policies** formuliert (`FamilyPlanner.Domain.Authorization.Policies`) und -serverseitig per `RequireAuthorization(policy)` erzwungen. Feingranulare Regeln, die vom Objekt abhängen -(„nur eigene Einträge löschen“, „nur eigene Termine“), laufen über ressourcenbasierte Autorisierung -(`IAuthorizationHandler` mit Ressource) bzw. reine Domänenfunktionen, die unit-getestet sind. +Die Rollenmatrix der Anforderungen ist als reine Funktion `PermissionMatrix` im Domain-Projekt abgebildet (ADR-0008): +je `Permission` und Rolle eine Zugriffsstufe **Full (F)**, **Restricted (R)** = erreichbar, aber objektbezogene Prüfung im Slice, +oder **None (–)**. Die Api registriert je Permission eine Policy `Permission:`; Endpunkte nutzen `.RequirePermission(…)`. -| Policy | Rollen | -|---|---| -| `Authenticated` | alle aktiven Benutzer | -| `FamilyMember` | Admin, Familie, Kind | -| `Parent` (Familie verwalten) | Admin, Familie | -| `Admin` | Admin | -| `ShoppingWrite` | Admin, Familie, Kind (+ Gast, falls in Einstellungen freigegeben) | -| `MealPlanEdit` | Admin, Familie | -| `MealWish` | Kind | -| `TaskAssign` | Admin, Familie | -| `ChatFamily` | Admin, Familie, Kind (+ Gast, falls freigegeben) | -| `DirectMessages` | Admin, Familie, Kind | +| Permission | Admin | Familie | Kind | Gast | Bedeutung von R | +|---|---|---|---|---|---| +| `DashboardView` | F | F | F | R | Gast: eingeschränkte Kacheln | +| `ShoppingRead` | F | F | F | F | | +| `ShoppingWrite` | F | F | F | – ¹ | | +| `ShoppingDelete` | F | F | R | – | nur eigene Einträge | +| `MealPlanRead` | F | F | F | F | | +| `MealPlanEdit` | F | F | R | – | nur Wünsche vorschlagen | +| `TasksRead` | F | F | R | R | Kind: eigene + Familie; Gast: nur zugewiesene | +| `TasksEdit` | F | F | R | – | nur eigene, private Aufgaben | +| `TasksAssign` | F | F | – | – | | +| `TasksCompleteAssigned` | F | F | F | F | | +| `CalendarRead` | F | F | F | R | nur freigegebene Kalender | +| `EventsEdit` | F | F | R | – | nur eigene Termine | +| `FamilyChat` | F | F | F | – ² | | +| `DirectMessages` | F | F | R | – | nur an Familienmitglieder | +| `UserManagement`, `CalendarManagement`, `SettingsManagement`, `AuditLogView` | F | – | – | – | | -Das UI blendet Funktionen anhand derselben Policy-Namen aus (`AuthorizeView Policy=…`), ist aber nie die einzige Sperre. -Ein Benutzer mit `MustChangePassword = true` erhält nur Zugriff auf `/api/auth/*` (Policy `PasswordChanged` als Fallback-Policy). +¹ `F`, wenn „Gäste dürfen die Einkaufsliste ergänzen/abhaken“ aktiviert ist. ² `F`, wenn „Gäste dürfen den Familienchat nutzen“ aktiviert ist. + +Zusätzlich gibt es grobe Rollen-Policies (`Admin`, `Parent`, `FamilyMember`, `AnyRole`). Alle Policies – auch die +**Fallback-Policy** für jeden Endpunkt ohne eigene Angabe – verlangen: angemeldet, Konto nicht abgelaufen (Claim `fp:expires_at`) +und kein ausstehender Passwortwechsel. Ein Benutzer mit `MustChangePassword = true` erreicht nur `/api/auth/*`. + +Das UI bekommt die wirksamen Berechtigungen über `/api/auth/me` und blendet anhand derselben Policy-Namen aus; es ist nie die einzige Sperre. ## 5. Authentifizierung @@ -123,13 +132,14 @@ Ein Benutzer mit `MustChangePassword = true` erhält nur Zugriff auf `/api/auth/ * Seed-Admin aus Konfiguration (`Seed:AdminUserName`, `Seed:AdminPassword`, `Seed:AdminEmail`), `MustChangePassword = true`. * Kalender-Abos (ICS) verwenden geheime Tokens in der URL (nur Hash in DB), da Kalender-Clients keine Cookies können (ADR-0005). -## 6. API-Übersicht (Ziel) +## 6. API-Übersicht (Ziel, ✅ = umgesetzt) | Bereich | Endpunkte | |---|---| -| Auth | `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`, `POST /api/auth/change-password` | -| Admin – Benutzer | `GET/POST /api/admin/users`, `PUT/DELETE /api/admin/users/{id}`, `POST …/{id}/reset-password`, `POST …/{id}/unlock` | -| Admin – Einstellungen | `GET/PUT /api/admin/settings`, `GET /api/admin/audit` | +| Auth ✅ | `POST /api/auth/login`, `POST /api/auth/pin-login` (Feature-Flag), `POST /api/auth/logout`, `GET /api/auth/me`, `POST /api/auth/change-password` | +| Admin – Benutzer ✅ | `GET/POST /api/admin/users`, `GET/PUT/DELETE /api/admin/users/{id}`, `POST …/{id}/reset-password`, `POST …/{id}/unlock`, `PUT …/{id}/pin`, `PUT/DELETE …/{id}/avatar` | +| Admin – Einstellungen ✅ | `GET /api/settings` (alle), `PUT /api/admin/settings`, `GET /api/admin/audit?page=&pageSize=` | +| Benutzerverzeichnis ✅ | `GET /api/users`, `GET /api/users/{id}/avatar` | | Einkauf | `GET/POST /api/shopping/lists`, `GET/POST /api/shopping/lists/{id}/items`, `PATCH/DELETE /api/shopping/items/{id}`, `POST /api/shopping/lists/{id}/clear-done`, `GET /api/shopping/suggestions?q=` | | Essen | `GET /api/meals?from=&to=`, `PUT /api/meals/{date}/{mealType}`, `POST /api/meals/{id}/move`, `POST /api/meals/{id}/to-shopping`, `GET/POST /api/meals/wishes` | | Aufgaben | `GET/POST /api/tasks`, `PUT/DELETE /api/tasks/{id}`, `POST /api/tasks/{id}/complete`, `POST /api/tasks/{id}/approve`, `GET /api/tasks/points` | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c2edf79..f757757 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -20,13 +20,20 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te - [x] CI-Pipeline (GitHub Actions) ## Phase 2 – Rollen & Admin -- [ ] Policies vollständig (siehe ARCHITECTURE §4) inkl. Unit-Tests je Policy -- [ ] Benutzerverwaltung: anlegen, bearbeiten, deaktivieren, löschen, Rolle, Farbe, Avatar -- [ ] Passwort-Reset durch Admin, Konto entsperren, Gast-Ablaufdatum -- [ ] Schutz „letzter Admin“ -- [ ] Allgemeine Einstellungen (Familienname, Zeitzone, Essenplan-Zeitraum, Mahlzeiten-Typen, Aufgaben-Kategorien, Punktesystem, Eltern-Bestätigung, Gast-Rechte) -- [ ] Audit-Log für administrative Aktionen + Ansicht -- [ ] Vorbereitung PIN-Login für Kinder (Datenmodell + Endpoint hinter Feature-Flag) +- [x] Rollenmatrix vollständig als `PermissionMatrix` (Full/Restricted/None) + Permission-Policies, Unit-Test je Matrixzelle (ADR-0008) +- [x] Konfigurierbare Ausnahmen: Gäste dürfen Einkaufsliste bearbeiten / Familienchat nutzen (wirken sofort auf Policies und `/me`) +- [x] Fallback-Policy prüft zusätzlich das Ablaufdatum (Claim) – abgelaufene Gäste verlieren sofort den Zugriff +- [x] Benutzerverwaltung (Api + UI): anlegen, bearbeiten, deaktivieren, löschen (Soft Delete mit Anonymisierung), Rolle, Anzeigename, Farbe +- [x] Profilbild-Upload (PNG/JPEG/WebP ≤ 1 MB, Signaturprüfung, in der DB, ADR-0009) +- [x] Passwort-Reset durch Admin (erzwungene Änderung), Konto entsperren, Gast-Ablaufdatum (Login danach verweigert) +- [x] Schutz „letzter aktiver Admin“ (löschen/deaktivieren/herabstufen/ablaufen → 409) +- [x] Sitzungen werden bei Rollen-/Status-Änderung über den Security Stamp ungültig (≤ 5 min) +- [x] Allgemeine Einstellungen als Entität + Admin-Seite (Familienname, Zeitzone, Essenplan-Zeitraum, Mahlzeiten, Aufgaben-Kategorien, Punktesystem, Eltern-Bestätigung, Gast-Rechte) +- [x] Audit-Log für alle administrativen Aktionen (inkl. Änderungsdetails) + Admin-Ansicht mit Paging +- [x] Benutzerverzeichnis `/api/users` (Namen, Farben, Avatare für spätere Zuweisungen) +- [x] Validierung mit FluentValidation (gemeinsame Validatoren für Api und Client), Migration `Phase2AdminAndSettings` +- [x] PIN-Login für Kinder vorbereitet: PIN setzen/entfernen (nur Kinder), Endpoint hinter `Features:PinLogin` (ADR-0010) +- [x] Tests: Unit (Matrix, Policies, Last-Admin, Validatoren, Avatar-Prüfung), Integration (Admin-Endpunkte, 403 für Nicht-Admins, Gast-Ablauf, Lockout, Audit), bUnit (Benutzerliste, Dialog) ## Phase 3 – Einkaufsliste - [ ] Mehrere Listen, Standardliste @@ -93,6 +100,9 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te - [ ] README: Voraussetzungen, Entwicklung, Konfiguration, Deployment, Update, Backup/Restore, Kalender-Abos ## Backlog +- PIN-Login-Oberfläche für ein Familien-Tablet (Profilauswahl + Ziffernblock) – Api ist vorbereitet +- Selbst-Upload des eigenen Profilbilds, Profilseite für Anzeigename/Farbe +- Endgültige Löschung eines Benutzers (DSGVO) zusätzlich zum Soft Delete - Belohnungskatalog für Ämtchen-Punkte - Web-Push für Termine und Chat - CalDAV-Server bzw. Microsoft-Graph-Synchronisation diff --git a/docs/adr/0008-berechtigungsmatrix.md b/docs/adr/0008-berechtigungsmatrix.md new file mode 100644 index 0000000..2b36945 --- /dev/null +++ b/docs/adr/0008-berechtigungsmatrix.md @@ -0,0 +1,26 @@ +# ADR-0008: Berechtigungen als Rollenmatrix mit Zugriffsstufen + +* Status: akzeptiert +* Datum: 2026-10-09 + +## Kontext +Die Anforderungen enthalten eine Matrix aus Funktion × Rolle mit Abstufungen wie „nur eigene“, „nur zugewiesene“, +„nur Wünsche vorschlagen“ oder „nur wenn freigegeben“. Einige Ausnahmen sind konfigurierbar (Gäste dürfen Einkaufsliste bearbeiten, +Gäste im Familienchat). Rein rollenbasierte Policies (`RequireRole`) bilden das nicht ab und würden sich über Slices verstreuen. + +## Entscheidung +* `FamilyPlanner.Domain.Authorization.PermissionMatrix` ist die **einzige Quelle** der Matrix: je `Permission` und Rolle eine + `AccessLevel` (`None`, `Restricted`, `Full`). Konfigurierbare Ausnahmen kommen als `PermissionOptions` aus den Familieneinstellungen. +* Die Api registriert je `Permission` eine Policy `Permission:` (`Policies.For(permission)`). Ein `PermissionHandler` + wertet die Matrix mit den aktuellen Einstellungen aus (gecacht über `ISettingsProvider`, beim Speichern invalidiert). + Endpunkte nutzen `.RequirePermission(Permission.X)`. +* `Restricted` heißt: Der Endpunkt ist erreichbar, der Slice **muss** objektbezogen prüfen (z. B. Kind löscht nur eigene Einträge). + Diese Prüfungen entstehen mit den jeweiligen Fach-Slices (Phasen 3–8) und werden dort getestet. +* Jede Permission-Policy schließt die Fallback-Bedingungen ein (angemeldet, nicht abgelaufen, Passwortwechsel erledigt). +* Der Client erhält die **wirksamen** Berechtigungen über `GET /api/auth/me` (`CurrentUserDto.Permissions`) und nutzt sie nur zum + Ein-/Ausblenden (Claims `fp:permission`, dieselben Policy-Namen). +* Die rollenbasierten Policies `Admin`, `Parent`, `FamilyMember`, `AnyRole` bleiben für grobe Bereiche (z. B. Seite `/admin`). + +## Konsequenzen +* Eine Änderung der Matrix ist eine Zeile im Domain-Projekt; Tests (`PermissionMatrixTests`, `ApiPolicyTests`) prüfen jede Zelle. +* Änderungen an den Gast-Ausnahmen wirken sofort (Cache-Invalidierung im selben Prozess; bei mehreren Api-Instanzen nach max. 1 Minute). diff --git a/docs/adr/0009-profilbilder-in-der-datenbank.md b/docs/adr/0009-profilbilder-in-der-datenbank.md new file mode 100644 index 0000000..d454823 --- /dev/null +++ b/docs/adr/0009-profilbilder-in-der-datenbank.md @@ -0,0 +1,19 @@ +# ADR-0009: Profilbilder in der Datenbank + +* Status: akzeptiert +* Datum: 2026-10-09 + +## Kontext +Benutzer erhalten ein optionales Profilbild. Die Datenmenge ist klein (wenige Familienmitglieder), Backups sollen einfach bleiben +(ein SQL-Backup, siehe `deploy/scripts/backup.sh`), und es soll kein zusätzliches Volume nötig sein. + +## Entscheidung +* Bilder liegen in der Tabelle `UserAvatars` (1:1 zum Benutzer, `varbinary(max)`), höchstens **1 MB**. +* Erlaubt sind PNG, JPEG und WebP. Der Typ wird serverseitig anhand der **Datei-Signatur** (Magic Bytes) bestimmt – Dateiname und + Content-Type des Clients werden ignoriert; SVG ist bewusst ausgeschlossen (Skript-Risiko). +* Auslieferung über `GET /api/users/{id}/avatar` (nur für angemeldete Benutzer). Die URL enthält eine Version (`?v=…`), daher + `Cache-Control: private, max-age=604800`. +* Hochladen/Entfernen nur über die Admin-Api (`PUT/DELETE /api/admin/users/{id}/avatar`). Ein Selbst-Upload durch Benutzer kann später ergänzt werden. + +## Konsequenzen +Keine Dateisystem-Abhängigkeit, Bilder sind Teil des Datenbank-Backups. Für große Bildmengen (z. B. Chat-Anhänge, Phase 8) wird separat entschieden. diff --git a/docs/adr/0010-benutzerlebenszyklus.md b/docs/adr/0010-benutzerlebenszyklus.md new file mode 100644 index 0000000..7cb8f47 --- /dev/null +++ b/docs/adr/0010-benutzerlebenszyklus.md @@ -0,0 +1,38 @@ +# ADR-0010: Benutzerlebenszyklus – Deaktivieren, Ablauf, Löschen, PIN-Login + +* Status: akzeptiert +* Datum: 2026-10-09 + +## Entscheidung + +**Anmelden verweigern:** `AppSignInManager.CanSignInAsync` lehnt deaktivierte, gelöschte und abgelaufene Konten ab +(`AppUser.CanSignIn`). Der Login-Endpunkt meldet „deaktiviert oder abgelaufen“ (403) nur bei korrektem Passwort, sonst die +allgemeine Fehlermeldung (401) – der Kontostatus wird nicht ohne Passwort preisgegeben. + +**Laufende Sitzungen:** Bei sicherheitsrelevanten Änderungen (Rolle, aktiv/inaktiv, Ablaufdatum, Löschen, Passwort-Reset) wird der +Security Stamp erneuert; das Cookie wird spätestens nach 5 Minuten ungültig. Zusätzlich steht das Ablaufdatum eines Gastzugangs als +Claim `fp:expires_at` im Cookie und wird von der Fallback-Policy bei **jedem** Request geprüft – ein Gast verliert den Zugriff also +exakt zum Ablaufzeitpunkt, ohne Datenbankabfrage. + +**Löschen = Soft Delete mit Anonymisierung:** `AppUser` implementiert `ISoftDeletable` (globaler Query-Filter, auch für Identity). +Beim Löschen bleiben Id und Anzeigename (für Verlauf, Audit, spätere Zuordnungen), Benutzername wird zu `geloescht-` +(der Name ist danach wieder frei), E-Mail, Passwort-Hash, PIN und Profilbild werden entfernt. + +**Letzter Admin:** `AdminGuard.WouldRemoveLastActiveAdmin` verhindert, dass der letzte aktive (nicht abgelaufene, nicht gelöschte) +Administrator gelöscht, deaktiviert, mit Ablaufdatum versehen oder herabgestuft wird (409 Conflict). + +**Eine Rolle je Benutzer:** Die Admin-Oberfläche vergibt genau eine der vier Rollen; ein Rollenwechsel ersetzt die bisherige. + +**Passwort-Reset durch Admin:** Neues vorläufiges Passwort (über Reset-Token, damit Passwortregeln greifen), `MustChangePassword = true`, +Kontosperre wird aufgehoben. + +**PIN-Login (vorbereitet):** Admins können für Kinder eine 4–6-stellige PIN setzen (gehasht wie Passwörter). `POST /api/auth/pin-login` +existiert, ist aber nur aktiv, wenn `Features:PinLogin=true` gesetzt ist (Standard: aus → 404). Fehlversuche zählen zur normalen +Kontosperre. Die Tablet-Oberfläche (Profilauswahl + Ziffernblock) folgt mit dem Wand-Tablet-Modus (Backlog). + +**Audit-Log:** Alle administrativen Aktionen schreiben einen Eintrag (`AuditLog`-Tabelle) mit Anzeigename des Handelnden, Aktion, +Ziel und Änderungsdetails (alt → neu, keine Passwörter). Ansicht mit Paging im Adminbereich. + +## Konsequenzen +Gelöschte Benutzer bleiben als Datensatz erhalten (Verweise in Aufgaben/Terminen/Chat bleiben gültig). Eine endgültige Löschung +(DSGVO-Auskunft/Löschung) kann später als eigener Admin-Vorgang ergänzt werden.