package web import ( "net/http" ) type kanzleiListItem struct { Name string } type kanzleiListData struct { Title string Kanzleien []kanzleiListItem } // handlePublicKanzleiList zeigt das öffentliche, kostenlose Kanzlei- // Verzeichnis — keine Anmeldung nötig. Bewusst nur Name, kein Kontakt- // Button/Routing (siehe CLAUDE.md, § 49b Abs. 3 BRAO): Nutzer wählen // selbst, es gibt keine Vermittlung. func (s *Server) handlePublicKanzleiList(w http.ResponseWriter, r *http.Request) { accounts, err := s.store.ListVerifiedKanzleien(r.Context()) if err != nil { http.Error(w, "Verzeichnis konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := kanzleiListData{Title: "Kanzlei-Verzeichnis"} for _, a := range accounts { data.Kanzleien = append(data.Kanzleien, kanzleiListItem{Name: a.Name}) } if err := s.templates.ExecuteTemplate(w, "kanzleien", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type adminDashboardData struct { Title string Nav navData AccountCount int UnverifiedCount int RecentAuditCount int } // handleAdminDashboard zeigt eine kurze Übersicht als Einstieg in den // Admin-Bereich. func (s *Server) handleAdminDashboard(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 } unverified := 0 for _, a := range accounts { if !a.Verified { unverified++ } } 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 := adminDashboardData{ Title: "Admin", Nav: navFor(r), AccountCount: len(accounts), UnverifiedCount: unverified, RecentAuditCount: len(auditLog), } if err := s.templates.ExecuteTemplate(w, "admin-dashboard", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type adminAccountListItem struct { ID string Name string Verified bool CreatedAt string } type adminAccountListData struct { Title string Nav navData Accounts []adminAccountListItem } // handleAdminAccountList listet alle Mandanten der Plattform. func (s *Server) handleAdminAccountList(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 := adminAccountListData{Title: "Accounts", Nav: navFor(r)} for _, a := range accounts { data.Accounts = append(data.Accounts, adminAccountListItem{ ID: a.ID, Name: a.Name, Verified: a.Verified, CreatedAt: a.CreatedAt.Format("02.01.2006 15:04"), }) } if err := s.templates.ExecuteTemplate(w, "admin-accounts", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type adminUserView struct { Email string Role string } type adminAccountDetailData struct { Title string Nav navData AccountID string Name string Verified bool HasKanzlei bool Users []adminUserView CreatedAt string } // handleAdminAccountDetail zeigt einen Mandanten mit seinen Logins und — // falls mindestens ein Login die Rolle "kanzlei" hat — der Möglichkeit, // die Freigabe fürs öffentliche Kanzlei-Verzeichnis zu setzen. func (s *Server) handleAdminAccountDetail(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 := adminAccountDetailData{ Title: "Account", Nav: navFor(r), AccountID: acc.ID, Name: acc.Name, Verified: acc.Verified, CreatedAt: acc.CreatedAt.Format("02.01.2006 15:04"), } for _, u := range users { data.Users = append(data.Users, adminUserView{Email: u.Email, Role: u.Role}) if u.Role == "kanzlei" { data.HasKanzlei = true } } if err := s.templates.ExecuteTemplate(w, "admin-account-detail", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleAdminSetVerified schaltet die Freigabe fürs Kanzlei-Verzeichnis // um und protokolliert die Aktion im Audit-Log. func (s *Server) handleAdminSetVerified(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } accountID := r.PathValue("id") ctx := r.Context() if _, err := s.store.GetAccount(ctx, accountID); err != nil { http.Error(w, "Account nicht gefunden", http.StatusNotFound) return } verified := r.FormValue("verified") == "true" updated, err := s.store.SetAccountVerified(ctx, accountID, verified) if err != nil { http.Error(w, "Freigabe konnte nicht gesetzt werden: "+err.Error(), http.StatusInternalServerError) return } action := "account.unverified" details := "Kanzlei-Verzeichnis: Freigabe entzogen" if updated.Verified { action = "account.verified" details = "Kanzlei-Verzeichnis: Freigabe erteilt" } if _, err := s.store.CreateAuditEntry(ctx, currentUser(r).ID, action, "account", accountID, details); err != nil { http.Error(w, "Audit-Log konnte nicht geschrieben werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/accounts/"+accountID, http.StatusSeeOther) } type adminAuditEntryView struct { CreatedAt string Action string TargetType string TargetID string Details string } type adminAuditLogData struct { Title string Nav navData Entries []adminAuditEntryView } // handleAdminAuditLog zeigt das Protokoll der Admin-Aktionen. func (s *Server) handleAdminAuditLog(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 := adminAuditLogData{Title: "Audit-Log", Nav: navFor(r)} for _, e := range entries { data.Entries = append(data.Entries, adminAuditEntryView{ 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, "admin-audit-log", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } }