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("") orders := store.NewOrderStore("") 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) registerOrderRoutes(mux, orders) 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) } } 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, err := itemSpecFromRequest(req) if err != nil { writeError(w, http.StatusBadRequest, err) return } 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 } spec, err := itemSpecFromRequest(req) if err != nil { writeError(w, http.StatusBadRequest, err) return } saved, err := s.Update(id, req.Name, spec) 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()}) }