Files
archivdms/internal/api/reminder_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

172 lines
5.7 KiB
Go

// Wiedervorlage (reminder) HTTP handlers:
// POST /api/documents/{id}/reminders
// GET /api/reminders?status=
// PATCH /api/reminders/{id}
// DELETE /api/reminders/{id}
//
// All routes require s.auth(...) (authenticated + tenant context). Ownership
// is enforced in the store layer (id+tenant_id+user_id). Every mutation is
// audit-logged, including failures.
package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"archivdms/internal/audit"
"archivdms/internal/storage"
)
type createReminderRequest struct {
DueDate string `json:"due_date"` // RFC3339
Note string `json:"note"`
}
// handleCreateReminder handles POST /api/documents/{id}/reminders.
func (s *Server) handleCreateReminder(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusForbidden, "tenant context required")
return
}
docID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid document id")
return
}
var req createReminderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
dueDate, err := time.Parse(time.RFC3339, req.DueDate)
if err != nil {
writeError(w, http.StatusBadRequest, "due_date must be RFC3339")
return
}
// Verify the document exists and belongs to the caller's tenant before
// attaching a reminder to it (the FK alone would only stop a fully
// nonexistent document_id, not a cross-tenant one).
if _, err := s.store.GetDocument(r.Context(), docID, *sess.TenantID); err != nil {
writeError(w, http.StatusNotFound, "document not found")
return
}
rem, err := s.store.CreateReminder(r.Context(), docID, *sess.TenantID, sess.UserID, dueDate, req.Note)
if err != nil {
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderCreate, Username: sess.Username, TenantID: sess.TenantID,
DocumentID: strconv.FormatInt(docID, 10), Success: false, Detail: err.Error(),
})
writeError(w, http.StatusInternalServerError, "create reminder failed")
return
}
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderCreate, Username: sess.Username, TenantID: sess.TenantID,
DocumentID: strconv.FormatInt(docID, 10), Success: true,
Detail: "reminder_id:" + strconv.FormatInt(rem.ID, 10),
})
writeJSON(w, http.StatusCreated, rem)
}
// handleListReminders handles GET /api/reminders?status=.
func (s *Server) handleListReminders(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusForbidden, "tenant context required")
return
}
status := r.URL.Query().Get("status")
if status != "" && status != storage.ReminderStatusOpen && status != storage.ReminderStatusDone && status != storage.ReminderStatusDismissed {
writeError(w, http.StatusBadRequest, "invalid status filter")
return
}
reminders, err := s.store.ListReminders(r.Context(), *sess.TenantID, sess.UserID, status)
if err != nil {
writeError(w, http.StatusInternalServerError, "list reminders failed")
return
}
writeJSON(w, http.StatusOK, reminders)
}
type updateReminderRequest struct {
Status string `json:"status"`
}
// handleUpdateReminder handles PATCH /api/reminders/{id}.
func (s *Server) handleUpdateReminder(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusForbidden, "tenant context required")
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid reminder id")
return
}
var req updateReminderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
switch req.Status {
case storage.ReminderStatusOpen, storage.ReminderStatusDone, storage.ReminderStatusDismissed:
default:
writeError(w, http.StatusBadRequest, "invalid status")
return
}
rem, err := s.store.UpdateReminderStatus(r.Context(), id, *sess.TenantID, sess.UserID, req.Status)
if err != nil {
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderStatusChange, Username: sess.Username, TenantID: sess.TenantID,
Detail: "reminder_id:" + strconv.FormatInt(id, 10) + " err:" + err.Error(), Success: false,
})
writeError(w, http.StatusNotFound, "reminder not found")
return
}
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderStatusChange, Username: sess.Username, TenantID: sess.TenantID,
DocumentID: strconv.FormatInt(rem.DocumentID, 10), Success: true,
Detail: "reminder_id:" + strconv.FormatInt(rem.ID, 10) + " status:" + rem.Status,
})
writeJSON(w, http.StatusOK, rem)
}
// handleDeleteReminder handles DELETE /api/reminders/{id}.
func (s *Server) handleDeleteReminder(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
if sess.TenantID == nil {
writeError(w, http.StatusForbidden, "tenant context required")
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid reminder id")
return
}
if err := s.store.DeleteReminder(r.Context(), id, *sess.TenantID, sess.UserID); err != nil {
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderDelete, Username: sess.Username, TenantID: sess.TenantID,
Detail: "reminder_id:" + strconv.FormatInt(id, 10) + " err:" + err.Error(), Success: false,
})
writeError(w, http.StatusNotFound, "reminder not found")
return
}
s.audlog.Log(audit.Entry{
EventType: audit.EventReminderDelete, Username: sess.Username, TenantID: sess.TenantID,
Detail: "reminder_id:" + strconv.FormatInt(id, 10), Success: true,
})
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}