package web import ( "net/http" "github.com/netcell-it/deklarix/internal/store" ) type submissionListItem struct { ID string Platform string PostType string Status string CreatedAt string FindingCount int HighestSeverity string } type submissionListData struct { Title string Submissions []submissionListItem } // handleSubmissionList zeigt die Archiv-Übersicht: alle Beiträge des // angemeldeten Mandanten mit Kurzfassung der Findings. func (s *Server) handleSubmissionList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() summaries, err := s.store.ListSubmissionsForAccount(ctx, currentUser(r).AccountID) if err != nil { http.Error(w, "Beiträge konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := submissionListData{Title: "Beiträge"} for _, sum := range summaries { data.Submissions = append(data.Submissions, submissionListItem{ ID: sum.ID, Platform: sum.Platform, PostType: sum.PostType, Status: sum.Status, CreatedAt: sum.CreatedAt.Format("02.01.2006 15:04"), FindingCount: sum.FindingCount, HighestSeverity: sum.HighestSeverity, }) } if err := s.templates.ExecuteTemplate(w, "beitraege", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type participantView struct { ID string Role string Name string Vorgegeben bool Freigegeben bool ApprovedAt string // leer, wenn noch nicht freigegeben } type submissionDetailData struct { Title string SubmissionID string Platform string PostType string Caption string Status string CreatedAt string CanArchive bool IsPublished bool DossierURL string Findings []findingView Participants []participantView } // loadOwnSubmission lädt eine Submission und prüft die Mandantenzugehörigkeit. // Wie in handleArchive/handleDossierDownload (siehe handlers.go): ein Beitrag // eines anderen Accounts wird wie ein nicht existierender behandelt. func (s *Server) loadOwnSubmission(r *http.Request, id string) (store.Submission, error) { sub, err := s.store.GetSubmission(r.Context(), id) if err != nil { return store.Submission{}, err } if sub.AccountID != currentUser(r).AccountID { return store.Submission{}, store.ErrNotFound } return sub, nil } func toParticipantViews(participants []store.Participant) []participantView { views := make([]participantView, len(participants)) for i, p := range participants { v := participantView{ID: p.ID, Role: p.Role, Name: p.Name, Vorgegeben: p.Vorgegeben, Freigegeben: p.Freigegeben} if p.ApprovedAt != nil { v.ApprovedAt = p.ApprovedAt.Format("02.01.2006 15:04") } views[i] = v } return views } // handleSubmissionDetail zeigt einen einzelnen Beitrag: Fakten, Findings, // Verantwortungsmatrix (Beteiligte) inklusive Verwaltung. func (s *Server) handleSubmissionDetail(w http.ResponseWriter, r *http.Request) { ctx := r.Context() sub, err := s.loadOwnSubmission(r, r.PathValue("id")) if err != nil { http.Error(w, "Beitrag nicht gefunden", http.StatusNotFound) return } storeFindings, err := s.store.ListCurrentFindings(ctx, sub.ID) if err != nil { http.Error(w, "Findings konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } findings := make([]findingView, len(storeFindings)) for i, f := range storeFindings { findings[i] = findingView{ RuleID: f.RuleID, Version: f.RuleVersion, Severity: f.Severity, Title: f.Title, Fix: f.Fix, Sources: f.Sources, } } participants, err := s.store.ListParticipants(ctx, sub.ID) if err != nil { http.Error(w, "Beteiligte konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := submissionDetailData{ Title: "Beitrag", SubmissionID: sub.ID, Platform: sub.Platform, PostType: sub.PostType, Caption: sub.Caption, Status: sub.Status, CreatedAt: sub.CreatedAt.Format("02.01.2006 15:04"), CanArchive: sub.Status == "checked", IsPublished: sub.Status == "published", DossierURL: "/dossier/" + sub.ID, Findings: findings, Participants: toParticipantViews(participants), } if err := s.templates.ExecuteTemplate(w, "beitrag", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } type participantListData struct { SubmissionID string Participants []participantView } // renderParticipantList rendert das Beteiligten-Fragment neu — Ziel für // htmx-Swaps nach Hinzufügen/Ändern/Löschen, damit die Seite nicht neu // geladen werden muss. func (s *Server) renderParticipantList(w http.ResponseWriter, r *http.Request, submissionID string) { participants, err := s.store.ListParticipants(r.Context(), submissionID) if err != nil { http.Error(w, "Beteiligte konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := participantListData{SubmissionID: submissionID, Participants: toParticipantViews(participants)} if err := s.templates.ExecuteTemplate(w, "beteiligte-liste", data); err != nil { http.Error(w, "Liste konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleAddParticipant fügt einen Beteiligten zu einem Beitrag hinzu. func (s *Server) handleAddParticipant(w http.ResponseWriter, r *http.Request) { sub, err := s.loadOwnSubmission(r, r.PathValue("id")) if err != nil { http.Error(w, "Beitrag nicht gefunden", http.StatusNotFound) return } if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } role := r.FormValue("role") name := r.FormValue("name") if role == "" || name == "" { http.Error(w, "Rolle und Name sind Pflichtfelder", http.StatusBadRequest) return } if _, err := s.store.CreateParticipant(r.Context(), sub.ID, role, name, false, false); err != nil { http.Error(w, "Beteiligter konnte nicht angelegt werden: "+err.Error(), http.StatusInternalServerError) return } s.renderParticipantList(w, r, sub.ID) } // participantBelongsToOwnSubmission prüft, dass der Beteiligte tatsächlich // zu einem Beitrag des angemeldeten Mandanten gehört — sonst könnte ein // fremder Mandant über eine erratene participant_id einen Beteiligten // eines anderen Accounts ändern oder löschen. func (s *Server) participantBelongsToOwnSubmission(r *http.Request, submissionIDFromURL, participantID string) (store.Participant, error) { p, err := s.store.GetParticipant(r.Context(), participantID) if err != nil { return store.Participant{}, err } if p.SubmissionID != submissionIDFromURL { return store.Participant{}, store.ErrNotFound } if _, err := s.loadOwnSubmission(r, p.SubmissionID); err != nil { return store.Participant{}, err } return p, nil } // handleUpdateParticipant setzt vorgegeben/freigegeben für einen Beteiligten. func (s *Server) handleUpdateParticipant(w http.ResponseWriter, r *http.Request) { submissionID := r.PathValue("id") p, err := s.participantBelongsToOwnSubmission(r, submissionID, r.PathValue("pid")) if err != nil { http.Error(w, "Beteiligter nicht gefunden", http.StatusNotFound) return } if err := r.ParseForm(); err != nil { http.Error(w, "ungültiges Formular", http.StatusBadRequest) return } vorgegeben := r.FormValue("vorgegeben") == "on" freigegeben := r.FormValue("freigegeben") == "on" if _, err := s.store.UpdateParticipant(r.Context(), p.ID, vorgegeben, freigegeben); err != nil { http.Error(w, "Beteiligter konnte nicht aktualisiert werden: "+err.Error(), http.StatusInternalServerError) return } s.renderParticipantList(w, r, submissionID) } // handleDeleteParticipant entfernt einen Beteiligten. func (s *Server) handleDeleteParticipant(w http.ResponseWriter, r *http.Request) { submissionID := r.PathValue("id") p, err := s.participantBelongsToOwnSubmission(r, submissionID, r.PathValue("pid")) if err != nil { http.Error(w, "Beteiligter nicht gefunden", http.StatusNotFound) return } if err := s.store.DeleteParticipant(r.Context(), p.ID); err != nil { http.Error(w, "Beteiligter konnte nicht gelöscht werden: "+err.Error(), http.StatusInternalServerError) return } s.renderParticipantList(w, r, submissionID) }