Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
141 lines
5.5 KiB
Go
141 lines
5.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"archivdms/internal/audit"
|
|
"archivdms/internal/storage"
|
|
)
|
|
|
|
// handleListSavedViews serves GET /api/saved-views — the caller's own saved
|
|
// search views plus every view shared tenant-wide (is_shared). Pure read, not
|
|
// audited (consistent with the rest of the project).
|
|
func (s *Server) handleListSavedViews(w http.ResponseWriter, r *http.Request) {
|
|
sess := sessionFromCtx(r.Context())
|
|
if sess.TenantID == nil {
|
|
writeError(w, http.StatusForbidden, "tenant context required")
|
|
return
|
|
}
|
|
views, err := s.store.ListSavedViews(r.Context(), *sess.TenantID, sess.UserID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "list saved views failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"views": views})
|
|
}
|
|
|
|
// savedViewRequest is the create/update body. Filters carries the serialized
|
|
// search query verbatim (see index.SearchQuery / handleSearchDocuments); it is
|
|
// stored as-is in the saved_views.filters JSONB column so the client can
|
|
// re-hydrate it 1:1 into a new search request.
|
|
type savedViewRequest struct {
|
|
Name string `json:"name"`
|
|
Filters json.RawMessage `json:"filters"`
|
|
IsShared bool `json:"is_shared"`
|
|
}
|
|
|
|
// handleCreateSavedView serves POST /api/saved-views.
|
|
func (s *Server) handleCreateSavedView(w http.ResponseWriter, r *http.Request) {
|
|
sess := sessionFromCtx(r.Context())
|
|
if sess.TenantID == nil {
|
|
writeError(w, http.StatusForbidden, "tenant context required")
|
|
return
|
|
}
|
|
var req savedViewRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
name := strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
// Default an empty/omitted filters payload to an empty JSON object so the
|
|
// NOT NULL JSONB column always receives valid JSON.
|
|
if len(req.Filters) == 0 {
|
|
req.Filters = json.RawMessage("{}")
|
|
}
|
|
|
|
view, err := s.store.CreateSavedView(r.Context(), *sess.TenantID, sess.UserID, name, req.Filters, req.IsShared)
|
|
if err != nil {
|
|
s.audlog.Log(audit.Entry{EventType: audit.EventSavedViewCreate, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: err.Error()})
|
|
writeError(w, http.StatusInternalServerError, "create saved view failed")
|
|
return
|
|
}
|
|
s.audlog.Log(audit.Entry{
|
|
EventType: audit.EventSavedViewCreate, Username: sess.Username, TenantID: sess.TenantID,
|
|
Success: true, Detail: "saved_view_id=" + strconv.FormatInt(view.ID, 10),
|
|
})
|
|
writeJSON(w, http.StatusCreated, view)
|
|
}
|
|
|
|
// handleUpdateSavedView serves PATCH /api/saved-views/{id}. Only the view's
|
|
// creator may update it (403 otherwise).
|
|
func (s *Server) handleUpdateSavedView(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 saved view id")
|
|
return
|
|
}
|
|
var req savedViewRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
name := strings.TrimSpace(req.Name)
|
|
if name == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if len(req.Filters) == 0 {
|
|
req.Filters = json.RawMessage("{}")
|
|
}
|
|
|
|
if err := s.store.UpdateSavedView(r.Context(), id, *sess.TenantID, sess.UserID, name, req.Filters, req.IsShared); err != nil {
|
|
status, msg := savedViewErrStatus(err, "update saved view failed")
|
|
s.audlog.Log(audit.Entry{EventType: audit.EventSavedViewUpdate, 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.EventSavedViewUpdate, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true})
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
|
}
|
|
|
|
// handleDeleteSavedView serves DELETE /api/saved-views/{id}. Only the view's
|
|
// creator may delete it (403 otherwise).
|
|
func (s *Server) handleDeleteSavedView(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 saved view id")
|
|
return
|
|
}
|
|
if err := s.store.DeleteSavedView(r.Context(), id, *sess.TenantID, sess.UserID); err != nil {
|
|
status, msg := savedViewErrStatus(err, "delete saved view failed")
|
|
s.audlog.Log(audit.Entry{EventType: audit.EventSavedViewDelete, 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.EventSavedViewDelete, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true})
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
|
}
|
|
|
|
// savedViewErrStatus maps store errors to an HTTP status + message: not-found
|
|
// -> 404, forbidden (exists but owned by another user) -> 403, else 500.
|
|
func savedViewErrStatus(err error, defaultMsg string) (int, string) {
|
|
switch {
|
|
case errors.Is(err, storage.ErrSavedViewNotFound):
|
|
return http.StatusNotFound, "saved view not found"
|
|
case errors.Is(err, storage.ErrSavedViewForbidden):
|
|
return http.StatusForbidden, "not allowed to modify this saved view"
|
|
default:
|
|
return http.StatusInternalServerError, defaultMsg
|
|
}
|
|
}
|