225 lines
5.8 KiB
Go
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)
|
|
}
|