feat: Archiv-Übersicht und Beteiligten-CRUD im Web-Layer
Bisher gab es nur Prüfen -> Archivieren, keine Möglichkeit, bereits
geprüfte Beiträge wieder anzusehen oder die Verantwortungsmatrix
(Beteiligte: wer hat vorgegeben, wer freigegeben) tatsächlich zu
pflegen — nur der Store-Layer dafür existierte schon.
Neu:
- GET /beitraege: Liste aller Beiträge des angemeldeten Mandanten
(Plattform, Status, höchste Finding-Schwere) via
ListSubmissionsForAccount.
- GET /beitraege/{id}: Detailseite mit Fakten, Findings, Archivieren-
Aktion und Verantwortungsmatrix.
- POST .../beteiligte, .../beteiligte/{pid}/aktualisieren,
.../beteiligte/{pid}/loeschen: echtes CRUD statt nur Ansicht, per
htmx ohne Seiten-Reload.
Mandantentrennung wie beim bestehenden Archiv-Download: fremde
Beiträge und fremde Beteiligte (auch über eine erratene participant_id)
liefern 404, nicht 403 — sonst würde eine 403 die Existenz der Ressource
bei einem anderen Mandanten bestätigen.
Web-Store-Interface um die bereits fertigen Store-Methoden erweitert,
fakeStore in server_test.go entsprechend nachgezogen. Volle Testsuite
inkl. echter Postgres-Tests (./scripts/test.sh) grün.
This commit is contained in:
@@ -66,6 +66,7 @@ type fakeStore struct {
|
||||
extractions map[string]store.Extraction
|
||||
findings map[string][]store.Finding
|
||||
evidencePkgs map[string]store.EvidencePackage
|
||||
participants map[string]store.Participant
|
||||
}
|
||||
|
||||
func newFakeStore() *fakeStore {
|
||||
@@ -78,6 +79,7 @@ func newFakeStore() *fakeStore {
|
||||
extractions: map[string]store.Extraction{},
|
||||
findings: map[string][]store.Finding{},
|
||||
evidencePkgs: map[string]store.EvidencePackage{},
|
||||
participants: map[string]store.Participant{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +249,106 @@ func (f *fakeStore) GetLatestEvidencePackage(ctx context.Context, submissionID s
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ListSubmissionsForAccount(ctx context.Context, accountID string) ([]store.SubmissionSummary, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []store.SubmissionSummary
|
||||
for _, sub := range f.submissions {
|
||||
if sub.AccountID != accountID {
|
||||
continue
|
||||
}
|
||||
sum := store.SubmissionSummary{Submission: sub}
|
||||
rank := 0
|
||||
for _, finding := range f.findings[sub.ID] {
|
||||
sum.FindingCount++
|
||||
r := severityRank(finding.Severity)
|
||||
if r > rank {
|
||||
rank = r
|
||||
sum.HighestSeverity = finding.Severity
|
||||
}
|
||||
}
|
||||
out = append(out, sum)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func severityRank(severity string) int {
|
||||
switch severity {
|
||||
case "hoch":
|
||||
return 3
|
||||
case "mittel":
|
||||
return 2
|
||||
case "niedrig":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateParticipant(ctx context.Context, submissionID, role, name string, vorgegeben, freigegeben bool) (store.Participant, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
p := store.Participant{
|
||||
ID: f.newID(), SubmissionID: submissionID, Role: role, Name: name,
|
||||
Vorgegeben: vorgegeben, Freigegeben: freigegeben, CreatedAt: time.Now(),
|
||||
}
|
||||
if freigegeben {
|
||||
now := time.Now()
|
||||
p.ApprovedAt = &now
|
||||
}
|
||||
f.participants[p.ID] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ListParticipants(ctx context.Context, submissionID string) ([]store.Participant, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []store.Participant
|
||||
for _, p := range f.participants {
|
||||
if p.SubmissionID == submissionID {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) GetParticipant(ctx context.Context, id string) (store.Participant, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
p, ok := f.participants[id]
|
||||
if !ok {
|
||||
return store.Participant{}, store.ErrNotFound
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) UpdateParticipant(ctx context.Context, id string, vorgegeben, freigegeben bool) (store.Participant, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
p, ok := f.participants[id]
|
||||
if !ok {
|
||||
return store.Participant{}, store.ErrNotFound
|
||||
}
|
||||
p.Vorgegeben = vorgegeben
|
||||
p.Freigegeben = freigegeben
|
||||
if freigegeben && p.ApprovedAt == nil {
|
||||
now := time.Now()
|
||||
p.ApprovedAt = &now
|
||||
}
|
||||
f.participants[id] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) DeleteParticipant(ctx context.Context, id string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.participants[id]; !ok {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
delete(f.participants, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// fakeTimestamper liefert einen offline erzeugten, strukturell gültigen
|
||||
// (selbstsignierten) RFC-3161-Token — genug, damit evidence.TimestampTime
|
||||
// ihn parsen kann, ohne eine echte TSA zu brauchen.
|
||||
|
||||
Reference in New Issue
Block a user