diff --git a/internal/handler/space.go b/internal/handler/space.go index 7296d11..12261fd 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -38,6 +38,21 @@ func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *serv } } +// getListForSpace fetches a shopping list and verifies it belongs to the given space. +// Returns the list on success, or writes an error response and returns nil. +func (h *SpaceHandler) getListForSpace(w http.ResponseWriter, spaceID, listID string) *model.ShoppingList { + list, err := h.listService.GetList(listID) + if err != nil { + http.Error(w, "List not found", http.StatusNotFound) + return nil + } + if list.SpaceID != spaceID { + http.Error(w, "Not Found", http.StatusNotFound) + return nil + } + return list +} + func (h *SpaceHandler) StreamEvents(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") @@ -150,6 +165,10 @@ func (h *SpaceHandler) UpdateList(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") listID := r.PathValue("listID") + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + if err := r.ParseForm(); err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) return @@ -171,6 +190,25 @@ func (h *SpaceHandler) UpdateList(w http.ResponseWriter, r *http.Request) { ui.Render(w, r, shoppinglist.ListNameHeader(spaceID, updatedList)) } +func (h *SpaceHandler) DeleteList(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + listID := r.PathValue("listID") + + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + + err := h.listService.DeleteList(listID) + if err != nil { + slog.Error("failed to delete list", "error", err, "list_id", listID) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/lists") + w.WriteHeader(http.StatusOK) +} + func (h *SpaceHandler) ListPage(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") listID := r.PathValue("listID") @@ -181,10 +219,8 @@ func (h *SpaceHandler) ListPage(w http.ResponseWriter, r *http.Request) { return } - list, err := h.listService.GetList(listID) - if err != nil { - slog.Error("failed to get list", "error", err, "list_id", listID) - http.Error(w, "List not found", http.StatusNotFound) + list := h.getListForSpace(w, spaceID, listID) + if list == nil { return } @@ -201,6 +237,11 @@ func (h *SpaceHandler) ListPage(w http.ResponseWriter, r *http.Request) { func (h *SpaceHandler) AddItemToList(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") listID := r.PathValue("listID") + + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + user := ctxkeys.User(r.Context()) if err := r.ParseForm(); err != nil { @@ -226,8 +267,13 @@ func (h *SpaceHandler) AddItemToList(w http.ResponseWriter, r *http.Request) { func (h *SpaceHandler) ToggleItem(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") + listID := r.PathValue("listID") itemID := r.PathValue("itemID") + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + item, err := h.listService.GetItem(itemID) if err != nil { slog.Error("failed to get item", "error", err, "item_id", itemID) @@ -235,6 +281,11 @@ func (h *SpaceHandler) ToggleItem(w http.ResponseWriter, r *http.Request) { return } + if item.ListID != listID { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + updatedItem, err := h.listService.UpdateItem(itemID, item.Name, !item.IsChecked) if err != nil { slog.Error("failed to toggle item", "error", err, "item_id", itemID) @@ -246,9 +297,27 @@ func (h *SpaceHandler) ToggleItem(w http.ResponseWriter, r *http.Request) { } func (h *SpaceHandler) DeleteItem(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + listID := r.PathValue("listID") itemID := r.PathValue("itemID") - err := h.listService.DeleteItem(itemID) + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + + item, err := h.listService.GetItem(itemID) + if err != nil { + slog.Error("failed to get item", "error", err, "item_id", itemID) + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + if item.ListID != listID { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + err = h.listService.DeleteItem(itemID) if err != nil { slog.Error("failed to delete item", "error", err, "item_id", itemID) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -550,6 +619,10 @@ func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Reque spaceID := r.PathValue("spaceID") listID := r.PathValue("listID") + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + items, err := h.listService.GetItemsForList(listID) if err != nil { slog.Error("failed to get items", "error", err, "list_id", listID) diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 37d8248..bd1735c 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -80,6 +80,10 @@ func SetupRoutes(a *app.App) http.Handler { updateListWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(updateListHandler) mux.Handle("PATCH /app/spaces/{spaceID}/lists/{listID}", updateListWithAccess) + deleteListHandler := middleware.RequireAuth(space.DeleteList) + deleteListWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(deleteListHandler) + mux.Handle("DELETE /app/spaces/{spaceID}/lists/{listID}", deleteListWithAccess) + addItemHandler := middleware.RequireAuth(space.AddItemToList) addItemWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(addItemHandler) mux.Handle("POST /app/spaces/{spaceID}/lists/{listID}/items", addItemWithAccess) diff --git a/internal/ui/pages/app_space_list_detail.templ b/internal/ui/pages/app_space_list_detail.templ index 64a1af3..4560f0b 100644 --- a/internal/ui/pages/app_space_list_detail.templ +++ b/internal/ui/pages/app_space_list_detail.templ @@ -4,6 +4,7 @@ import ( "git.juancwu.dev/juancwu/budgit/internal/model" "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" "git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf" + "git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog" "git.juancwu.dev/juancwu/budgit/internal/ui/components/input" "git.juancwu.dev/juancwu/budgit/internal/ui/components/shoppinglist" "git.juancwu.dev/juancwu/budgit/internal/ui/layouts" @@ -12,7 +13,41 @@ import ( templ SpaceListDetailPage(space *model.Space, list *model.ShoppingList, items []*model.ListItem) { @layouts.Space(list.Name, space) {