feat: wire auth into the web layer (Schritt 2, part 2/2)

Registration creates a new account plus its first user; login
authenticates an existing one; both set a deklarix_session cookie
(HttpOnly, SameSite=Strict, Secure only when the request itself came
over TLS — hardcoding Secure=true would break local http://localhost
development, since browsers won't store a Secure cookie over plaintext).

requirePage protects full-page GETs (redirects to /login); requireAPI
protects the htmx/download endpoints (401, since those are only ever
called from an already-authenticated page — an unauthenticated hit
there is the exception, e.g. a session expiring mid-use).

handleCheck now creates submissions under the current account.
handleArchive and handleDossierDownload compare the submission's
account against the caller's and return 404 on mismatch — not 403,
which would confirm the ID exists to a different tenant. Login failure
uses the same message for "no such email" and "wrong password" to avoid
account enumeration.

Restructured templates along the way: layout.html now only holds
reusable fragments ("head", "nav"); each full page (index/login/register)
is its own top-level named template. The previous layout+content nesting
would have broken the moment a second page defined "content" — Go's
html/template keys blocks by name across the whole parsed set, not per
file, so two pages both defining "content" would silently overwrite each
other.

Verified against a real running instance (not just Go's test recorder):
started the compiled binary against a fresh Postgres and drove the whole
flow with curl — anonymous redirect, registration setting a real cookie,
authenticated page load, logout clearing both the cookie and the
server-side session row, and being locked out again afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-08-27 15:56:11 +02:00
parent 2a16dc2200
commit 5156fe62da
12 changed files with 755 additions and 128 deletions

View File

@@ -121,8 +121,16 @@ Managed Postgres in der EU (DSGVO).
## Datenmodell
Fünf Tabellen, mehr braucht der MVP nicht:
Plus drei Tabellen für Auth/Mandantentrennung (`account`, `app_user`,
`session`), mehr braucht der MVP nicht:
- `account` — ein Mandant (Creator, Agentur, Marke oder Kanzlei als
eigene Organisation); jede Submission gehört genau einem Account
- `app_user` — ein Login innerhalb eines Accounts (E-Mail, Passwort-
Hash, Rolle)
- `session` — eine angemeldete Sitzung (Token, Ablaufzeit); bewusst
eine echte Tabelle statt zustandsloser signierter Tokens, damit
Logout eine Sitzung wirklich beendet
- `submission` — ein eingereichter Beitrag, Status, Zeitpunkte
- `asset` — Bild oder Datei, Pfad, SHA-256
- `extraction` — das JSON aus Stufe 1, Modellversion, Prompt-Version
@@ -146,6 +154,16 @@ Fundstellen, Vertrags- und Briefing-Bezug, Verantwortungsmatrix, alle
Hashes und das Zeitstempel-Token. Vollständiger Export muss für den
Nutzer jederzeit möglich sein — wer kündigt, bekommt sein Archiv.
**Auth/Mandantentrennung:** `POST /register` legt einen neuen Account
plus den ersten Nutzer darin an, `POST /login` meldet einen bestehenden
Nutzer an — beides setzt ein `deklarix_session`-Cookie (HttpOnly,
SameSite=Strict, Secure sobald über TLS erreicht). `GET /` verlangt eine
gültige Sitzung (sonst Redirect zu `/login`); `POST /pruefen`,
`POST /veroeffentlichen` und `GET /dossier/{id}` verlangen sie ebenfalls
(sonst 401). Ein Beitrag eines fremden Accounts wird wie ein nicht
existierender behandelt (404), nie mit einer expliziten 403 bestätigt —
sonst würde die Antwort selbst verraten, dass die ID existiert.
---
## Go Commands