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:
@@ -1,10 +1,8 @@
|
||||
// Package web ist die HTTP-Schicht: Routing, Templates, Handler.
|
||||
// Bewusst html/template + htmx, kein Frontend-Build (siehe CLAUDE.md,
|
||||
// Stack). Persistenz (Submission/Finding/Evidence speichern) ist noch
|
||||
// nicht angebunden — das ist der nächste Schritt (Verdrahtung über
|
||||
// internal/store). Dieser Server führt Pre-Publish-Prüfungen aus
|
||||
// (extrahieren + bewerten) und zeigt das Ergebnis an, ohne es
|
||||
// aufzubewahren.
|
||||
// Stack). Jede Submission gehört einem Account (Mandant); Auth-Cookies
|
||||
// binden einen Request an einen angemeldeten Nutzer und damit an dessen
|
||||
// Account — siehe middleware.go.
|
||||
package web
|
||||
|
||||
import (
|
||||
@@ -13,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/netcell-it/deklarix/internal/evidence"
|
||||
"github.com/netcell-it/deklarix/internal/extract"
|
||||
@@ -39,7 +38,7 @@ type Extractor interface {
|
||||
// komplette *store.Store. *store.Store erfüllt sie; Tests injizieren
|
||||
// einen Fake statt eine echte Datenbank zu brauchen.
|
||||
type Store interface {
|
||||
CreateSubmission(ctx context.Context, platform, postType, caption string) (store.Submission, error)
|
||||
CreateSubmission(ctx context.Context, accountID, platform, postType, caption string) (store.Submission, error)
|
||||
GetSubmission(ctx context.Context, id string) (store.Submission, error)
|
||||
SetSubmissionStatus(ctx context.Context, id, status string) error
|
||||
CreateExtraction(ctx context.Context, submissionID string, payload []byte, modelVersion, promptVersion string) (store.Extraction, error)
|
||||
@@ -48,6 +47,14 @@ type Store interface {
|
||||
ListCurrentFindings(ctx context.Context, submissionID string) ([]store.Finding, error)
|
||||
CreateEvidencePackage(ctx context.Context, submissionID, dossierPath, sha256Hex string, timestampToken []byte) (store.EvidencePackage, error)
|
||||
GetLatestEvidencePackage(ctx context.Context, submissionID string) (store.EvidencePackage, error)
|
||||
|
||||
CreateAccount(ctx context.Context, name string) (store.Account, error)
|
||||
CreateUser(ctx context.Context, accountID, email, passwordHash, role string) (store.User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (store.User, error)
|
||||
GetUser(ctx context.Context, id string) (store.User, error)
|
||||
CreateSession(ctx context.Context, token, userID string, expiresAt time.Time) (store.Session, error)
|
||||
GetSession(ctx context.Context, token string) (store.Session, error)
|
||||
DeleteSession(ctx context.Context, token string) error
|
||||
}
|
||||
|
||||
// Server bündelt Routing und Abhängigkeiten der Web-Schicht.
|
||||
@@ -81,10 +88,15 @@ func NewServer(extractor Extractor, ruleSet []rules.Rule, st Store, timestamper
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /health", s.handleHealth)
|
||||
mux.HandleFunc("GET /{$}", s.handleIndex)
|
||||
mux.HandleFunc("POST /pruefen", s.handleCheck)
|
||||
mux.HandleFunc("POST /veroeffentlichen", s.handleArchive)
|
||||
mux.HandleFunc("GET /dossier/{id}", s.handleDossierDownload)
|
||||
mux.HandleFunc("GET /register", s.handleRegisterForm)
|
||||
mux.HandleFunc("POST /register", s.handleRegister)
|
||||
mux.HandleFunc("GET /login", s.handleLoginForm)
|
||||
mux.HandleFunc("POST /login", s.handleLogin)
|
||||
mux.HandleFunc("POST /logout", s.handleLogout)
|
||||
mux.HandleFunc("GET /{$}", s.requirePage(s.handleIndex))
|
||||
mux.HandleFunc("POST /pruefen", s.requireAPI(s.handleCheck))
|
||||
mux.HandleFunc("POST /veroeffentlichen", s.requireAPI(s.handleArchive))
|
||||
mux.HandleFunc("GET /dossier/{id}", s.requireAPI(s.handleDossierDownload))
|
||||
mux.Handle("GET /static/", http.FileServerFS(staticFS))
|
||||
s.mux = mux
|
||||
|
||||
|
||||
Reference in New Issue
Block a user