diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 7af29fc..c2c968f 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -10,8 +10,10 @@ import ( "bookhoard/templates" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo-jwt/v4" + "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" + + "github.com/google/uuid" ) func registerFrontendRoutes(cfg *Config) { @@ -24,23 +26,16 @@ func registerFrontendRoutes(cfg *Config) { // All authenticated frontend routes use the jwtMiddleware to validate tokens. // ============================================================================ - // JWT middleware for protected routes - jwtMiddleware := echojwt.WithConfig(echojwt.Config{ - SigningKey: []byte(cfg.Cfg.JWTSecret), - ContextKey: "user", - SuccessHandler: func(c echo.Context) { - token := c.Get("user").(*jwt.Token) - claims := token.Claims.(jwt.MapClaims) - c.Set("user_id", claims["user_id"]) - c.Set("user_role", claims["user_role"]) - c.Set("user_email", claims["user_email"]) - c.Set("user_username", claims["user_username"]) - }, - }) + // Use existing JWT middleware (sets database user object in context) + jwtMiddleware := createJWTMiddleware(cfg) - // Protected route group + // Protected route group for API routes protected := e.Group("/api", jwtMiddleware) + // ============================================================================ + // PUBLIC FRONTEND ROUTES (No authentication required) + // ============================================================================ + // Public routes for login and registration pages e.GET("/login", func(c echo.Context) error { var buf bytes.Buffer @@ -90,70 +85,83 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) }) - // Public redirect routes - e.GET("/bookshelf", func(c echo.Context) error { - return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf") + // ============================================================================ + // PROTECTED FRONTEND ROUTES (Authentication required) + // ============================================================================ + + // Protected frontend routes (no /api prefix) + frontendProtected := e.Group("", jwtMiddleware) + + // Helper to extract text from pgtype.Text + getText := func(t pgtype.Text) string { + if t.Valid { + return t.String + } + return "" + } + + // Dashboard page + frontendProtected.GET("/dashboard", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.Dashboard(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) }) - e.GET("/dashboard", func(c echo.Context) error { - return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf") + // Collections page + frontendProtected.GET("/collections", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + collections, err := cfg.CollectionHandler.GetCollectionsData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading collections") + } + colData := make([]templates.CollectionData, len(collections)) + for i, col := range collections { + colData[i] = templates.CollectionData{ + ID: uuid.UUID(col.ID.Bytes).String(), + Name: col.Name, + Description: getText(col.Description), + Color: getText(col.Color), + Icon: getText(col.Icon), + } + } + var buf bytes.Buffer + err = templates.Collection(user, colData).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) }) - // Admin routes - e.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error { + // Progress page + frontendProtected.GET("/progress", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return c.HTML(http.StatusInternalServerError, "Error loading user") } + progressData, err := cfg.ScannerHandler.GetAllProgressData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading progress") + } var buf bytes.Buffer - err = templates.Admin(user).Render(c.Request().Context(), &buf) + err = templates.Progress(user, progressData).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) - })) - - e.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error { - user, err := getTemplateUserWithTheme(c, cfg) - if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") - } - var buf bytes.Buffer - err = templates.Admin(user).Render(c.Request().Context(), &buf) - if err != nil { - return err - } - return c.HTML(http.StatusOK, buf.String()) - })) - - e.GET("/admin/profile", handlers.AdminMiddleware(func(c echo.Context) error { - user, err := getTemplateUserWithTheme(c, cfg) - if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") - } - var buf bytes.Buffer - err = templates.AdminProfile(user).Render(c.Request().Context(), &buf) - if err != nil { - return err - } - return c.HTML(http.StatusOK, buf.String()) - })) - - e.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error { - user, err := getTemplateUserWithTheme(c, cfg) - if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") - } - var buf bytes.Buffer - err = templates.AdminLibrary(user).Render(c.Request().Context(), &buf) - if err != nil { - return err - } - return c.HTML(http.StatusOK, buf.String()) - })) + }) // Devices page - protected.GET("/devices-page", func(c echo.Context) error { + frontendProtected.GET("/devices", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return c.HTML(http.StatusInternalServerError, "Error loading user") @@ -176,6 +184,121 @@ func registerFrontendRoutes(cfg *Config) { }) // Conflicts page + frontendProtected.GET("/conflicts", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + conflictsData, total, unresolved, err := cfg.ConflictHandler.GetConflictsData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading conflicts") + } + var buf bytes.Buffer + err = templates.Conflicts(user, conflictsData, total, unresolved).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + + // Analytics page + frontendProtected.GET("/analytics", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.Analytics(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + + // ============================================================================ + // ADMIN FRONTEND ROUTES + // ============================================================================ + + // Admin routes + frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.Admin(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + + frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.Admin(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + + frontendProtected.GET("/admin/profile", handlers.AdminMiddleware(func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.AdminProfile(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + + frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + var buf bytes.Buffer + err = templates.AdminLibrary(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + + // ============================================================================ + // LEGACY API ROUTES (for backward compatibility) + // ============================================================================ + + // Keep legacy routes under /api for existing API consumers + protected.GET("/devices-page", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading user") + } + devices, err := cfg.DeviceHandler.GetDevicesData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading devices") + } + pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading pending") + } + pendingList := convertPending(pendingMaps) + var buf bytes.Buffer + err = templates.Devices(user, devices, pendingList, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + protected.GET("/conflicts-page", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { @@ -193,6 +316,10 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) }) + // ============================================================================ + // HEALTH CHECK + // ============================================================================ + // Health check e.GET("/health", func(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)