- 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>
99 lines
3.5 KiB
Go
99 lines
3.5 KiB
Go
// Löschfristen je Datenklasse (Migration 0020) — schließt rules/OPEN.md,
|
|
// Punkt 4. Die Tageswerte sind KEINE gesetzliche Vorgabe (die DSGVO
|
|
// nennt keine festen Fristen, nur den Grundsatz "so lange wie für den
|
|
// Zweck nötig", Art. 5 Abs. 1 lit. e) — deshalb pro Mandant einstellbar,
|
|
// vom Datenschutzbeauftragten hinterlegt, mit risikogestaffelten
|
|
// Vorschlagswerten vorbelegt. Wie alle anderen /verwaltung/*-Seiten
|
|
// bewusst admin-only (konsistent mit Nutzer-/Abteilungs-/Genehmiger-
|
|
// Rollen-Verwaltung) statt an den Namen einer Genehmiger-Rolle
|
|
// gekoppelt — Genehmiger-Rollen sind laut Architektur explizit ohne
|
|
// eigenes Zugriffsrecht (siehe CLAUDE.md, Freigabe-Workflow).
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
)
|
|
|
|
// standardLoeschfristen sind risikogestaffelte Vorschlagswerte, mit
|
|
// denen jede neue Firma vorbelegt wird — je sensibler die Datenklasse,
|
|
// desto kürzer die vorgeschlagene Frist. Frei editierbar unter
|
|
// /verwaltung/loeschfristen, keine Rechtsvorgabe.
|
|
var standardLoeschfristen = []struct {
|
|
DatenklasseID string
|
|
MaxTage int
|
|
}{
|
|
{"oeffentlich", 365},
|
|
{"intern", 180},
|
|
{"auftragsdaten", 90},
|
|
{"personenbezogen", 90},
|
|
{"berufsgeheimnis", 30},
|
|
{"besondere_kategorie", 30},
|
|
}
|
|
|
|
// seedStandardLoeschfristen wird direkt nach dem Anlegen einer neuen
|
|
// Firma aufgerufen (öffentliche Registrierung und Betreiber-Firmenanlage),
|
|
// analog zu seedStandardGenehmigerRollen.
|
|
func (s *Server) seedStandardLoeschfristen(ctx context.Context, accountID string) error {
|
|
for _, l := range standardLoeschfristen {
|
|
if _, err := s.store.UpsertLoeschfristEinstellung(ctx, accountID, l.DatenklasseID, l.MaxTage); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type loeschfristZeile struct {
|
|
DatenklasseID string
|
|
MaxTage int
|
|
}
|
|
|
|
type loeschfristenListeData struct {
|
|
Title string
|
|
Nav navData
|
|
Zeilen []loeschfristZeile
|
|
Error string
|
|
Gespeichert bool
|
|
}
|
|
|
|
func (s *Server) handleLoeschfristenListe(w http.ResponseWriter, r *http.Request) {
|
|
accountID := currentUser(r).AccountID
|
|
einstellungen, err := s.store.ListLoeschfristEinstellungenForAccount(r.Context(), accountID)
|
|
if err != nil {
|
|
http.Error(w, "Löschfristen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data := loeschfristenListeData{Title: "Löschfristen", Nav: navFor(r), Gespeichert: r.URL.Query().Get("gespeichert") == "1"}
|
|
for _, e := range einstellungen {
|
|
data.Zeilen = append(data.Zeilen, loeschfristZeile{DatenklasseID: e.DatenklasseID, MaxTage: e.MaxTage})
|
|
}
|
|
if err := s.templates.ExecuteTemplate(w, "loeschfristen-liste", data); err != nil {
|
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleLoeschfristenSpeichern(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
|
return
|
|
}
|
|
accountID := currentUser(r).AccountID
|
|
for _, l := range standardLoeschfristen {
|
|
raw := r.FormValue("max_tage_" + l.DatenklasseID)
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
maxTage, err := strconv.Atoi(raw)
|
|
if err != nil || maxTage < 0 {
|
|
http.Error(w, "ungültiger Wert für "+l.DatenklasseID, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := s.store.UpsertLoeschfristEinstellung(r.Context(), accountID, l.DatenklasseID, maxTage); err != nil {
|
|
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
http.Redirect(w, r, "/verwaltung/loeschfristen?gespeichert=1", http.StatusSeeOther)
|
|
}
|