feat: Löschfristen je Datenklasse, Passwort-Zurücksetzen, DPF-Recherche

- Löschfristen (Migration 0020): pro Mandant einstellbar statt fest im
  Regelwerk, da die DSGVO selbst keine festen Fristen nennt (Art. 5
  Abs. 1 lit. e). Jede Firma wird mit risikogestaffelten Vorschlagswerten
  vorbelegt, loeschfrist_max_tage wird jetzt tatsächlich hart gegen
  Werkzeuge gefiltert (schließt rules/OPEN.md Punkt 4).
- Neues internal/mail-Paket (SMTP-Versand + Test-Doppel) und darauf
  aufbauend Passwort-Zurücksetzen (Migration 0019) - bisher nur als
  Absicht in der Rollentabelle genannt, nie gebaut.
- dpf_zertifiziert für alle 20 Katalogeinträge gegen das offizielle
  DPF-Register recherchiert und in der Produktions-DB aktualisiert
  (12 zertifiziert, 5 recherchiert-nicht-gefunden, 3 nicht anwendbar).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-01 07:38:12 +02:00
parent 690660b655
commit 8a8295dacd
30 changed files with 1120 additions and 63 deletions

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"time"
"github.com/netcell-it/deklarix/internal/mail"
"github.com/netcell-it/deklarix/internal/rules"
"github.com/netcell-it/deklarix/internal/store"
)
@@ -39,6 +40,12 @@ type Store interface {
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
SetUserPassword(ctx context.Context, id, passwordHash string) error
UpsertLoeschfristEinstellung(ctx context.Context, accountID, datenklasseID string, maxTage int) (store.LoeschfristEinstellung, error)
ListLoeschfristEinstellungenForAccount(ctx context.Context, accountID string) ([]store.LoeschfristEinstellung, error)
CreatePasswordResetToken(ctx context.Context, userID, token string) (store.PasswordResetToken, error)
GetValidPasswordResetToken(ctx context.Context, token string) (store.PasswordResetToken, error)
MarkPasswordResetTokenUsed(ctx context.Context, id string) error
CreateSession(ctx context.Context, token, userID string, expiresAt time.Time) (store.Session, error)
CreateImpersonatedSession(ctx context.Context, token, userID, impersonatedByUserID string, expiresAt time.Time) (store.Session, error)
GetSession(ctx context.Context, token string) (store.Session, error)
@@ -115,11 +122,15 @@ type Server struct {
store Store
regelwerk Regelwerk
templates *template.Template
mailer mail.Mailer
}
// 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) {
// wird einmal beim Start geladen, nicht pro Request. mailer ist
// austauschbar (siehe mail.FakeMailer für Tests) — ohne konfigurierten
// SMTP-Server schlägt nur der tatsächliche Versand fehl (z. B.
// Passwort-Zurücksetzen), der restliche Server bleibt funktionsfähig.
func NewServer(st Store, regelwerk Regelwerk, mailer mail.Mailer) (*Server, error) {
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("web: templates parsen: %w", err)
@@ -129,6 +140,7 @@ func NewServer(st Store, regelwerk Regelwerk) (*Server, error) {
store: st,
regelwerk: regelwerk,
templates: tmpl,
mailer: mailer,
}
mux := http.NewServeMux()
@@ -139,6 +151,12 @@ func NewServer(st Store, regelwerk Regelwerk) (*Server, error) {
mux.HandleFunc("POST /einladung/{token}", s.handleEinladungAnnehmen)
mux.HandleFunc("GET /login", s.handleLoginForm)
mux.HandleFunc("POST /login", s.handleLogin)
mux.HandleFunc("GET /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenListe))
mux.HandleFunc("POST /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenSpeichern))
mux.HandleFunc("GET /passwort-vergessen", s.handlePasswortVergessenForm)
mux.HandleFunc("POST /passwort-vergessen", s.handlePasswortVergessen)
mux.HandleFunc("GET /passwort-zuruecksetzen/{token}", s.handlePasswortZuruecksetzenForm)
mux.HandleFunc("POST /passwort-zuruecksetzen/{token}", s.handlePasswortZuruecksetzen)
mux.HandleFunc("POST /logout", s.handleLogout)
mux.HandleFunc("GET /{$}", s.requirePage(s.handleIndex))
mux.HandleFunc("GET /antraege", s.requirePage(s.handleAntragList))