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"}) }