278 lines
7.4 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"os"
"time"
"printer.backend/internal/model"
"printer.backend/internal/store"
)
// NewHandler returns the API HTTP handler (routes under /).
func NewHandler() http.Handler {
plates := store.NewPlateStore("")
items := store.NewItemStore("")
configs := store.NewConfigurationStore("")
mux := http.NewServeMux()
mux.HandleFunc("GET /health", health)
mux.HandleFunc("GET /", root)
mux.HandleFunc("GET /plates", listPlates(plates))
mux.HandleFunc("POST /plates", savePlate(plates))
mux.HandleFunc("PUT /plates/{id}", updatePlate(plates))
mux.HandleFunc("DELETE /plates/{id}", deletePlate(plates))
mux.HandleFunc("GET /items", listItems(items))
mux.HandleFunc("POST /items", saveItem(items))
mux.HandleFunc("PUT /items/{id}", updateItem(items))
mux.HandleFunc("DELETE /items/{id}", deleteItem(items))
mux.HandleFunc("GET /items/{id}/svg", serveItemSVG(items))
registerConfigurationRoutes(mux, configs, plates, items)
return withCORS(mux)
}
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func health(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func root(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"service": "printer-backend-api",
})
}
func listPlates(s *store.PlateStore) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
plates, err := s.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
if plates == nil {
plates = []model.Plate{}
}
writeJSON(w, http.StatusOK, plates)
}
}
type savePlateRequest struct {
Name string `json:"name"`
WidthMM float64 `json:"width_mm"`
HeightMM float64 `json:"height_mm"`
MarginTop float64 `json:"margin_top_mm"`
MarginRight float64 `json:"margin_right_mm"`
MarginBottom float64 `json:"margin_bottom_mm"`
MarginLeft float64 `json:"margin_left_mm"`
}
func savePlate(s *store.PlateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req savePlateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if req.WidthMM <= 0 || req.HeightMM <= 0 {
writeError(w, http.StatusBadRequest, errors.New("width_mm and height_mm must be positive"))
return
}
p := model.Plate{
Name: req.Name,
WidthMM: req.WidthMM,
HeightMM: req.HeightMM,
MarginTop: req.MarginTop,
MarginRight: req.MarginRight,
MarginBottom: req.MarginBottom,
MarginLeft: req.MarginLeft,
CreatedAt: time.Now().UTC(),
}
saved, err := s.Save(p)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusCreated, saved)
}
}
func updatePlate(s *store.PlateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, errors.New("id required"))
return
}
var req savePlateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if req.WidthMM <= 0 || req.HeightMM <= 0 {
writeError(w, http.StatusBadRequest, errors.New("width_mm and height_mm must be positive"))
return
}
p := model.Plate{
Name: req.Name,
WidthMM: req.WidthMM,
HeightMM: req.HeightMM,
MarginTop: req.MarginTop,
MarginRight: req.MarginRight,
MarginBottom: req.MarginBottom,
MarginLeft: req.MarginLeft,
}
saved, err := s.Update(id, p)
if err != nil {
writeError(w, http.StatusNotFound, err)
return
}
writeJSON(w, http.StatusOK, saved)
}
}
func deletePlate(s *store.PlateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, errors.New("id required"))
return
}
if err := s.Delete(id); err != nil {
writeError(w, http.StatusNotFound, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func listItems(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
list, err := s.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
if list == nil {
list = []model.Item{}
}
writeJSON(w, http.StatusOK, list)
}
}
type saveItemRequest struct {
Name string `json:"name"`
SizeMM float64 `json:"size_mm"`
BleedMM float64 `json:"bleed_mm"`
MarginMM float64 `json:"margin_mm"`
PaddingMM float64 `json:"padding_mm"`
}
func saveItem(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req saveItemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
spec := model.ItemSpec{
SizeMM: req.SizeMM,
BleedMM: req.BleedMM,
MarginMM: req.MarginMM,
PaddingMM: req.PaddingMM,
}
saved, err := s.Create(req.Name, spec)
if err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusCreated, saved)
}
}
func updateItem(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, errors.New("id required"))
return
}
var req saveItemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
saved, err := s.Update(id, req.Name, model.ItemSpec{
SizeMM: req.SizeMM,
BleedMM: req.BleedMM,
MarginMM: req.MarginMM,
PaddingMM: req.PaddingMM,
})
if err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusOK, saved)
}
}
func deleteItem(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, errors.New("id required"))
return
}
if err := s.Delete(id); err != nil {
writeError(w, http.StatusNotFound, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
func serveItemSVG(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
path, err := s.SVGPath(id)
if err != nil {
writeError(w, http.StatusNotFound, err)
return
}
data, err := os.ReadFile(path)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "no-cache")
_, _ = w.Write(data)
}
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, code int, err error) {
writeJSON(w, code, map[string]string{"error": err.Error()})
}