C-1. Session JWT signature verification (authZ bypass fix) - Add SUPABASE_JWT_SECRET env var; fail-fast at startup if unset. - auth.Client.VerifyToken uses github.com/golang-jwt/jwt/v5 to verify HS256 signatures, reject alg=none, enforce exp/nbf/iat. - Middleware stores verified claims in request context; WithUserID reads only verified claims (no more raw-cookie sub decoding). - API requests get 401 on missing/invalid token (was 302 redirect). - Refresh flow only runs on expiry; other signature failures reject outright and clear cookies. C-2. Dashboard Termine cross-user privacy leak - dashboard_service.loadUpcomingAppointments now mirrors TerminService.canSee: personal Termine (akte_id IS NULL) are creator-only; admins do NOT see other users' personal Termine. C-3. Role gate on Parteien + Termine mutations - ParteienService.Delete now partner/admin only (matches FristService). - TerminService.Update / Delete on Akte-linked Termine now require partner/admin (or the original creator). Personal Termine stay creator-only. C-4. Email gate → ALLOWED_EMAIL_DOMAINS whitelist - isHoganLovellsEmail → isAllowedEmailDomain reading the env var (default: hoganlovells.com,hlc.com,hlc.de). Case-insensitive, whitespace-tolerant. - login.tsx placeholder: name@hoganlovells.com → name@hlc.com - Error strings + login.hint (de/en) rewritten for HLC branding. C-5. Docker compose env wiring - docker-compose.yml gains SUPABASE_JWT_SECRET, CALDAV_ENCRYPTION_KEY, and ALLOWED_EMAIL_DOMAINS passthrough; commented-out ANTHROPIC_API_KEY line for Phase H readiness. Tests - auth_test.go: valid/wrong-secret/expired/alg-none/missing-sub/garbage token cases for VerifyToken. - handlers/auth_test.go: default + env-override cases for the email whitelist. - go build ./..., go vet ./..., go test ./... all clean.
66 lines
2.0 KiB
Go
66 lines
2.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"mgit.msbls.de/m/patholo/internal/auth"
|
|
)
|
|
|
|
// GET /api/dashboard — returns the DashboardData JSON for the logged-in user.
|
|
// Returns 503 if DATABASE_URL is unset.
|
|
func handleDashboardAPI(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
data, err := dbSvc.dashboard.Get(r.Context(), uid)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, data)
|
|
}
|
|
|
|
// GET /dashboard — protected shell page. The client boots, reads the initial
|
|
// payload inlined by the server into window.__PALIAD_DASHBOARD__, and renders
|
|
// without a second round-trip (audit §2.3: no skeleton→fetch waterfall).
|
|
func handleDashboardPage(w http.ResponseWriter, r *http.Request) {
|
|
uid, hasUser := auth.UserIDFromContext(r.Context())
|
|
var payload []byte
|
|
if hasUser && dbSvc != nil {
|
|
// Best-effort server-render. If the DB read fails we still serve the
|
|
// shell; the client will show the inline error state instead of the
|
|
// zero-count cards.
|
|
if data, err := dbSvc.dashboard.Get(r.Context(), uid); err == nil {
|
|
payload = mustJSON(data)
|
|
}
|
|
}
|
|
serveDashboardShell(w, r, payload)
|
|
}
|
|
|
|
// handleRootPage is the public `/` route. Unauthenticated visitors get the
|
|
// marketing landing; authenticated users get a 302 to /dashboard so `/` feels
|
|
// like a no-op they can bookmark.
|
|
func handleRootPage(w http.ResponseWriter, r *http.Request) {
|
|
if hasValidSession(r) {
|
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, "dist/index.html")
|
|
}
|
|
|
|
// hasValidSession returns true when the session cookie carries a signed,
|
|
// unexpired Supabase JWT. Verification is the same one Middleware uses —
|
|
// forged or tampered cookies never reach the authenticated branch.
|
|
func hasValidSession(r *http.Request) bool {
|
|
cookie, err := r.Cookie(auth.SessionCookieName)
|
|
if err != nil || cookie.Value == "" {
|
|
return false
|
|
}
|
|
_, err = authClient.VerifyToken(cookie.Value)
|
|
return err == nil
|
|
}
|