// Package web ist die HTTP-Schicht: Routing, Templates, Handler. // Bewusst html/template + htmx, kein Frontend-Build (siehe CLAUDE.md, // Stack). Jeder Antrag 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 ( "context" "embed" "fmt" "html/template" "net/http" "time" "github.com/netcell-it/deklarix/internal/store" ) //go:embed templates/*.html var templatesFS embed.FS //go:embed static/* var staticFS embed.FS // Store ist die Schnittstelle, die der Server zur Persistenz braucht — // bewusst schmal (nur was diese Handler tatsächlich nutzen), nicht der // komplette *store.Store. *store.Store erfüllt sie; Tests injizieren // einen Fake statt eine echte Datenbank zu brauchen. type Store interface { CreateAccount(ctx context.Context, name string) (store.Account, error) GetAccount(ctx context.Context, id string) (store.Account, error) ListAccounts(ctx context.Context) ([]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) ListUsersForAccount(ctx context.Context, accountID 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 CreateAuditEntry(ctx context.Context, actorUserID, action, targetType, targetID, details string) (store.AuditEntry, error) ListAuditLog(ctx context.Context, limit int) ([]store.AuditEntry, error) ListAbteilungenForAccount(ctx context.Context, accountID string) ([]store.Abteilung, error) CreateAntrag(ctx context.Context, accountID, erstellerUserID string, abteilungID *string, titel string) (store.Antrag, error) GetAntrag(ctx context.Context, id string) (store.Antrag, error) UpdateAntragFelder(ctx context.Context, id, titel, beschreibung, ergebnis, haeufigkeit string, antworten []byte) (store.Antrag, error) SetAntragStatus(ctx context.Context, id, status string) error ListAntraegeForUser(ctx context.Context, erstellerUserID string) ([]store.Antrag, error) } // Server bündelt Routing und Abhängigkeiten der Web-Schicht. type Server struct { mux *http.ServeMux store Store templates *template.Template } // NewServer erstellt den Server. func NewServer(st Store) (*Server, error) { tmpl, err := template.ParseFS(templatesFS, "templates/*.html") if err != nil { return nil, fmt.Errorf("web: templates parsen: %w", err) } s := &Server{ store: st, templates: tmpl, } mux := http.NewServeMux() mux.HandleFunc("GET /health", s.handleHealth) 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("GET /antraege", s.requirePage(s.handleAntragList)) mux.HandleFunc("GET /antraege/neu", s.requirePage(s.handleAntragNewForm)) mux.HandleFunc("POST /antraege", s.requirePage(s.handleAntragCreate)) mux.HandleFunc("GET /antraege/{id}", s.requirePage(s.handleAntragDetail)) mux.HandleFunc("GET /betreiber", s.requireBetreiber(s.handleBetreiberDashboard)) mux.HandleFunc("GET /betreiber/accounts", s.requireBetreiber(s.handleBetreiberAccountList)) mux.HandleFunc("GET /betreiber/accounts/{id}", s.requireBetreiber(s.handleBetreiberAccountDetail)) mux.HandleFunc("GET /betreiber/audit-log", s.requireBetreiber(s.handleBetreiberAuditLog)) mux.Handle("GET /static/", http.FileServerFS(staticFS)) s.mux = mux return s, nil } // ServeHTTP macht Server zu einem http.Handler. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) }