Deklarix war eine Pre-Publish-Kennzeichnungsprüfung für Werbe-Content
(UWG/MStV). Dieser Scope wird komplett verworfen und durch eine
KI-Antragsprüfung ersetzt: Mitarbeitende beschreiben ein KI-Vorhaben,
das System leitet Datenklasse und KI-VO-Einstufung ab, gleicht sie
gegen einen Werkzeugkatalog ab und erzeugt einen Entscheidungsvorschlag
mit Herleitung — ein Mensch entscheidet, das System bereitet nur vor.
BREAKING CHANGE: Migration 0008 droppt alle werberechtsspezifischen
Tabellen (submission, finding, extraction, evidence_package,
participant, platform_connection, asset). account/app_user/session/
audit_log bleiben (Mandantentrennung, Login, Protokollierung sind
produktunabhängig) — app_user.role wechselt von
creator/agentur/marke/kanzlei/admin zu den fünf neuen Rollen
mitarbeiter/verantwortlicher/pruefer/admin/betreiber (vier
Mandanten-Rollen + eine plattformweite, siehe CLAUDE.md).
Entfernt: internal/extract, internal/dossier, internal/evidence,
internal/socialconnect, alte rules/*.yaml (UWG-Regeln), testdata/golden
— alles ausschließlich für das alte Produkt.
Neu, Phase 1 der Baureihenfolge ("Datenmodell, Regelwerk als YAML,
Katalogstruktur"):
- Store: abteilung (Stammdaten), werkzeug + werkzeug_sperre (der
eigentliche Wert des Produkts — zentral gepflegter Katalog mit
mandantenspezifischen Ergänzungen/Sperrungen, Pflichtfelder
letzte_pruefung/quelle für jede Zusicherung), antrag (Fragebogen-
Grundgerüst, Antworten als JSONB für den adaptiven Fragebogen aus
Phase 2).
- internal/rules komplett neu: lädt und validiert drei YAML-
Regelwerke (Datenklasse-Ableitung, KI-VO-Einstufung, Anforderungs-
profil) aus rules/*.yaml — noch ohne Auswertungslogik gegen echte
Fragebogen-Antworten (das ist Phase 3, bewusst erst nach dem
Fragebogen aus Phase 2, der die exakten Fakten-Feldnamen festlegt).
Offene fachliche Annahmen (Rangfolge der Datenklassen, Fragebogen-
Lücke für die "verboten"-Varianten) explizit in rules/OPEN.md
dokumentiert statt geraten.
- Web-Layer auf Minimalgerüst reduziert, das kompiliert und die neue
Rollenwelt trägt: Firma-Registrierung (Ebene 1, erster Nutzer wird
admin), Login/Logout, Plattform-Bereich (Ebene 5, nur betreiber:
Dashboard, Accounts-Übersicht, Audit-Log) — Fragebogen (Ebene 2) und
Fachebene (Ebene 3) folgen in den nächsten Phasen.
- CLAUDE.md komplett neu geschrieben: Produktbeschreibung, Fünf-Ebenen-
Rollenmodell, Fragebogen-Spezifikation, Ableitungstabellen,
Werkzeugkatalog, Bewertungslogik (geplant), Onboarding, offene
Punkte (u. a. Postgres-RLS-Frage aus der Frontend-Spezifikation
noch nicht entschieden, "Admin und Verantwortlicher gleichzeitig"
beim Onboarding noch nicht datenmodelliert).
Volle Testsuite inkl. echter Postgres-Tests grün. End-to-End gegen
einen laufenden Server verifiziert: Firma-Registrierung legt Account +
admin-Nutzer an, Betreiber-Login leitet zu /betreiber, mandanten-
übergreifende Accounts-Liste sichtbar für betreiber, 404 für
mitarbeiter auf /betreiber, 303 zu /login ohne Sitzung.
85 lines
3.1 KiB
Go
85 lines
3.1 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/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)
|
|
}
|
|
|
|
// 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 /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)
|
|
}
|