package httpapi import ( "log/slog" "net/http" "strings" "github.com/google/uuid" "github.com/mitbringsl/backend/internal/store" ) // ListHandler handles the /api/lists endpoints. type ListHandler struct { lists *store.ListStore items *store.ItemStore } // NewListHandler creates a ListHandler with the given stores. func NewListHandler(lists *store.ListStore, items *store.ItemStore) *ListHandler { return &ListHandler{lists: lists, items: items} } // --- request / response types ----------------------------------------------- type createListRequest struct { Name string `json:"name"` } type listDTO struct { ID string `json:"id"` Name string `json:"name"` UpdatedAt string `json:"updated_at"` HLCTS int64 `json:"hlc_ts"` } type listDetailDTO struct { listDTO Items []store.Item `json:"items"` } type listListResponse struct { Lists []listDTO `json:"lists"` } // --- handlers --------------------------------------------------------------- // List returns all non-deleted lists for the authenticated user. // GET /api/lists func (h *ListHandler) List(w http.ResponseWriter, r *http.Request) { userID, ok := userIDFromCtx(r) if !ok { renderError(w, http.StatusUnauthorized, "Unauthorized", "Missing user context.") return } ls, err := h.lists.GetLists(r.Context(), userID) if err != nil { slog.Error("list lists failed", "error", err, "user_id", userID) renderError(w, http.StatusInternalServerError, "Internal error", "Could not load lists.") return } dtos := make([]listDTO, len(ls)) for i, l := range ls { dtos[i] = listDTO{ ID: l.ID.String(), Name: l.Name, UpdatedAt: l.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), HLCTS: l.HLCTS, } } renderJSON(w, http.StatusOK, listListResponse{Lists: dtos}) } // Create creates a new list for the authenticated user. // POST /api/lists func (h *ListHandler) Create(w http.ResponseWriter, r *http.Request) { userID, ok := userIDFromCtx(r) if !ok { renderError(w, http.StatusUnauthorized, "Unauthorized", "Missing user context.") return } var req createListRequest if !decodeJSON(w, r, &req) { return } name := strings.TrimSpace(req.Name) if name == "" { renderError(w, http.StatusBadRequest, "Bad request", "List name must not be empty.") return } l, err := h.lists.CreateList(r.Context(), userID, name) if err != nil { slog.Error("create list failed", "error", err, "user_id", userID) renderError(w, http.StatusInternalServerError, "Internal error", "Could not create list.") return } renderJSON(w, http.StatusCreated, listDTO{ ID: l.ID.String(), Name: l.Name, UpdatedAt: l.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), HLCTS: l.HLCTS, }) } // Get returns a single list with its items. // GET /api/lists/{id} func (h *ListHandler) Get(w http.ResponseWriter, r *http.Request) { userID, ok := userIDFromCtx(r) if !ok { renderError(w, http.StatusUnauthorized, "Unauthorized", "Missing user context.") return } listID, err := uuid.Parse(r.PathValue("id")) if err != nil { renderError(w, http.StatusBadRequest, "Bad request", "Invalid list ID.") return } l, err := h.lists.GetList(r.Context(), listID, userID) if err != nil { if !apiError(w, err) { slog.Error("get list failed", "error", err, "list_id", listID) renderError(w, http.StatusInternalServerError, "Internal error", "Could not load list.") } return } its, err := h.items.GetItems(r.Context(), listID) if err != nil { slog.Error("get items failed", "error", err, "list_id", listID) renderError(w, http.StatusInternalServerError, "Internal error", "Could not load items.") return } if its == nil { its = []store.Item{} } renderJSON(w, http.StatusOK, listDetailDTO{ listDTO: listDTO{ ID: l.ID.String(), Name: l.Name, UpdatedAt: l.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), HLCTS: l.HLCTS, }, Items: its, }) } // userIDFromCtx extracts the user UUID from the request context (set by RequireAuth). func userIDFromCtx(r *http.Request) (uuid.UUID, bool) { v := r.Context().Value(ctxKeyUserID) if v == nil { return uuid.Nil, false } id, ok := v.(uuid.UUID) return id, ok }