feat: Standbild-Upload bei der Pre-Publish-Prüfung

CLAUDE.md beschreibt die Prüfung seit dem ersten Commit als "Caption,
Standbild und Vertragslage rein" — bisher wurde nur die Caption
verarbeitet, das asset-Schema aus Migration 0001 blieb ungenutzt.

- internal/store/asset.go: CreateAsset/GetLatestAssetForSubmission.
  Migration 0005 macht asset append-only (Trigger fehlte seit 0001,
  weil bis jetzt nichts hineinschrieb) — ein hochgeladenes Beweisstück
  wird nicht nachträglich ausgetauscht, aus demselben Grund wie bei
  extraction/finding/evidence_package.
- handleCheck liest ein optionales "standbild"-Formularfeld (Bild-
  Upload, max. 8 MiB, Content-Type muss image/* sein), validiert es
  VOR dem Anlegen der Submission (ein ungültiger Upload hinterlässt so
  keine leere Beitrags-Zeile), speichert es danach unter ASSET_DIR und
  legt die Asset-Zeile an.
- handleArchive bindet den Asset-Hash (falls vorhanden) in den
  Metadaten-Hash und ins PDF-Dossier ein (dossier.Data.AssetHash war
  bereits vorbereitet, wurde aber nie befüllt).
- index.html: Formular auf multipart/form-data umgestellt
  (hx-encoding + enctype), neues optionales Dateifeld. handleCheck
  bleibt abwärtskompatibel zu urlencoded-Requests (ParseMultipartForm
  liefert ErrNotMultipart, das wird wie "kein Bild hochgeladen"
  behandelt, nicht wie ein Fehler).
- ASSET_DIR neue Konfigurationsvariable (Default "assets", wie
  DOSSIER_DIR relativ zu WorkingDirectory=/var/lib/deklarix — kein
  postinst-Healing nötig, anders als bei RULES_DIR, dessen Default
  nicht zum installierten Pfad passt).

Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Upload, Hash in DB, Hash im
erzeugten PDF via pdftotext, Ablehnung bei falschem Dateityp).
This commit is contained in:
noroot
2026-08-27 21:45:03 +02:00
parent ae59e745c0
commit 790ab20651
12 changed files with 538 additions and 17 deletions

View File

@@ -70,6 +70,8 @@ type Store interface {
DeleteSession(ctx context.Context, token string) error
CreateAuditEntry(ctx context.Context, actorUserID, action, targetType, targetID, details string) (store.AuditEntry, error)
ListAuditLog(ctx context.Context, limit int) ([]store.AuditEntry, error)
CreateAsset(ctx context.Context, submissionID, kind, path, sha256Hex string) (store.Asset, error)
GetLatestAssetForSubmission(ctx context.Context, submissionID string) (store.Asset, error)
}
// Server bündelt Routing und Abhängigkeiten der Web-Schicht.
@@ -80,13 +82,15 @@ type Server struct {
store Store
timestamper evidence.Timestamper
dossierDir string
assetDir string
templates *template.Template
}
// NewServer erstellt den Server. ruleSet kommt von rules.Load und wird
// einmal beim Start geladen, nicht pro Request. dossierDir ist das
// Verzeichnis, in das erzeugte Nachweis-Dossiers geschrieben werden.
func NewServer(extractor Extractor, ruleSet []rules.Rule, st Store, timestamper evidence.Timestamper, dossierDir string) (*Server, error) {
// Verzeichnis, in das erzeugte Nachweis-Dossiers geschrieben werden;
// assetDir das Verzeichnis für hochgeladene Standbilder.
func NewServer(extractor Extractor, ruleSet []rules.Rule, st Store, timestamper evidence.Timestamper, dossierDir, assetDir string) (*Server, error) {
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("web: templates parsen: %w", err)
@@ -98,6 +102,7 @@ func NewServer(extractor Extractor, ruleSet []rules.Rule, st Store, timestamper
store: st,
timestamper: timestamper,
dossierDir: dossierDir,
assetDir: assetDir,
templates: tmpl,
}