Files
archivdms/internal/api/document_note_handlers.go
T
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

112 lines
4.3 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"archivdms/internal/audit"
"archivdms/internal/auth"
"archivdms/internal/storage"
"archivdms/internal/userstore"
)
// handleListDocumentNotes returns all free-text notes on a document
// (GET /api/documents/{id}/notes). Tenant-scoped: ListDocumentNotes filters
// WHERE tenant_id, so a foreign-tenant id simply yields an empty list. Pure
// read — no audit entry, consistent with the other GET handlers.
func (s *Server) handleListDocumentNotes(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "invalid document id")
return
}
notes, err := s.store.ListDocumentNotes(r.Context(), id, *sess.TenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "list notes failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"notes": notes})
}
type createNoteRequest struct {
Text string `json:"text"`
}
// handleCreateDocumentNote adds a free-text note to a document
// (POST /api/documents/{id}/notes, body {"text": "..."}). The author is the
// authenticated user. CreateDocumentNote verifies the document belongs to the
// tenant before inserting (IDOR guard).
func (s *Server) handleCreateDocumentNote(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "invalid document id")
return
}
var req createNoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
text := strings.TrimSpace(req.Text)
if text == "" {
writeError(w, http.StatusBadRequest, "text is required")
return
}
note, err := s.store.CreateDocumentNote(r.Context(), id, *sess.TenantID, sess.UserID, text)
if err != nil {
status := http.StatusInternalServerError
msg := "create note failed"
if errors.Is(err, storage.ErrDocumentNotFound) {
status = http.StatusNotFound
msg = "document not found"
}
s.audlog.Log(audit.Entry{EventType: audit.EventNoteCreate, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: false, Detail: err.Error()})
writeError(w, status, msg)
return
}
s.audlog.Log(audit.Entry{EventType: audit.EventNoteCreate, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true, Detail: "note_id=" + strconv.FormatInt(note.ID, 10)})
writeJSON(w, http.StatusCreated, note)
}
// handleDeleteDocumentNote hard-deletes a note
// (DELETE /api/documents/{id}/notes/{noteId}). Only the note's author or a
// domain admin may delete it. 403 when not permitted, 404 when the note does
// not exist for this document/tenant.
func (s *Server) handleDeleteDocumentNote(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || sess.TenantID == nil {
writeError(w, http.StatusBadRequest, "invalid document id")
return
}
noteID, err := strconv.ParseInt(r.PathValue("noteId"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note id")
return
}
isAdmin := auth.HasRole(sess.Role, userstore.RoleDomainAdmin)
if err := s.store.DeleteDocumentNote(r.Context(), noteID, id, *sess.TenantID, sess.UserID, isAdmin); err != nil {
status := http.StatusInternalServerError
msg := "delete note failed"
if errors.Is(err, storage.ErrNoteForbidden) {
status = http.StatusForbidden
msg = "not allowed to delete this note"
} else if errors.Is(err, storage.ErrNoteNotFound) {
status = http.StatusNotFound
msg = "note not found"
}
s.audlog.Log(audit.Entry{EventType: audit.EventNoteDelete, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: false, Detail: "note_id=" + r.PathValue("noteId") + ": " + err.Error()})
writeError(w, status, msg)
return
}
s.audlog.Log(audit.Entry{EventType: audit.EventNoteDelete, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true, Detail: "note_id=" + r.PathValue("noteId")})
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}