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