dealspace/api/routes.go

129 lines
4.0 KiB
Go

package api
import (
"io/fs"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/mish/dealspace/lib"
)
// NewRouter creates the main router with all routes registered.
func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.FS, portalFS fs.FS) *chi.Mux {
r := chi.NewRouter()
h := NewHandlers(db, cfg, store)
// Global middleware
r.Use(LoggingMiddleware)
r.Use(CORSMiddleware)
r.Use(SecurityHeadersMiddleware)
r.Use(RateLimitMiddleware(120)) // 120 req/min per IP
// Health check (unauthenticated)
r.Get("/health", h.Health)
// Chat endpoint (unauthenticated, for Aria chatbot)
r.Post("/api/chat", h.ChatHandler)
r.Options("/api/chat", h.ChatHandler)
// Auth endpoints (unauthenticated — email challenge OTP flow)
r.Post("/api/auth/challenge", h.Challenge)
r.Post("/api/auth/verify", h.Verify)
// Auth endpoints (need token for logout/me)
r.Group(func(r chi.Router) {
r.Use(AuthMiddleware(db, cfg.JWTSecret))
r.Post("/api/auth/logout", h.Logout)
r.Get("/api/auth/me", h.Me)
})
// API routes (authenticated)
r.Route("/api", func(r chi.Router) {
r.Use(AuthMiddleware(db, cfg.JWTSecret))
// Tasks (cross-project)
r.Get("/tasks", h.GetAllTasks)
// Projects
r.Get("/projects", h.GetAllProjects)
r.Post("/projects", h.CreateProject)
r.Get("/projects/{projectID}", h.GetProjectDetail)
r.Get("/projects/{projectID}/detail", h.GetProjectDetail) // legacy alias
// Workstreams
r.Post("/projects/{projectID}/workstreams", h.CreateWorkstream)
// Entries
r.Get("/projects/{projectID}/entries", h.ListEntries)
r.Post("/projects/{projectID}/entries", h.CreateEntry)
r.Put("/projects/{projectID}/entries/{entryID}", h.UpdateEntry)
r.Delete("/projects/{projectID}/entries/{entryID}", h.DeleteEntry)
// Task inbox (per-project)
r.Get("/projects/{projectID}/tasks", h.GetMyTasks)
// Requests (list, tree, and import)
r.Get("/projects/{projectID}/requests", h.ListRequests)
r.Get("/projects/{projectID}/requests/tree", h.ListRequestTree)
r.Post("/projects/{projectID}/requests/import", h.ImportRequests)
// Request detail
r.Get("/requests/{requestID}", h.GetRequestDetail)
// Entry move (drag & drop)
r.Post("/projects/{projectID}/entries/{entryID}/move", h.MoveEntry)
// Answer links
r.Get("/projects/{projectID}/requests/{requestID}/links", h.ListAnswerLinks)
r.Post("/projects/{projectID}/requests/{requestID}/links", h.CreateAnswerLink)
r.Delete("/projects/{projectID}/requests/{requestID}/links/{answerID}", h.DeleteAnswerLink)
// Answers (picker)
r.Get("/projects/{projectID}/answers", h.ListAnswers)
// File upload/download
r.Post("/projects/{projectID}/objects", h.UploadObject)
r.Get("/projects/{projectID}/objects/{objectID}", h.DownloadObject)
// Super admin endpoints
// Organizations (platform level)
r.Get("/orgs", h.ListOrgs)
r.Post("/orgs", h.CreateOrg)
r.Get("/orgs/{orgID}", h.GetOrg)
r.Patch("/orgs/{orgID}", h.UpdateOrg)
// Deal orgs (per project)
r.Get("/projects/{projectID}/orgs", h.ListDealOrgs)
r.Post("/projects/{projectID}/orgs", h.CreateDealOrg)
r.Delete("/projects/{projectID}/orgs/{dealOrgID}", h.DeleteDealOrg)
r.Get("/admin/users", h.AdminListUsers)
r.Get("/admin/projects", h.AdminListProjects)
r.Get("/admin/audit", h.AdminAuditLog)
r.Post("/admin/impersonate", h.AdminImpersonate)
})
// Portal app routes (serve templates, auth checked client-side via JS)
r.Get("/app", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/projects", http.StatusFound)
})
r.Get("/app/login", h.ServeLogin)
r.Get("/app/tasks", h.ServeAppTasks)
r.Get("/app/projects", h.ServeAppProjects)
r.Get("/app/projects/{id}", h.ServeAppProject)
r.Get("/app/requests/{id}", h.ServeAppRequest)
r.Get("/app/orgs", h.ServeAppOrgs)
// Admin UI (super admin only, auth checked client-side)
r.Get("/admin", h.ServeAdmin)
r.Get("/admin/*", h.ServeAdmin)
// Marketing website (embedded static files) — serves at root, must be last
if websiteFS != nil {
websiteHandler := http.FileServerFS(websiteFS)
r.Handle("/*", websiteHandler)
}
return r
}