Account-Verwaltung — Design Spec
Datum: 2026-04-13 Status: Umgesetzt (E-Mail-Handler ausstehend) Scope: Stufe 1 — Rudimentaere Account-Verwaltung
Ueberblick
Abschnitt betitelt „Ueberblick“Vier zusammenhaengende Features fuer die Account-Verwaltung:
- Eigenes Profil bearbeiten — Display-Name, Passwort aendern
- Password-Reset durch Admins — Code-basiert (6-stellig per E-Mail), kein Self-Service
- Team-Verwaltung — Accounts anlegen, deaktivieren/reaktivieren, Rollen
- Sessions verwalten — Aktive Sessions einsehen, einzeln oder alle terminieren
Berechtigungsmodell
Abschnitt betitelt „Berechtigungsmodell“| Aktion | agency_owner | agency_employee | tenant_admin | tenant_member |
|---|---|---|---|---|
| Eigenes Profil bearbeiten | ja | ja | ja | ja |
| Eigenes Passwort aendern | ja | ja | ja | ja |
| Eigene Sessions verwalten | ja | ja | ja | ja |
| Accounts anlegen | alle Rollen | — | tenant_member | — |
| Accounts deaktivieren/reaktivieren | alle ausser sich selbst | — | eigene tenant_member | — |
| Password-Reset ausloesen | alle ausser sich selbst | — | eigene tenant_member | — |
Datenmodell
Abschnitt betitelt „Datenmodell“Neue Migration (000004)
Abschnitt betitelt „Neue Migration (000004)“Tabelle password_reset (neu):
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | UUID PK | UUIDv7 |
| account_id | UUID FK → account | Betroffener Account |
| code | VARCHAR(6) | Kryptographisch zufaelliger 6-stelliger Code |
| initiated_by | UUID FK → account | Admin der den Reset ausgeloest hat |
| expires_at | TIMESTAMPTZ | 30 Minuten Gueltigkeit |
| verified_at | TIMESTAMPTZ (nullable) | Zeitpunkt der Code-Verifikation |
| used_at | TIMESTAMPTZ (nullable) | Zeitpunkt der Passwort-Aenderung |
| Audit-Felder | created_at, created_by, row_version, row_period |
Tabelle account — neue Spalte:
localeVARCHAR(5) DEFAULT ‘de’ NOT NULL — Bevorzugte CMS-Sprache (de/en), vom User aenderbar
Bestehende Tabellen — keine strukturellen Aenderungen:
account.active— existiert bereits fuer Deaktivierungsession.terminated_at— existiert bereits fuer Session-Terminierung
Neue E-Mail-Templates (Seed in Migration):
password_reset_code— Reset-Code an betroffenen Accountpassword_changed— Benachrichtigung nach Passwort-Aenderungaccount_deactivated— Benachrichtigung bei Deaktivierungnew_device_login— Benachrichtigung bei Login von unbekanntem Geraet
Neue Uebersetzungen (DE + EN Seeds fuer alle Templates).
Feature-Package account (Self-Service)
Abschnitt betitelt „Feature-Package account (Self-Service)“UseCases
Abschnitt betitelt „UseCases“- UpdateProfile(ctx, accountID, displayName, locale) — eigenen Display-Name und Sprache aendern
- ChangePassword(ctx, accountID, oldPassword, newPassword) — eigenes Passwort aendern (altes Passwort als Bestaetigung), loest
password_changedE-Mail aus - ListSessions(ctx, accountID) — eigene aktive Sessions mit Geraet, IP, Geolocation, letzter Aktivitaet, erstellt-am
- TerminateSession(ctx, accountID, sessionID) — einzelne eigene Session beenden
- TerminateOtherSessions(ctx, accountID, currentSessionID) — alle Sessions ausser der aktuellen beenden
Repositories
Abschnitt betitelt „Repositories“AccountRepository— FindByID, UpdateProfile (displayName + locale), UpdatePasswordHashSessionRepository— ListActiveByAccount (JOIN geolocation), TerminateByID, TerminateAllExcept
API-Routen (protected)
Abschnitt betitelt „API-Routen (protected)“PUT /api/protected/account/profilePUT /api/protected/account/passwordGET /api/protected/account/sessionsDELETE /api/protected/account/sessions/{id}DELETE /api/protected/account/sessionsErrInvalidCurrentPassword— altes Passwort falschErrSessionNotFound— Session existiert nicht oder gehoert nicht dem UserErrCannotTerminateCurrentSession— bei DELETE /sessions/{id} wenn es die eigene aktuelle Session ist
Feature-Package team (Admin-Verwaltung)
Abschnitt betitelt „Feature-Package team (Admin-Verwaltung)“UseCases
Abschnitt betitelt „UseCases“- CreateAccount(ctx, adminID, email, displayName, role, tenantID?, locale?) — neuen Account anlegen (ohne Passwort), loest
welcomeE-Mail aus. Account muss anschliessend per Password-Reset sein Passwort setzen (Admin loest Reset aus oder automatisch nach Erstellung)agency_owner→ kannagency_employee,tenant_admin,tenant_memberanlegentenant_admin→ kann nurtenant_memberim eigenen Tenant anlegen
- ListAccounts(ctx, adminID) — Accounts auflisten die man verwalten darf
agency_owner→ alle Accountstenant_admin→ nur eigener Tenant
- DeactivateAccount(ctx, adminID, targetAccountID) — Account deaktivieren (active=false), alle Sessions terminieren, loest
account_deactivatedE-Mail aus - ReactivateAccount(ctx, adminID, targetAccountID) — Account wieder aktivieren
- InitiatePasswordReset(ctx, adminID, targetAccountID) — Reset-Code generieren, per E-Mail an Ziel-Account senden
- VerifyResetCode(ctx, accountID, code) — Code pruefen
- CompletePasswordReset(ctx, accountID, code, newPassword) — neues Passwort setzen, used_at setzen, alle Sessions terminieren, loest
password_changedE-Mail aus
Berechtigungspruefung
Abschnitt betitelt „Berechtigungspruefung“In jedem UseCase:
agency_owner→ darf Agency-Accounts + alle Tenant-Accounts verwaltentenant_admin→ darf nurtenant_memberim eigenen Tenant verwalten- Niemand kann sich selbst deaktivieren oder eigenen Reset ausloesen
API-Routen
Abschnitt betitelt „API-Routen“Agency-Routen (agency_owner):
POST /api/agency/teamGET /api/agency/teamPUT /api/agency/team/{id}/deactivatePUT /api/agency/team/{id}/reactivatePOST /api/agency/team/{id}/reset-passwordClient-Routen (tenant_admin):
POST /api/client/teamGET /api/client/teamPUT /api/client/team/{id}/deactivatePUT /api/client/team/{id}/reactivatePOST /api/client/team/{id}/reset-passwordPublic-Routen (Password-Reset Completion):
POST /api/public/auth/verify-resetPOST /api/public/auth/complete-resetDie Verify/Complete-Routen sind public, weil der betroffene User moeglicherweise nicht eingeloggt ist (Admin hat Reset ausgeloest).
ErrInsufficientPermission— keine Berechtigung fuer Ziel-AccountErrCannotModifySelf— Selbst-Deaktivierung/Reset nicht erlaubtErrAccountAlreadyActive/ErrAccountAlreadyInactiveErrResetCodeInvalid/ErrResetCodeExpired/ErrResetCodeUsedErrEmailAlreadyExists— E-Mail bereits vergeben
Frontend
Abschnitt betitelt „Frontend“Neue Routen
Abschnitt betitelt „Neue Routen“/_app/account/profile — Display-Name bearbeiten/_app/account/password — Passwort aendern/_app/account/sessions — Aktive Sessions verwalten/_app/team — Account-Liste (agency oder tenant)/_app/team/create — Neuen Account anlegen/_auth/reset-password — Code + neues Passwort eingeben (public)UI-Komponenten
Abschnitt betitelt „UI-Komponenten“- ProfileForm — Display-Name Feld, Sprache (DE/EN Dropdown), Speichern-Button
- ChangePasswordForm — Altes Passwort, Neues Passwort, Bestaetigung, Zod-Validierung
- SessionList — Tabelle mit Geraet, IP, Standort (DE), Datum, “Abmelden”-Button, “Alle anderen abmelden”-Button, aktuelle Session markiert
- TeamTable — Account-Liste mit Name, E-Mail, Rolle, Status (aktiv/inaktiv), Aktionen
- CreateAccountForm — E-Mail, Display-Name, Rolle (Dropdown je nach eigenem Level), Tenant (nur fuer Agency)
- ResetPasswordForm — 6-stelliger Code + neues Passwort (aehnlich wie 2FA-Form)
Navigation
Abschnitt betitelt „Navigation“AppShell bekommt Sidebar/Menu mit:
- “Konto” (Profil, Passwort, Sessions) — fuer alle Rollen
- “Team” — nur fuer
agency_ownerundtenant_admin
Alle Strings in de.ts und en.ts, Keys unter account.* und team.*.
Sprachumschaltung
Abschnitt betitelt „Sprachumschaltung“GET /api/public/auth/sessionliefertlocaleim Response mit- Frontend setzt
i18next.changeLanguage(locale)beim App-Start basierend auf Session-Response - Sprachwechsel im Profil aktualisiert sofort die UI-Sprache (optimistic update)
sessionResponseSchemabekommt neues Feldlocale
E-Mail-Benachrichtigungen
Abschnitt betitelt „E-Mail-Benachrichtigungen“Neue Templates
Abschnitt betitelt „Neue Templates“| Template | Ausloeser | Platzhalter |
|---|---|---|
password_reset_code | Admin loest Reset aus | {{code}}, {{initiated_by}}, {{expires_minutes}} |
password_changed | Passwort geaendert (Self-Service oder Reset) | {{display_name}}, {{timestamp}}, {{device}}, {{ip}} |
account_deactivated | Admin deaktiviert Account | {{display_name}}, {{initiated_by}} |
new_device_login | Login von unbekanntem Geraet (nach 2FA) | {{display_name}}, {{device}}, {{location}}, {{ip}}, {{timestamp}} |
Integration
Abschnitt betitelt „Integration“new_device_login wird im bestehenden auth-Feature nach erfolgreicher 2FA-Verifikation ausgeloest — kleine Ergaenzung, kein neues Feature.
E-Mail-Worker (bestehende Architektur)
Abschnitt betitelt „E-Mail-Worker (bestehende Architektur)“Nutzt das 3-Stufen-System (Handler → Preparation → Dispatch). Neue Handler:
PasswordResetCodeHandler— prueftpassword_resetauf neue EintraegePasswordChangedHandler— prueftaccountauf kuerzliche Passwort-AenderungenAccountDeactivatedHandler— prueftaccountauf kuerzliche DeaktivierungenNewDeviceLoginHandler— prueftknown_deviceauf neue Eintraege