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