package router import ( "bytes" "context" "net/http" "time" "bookhoard/internal/handlers" "bookhoard/templates" "github.com/golang-jwt/jwt/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" "github.com/google/uuid" ) func registerFrontendRoutes(cfg *Config) { e := cfg.Echo // ============================================================================ // FRONTEND ROUTES - DO NOT DELETE // These routes serve Server-Side Rendered (SSR) HTML pages for the web UI. // They are NOT API endpoints and should NOT be removed during refactors. // All authenticated frontend routes use the jwtMiddleware to validate tokens. // ============================================================================ // Use existing JWT middleware (sets database user object in context) jwtMiddleware := createJWTMiddleware(cfg) // 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 sessionExpired := c.QueryParam("session") == "expired" err := templates.Login(sessionExpired).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) }) e.GET("/register", func(c echo.Context) error { var buf bytes.Buffer err := templates.Register().Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) }) // Root route - landing page with smart login detection e.GET("/", func(c echo.Context) error { var buf bytes.Buffer var err error tokenString := c.Request().Header.Get("Authorization") if tokenString != "" && len(tokenString) > 7 && tokenString[:7] == "Bearer " { tokenString = tokenString[7:] } else { cookie, err := c.Cookie("token") if err == nil { tokenString = cookie.Value } } loggedIn := false if tokenString != "" { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(cfg.Cfg.JWTSecret), nil }) loggedIn = err == nil && token.Valid } err = templates.Index(loggedIn).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) }) // ============================================================================ // 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()) }) // 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()) }) // 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.Progress(user, progressData).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) }) // Devices page frontendProtected.GET("/devices", 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()) }) // 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 { 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()) }) // ============================================================================ // HEALTH CHECK // ============================================================================ // Health check e.GET("/health", func(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second) defer cancel() if err := pingDB(cfg, ctx); err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{ "status": "unhealthy", "error": "database unavailable", }) } return c.JSON(http.StatusOK, map[string]string{ "status": "healthy", "database": "connected", }) }) }