// Konfigurierbarer Mehrfach-Freigabe-Workflow — siehe Migration 0017 // und CLAUDE.md. Drei Seitengruppen: // - Admin (Ebene 4): Genehmiger-Rollen anlegen/Mitglieder zuweisen, // Freigabe-Regeln definieren ("wenn Bedingung X, dann Freigabe // durch Rolle Y nötig"). // - Jeder angemeldete Nutzer: "Meine Freigaben" — eigene ausstehende // Freigabeschritte, unabhängig von app_user.role (eine // Genehmiger-Rolle ist orthogonal zur Zugriffsrolle). package web import ( "context" "net/http" "strings" "github.com/netcell-it/deklarix/internal/store" ) // standardGenehmigerRollen wird bei jeder neuen Firma automatisch angelegt // (ohne Mitglieder/Freigabe-Regeln) — der Admin muss nur noch Personen // zuordnen und bei Bedarf Freigabe-Regeln ergänzen, statt bei null // anzufangen. Die Beschreibung ist reine Orientierungshilfe im UI; welche // Bedingung welche Rolle tatsächlich auslöst, entscheidet weiterhin jede // Firma selbst über /verwaltung/freigabe-regeln — das System schreibt // keine fachliche Bindung vor. var standardGenehmigerRollen = []struct { Name string Beschreibung string }{ {"Datenschutzbeauftragter", "Prüft Anträge mit besonderen Datenschutzanforderungen, z. B. wenn eine Datenschutz-Folgenabschätzung erforderlich ist."}, {"Geschäftsführer", "Trifft die unternehmerische Entscheidung bei besonders risikobehafteten Vorhaben."}, {"KI-Manager", "Bewertet den Antrag fachlich."}, {"CISO", "Prüft den Antrag aus Sicherheitssicht (Auditor)."}, } // seedStandardGenehmigerRollen wird direkt nach dem Anlegen einer neuen // Firma aufgerufen (öffentliche Registrierung und Betreiber-Firmenanlage). func (s *Server) seedStandardGenehmigerRollen(ctx context.Context, accountID string) error { for _, r := range standardGenehmigerRollen { if _, err := s.store.CreateGenehmigerRolle(ctx, accountID, r.Name, r.Beschreibung); err != nil { return err } } return nil } // ─── Admin: Genehmiger-Rollen ─────────────────────────────────────── type genehmigerRolleItem struct { ID string Name string Beschreibung string Mitglieder []nutzerOption } type nutzerOption struct { ID string Email string } type genehmigerRollenListeData struct { Title string Nav navData Rollen []genehmigerRolleItem Nutzer []nutzerOption Error string } func (s *Server) handleGenehmigerRollenListe(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID rollen, err := s.store.ListGenehmigerRollenForAccount(r.Context(), accountID) if err != nil { http.Error(w, "Genehmiger-Rollen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } users, err := s.store.ListUsersForAccount(r.Context(), accountID) if err != nil { http.Error(w, "Nutzer konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := genehmigerRollenListeData{Title: "Genehmiger-Rollen", Nav: navFor(r)} for _, u := range users { data.Nutzer = append(data.Nutzer, nutzerOption{ID: u.ID, Email: u.Email}) } for _, g := range rollen { item := genehmigerRolleItem{ID: g.ID, Name: g.Name, Beschreibung: g.Beschreibung} mitglieder, err := s.store.ListNutzerForGenehmigerRolle(r.Context(), g.ID) if err == nil { for _, m := range mitglieder { item.Mitglieder = append(item.Mitglieder, nutzerOption{ID: m.UserID, Email: m.Email}) } } data.Rollen = append(data.Rollen, item) } if err := s.templates.ExecuteTemplate(w, "genehmiger-rollen-liste", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } func (s *Server) handleGenehmigerRolleCreate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { http.Error(w, "Name ist Pflicht", http.StatusBadRequest) return } beschreibung := strings.TrimSpace(r.FormValue("beschreibung")) if _, err := s.store.CreateGenehmigerRolle(r.Context(), currentUser(r).AccountID, name, beschreibung); err != nil { http.Error(w, "Genehmiger-Rolle konnte nicht angelegt werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verwaltung/genehmiger-rollen", http.StatusSeeOther) } // handleGenehmigerRolleDelete entfernt eine Genehmiger-Rolle. Solange // noch Mitglieder zugewiesen sind oder eine Freigabe-Regel darauf // verweist, schlägt das per Fremdschlüssel fehl — bewusst kein CASCADE, // ein stillschweigendes Mit-Löschen wirksamer Freigabe-Regeln wäre // überraschend und sicherheitsrelevant. func (s *Server) handleGenehmigerRolleDelete(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") rolle, err := s.store.GetGenehmigerRolle(r.Context(), id) if err != nil || rolle.AccountID != currentUser(r).AccountID { http.Error(w, "Genehmiger-Rolle nicht gefunden", http.StatusNotFound) return } if err := s.store.DeleteGenehmigerRolle(r.Context(), id); err != nil { http.Error(w, "Genehmiger-Rolle wird noch verwendet (Mitglieder oder Freigabe-Regeln) und kann nicht gelöscht werden", http.StatusBadRequest) return } http.Redirect(w, r, "/verwaltung/genehmiger-rollen", http.StatusSeeOther) } func (s *Server) handleGenehmigerRolleMitgliedHinzufuegen(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID rolleID := r.PathValue("id") rolle, err := s.store.GetGenehmigerRolle(r.Context(), rolleID) if err != nil || rolle.AccountID != accountID { http.Error(w, "Genehmiger-Rolle nicht gefunden", http.StatusNotFound) return } if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } userID := r.FormValue("user_id") target, err := s.store.GetUser(r.Context(), userID) if err != nil || target.AccountID != accountID { http.Error(w, "Nutzer nicht gefunden", http.StatusNotFound) return } if err := s.store.AddNutzerGenehmigerRolle(r.Context(), userID, rolleID); err != nil { http.Error(w, "Zuweisung fehlgeschlagen: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verwaltung/genehmiger-rollen", http.StatusSeeOther) } func (s *Server) handleGenehmigerRolleMitgliedEntfernen(w http.ResponseWriter, r *http.Request) { accountID := currentUser(r).AccountID rolleID := r.PathValue("id") rolle, err := s.store.GetGenehmigerRolle(r.Context(), rolleID) if err != nil || rolle.AccountID != accountID { http.Error(w, "Genehmiger-Rolle nicht gefunden", http.StatusNotFound) return } userID := r.PathValue("userID") if err := s.store.RemoveNutzerGenehmigerRolle(r.Context(), userID, rolleID); err != nil { http.Error(w, "Entfernen fehlgeschlagen: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verwaltung/genehmiger-rollen", http.StatusSeeOther) } // ─── Admin: Freigabe-Regeln ───────────────────────────────────────── // bedingungOption ist ein Auswahlpunkt im Freigabe-Regel-Formular — // Value kodiert "typ:wert" (z. B. "anforderung:dsfa_erforderlich"), // damit ein einziges