Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
112 lines
4.3 KiB
Go
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"})
|
|
}
|