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>
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
const sessionCookieName = "deklarix_session"
|
|
|
|
type contextKey int
|
|
|
|
const userContextKey contextKey = iota
|
|
|
|
// authenticate liest das Session-Cookie, prüft die Sitzung (existiert,
|
|
// nicht abgelaufen) und lädt den zugehörigen Nutzer. Liefert (User{}, false),
|
|
// wenn irgendein Schritt fehlschlägt — die Gründe (kein Cookie, unbekanntes
|
|
// Token, abgelaufen, Nutzer weg) werden bewusst nicht unterschieden, damit
|
|
// requirePage/requireAPI immer denselben, einzigen Fehlerpfad haben.
|
|
func (s *Server) authenticate(r *http.Request) (store.User, bool) {
|
|
cookie, err := r.Cookie(sessionCookieName)
|
|
if err != nil || cookie.Value == "" {
|
|
return store.User{}, false
|
|
}
|
|
|
|
sess, err := s.store.GetSession(r.Context(), cookie.Value)
|
|
if err != nil {
|
|
return store.User{}, false
|
|
}
|
|
if time.Now().After(sess.ExpiresAt) {
|
|
return store.User{}, false
|
|
}
|
|
|
|
user, err := s.store.GetUser(r.Context(), sess.UserID)
|
|
if err != nil {
|
|
return store.User{}, false
|
|
}
|
|
return user, true
|
|
}
|
|
|
|
// requirePage schützt volle Seitenaufrufe — ohne gültige Sitzung geht
|
|
// es zurück zu /login (eine echte Navigation, kein htmx-Fragment).
|
|
func (s *Server) requirePage(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := s.authenticate(r)
|
|
if !ok {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next(w, r.WithContext(context.WithValue(r.Context(), userContextKey, user)))
|
|
}
|
|
}
|
|
|
|
// requireAPI schützt htmx-Endpunkte (Formular-Posts, Downloads) — diese
|
|
// werden nur aus einer bereits authentifizierten Seite heraus
|
|
// aufgerufen, ein Fehlschlag hier ist der Ausnahmefall (z. B. Sitzung
|
|
// mitten in der Nutzung abgelaufen), daher schlicht 401 statt Redirect.
|
|
func (s *Server) requireAPI(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := s.authenticate(r)
|
|
if !ok {
|
|
http.Error(w, "nicht angemeldet", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next(w, r.WithContext(context.WithValue(r.Context(), userContextKey, user)))
|
|
}
|
|
}
|
|
|
|
// currentUser liest den Nutzer, den requirePage/requireAPI in den
|
|
// Kontext gelegt haben. Panics, wenn es aufgerufen wird, ohne dass eine
|
|
// dieser Middlewares vorgeschaltet war — das ist ein Programmierfehler,
|
|
// kein Laufzeitfall, den man abfangen sollte.
|
|
func currentUser(r *http.Request) store.User {
|
|
user, ok := r.Context().Value(userContextKey).(store.User)
|
|
if !ok {
|
|
panic("web: currentUser aufgerufen ohne requirePage/requireAPI")
|
|
}
|
|
return user
|
|
}
|