Files
patrickandClaude Sonnet 5 46c5b8ee64
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
UX-10: Bearer-Token statt caller in dms-browser, dms-upload, pdf-viewer; Bulk-Edit-Actor aus dem Token
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:06:17 +02:00

109 lines
3.4 KiB
Go

// Package bulkeditapi implementiert MET-03s Backend-Voraussetzung für die
// Massenbearbeitung: EIN HTTP-Endpunkt über internal/bulkedit (DOC-15).
// Keine eigene Massenbearbeitungslogik.
package bulkeditapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/bulkedit"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/metadatafield"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
)
type Server struct {
pool *pgxpool.Pool
lockChecker bulkedit.LockChecker
}
func NewServer(pool *pgxpool.Pool, lockChecker bulkedit.LockChecker) *Server {
return &Server{pool: pool, lockChecker: lockChecker}
}
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /bulk-edit", s.handleApply)
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
// Mit Modul-Token gilt allein dessen Nutzer; ein abweichendes Feld "actor" im Body wird ignoriert.
func resolveActor(ctx context.Context, bodyActor string) string {
if id, ok := moduleauth.IdentityFrom(ctx); ok && id.UserID != "" {
return id.UserID
}
return bodyActor
}
type metadataChangeDTO struct {
FieldID string `json:"field_id"`
Value *string `json:"value"`
}
type applyRequest struct {
Actor string `json:"actor"`
DocumentIDs []string `json:"document_ids"`
AddTagIDs []string `json:"add_tag_ids"`
RemoveTagIDs []string `json:"remove_tag_ids"`
MetadataChanges []metadataChangeDTO `json:"metadata_changes"`
}
type skippedDTO struct {
DocumentID string `json:"document_id"`
Reason string `json:"reason"`
}
type applyResult struct {
Applied []string `json:"applied"`
Skipped []skippedDTO `json:"skipped"`
}
// Akzeptanzkriterium 3 (MET-03) / Akzeptanzkriterium 1 (DOC-15):
// Massenbearbeitung über eine Dokumentauswahl, gesperrte Dokumente werden
// einzeln übersprungen statt die Operation als Ganzes abzulehnen.
func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
var req applyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error())
return
}
req.Actor = resolveActor(r.Context(), req.Actor)
if req.Actor == "" {
writeError(w, http.StatusBadRequest, "fehlendes feld \"actor\"")
return
}
changes := make([]bulkedit.MetadataChange, len(req.MetadataChanges))
for i, c := range req.MetadataChanges {
changes[i] = bulkedit.MetadataChange{FieldID: c.FieldID, Value: c.Value}
}
result, err := bulkedit.Apply(r.Context(), s.pool, s.lockChecker, req.Actor, bulkedit.Request{
DocumentIDs: req.DocumentIDs, AddTagIDs: req.AddTagIDs, RemoveTagIDs: req.RemoveTagIDs, MetadataChanges: changes,
})
if err != nil {
var ve *metadatafield.ErrValidation
if errors.As(err, &ve) {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
dto := applyResult{Applied: result.Applied, Skipped: make([]skippedDTO, len(result.Skipped))}
for i, sk := range result.Skipped {
dto.Skipped[i] = skippedDTO{DocumentID: sk.DocumentID, Reason: sk.Reason}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(dto)
}