dealspace/api/handlers.go

198 lines
5.5 KiB
Go

package api
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/mish/dealspace/lib"
)
// Handlers holds dependencies for HTTP handlers.
type Handlers struct {
DB *lib.DB
Cfg *lib.Config
Store lib.ObjectStore
}
// NewHandlers creates a new Handlers instance.
func NewHandlers(db *lib.DB, cfg *lib.Config, store lib.ObjectStore) *Handlers {
return &Handlers{DB: db, Cfg: cfg, Store: store}
}
// Health returns server status.
func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, http.StatusOK, map[string]string{"status": "ok"})
}
// ListEntries returns entries for a project, filtered by query params.
func (h *Handlers) ListEntries(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")
if projectID == "" {
ErrorResponse(w, http.StatusBadRequest, "missing_project", "Project ID required")
return
}
filter := lib.EntryFilter{
ProjectID: projectID,
Type: r.URL.Query().Get("type"),
Stage: r.URL.Query().Get("stage"),
}
if parent := r.URL.Query().Get("parent_id"); parent != "" {
filter.ParentID = &parent
}
entries, err := lib.EntryRead(h.DB, h.Cfg, actorID, projectID, filter)
if err != nil {
if err == lib.ErrAccessDenied {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to read entries")
return
}
JSONResponse(w, http.StatusOK, entries)
}
// CreateEntry creates a new entry.
func (h *Handlers) CreateEntry(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
var req struct {
ProjectID string `json:"project_id"`
ParentID string `json:"parent_id"`
Type string `json:"type"`
Depth int `json:"depth"`
Summary string `json:"summary"`
Data string `json:"data"`
Stage string `json:"stage"`
AssigneeID string `json:"assignee_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
entry := &lib.Entry{
ProjectID: req.ProjectID,
ParentID: req.ParentID,
Type: req.Type,
Depth: req.Depth,
SummaryText: req.Summary,
DataText: req.Data,
Stage: req.Stage,
AssigneeID: req.AssigneeID,
}
if entry.Stage == "" {
entry.Stage = lib.StagePreDataroom
}
if err := lib.EntryWrite(h.DB, h.Cfg, actorID, entry); err != nil {
if err == lib.ErrAccessDenied {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create entry")
return
}
JSONResponse(w, http.StatusCreated, entry)
}
// UpdateEntry updates an existing entry.
func (h *Handlers) UpdateEntry(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
entryID := chi.URLParam(r, "entryID")
var req struct {
ProjectID string `json:"project_id"`
ParentID string `json:"parent_id"`
Type string `json:"type"`
Depth int `json:"depth"`
Summary string `json:"summary"`
Data string `json:"data"`
Stage string `json:"stage"`
AssigneeID string `json:"assignee_id"`
Version int `json:"version"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
entry := &lib.Entry{
EntryID: entryID,
ProjectID: req.ProjectID,
ParentID: req.ParentID,
Type: req.Type,
Depth: req.Depth,
SummaryText: req.Summary,
DataText: req.Data,
Stage: req.Stage,
AssigneeID: req.AssigneeID,
Version: req.Version,
}
if err := lib.EntryWrite(h.DB, h.Cfg, actorID, entry); err != nil {
if err == lib.ErrAccessDenied {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
if err == lib.ErrVersionConflict {
ErrorResponse(w, http.StatusConflict, "version_conflict", err.Error())
return
}
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to update entry")
return
}
JSONResponse(w, http.StatusOK, entry)
}
// DeleteEntry soft-deletes an entry.
func (h *Handlers) DeleteEntry(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
entryID := chi.URLParam(r, "entryID")
projectID := chi.URLParam(r, "projectID")
if err := lib.EntryDelete(h.DB, actorID, projectID, entryID); err != nil {
if err == lib.ErrAccessDenied {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
if err == lib.ErrNotFound {
ErrorResponse(w, http.StatusNotFound, "not_found", "Entry not found")
return
}
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to delete entry")
return
}
JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// GetMyTasks returns entries assigned to the current user.
func (h *Handlers) GetMyTasks(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")
filter := lib.EntryFilter{
ProjectID: projectID,
AssigneeID: actorID,
}
entries, err := lib.EntryRead(h.DB, h.Cfg, actorID, projectID, filter)
if err != nil {
if err == lib.ErrAccessDenied {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to read tasks")
return
}
JSONResponse(w, http.StatusOK, entries)
}