Schließt den zweiten Punkt der geplanten Onboarding-Reihenfolge. Admin lädt eine CSV mit Spalten email (Pflicht) und role (optional, Standard mitarbeiter) unter GET /verwaltung/nutzer/import hoch. Wie beim einzelnen Nutzer-Anlegen gibt es keine Einladungsmail — pro Zeile wird ein Einmalpasswort erzeugt und nur auf der Ergebnisseite direkt nach dem Import angezeigt, danach nirgendwo mehr abrufbar. Kein Alles-oder-nichts-Import: eine ungültige Zeile blockiert nicht die übrigen, jede Zeile bekommt ihr eigenes Ergebnis. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
9.7 KiB
Go
172 lines
9.7 KiB
Go
// 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/rules"
|
|
"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)
|
|
GetAccountByEinladungToken(ctx context.Context, token string) (store.Account, error)
|
|
RegenerateEinladungToken(ctx context.Context, accountID, newToken string) 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)
|
|
SetUserActive(ctx context.Context, id string, active bool) 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)
|
|
CreateAbteilung(ctx context.Context, accountID, name string) (store.Abteilung, error)
|
|
DeleteAbteilung(ctx context.Context, id string) 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)
|
|
ListAntraegeForAccount(ctx context.Context, accountID string) ([]store.Antrag, error)
|
|
|
|
ListWerkzeugeForAccount(ctx context.Context, accountID string) ([]store.Werkzeug, error)
|
|
ListZentraleWerkzeuge(ctx context.Context) ([]store.Werkzeug, error)
|
|
CreateWerkzeugSperre(ctx context.Context, accountID, werkzeugID, grund string) (store.WerkzeugSperre, error)
|
|
DeleteWerkzeugSperre(ctx context.Context, accountID, werkzeugID string) error
|
|
ListWerkzeugSperrenForAccount(ctx context.Context, accountID string) ([]store.WerkzeugSperre, error)
|
|
GetWerkzeug(ctx context.Context, id string) (store.Werkzeug, error)
|
|
CreateWerkzeug(ctx context.Context, in store.WerkzeugInput) (store.Werkzeug, error)
|
|
UpdateWerkzeug(ctx context.Context, id string, in store.WerkzeugInput) (store.Werkzeug, error)
|
|
DeleteWerkzeug(ctx context.Context, id string) error
|
|
CurrentKatalogVersion(ctx context.Context) (string, error)
|
|
CreateBewertung(ctx context.Context, in store.BewertungInput) (store.Bewertung, error)
|
|
GetLatestBewertungForAntrag(ctx context.Context, antragID string) (store.Bewertung, error)
|
|
|
|
CreateEntscheidung(ctx context.Context, in store.EntscheidungInput) (store.Entscheidung, error)
|
|
GetLatestEntscheidungForAntrag(ctx context.Context, antragID string) (store.Entscheidung, error)
|
|
ListAktiveGenehmigungenForAccount(ctx context.Context, accountID string) ([]store.Entscheidung, error)
|
|
|
|
GetAbteilung(ctx context.Context, id string) (store.Abteilung, error)
|
|
CreateRegistereintrag(ctx context.Context, in store.RegistereintragInput) (store.Registereintrag, error)
|
|
ListRegistereintraegeForAccount(ctx context.Context, accountID string) ([]store.Registereintrag, error)
|
|
}
|
|
|
|
// Regelwerk bündelt die drei geladenen Regelwerk-Dateien, die die
|
|
// Bewertung eines Antrags braucht (siehe internal/rules). Ein eigener
|
|
// Typ statt drei einzelne Server-Felder, damit main.go sie an einer
|
|
// Stelle zusammenstellt.
|
|
type Regelwerk struct {
|
|
Datenklasse rules.DatenklasseRegelwerk
|
|
Einstufung rules.EinstufungRegelwerk
|
|
Anforderungen rules.AnforderungsRegelwerk
|
|
}
|
|
|
|
// Server bündelt Routing und Abhängigkeiten der Web-Schicht.
|
|
type Server struct {
|
|
mux *http.ServeMux
|
|
store Store
|
|
regelwerk Regelwerk
|
|
templates *template.Template
|
|
}
|
|
|
|
// NewServer erstellt den Server. regelwerk kommt von rules.Load* und
|
|
// wird einmal beim Start geladen, nicht pro Request.
|
|
func NewServer(st Store, regelwerk Regelwerk) (*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,
|
|
regelwerk: regelwerk,
|
|
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 /einladung/{token}", s.handleEinladungForm)
|
|
mux.HandleFunc("POST /einladung/{token}", s.handleEinladungAnnehmen)
|
|
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 /faelle", s.requireFachebene(s.handleFaelleListe))
|
|
mux.HandleFunc("GET /faelle/{id}", s.requireFachebene(s.handleFallDetail))
|
|
mux.HandleFunc("POST /faelle/{id}/entscheiden", s.requireFachebene(s.handleFallEntscheiden))
|
|
mux.HandleFunc("GET /registereintraege", s.requireFachebene(s.handleRegisterListe))
|
|
mux.HandleFunc("GET /registereintraege/export.csv", s.requireFachebene(s.handleRegisterExportCSV))
|
|
mux.HandleFunc("GET /registereintraege/export.pdf", s.requireFachebene(s.handleRegisterExportPDF))
|
|
mux.HandleFunc("GET /wiedervorlage", s.requireFachebene(s.handleWiedervorlageListe))
|
|
mux.HandleFunc("GET /verwaltung/nutzer", s.requireAdmin(s.handleNutzerListe))
|
|
mux.HandleFunc("GET /verwaltung/nutzer/neu", s.requireAdmin(s.handleNutzerNeuForm))
|
|
mux.HandleFunc("POST /verwaltung/nutzer", s.requireAdmin(s.handleNutzerCreate))
|
|
mux.HandleFunc("POST /verwaltung/nutzer/{id}/deaktivieren", s.requireAdmin(s.handleNutzerDeaktivieren))
|
|
mux.HandleFunc("POST /verwaltung/nutzer/{id}/aktivieren", s.requireAdmin(s.handleNutzerAktivieren))
|
|
mux.HandleFunc("GET /verwaltung/nutzer/import", s.requireAdmin(s.handleNutzerCSVImportForm))
|
|
mux.HandleFunc("POST /verwaltung/nutzer/import", s.requireAdmin(s.handleNutzerCSVImportUpload))
|
|
mux.HandleFunc("GET /verwaltung/einladung", s.requireAdmin(s.handleEinladungVerwaltung))
|
|
mux.HandleFunc("POST /verwaltung/einladung/erneuern", s.requireAdmin(s.handleEinladungErneuern))
|
|
mux.HandleFunc("GET /verwaltung/abteilungen", s.requireAdmin(s.handleAbteilungListe))
|
|
mux.HandleFunc("POST /verwaltung/abteilungen", s.requireAdmin(s.handleAbteilungCreate))
|
|
mux.HandleFunc("POST /verwaltung/abteilungen/{id}/loeschen", s.requireAdmin(s.handleAbteilungDelete))
|
|
mux.HandleFunc("GET /verwaltung/werkzeuge", s.requireAdmin(s.handleMandantWerkzeugListe))
|
|
mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/sperren", s.requireAdmin(s.handleMandantWerkzeugSperren))
|
|
mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/entsperren", s.requireAdmin(s.handleMandantWerkzeugEntsperren))
|
|
mux.HandleFunc("GET /verwaltung/werkzeuge/neu", s.requireAdmin(s.handleMandantWerkzeugNeuForm))
|
|
mux.HandleFunc("POST /verwaltung/werkzeuge", s.requireAdmin(s.handleMandantWerkzeugCreate))
|
|
mux.HandleFunc("GET /verwaltung/werkzeuge/{id}", s.requireAdmin(s.handleMandantWerkzeugEditForm))
|
|
mux.HandleFunc("POST /verwaltung/werkzeuge/{id}", s.requireAdmin(s.handleMandantWerkzeugUpdate))
|
|
mux.HandleFunc("POST /verwaltung/werkzeuge/{id}/loeschen", s.requireAdmin(s.handleMandantWerkzeugDelete))
|
|
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.HandleFunc("GET /betreiber/werkzeuge", s.requireBetreiber(s.handleBetreiberWerkzeugListe))
|
|
mux.HandleFunc("GET /betreiber/werkzeuge/neu", s.requireBetreiber(s.handleBetreiberWerkzeugNeuForm))
|
|
mux.HandleFunc("POST /betreiber/werkzeuge", s.requireBetreiber(s.handleBetreiberWerkzeugCreate))
|
|
mux.HandleFunc("GET /betreiber/werkzeuge/{id}", s.requireBetreiber(s.handleBetreiberWerkzeugEditForm))
|
|
mux.HandleFunc("POST /betreiber/werkzeuge/{id}", s.requireBetreiber(s.handleBetreiberWerkzeugUpdate))
|
|
mux.HandleFunc("POST /betreiber/werkzeuge/{id}/loeschen", s.requireBetreiber(s.handleBetreiberWerkzeugDelete))
|
|
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)
|
|
}
|