feat: Admin-Bereich (Accounts, Kanzlei-Verzeichnis-Freigabe, Audit-Log)

Bislang gab es keine vom Nutzer-Rollenmodell (creator/agentur/marke/
kanzlei) getrennte Betreiber-Rolle — jede Verwaltungsaufgabe (welche
Kanzlei darf im öffentlichen Verzeichnis stehen, wer sind unsere
Accounts) wäre nur per Hand in der Datenbank möglich gewesen. Admin
ist von Anfang an als fünfte app_user-Rolle im Datenmodell verankert,
nicht nachträglich aufgesetzt.

Migration 0004:
- app_user.role erlaubt zusätzlich 'admin' (kein Self-Service-Weg
  dorthin — /register bietet die Rolle nicht an, erster Admin wird
  einmalig per SQL angelegt, siehe CLAUDE.md).
- account.verified: Freigabe fürs kostenlose Kanzlei-Verzeichnis
  (§ 49b Abs. 3 BRAO: reine Auflistung, kein Routing/keine Vermittlung).
- audit_log: append-only-Protokoll jeder Admin-Aktion (gleicher Trigger
  wie finding/extraction/evidence_package).

Neue Routen:
- GET /admin, /admin/accounts, /admin/accounts/{id}: Accounts-Übersicht
  und -Detail (Logins je Account), requireAdmin (404 statt 403 für
  angemeldete Nicht-Admins, wie beim bestehenden Mandanten-404-Muster).
- POST /admin/accounts/{id}/verifizieren: Kanzlei-Freigabe umschalten,
  schreibt einen Audit-Log-Eintrag.
- GET /admin/audit-log: Protokoll ansehen.
- GET /kanzleien: öffentliches Verzeichnis (kein Login), zeigt nur
  Accounts, die sowohl verified sind als auch einen Nutzer der Rolle
  "kanzlei" haben.

Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End manuell
gegen einen laufenden Server verifiziert (Admin-Login, Verify-Toggle,
Erscheinen im öffentlichen Verzeichnis, Audit-Log-Eintrag, 404 für
Nicht-Admin-Zugriff).
This commit is contained in:
noroot
2026-08-27 18:17:45 +02:00
parent c9d71b0d54
commit 835ad9f0a7
18 changed files with 1151 additions and 12 deletions

View File

@@ -0,0 +1,6 @@
DROP TRIGGER audit_log_append_only ON audit_log;
DROP TABLE audit_log;
ALTER TABLE account DROP COLUMN verified;
ALTER TABLE app_user DROP CONSTRAINT app_user_role_check;
ALTER TABLE app_user ADD CONSTRAINT app_user_role_check
CHECK (role IN ('creator', 'agentur', 'marke', 'kanzlei'));

View File

@@ -0,0 +1,36 @@
-- "admin" ist eine fünfte app_user-Rolle: Betreiber-Personal (Netcell-IT),
-- nicht an einen Mandanten-Geschäftszweck (creator/agentur/marke/kanzlei)
-- gebunden, sondern zuständig für die Plattform selbst (Accounts,
-- Kanzlei-Verzeichnis, Audit-Log). Bewusst KEIN eigenes account_role-Feld
-- getrennt von app_user.role — ein Admin-Login ist genauso ein app_user
-- wie jeder andere, nur mit einer anderen Rolle. Es gibt bewusst keine
-- Selbstregistrierung für "admin" über /register (siehe internal/web) —
-- der erste Admin wird per SQL angelegt (siehe CLAUDE.md).
ALTER TABLE app_user DROP CONSTRAINT app_user_role_check;
ALTER TABLE app_user ADD CONSTRAINT app_user_role_check
CHECK (role IN ('creator', 'agentur', 'marke', 'kanzlei', 'admin'));
-- verified markiert eine Kanzlei-Account als für das öffentliche,
-- kostenlose Kanzlei-Verzeichnis freigegeben (siehe CLAUDE.md, § 49b
-- Abs. 3 BRAO: keine Sachvorteile, kein Routing — das Verzeichnis ist
-- eine reine Auflistung, keine Vermittlung). Nur für Accounts mit
-- mindestens einem Nutzer der Rolle "kanzlei" sinnvoll; das erzwingt die
-- Anwendungsschicht, nicht die Datenbank.
ALTER TABLE account ADD COLUMN verified BOOLEAN NOT NULL DEFAULT false;
-- Audit-Log für Admin-Aktionen: append-only aus demselben Grund wie
-- finding/extraction/evidence_package — ein Protokoll, das man ändern
-- kann, ist kein Nachweis mehr.
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
actor_user_id UUID NOT NULL REFERENCES app_user (id),
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
details TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TRIGGER audit_log_append_only
BEFORE UPDATE OR DELETE ON audit_log
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();