diff --git a/internal/router/frontend.go b/internal/router/frontend.go index e9d47ad..f3f2239 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -3,10 +3,13 @@ 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" @@ -16,6 +19,16 @@ import ( "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 @@ -91,7 +104,7 @@ func registerFrontendRoutes(cfg *Config) { // ============================================================================ // Protected frontend routes (no /api prefix) - frontendProtected := e.Group("", jwtMiddleware) + frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg)) // Helper to extract text from pgtype.Text getText := func(t pgtype.Text) string { @@ -105,9 +118,11 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/dashboard", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } + var errorMsg string + libraryID := c.QueryParam("library_id") if libraryID == "" { userUUID, _ := uuid.Parse(user.ID) @@ -123,7 +138,8 @@ func registerFrontendRoutes(cfg *Config) { prefs, _ := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID) - sections, err := cfg.DashboardService.GetDashboardSections( + var sections []services.DashboardSection + sections, err = cfg.DashboardService.GetDashboardSections( c.Request().Context(), userUUID, libUUID, @@ -132,13 +148,19 @@ func registerFrontendRoutes(cfg *Config) { prefs.HiddenCollections, ) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading dashboard") + 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 { - return c.HTML(http.StatusInternalServerError, "Error loading libraries") + log.Printf("GetUserVisibleLibraries failed: %v", err) + libraries = []database.GetUserVisibleLibrariesRow{} + if errorMsg == "" { + errorMsg = "Error loading libraries" + } } libData := make([]templates.LibraryData, len(libraries)) @@ -155,7 +177,7 @@ func registerFrontendRoutes(cfg *Config) { sectionData := handlers.BuildSections(sections) var buf bytes.Buffer - err = templates.Dashboard(user, sectionData, libData, libraryID).Render(c.Request().Context(), &buf) + err = templates.Dashboard(user, sectionData, libData, libraryID, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -166,12 +188,19 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/collections", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } - collections, err := cfg.CollectionHandler.GetCollectionsData(c) + + var errorMsg string + var collections []database.Collections + + collections, err = cfg.CollectionHandler.GetCollectionsData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading collections") + 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{ @@ -182,8 +211,9 @@ func registerFrontendRoutes(cfg *Config) { Icon: getText(col.Icon), } } + var buf bytes.Buffer - err = templates.Collection(user, colData).Render(c.Request().Context(), &buf) + err = templates.Collection(user, colData, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -194,13 +224,18 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/custom-section", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + 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)) + libraries, err = cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID)) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading libraries") + log.Printf("GetUserVisibleLibraries failed: %v", err) + libraries = []database.GetUserVisibleLibrariesRow{} + errorMsg = "Error loading libraries" } libData := make([]templates.LibraryData, len(libraries)) @@ -215,7 +250,7 @@ func registerFrontendRoutes(cfg *Config) { } var buf bytes.Buffer - err = templates.CustomSectionBuilder(user, libData).Render(c.Request().Context(), &buf) + err = templates.CustomSectionBuilder(user, libData, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -226,14 +261,21 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/progress", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } - progressData, err := cfg.ScannerHandler.GetAllProgressData(c) + + var errorMsg string + var progressData []handlers.ProgressWithMedia + + progressData, err = cfg.ScannerHandler.GetAllProgressData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading progress") + log.Printf("GetAllProgressData failed: %v", err) + progressData = []handlers.ProgressWithMedia{} + errorMsg = "Error loading progress" } + var buf bytes.Buffer - err = templates.Progress(user, progressData).Render(c.Request().Context(), &buf) + err = templates.Progress(user, progressData, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -244,19 +286,33 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/devices", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } - devices, err := cfg.DeviceHandler.GetDevicesData(c) + + var errorMsg string + var devices []handlers.DeviceInfo + var pendingList []templates.PendingRegistrationData + + devices, err = cfg.DeviceHandler.GetDevicesData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading devices") + log.Printf("GetDevicesData failed: %v", err) + devices = []handlers.DeviceInfo{} + errorMsg = "Error loading devices" } + pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading pending") + log.Printf("GetPendingRegistrationsData failed: %v", err) + pendingList = []templates.PendingRegistrationData{} + if errorMsg == "" { + errorMsg = "Error loading pending registrations" + } + } else { + pendingList = convertPending(pendingMaps) } - pendingList := convertPending(pendingMaps) + var buf bytes.Buffer - err = templates.Devices(user, devices, pendingList, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf) + err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -267,14 +323,24 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/conflicts", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } - conflictsData, total, unresolved, err := cfg.ConflictHandler.GetConflictsData(c) + + var errorMsg string + var conflictsData []handlers.ConflictDetailResponse + var total, unresolved int + + conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading conflicts") + 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).Render(c.Request().Context(), &buf) + err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -285,7 +351,7 @@ func registerFrontendRoutes(cfg *Config) { frontendProtected.GET("/analytics", func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading user") + return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.Analytics(user).Render(c.Request().Context(), &buf) @@ -303,7 +369,7 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.Admin(user).Render(c.Request().Context(), &buf) @@ -316,7 +382,7 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.Admin(user).Render(c.Request().Context(), &buf) @@ -329,7 +395,7 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.AdminProfile(user).Render(c.Request().Context(), &buf) @@ -342,7 +408,7 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } var buf bytes.Buffer err = templates.AdminLibrary(user).Render(c.Request().Context(), &buf) @@ -360,19 +426,32 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } - devices, err := cfg.DeviceHandler.GetDevicesData(c) + var errorMsg string + var devices []handlers.DeviceInfo + var pendingList []templates.PendingRegistrationData + + devices, err = cfg.DeviceHandler.GetDevicesData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading devices") + log.Printf("GetDevicesData failed: %v", err) + devices = []handlers.DeviceInfo{} + errorMsg = "Error loading devices" } + pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading pending") + log.Printf("GetPendingRegistrationsData failed: %v", err) + pendingList = []templates.PendingRegistrationData{} + if errorMsg == "" { + errorMsg = "Error loading pending registrations" + } + } else { + pendingList = convertPending(pendingMaps) } - pendingList := convertPending(pendingMaps) + var buf bytes.Buffer - err = templates.Devices(user, devices, pendingList, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf) + err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -382,14 +461,24 @@ func registerFrontendRoutes(cfg *Config) { 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") + return renderErrorPage(c, "Error loading user", "user_load_error") } - conflictsData, total, unresolved, err := cfg.ConflictHandler.GetConflictsData(c) + + var errorMsg string + var conflictsData []handlers.ConflictDetailResponse + var total, unresolved int + + conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c) if err != nil { - return c.HTML(http.StatusInternalServerError, "Error loading conflicts") + 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).Render(c.Request().Context(), &buf) + err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } diff --git a/internal/router/helpers.go b/internal/router/helpers.go index 101a57a..9167f81 100644 --- a/internal/router/helpers.go +++ b/internal/router/helpers.go @@ -2,6 +2,7 @@ package router import ( "context" + "log" "bookhoard/templates" @@ -19,11 +20,13 @@ func getTemplateUserWithTheme(c echo.Context, cfg *Config) (templates.User, erro userUUID, err := uuid.Parse(userID) if err != nil { + log.Printf("getTemplateUserWithTheme failed: invalid UUID '%s': %v", userID, err) return templates.User{}, err } userDB, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID)) if err != nil { + log.Printf("getTemplateUserWithTheme failed: database query error for user ID %s: %v", userID, err) return templates.User{}, err } diff --git a/internal/router/router.go b/internal/router/router.go index 643d17b..a2a515d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -8,6 +8,8 @@ import ( ratelimit "bookhoard/internal/middleware" "bookhoard/internal/services" "bookhoard/internal/sync" + "bookhoard/templates" + "bytes" "log" "net/http" "strings" @@ -105,6 +107,45 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc { }) } +// ensureUserExistsMiddleware checks if the authenticated user still exists in the database +func ensureUserExistsMiddleware(cfg *Config) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + userIDStr, ok := c.Get("user_id").(string) + if !ok { + return next(c) + } + + userUUID, err := uuid.Parse(userIDStr) + if err != nil { + log.Printf("Invalid UUID in user existence check: %v", err) + return next(c) + } + + // Check if user exists in database + _, err = cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID)) + if err != nil { + log.Printf("User not found in database: %s", userIDStr) + + // Clear invalid cookie + c.SetCookie(&http.Cookie{ + Name: "token", + Value: "", + Expires: time.Now().Add(-24 * time.Hour), + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + + // Redirect to login with session=invalid + return c.Redirect(http.StatusFound, "/login?session=invalid") + } + + return next(c) + } + } +} + // wantsHTML determines if the request expects HTML response func wantsHTML(header http.Header) bool { // Check Accept header @@ -180,6 +221,16 @@ func RegisterRoutes(cfg *Config) *handlers.Handler { registerDocumentationRoutes(cfg) e.Static("/static", "web/static") + // Catch-all 404 handler - must be last + e.GET("/*", func(c echo.Context) error { + var buf bytes.Buffer + err := templates.ErrorPage("Page not found", "404").Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusNotFound, buf.String()) + }) + // Start background tasks (queue processor and connection cleanup) scannerHandler.StartBackgroundTasks()