Postgres-RLS-Policies auf allen Tabellen mit echten Mandanten-
Geschäftsdaten (antrag und alles darüber verkettete, abteilung,
werkzeug/werkzeug_sperre, genehmiger_rolle/freigabe_regel,
loeschfrist_einstellung). Kritischer Fund vor der Umsetzung: die
Anwendung verbindet als postgres-Superuser, der RLS immer umgeht -
Migration 0021 legt deshalb zusätzlich eine eingeschränkte Rolle
"deklarix_app" an, nur für die greifen die Policies tatsächlich.
internal/store/tenant_scope.go: WithTenantScope öffnet eine Transaktion
und setzt Sitzungsvariablen (app.account_id/app.is_betreiber) per
set_config mit Parameterbindung; alle Store-Methoden laufen jetzt über
s.db(ctx) statt direkt s.Pool. Jede require*-Middleware umschließt die
komplette Handler-Ausführung damit - jeder Request läuft dadurch auch
atomar in einer Transaktion (positiver Nebeneffekt).
Live end-to-end verifiziert (echter HTTP-Server + DATABASE_URL_APP auf
die eingeschränkte Rolle gesetzt): zwei Firmen registriert, Isolation
über Abteilung/Antrag/Bewertung bestätigt, zentraler NULL-Katalog für
beide sichtbar. Produktivbetrieb braucht noch einen manuellen Schritt
(Passwort für deklarix_app setzen + DATABASE_URL_APP konfigurieren,
siehe CLAUDE.md) - die Migration allein aktiviert noch nichts, solange
die App weiter als Superuser verbindet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Schließt eine echte Lücke: der Betreiber-Bereich konnte Accounts
bisher nur lesend anzeigen, eine neue Firma entstand ausschließlich
über die öffentliche Selbstregistrierung. GET/POST
/betreiber/accounts/neu legt jetzt eine Firma samt erstem admin-Login
direkt vom Betreiber aus an (z. B. für vertriebsunterstütztes
Onboarding oder Testkonten) — erzeugt einen audit_log-Eintrag. POST
/betreiber/accounts/{id}/umbenennen korrigiert den Firmennamen
(store.UpdateAccount). Bewusst kein Löschen: ein Hard-Delete würde
gegen die Fremdschlüssel aus antrag/app_user/audit_log laufen und
Historie zerstören — dasselbe Prinzip wie bei Nutzern (deaktivieren
statt löschen), ein Sperren/Deaktivieren für Accounts fehlt aber noch
und hängt an der noch nicht getroffenen Abrechnungsarchitektur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Schließt den ersten Punkt der geplanten Onboarding-Reihenfolge (siehe
CLAUDE.md): ein Admin sieht unter GET /verwaltung/einladung einen
Sammellink (account.einladung_token, Migration 0013), den beliebig
viele Mitarbeitende nutzen können, um sich selbst mit eigenem Passwort
anzumelden (GET/POST /einladung/{token}, öffentlich, Rolle
mitarbeiter). Kein Ablaufdatum, aber per Admin jederzeit erneuerbar —
das macht den alten Link sofort ungültig, falls er versehentlich
außerhalb des Unternehmens geteilt wurde. Keine E-Mail-Bestätigung
(bewusst konsistent mit dem Rest des Produkts, das noch keine
E-Mail-Infrastruktur hat).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deklarix war eine Pre-Publish-Kennzeichnungsprüfung für Werbe-Content
(UWG/MStV). Dieser Scope wird komplett verworfen und durch eine
KI-Antragsprüfung ersetzt: Mitarbeitende beschreiben ein KI-Vorhaben,
das System leitet Datenklasse und KI-VO-Einstufung ab, gleicht sie
gegen einen Werkzeugkatalog ab und erzeugt einen Entscheidungsvorschlag
mit Herleitung — ein Mensch entscheidet, das System bereitet nur vor.
BREAKING CHANGE: Migration 0008 droppt alle werberechtsspezifischen
Tabellen (submission, finding, extraction, evidence_package,
participant, platform_connection, asset). account/app_user/session/
audit_log bleiben (Mandantentrennung, Login, Protokollierung sind
produktunabhängig) — app_user.role wechselt von
creator/agentur/marke/kanzlei/admin zu den fünf neuen Rollen
mitarbeiter/verantwortlicher/pruefer/admin/betreiber (vier
Mandanten-Rollen + eine plattformweite, siehe CLAUDE.md).
Entfernt: internal/extract, internal/dossier, internal/evidence,
internal/socialconnect, alte rules/*.yaml (UWG-Regeln), testdata/golden
— alles ausschließlich für das alte Produkt.
Neu, Phase 1 der Baureihenfolge ("Datenmodell, Regelwerk als YAML,
Katalogstruktur"):
- Store: abteilung (Stammdaten), werkzeug + werkzeug_sperre (der
eigentliche Wert des Produkts — zentral gepflegter Katalog mit
mandantenspezifischen Ergänzungen/Sperrungen, Pflichtfelder
letzte_pruefung/quelle für jede Zusicherung), antrag (Fragebogen-
Grundgerüst, Antworten als JSONB für den adaptiven Fragebogen aus
Phase 2).
- internal/rules komplett neu: lädt und validiert drei YAML-
Regelwerke (Datenklasse-Ableitung, KI-VO-Einstufung, Anforderungs-
profil) aus rules/*.yaml — noch ohne Auswertungslogik gegen echte
Fragebogen-Antworten (das ist Phase 3, bewusst erst nach dem
Fragebogen aus Phase 2, der die exakten Fakten-Feldnamen festlegt).
Offene fachliche Annahmen (Rangfolge der Datenklassen, Fragebogen-
Lücke für die "verboten"-Varianten) explizit in rules/OPEN.md
dokumentiert statt geraten.
- Web-Layer auf Minimalgerüst reduziert, das kompiliert und die neue
Rollenwelt trägt: Firma-Registrierung (Ebene 1, erster Nutzer wird
admin), Login/Logout, Plattform-Bereich (Ebene 5, nur betreiber:
Dashboard, Accounts-Übersicht, Audit-Log) — Fragebogen (Ebene 2) und
Fachebene (Ebene 3) folgen in den nächsten Phasen.
- CLAUDE.md komplett neu geschrieben: Produktbeschreibung, Fünf-Ebenen-
Rollenmodell, Fragebogen-Spezifikation, Ableitungstabellen,
Werkzeugkatalog, Bewertungslogik (geplant), Onboarding, offene
Punkte (u. a. Postgres-RLS-Frage aus der Frontend-Spezifikation
noch nicht entschieden, "Admin und Verantwortlicher gleichzeitig"
beim Onboarding noch nicht datenmodelliert).
Volle Testsuite inkl. echter Postgres-Tests grün. End-to-End gegen
einen laufenden Server verifiziert: Firma-Registrierung legt Account +
admin-Nutzer an, Betreiber-Login leitet zu /betreiber, mandanten-
übergreifende Accounts-Liste sichtbar für betreiber, 404 für
mitarbeiter auf /betreiber, 303 zu /login ohne Sitzung.
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).
internal/auth is pure logic (bcrypt hashing, session token generation)
with no DB access — persistence for account/app_user/session lives in
internal/store like everything else, via migration 0003.
account is the tenant (Mandant); app_user is a login inside one account;
session is a real server-side row (not a signed stateless token) so
logout can actually end a session rather than the client just
forgetting a JWT. submission.account_id is NOT NULL — added directly
rather than the nullable-then-backfill dance, since no submission rows
exist anywhere yet (verified empty on the test server before writing
the migration). Added as migration 0003 (new file), not folded into an
earlier one, since 0001/0002 are already applied on the test server.
store.ErrNotFound lets callers distinguish "wrong email" / "unknown
session" from a genuine DB error — matters for login, where those two
cases should both fail closed but for different reasons.
Not yet wired into internal/web — that's the next commit. All of this
is tested against real Postgres (14 store tests green) but isn't
reachable from any HTTP handler yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>