198 lines
5.5 KiB
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)
|
|
}
|