Files
havenllo/internal/api/handlers.go
Hermes afd23ca751
Some checks failed
Build and deploy Havenllo / Verify Go and frontend (push) Failing after 29s
Build and deploy Havenllo / Build and push image (push) Has been skipped
Build and deploy Havenllo / Apply and restart Havenllo (push) Has been skipped
ui overhaul
2026-07-14 11:09:54 -03:00

225 lines
5.8 KiB
Go

// Package api exposes Havenllo's same-origin JSON API and embedded SPA handler.
package api
import (
"io/fs"
"net/http"
"path"
"strings"
"havenllo/internal/app"
)
type Handler struct {
service *app.Service
static fs.FS
}
func NewHandler(service *app.Service, static fs.FS) http.Handler {
h := &Handler{service: service, static: static}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/health", h.health)
mux.HandleFunc("GET /api/board", h.board)
mux.HandleFunc("GET /api/lists", h.lists)
mux.HandleFunc("POST /api/lists", h.createList)
mux.HandleFunc("GET /api/lists/{id}", h.list)
mux.HandleFunc("PATCH /api/lists/{id}", h.updateList)
mux.HandleFunc("DELETE /api/lists/{id}", h.deleteList)
mux.HandleFunc("GET /api/lists/{id}/cards", h.cards)
mux.HandleFunc("POST /api/lists/{id}/cards", h.createCard)
mux.HandleFunc("GET /api/cards/{id}", h.card)
mux.HandleFunc("PATCH /api/cards/{id}", h.updateCard)
mux.HandleFunc("DELETE /api/cards/{id}", h.deleteCard)
mux.HandleFunc("/api/", h.apiNotFound)
mux.HandleFunc("/", h.serveApp)
return mux
}
func (h *Handler) health(w http.ResponseWriter, r *http.Request) {
if err := h.service.Health(r.Context()); err != nil {
writeError(w, http.StatusServiceUnavailable, "unavailable", "database is unavailable")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *Handler) board(w http.ResponseWriter, r *http.Request) {
snapshot, err := h.service.Board(r.Context())
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, snapshot)
}
func (h *Handler) lists(w http.ResponseWriter, r *http.Request) {
lists, err := h.service.Lists(r.Context())
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, listsResponse{Lists: listSummaries(lists)})
}
func (h *Handler) createList(w http.ResponseWriter, r *http.Request) {
if !requireJSON(w, r) {
return
}
var request createListRequest
if !decodeJSON(w, r, &request) {
return
}
list, err := h.service.CreateList(r.Context(), request.Name, request.Position)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusCreated, listResponse{List: list})
}
func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok {
return
}
list, err := h.service.List(r.Context(), id)
if err != nil {
writeDomainError(w, err)
return
}
cards, err := h.service.Cards(r.Context(), id)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, listDetailsResponse{List: list, Cards: cards})
}
func (h *Handler) updateList(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok || !requireJSON(w, r) {
return
}
var request updateListRequest
if !decodeJSON(w, r, &request) {
return
}
list, err := h.service.UpdateList(r.Context(), id, request.Name, request.Position)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, listResponse{List: list})
}
func (h *Handler) deleteList(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok {
return
}
if err := h.service.DeleteList(r.Context(), id); err != nil {
writeDomainError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) cards(w http.ResponseWriter, r *http.Request) {
listID, ok := pathID(w, r, "id")
if !ok {
return
}
cards, err := h.service.Cards(r.Context(), listID)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, cardsResponse{Cards: cards})
}
func (h *Handler) createCard(w http.ResponseWriter, r *http.Request) {
listID, ok := pathID(w, r, "id")
if !ok || !requireJSON(w, r) {
return
}
var request createCardRequest
if !decodeJSON(w, r, &request) {
return
}
card, err := h.service.CreateCard(r.Context(), listID, request.Title, request.Description, request.Position)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusCreated, cardResponse{Card: card})
}
func (h *Handler) card(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok {
return
}
card, err := h.service.Card(r.Context(), id)
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, cardResponse{Card: card})
}
func (h *Handler) updateCard(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok || !requireJSON(w, r) {
return
}
var request updateCardRequest
if !decodeJSON(w, r, &request) {
return
}
card, err := h.service.UpdateCard(r.Context(), id, app.CardPatch{
Title: request.Title, Description: request.Description, ListID: request.ListID, Position: request.Position,
})
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusOK, cardResponse{Card: card})
}
func (h *Handler) deleteCard(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r, "id")
if !ok {
return
}
if err := h.service.DeleteCard(r.Context(), id); err != nil {
writeDomainError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) apiNotFound(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "not_found", "API endpoint not found")
}
func (h *Handler) serveApp(w http.ResponseWriter, r *http.Request) {
if h.static == nil {
writeError(w, http.StatusNotFound, "not_found", "application bundle not found")
return
}
requested := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
if requested != "" {
if _, err := fs.Stat(h.static, requested); err == nil {
http.FileServer(http.FS(h.static)).ServeHTTP(w, r)
return
}
}
index, err := fs.ReadFile(h.static, "index.html")
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "application bundle not found")
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(index)
}