package web import ( "net/http" "time" "github.com/netcell-it/deklarix/internal/auth" ) type betreiberDashboardData struct { Title string Nav navData AccountCount int WerkzeugCount int RecentAuditCount int } // handleBetreiberDashboard zeigt eine kurze Übersicht als Einstieg in // den Plattform-Bereich (Ebene 5 — nur Betreiber, technisch getrennt // vom Mandantenbereich). func (s *Server) handleBetreiberDashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() accounts, err := s.store.ListAccounts(ctx) if err != nil { http.Error(w, "Accounts konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } werkzeuge, err := s.store.ListZentraleWerkzeuge(ctx) if err != nil { http.Error(w, "Werkzeugkatalog konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } auditLog, err := s.store.ListAuditLog(ctx, 5) if err != nil { http.Error(w, "Audit-Log konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := betreiberDashboardData{ Title: "Plattform", Nav: navFor(r), AccountCount: len(accounts), WerkzeugCount: len(werkzeuge), RecentAuditCount: len(auditLog), } if err := s.templates.ExecuteTemplate(w, "betreiber-dashboard", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type betreiberAccountListItem struct { ID string Name string CreatedAt string } type betreiberAccountListData struct { Title string Nav navData Accounts []betreiberAccountListItem } // handleBetreiberAccountList listet alle Mandanten der Plattform. func (s *Server) handleBetreiberAccountList(w http.ResponseWriter, r *http.Request) { accounts, err := s.store.ListAccounts(r.Context()) if err != nil { http.Error(w, "Accounts konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := betreiberAccountListData{Title: "Accounts", Nav: navFor(r)} for _, a := range accounts { data.Accounts = append(data.Accounts, betreiberAccountListItem{ ID: a.ID, Name: a.Name, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), }) } if err := s.templates.ExecuteTemplate(w, "betreiber-accounts", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type betreiberUserView struct { ID string Email string Role string Active bool } type betreiberAccountDetailData struct { Title string Nav navData AccountID string Name string Users []betreiberUserView CreatedAt string } // handleBetreiberAccountDetail zeigt einen Mandanten mit seinen Logins. func (s *Server) handleBetreiberAccountDetail(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := s.store.GetAccount(ctx, r.PathValue("id")) if err != nil { http.Error(w, "Account nicht gefunden", http.StatusNotFound) return } users, err := s.store.ListUsersForAccount(ctx, acc.ID) if err != nil { http.Error(w, "Nutzer konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := betreiberAccountDetailData{ Title: "Account", Nav: navFor(r), AccountID: acc.ID, Name: acc.Name, CreatedAt: acc.CreatedAt.Format("02.01.2006 15:04"), } for _, u := range users { data.Users = append(data.Users, betreiberUserView{ID: u.ID, Email: u.Email, Role: u.Role, Active: u.Active}) } if err := s.templates.ExecuteTemplate(w, "betreiber-account-detail", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type betreiberAuditEntryView struct { CreatedAt string Action string TargetType string TargetID string Details string } type betreiberAuditLogData struct { Title string Nav navData Entries []betreiberAuditEntryView } // handleBetreiberAuditLog zeigt das plattformweite Audit-Log. func (s *Server) handleBetreiberAuditLog(w http.ResponseWriter, r *http.Request) { entries, err := s.store.ListAuditLog(r.Context(), 200) if err != nil { http.Error(w, "Audit-Log konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := betreiberAuditLogData{Title: "Audit-Log", Nav: navFor(r)} for _, e := range entries { data.Entries = append(data.Entries, betreiberAuditEntryView{ CreatedAt: e.CreatedAt.Format("02.01.2006 15:04:05"), Action: e.Action, TargetType: e.TargetType, TargetID: e.TargetID, Details: e.Details, }) } if err := s.templates.ExecuteTemplate(w, "betreiber-audit-log", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleBetreiberLoginAls meldet den Betreiber als einen bestimmten // Kunden-Nutzer an (Support-Login) — ohne dessen Passwort zu kennen. // Nur für aktive Nutzer möglich (ein deaktivierter Login soll auch per // Support-Zugriff nicht nutzbar sein). Die neue Sitzung ersetzt die // eigene Betreiber-Sitzung (kein Sitzungs-Stack) — der Betreiber meldet // sich danach mit den eigenen Zugangsdaten wieder an, das ist für die // seltene Nutzung dieser Funktion einfacher als ein "Zurück zum // Betreiber"-Mechanismus. Jede Nutzung wird append-only im Audit-Log // festgehalten (Actor = Betreiber, Target = der Kunden-Nutzer) und die // entstehende Sitzung trägt den Betreiber als ImpersonatedByUserID — // die Nav zeigt während der gesamten Sitzung einen auffälligen Hinweis // (siehe currentImpersonator, layout.html), damit nie unklar ist, dass // gerade im Kontext eines fremden Kontos gehandelt wird. func (s *Server) handleBetreiberLoginAls(w http.ResponseWriter, r *http.Request) { betreiber := currentUser(r) ziel, err := s.store.GetUser(r.Context(), r.PathValue("userID")) if err != nil || ziel.AccountID != r.PathValue("accountID") { http.Error(w, "Nutzer nicht gefunden", http.StatusNotFound) return } if !ziel.Active { http.Error(w, "Nutzer ist deaktiviert", http.StatusBadRequest) return } token, err := auth.NewSessionToken() if err != nil { http.Error(w, "Sitzung konnte nicht erzeugt werden: "+err.Error(), http.StatusInternalServerError) return } expiresAt := time.Now().Add(auth.SessionDuration) if _, err := s.store.CreateImpersonatedSession(r.Context(), token, ziel.ID, betreiber.ID, expiresAt); err != nil { http.Error(w, "Sitzung konnte nicht erzeugt werden: "+err.Error(), http.StatusInternalServerError) return } if _, err := s.store.CreateAuditEntry(r.Context(), betreiber.ID, "betreiber_login_als_nutzer", "app_user", ziel.ID, ziel.Email); err != nil { http.Error(w, "Audit-Log konnte nicht geschrieben werden: "+err.Error(), http.StatusInternalServerError) return } setSessionCookie(w, r, token, expiresAt) http.Redirect(w, r, "/", http.StatusSeeOther) }