package router import ( "bytes" "context" "log" "net/http" "time" "bookhoard/internal/database" "bookhoard/internal/handlers" "bookhoard/internal/services" "bookhoard/templates" "github.com/golang-jwt/jwt/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" "github.com/google/uuid" ) func renderErrorPage(c echo.Context, message string, errorType string) error { var buf bytes.Buffer err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf) if err != nil { log.Printf("renderErrorPage failed to render template: %v", err) return c.HTML(http.StatusInternalServerError, "Internal server error") } return c.HTML(http.StatusInternalServerError, buf.String()) } 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" deleted := c.QueryParam("deleted") == "true" err := templates.Login(sessionExpired, deleted).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, ensureUserExistsMiddleware(cfg)) // 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 renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string libraryID := c.QueryParam("library_id") if libraryID == "" { userUUID, _ := uuid.Parse(user.ID) libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) if err == nil && len(libraries) > 0 { libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16]) libraryID = libUUID.String() } } libUUID, _ := uuid.Parse(libraryID) userUUID, _ := uuid.Parse(user.ID) prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID) var sections []services.DashboardSection sections, err = cfg.DashboardService.GetDashboardSections( c.Request().Context(), userUUID, libUUID, int(prefs.ItemsPerSection.Int32), prefs.CollectionOrder, prefs.HiddenCollections, ) if err != nil { log.Printf("Dashboard sections query failed: %v", err) sections = []services.DashboardSection{} errorMsg = "Error loading dashboard" } userUUID2, _ := uuid.Parse(user.ID) libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2)) if err != nil { log.Printf("GetUserVisibleLibraries failed: %v", err) libraries = []database.GetUserVisibleLibrariesRow{} if errorMsg == "" { errorMsg = "Error loading libraries" } } libData := make([]templates.LibraryData, len(libraries)) for i, lib := range libraries { libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, } } sectionData := handlers.BuildSections(sections) var buf bytes.Buffer err = templates.Dashboard(user, sectionData, libData, libraryID, errorMsg).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 renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string var collections []database.Collections collections, err = cfg.CollectionHandler.GetCollectionsData(c) if err != nil { log.Printf("GetCollectionsData failed: %v", err) collections = []database.Collections{} errorMsg = "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, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) }) // Custom Section Builder page frontendProtected.GET("/custom-section", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string var libraries []database.GetUserVisibleLibrariesRow userUUID, _ := uuid.Parse(user.ID) libraries, err = cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) if err != nil { log.Printf("GetUserVisibleLibraries failed: %v", err) libraries = []database.GetUserVisibleLibrariesRow{} errorMsg = "Error loading libraries" } libData := make([]templates.LibraryData, len(libraries)) for i, lib := range libraries { libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, } } var buf bytes.Buffer err = templates.CustomSectionBuilder(user, libData, errorMsg).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 renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string var progressData []handlers.ProgressWithMedia progressData, err = cfg.ScannerHandler.GetAllProgressData(c) if err != nil { log.Printf("GetAllProgressData failed: %v", err) progressData = []handlers.ProgressWithMedia{} errorMsg = "Error loading progress" } var buf bytes.Buffer err = templates.Progress(user, progressData, errorMsg).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 renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string var devices []handlers.DeviceInfo var pendingList []templates.PendingRegistrationData devices, err = cfg.DeviceHandler.GetDevicesData(c) if err != nil { log.Printf("GetDevicesData failed: %v", err) devices = []handlers.DeviceInfo{} errorMsg = "Error loading devices" } pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c) if err != nil { log.Printf("GetPendingRegistrationsData failed: %v", err) pendingList = []templates.PendingRegistrationData{} if errorMsg == "" { errorMsg = "Error loading pending registrations" } } else { pendingList = convertPending(pendingMaps) } var buf bytes.Buffer err = templates.Devices(user, devices, pendingList, errorMsg, 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 renderErrorPage(c, "Error loading user", "user_load_error") } var errorMsg string var conflictsData []handlers.ConflictDetailResponse var total, unresolved int conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c) if err != nil { log.Printf("GetConflictsData failed: %v", err) conflictsData = []handlers.ConflictDetailResponse{} total = 0 unresolved = 0 errorMsg = "Error loading conflicts" } var buf bytes.Buffer err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).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 renderErrorPage(c, "Error loading user", "user_load_error") } 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()) }) // Profile page (all users) frontendProtected.GET("/profile", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.Profile(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 renderErrorPage(c, "Error loading user", "user_load_error") } 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 renderErrorPage(c, "Error loading user", "user_load_error") } 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/library", handlers.AdminMiddleware(func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } // Fetch all libraries (admin-only) libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context()) if err != nil { log.Printf("ListLibrariesData failed: %v", err) libraries = []database.ListLibrariesRow{} } // Convert to template LibraryData libData := make([]templates.LibraryData, len(libraries)) for i, lib := range libraries { libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16]) libData[i] = templates.LibraryData{ ID: libUUID.String(), Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, } } // Fetch all users for visibility management users, err := cfg.Queries.ListUsers(c.Request().Context()) if err != nil { log.Printf("ListUsers failed: %v", err) users = []database.ListUsersRow{} } // Convert to template User types userData := make([]templates.User, len(users)) for i, u := range users { userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16]) userData[i] = templates.User{ ID: userUUID.String(), Username: u.Username, Email: u.Email, Role: u.Role, } } var buf bytes.Buffer err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) })) // Admin users page frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { return renderErrorPage(c, "Error loading user", "user_load_error") } // Fetch all users users, err := cfg.Queries.ListUsers(c.Request().Context()) if err != nil { return renderErrorPage(c, "Error loading users", "users_load_error") } // Count admins for UI protection adminCount := 0 for _, u := range users { if u.Role == "admin" { adminCount++ } } // Convert to template users templateUsers := make([]templates.User, len(users)) for i, u := range users { templateUsers[i] = templates.User{ ID: uuid.UUID(u.ID.Bytes).String(), Username: u.Username, Email: u.Email, Role: u.Role, Theme: getText(u.Theme), FirstName: getText(u.FirstName), LastName: getText(u.LastName), CreatedAt: u.CreatedAt.Time, } } var buf bytes.Buffer err = templates.AdminUsers(user, templateUsers, adminCount).Render(c.Request().Context(), &buf) if err != nil { return err } return c.HTML(http.StatusOK, buf.String()) })) // Admin: Get profile modal for editing user frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c echo.Context) error { // Get target user ID from URL targetUserID := c.Param("id") parsedUUID, err := uuid.Parse(targetUserID) if err != nil { return c.HTML(http.StatusBadRequest, "