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.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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"})
|
||||
}
|
||||
Reference in New Issue
Block a user