package savedrecipe import ( "encoding/json" "errors" "log/slog" "net/http" "github.com/food-ai/backend/internal/middleware" "github.com/go-chi/chi/v5" ) const maxBodySize = 1 << 20 // 1 MB // Handler handles HTTP requests for saved recipes. type Handler struct { repo *Repository } // NewHandler creates a new Handler. func NewHandler(repo *Repository) *Handler { return &Handler{repo: repo} } // Save handles POST /saved-recipes. func (h *Handler) Save(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, http.StatusUnauthorized, "unauthorized") return } r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) var req SaveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErrorJSON(w, http.StatusBadRequest, "invalid request body") return } if req.Title == "" { writeErrorJSON(w, http.StatusBadRequest, "title is required") return } rec, err := h.repo.Save(r.Context(), userID, req) if err != nil { slog.Error("save recipe", "err", err) writeErrorJSON(w, http.StatusInternalServerError, "failed to save recipe") return } writeJSON(w, http.StatusCreated, rec) } // List handles GET /saved-recipes. func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, http.StatusUnauthorized, "unauthorized") return } recipes, err := h.repo.List(r.Context(), userID) if err != nil { slog.Error("list saved recipes", "err", err) writeErrorJSON(w, http.StatusInternalServerError, "failed to list saved recipes") return } if recipes == nil { recipes = []*SavedRecipe{} } writeJSON(w, http.StatusOK, recipes) } // GetByID handles GET /saved-recipes/{id}. func (h *Handler) GetByID(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, http.StatusUnauthorized, "unauthorized") return } id := chi.URLParam(r, "id") rec, err := h.repo.GetByID(r.Context(), userID, id) if err != nil { slog.Error("get saved recipe", "id", id, "err", err) writeErrorJSON(w, http.StatusInternalServerError, "failed to get saved recipe") return } if rec == nil { writeErrorJSON(w, http.StatusNotFound, "recipe not found") return } writeJSON(w, http.StatusOK, rec) } // Delete handles DELETE /saved-recipes/{id}. func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromCtx(r.Context()) if userID == "" { writeErrorJSON(w, http.StatusUnauthorized, "unauthorized") return } id := chi.URLParam(r, "id") if err := h.repo.Delete(r.Context(), userID, id); err != nil { if errors.Is(err, ErrNotFound) { writeErrorJSON(w, http.StatusNotFound, "recipe not found") return } slog.Error("delete saved recipe", "id", id, "err", err) writeErrorJSON(w, http.StatusInternalServerError, "failed to delete recipe") return } w.WriteHeader(http.StatusNoContent) } type errorResponse struct { Error string `json:"error"` } func writeErrorJSON(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(errorResponse{Error: msg}); err != nil { slog.Error("write error response", "err", err) } } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("write JSON response", "err", err) } }