From 3fc8aae7a025743283376d67df31db83283051d5 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Mon, 18 May 2026 15:46:30 +0200 Subject: [PATCH] feat(20-01): GetTabloDetailPage handler + GET /tablos/{tabloID} route registration - tabloDetailRepository interface with ListTasksByTablo - GetTabloDetailPage: auth check, UUID parse, ownership-scoped tablo lookup, task fetch, vm build, render - router.go: mux.Get('/tablos/{tabloID}') registered before /edit route - activePath='/tablos' so sidebar Tablos item stays highlighted - Threat T-20-01 mitigated: findTabloByID filters by OwnerID from session --- .../internal/web/handlers/tablo_detail.go | 83 +++++++++++++++++++ go-backend/router.go | 1 + 2 files changed, 84 insertions(+) create mode 100644 go-backend/internal/web/handlers/tablo_detail.go diff --git a/go-backend/internal/web/handlers/tablo_detail.go b/go-backend/internal/web/handlers/tablo_detail.go new file mode 100644 index 0000000..5407103 --- /dev/null +++ b/go-backend/internal/web/handlers/tablo_detail.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/google/uuid" + "xtablo-backend/internal/web/views" +) + +// tabloDetailRepository is the interface for fetching tasks for a specific tablo. +type tabloDetailRepository interface { + ListTasksByTablo(ctx context.Context, input ListTasksByTabloInput) ([]TaskRecord, error) +} + +// GetTabloDetailPage handles GET /tablos/{tabloID}. +// It authenticates the user, fetches the tablo (with ownership check), fetches tasks, +// and renders the tablo detail page. +func (h *AuthHandler) GetTabloDetailPage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticatedUser(r.Context(), r) + if !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + tabloID, err := uuid.Parse(r.PathValue("tabloID")) + if err != nil { + http.Error(w, "invalid tablo id", http.StatusBadRequest) + return + } + + // Fetch all tablos owned by the user — ownership check is implicit via OwnerID filter + tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID}) + if err != nil { + http.Error(w, "failed to load tablos", http.StatusInternalServerError) + return + } + + // Find the specific tablo (returns 404 if not owned by this user) + tablo, ok := findTabloByID(tablos, tabloID) + if !ok { + http.Error(w, "tablo not found", http.StatusNotFound) + return + } + + // Type-assert to tabloDetailRepository to access ListTasksByTablo + taskRepo, ok := h.repo.(tabloDetailRepository) + if !ok { + http.Error(w, "tasks repository not configured", http.StatusInternalServerError) + return + } + + tasks, err := taskRepo.ListTasksByTablo(r.Context(), ListTasksByTabloInput{ + OwnerID: user.ID, + TabloID: tabloID, + }) + if err != nil { + http.Error(w, "failed to load tasks", http.StatusInternalServerError) + return + } + + // Fetch owner display name (best-effort; falls back to empty string) + ownerName := "" + if owner, err := h.repo.GetPublicUserByID(r.Context(), user.ID); err == nil { + ownerName = owner.DisplayName + } + + vm := views.NewTabloDetailViewModel(tablo, tasks, ownerName) + content := views.TabloDetailPage(vm) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + var renderErr error + if isHXRequest(r) { + renderErr = views.DashboardContentSwapWithMainClass("/tablos", tablos, "flex-1 overflow-auto", content).Render(r.Context(), w) + } else { + renderErr = views.DashboardPageWithMainClass("/tablos", tablos, "flex-1 overflow-auto", content).Render(r.Context(), w) + } + if renderErr != nil { + http.Error(w, "failed to render page", http.StatusInternalServerError) + } + } +} diff --git a/go-backend/router.go b/go-backend/router.go index 9a47525..fe64970 100644 --- a/go-backend/router.go +++ b/go-backend/router.go @@ -42,6 +42,7 @@ func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler { mux.Get("/files", authHandler.GetFilesPage()) mux.Get("/feedback", authHandler.GetFeedbackPage()) mux.Post("/tablos", authHandler.PostTablos()) + mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal()) mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate()) mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())