refactor(router): update routes and middleware for Echo v5

Update all router files to use Echo v5 APIs and type signatures.

Changes in router.go:
- Replace echomiddleware.Logger() with RequestLogger() (line 144)
- Update import from echo/v4 to echo/v5

Changes in frontend.go:
- Update frontend handler signatures to use *echo.Context
- Fix middleware registration for v5 compatibility

Changes in auth.go, library.go, scanner.go, sync.go, helpers.go:
- Update handler function signatures to *echo.Context
- Ensure consistent type usage across all route handlers

All routes now properly implement Echo v5's middleware and handler patterns.
This commit is contained in:
2026-03-06 14:00:17 -05:00
parent 0438ec4625
commit 784326e2c4
7 changed files with 45 additions and 44 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ package router
import ( import (
"bookhoard/internal/handlers" "bookhoard/internal/handlers"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) { func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) {
+25 -25
View File
@@ -15,12 +15,12 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/google/uuid" "github.com/google/uuid"
) )
func renderErrorPage(c echo.Context, message string, errorType string) error { func renderErrorPage(c *echo.Context, message string, errorType string) error {
var buf bytes.Buffer var buf bytes.Buffer
err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf) err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -51,7 +51,7 @@ func registerFrontendRoutes(cfg *Config) {
// ============================================================================ // ============================================================================
// Public routes for login and registration pages // Public routes for login and registration pages
e.GET("/login", func(c echo.Context) error { e.GET("/login", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
sessionExpired := c.QueryParam("session") == "expired" sessionExpired := c.QueryParam("session") == "expired"
deleted := c.QueryParam("deleted") == "true" deleted := c.QueryParam("deleted") == "true"
@@ -62,7 +62,7 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
}) })
e.GET("/register", func(c echo.Context) error { e.GET("/register", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
err := templates.Register().Render(c.Request().Context(), &buf) err := templates.Register().Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -72,7 +72,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Root route - landing page with smart login detection // Root route - landing page with smart login detection
e.GET("/", func(c echo.Context) error { e.GET("/", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
var err error var err error
@@ -117,7 +117,7 @@ func registerFrontendRoutes(cfg *Config) {
} }
// Dashboard page // Dashboard page
frontendProtected.GET("/dashboard", func(c echo.Context) error { frontendProtected.GET("/dashboard", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -203,7 +203,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Collections page // Collections page
frontendProtected.GET("/collections", func(c echo.Context) error { frontendProtected.GET("/collections", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -239,7 +239,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Collection create modal // Collection create modal
frontendProtected.GET("/collections/create-modal", func(c echo.Context) error { frontendProtected.GET("/collections/create-modal", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
err := templates.CollectionModal(templates.CollectionData{}).Render(c.Request().Context(), &buf) err := templates.CollectionModal(templates.CollectionData{}).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -249,7 +249,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Collection edit modal // Collection edit modal
frontendProtected.GET("/collections/:id/edit-modal", func(c echo.Context) error { frontendProtected.GET("/collections/:id/edit-modal", func(c *echo.Context) error {
// Parse collection ID // Parse collection ID
collectionID := c.Param("id") collectionID := c.Param("id")
collUUID, err := uuid.Parse(collectionID) collUUID, err := uuid.Parse(collectionID)
@@ -286,7 +286,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Restore system collection modal // Restore system collection modal
frontendProtected.GET("/collections/restore-modal", func(c echo.Context) error { frontendProtected.GET("/collections/restore-modal", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
err := templates.RestoreSystemCollectionModal().Render(c.Request().Context(), &buf) err := templates.RestoreSystemCollectionModal().Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
@@ -296,7 +296,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Collection detail page (works for both system and user collections) // Collection detail page (works for both system and user collections)
frontendProtected.GET("/collections/:id", func(c echo.Context) error { frontendProtected.GET("/collections/:id", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -440,7 +440,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Custom Section Builder page // Custom Section Builder page
frontendProtected.GET("/custom-section", func(c echo.Context) error { frontendProtected.GET("/custom-section", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -477,7 +477,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Progress page // Progress page
frontendProtected.GET("/progress", func(c echo.Context) error { frontendProtected.GET("/progress", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -502,7 +502,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Devices page // Devices page
frontendProtected.GET("/devices", func(c echo.Context) error { frontendProtected.GET("/devices", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -539,7 +539,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Conflicts page // Conflicts page
frontendProtected.GET("/conflicts", func(c echo.Context) error { frontendProtected.GET("/conflicts", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -567,7 +567,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Analytics page // Analytics page
frontendProtected.GET("/analytics", func(c echo.Context) error { frontendProtected.GET("/analytics", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -581,7 +581,7 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Profile page (all users) // Profile page (all users)
frontendProtected.GET("/profile", func(c echo.Context) error { frontendProtected.GET("/profile", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -600,7 +600,7 @@ func registerFrontendRoutes(cfg *Config) {
// ============================================================================ // ============================================================================
// Admin routes // Admin routes
frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error { frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -613,7 +613,7 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
})) }))
frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error { frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -626,7 +626,7 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
})) }))
frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error { frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -679,7 +679,7 @@ func registerFrontendRoutes(cfg *Config) {
})) }))
// Admin users page // Admin users page
frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c echo.Context) error { frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -723,7 +723,7 @@ func registerFrontendRoutes(cfg *Config) {
})) }))
// Admin: Get profile modal for editing user // Admin: Get profile modal for editing user
frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c echo.Context) error { frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c *echo.Context) error {
// Get target user ID from URL // Get target user ID from URL
targetUserID := c.Param("id") targetUserID := c.Param("id")
parsedUUID, err := uuid.Parse(targetUserID) parsedUUID, err := uuid.Parse(targetUserID)
@@ -763,7 +763,7 @@ func registerFrontendRoutes(cfg *Config) {
// ============================================================================ // ============================================================================
// Keep legacy routes under /api for existing API consumers // Keep legacy routes under /api for existing API consumers
protected.GET("/devices-page", func(c echo.Context) error { protected.GET("/devices-page", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -798,7 +798,7 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
}) })
protected.GET("/conflicts-page", func(c echo.Context) error { protected.GET("/conflicts-page", func(c *echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg) user, err := getTemplateUserWithTheme(c, cfg)
if err != nil { if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error") return renderErrorPage(c, "Error loading user", "user_load_error")
@@ -833,7 +833,7 @@ func registerFrontendRoutes(cfg *Config) {
// ============================================================================ // ============================================================================
// Health check // Health check
func (cfg *Config) GetHealth(c echo.Context) error { func (cfg *Config) GetHealth(c *echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second) ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
defer cancel() defer cancel()
if err := pingDB(cfg, ctx); err != nil { if err := pingDB(cfg, ctx); err != nil {
+2 -2
View File
@@ -9,10 +9,10 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
func getTemplateUserWithTheme(c echo.Context, cfg *Config) (templates.User, error) { func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, error) {
userID := c.Get("user_id").(string) userID := c.Get("user_id").(string)
userEmail := c.Get("user_email").(string) userEmail := c.Get("user_email").(string)
userUsername := c.Get("user_username").(string) userUsername := c.Get("user_username").(string)
+3 -3
View File
@@ -3,7 +3,7 @@ package router
import ( import (
"bookhoard/internal/handlers" "bookhoard/internal/handlers"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
func registerLibraryRoutes(cfg *Config) { func registerLibraryRoutes(cfg *Config) {
@@ -36,7 +36,7 @@ func registerLibraryRoutes(cfg *Config) {
adminLibrary.GET("/:id/folders", cfg.LibraryHandler.GetLibraryFolders) adminLibrary.GET("/:id/folders", cfg.LibraryHandler.GetLibraryFolders)
adminLibrary.DELETE("/:id/folders", cfg.LibraryHandler.DeleteLibraryFolder) adminLibrary.DELETE("/:id/folders", cfg.LibraryHandler.DeleteLibraryFolder)
adminLibrary.GET("/:id/stats", cfg.LibraryHandler.GetLibraryStats) adminLibrary.GET("/:id/stats", cfg.LibraryHandler.GetLibraryStats)
adminLibrary.POST("/:id/scan", func(c echo.Context) error { adminLibrary.POST("/:id/scan", func(c *echo.Context) error {
libraryID := c.Param("id") libraryID := c.Param("id")
scanReq := map[string]interface{}{ scanReq := map[string]interface{}{
"library_id": libraryID, "library_id": libraryID,
@@ -44,7 +44,7 @@ func registerLibraryRoutes(cfg *Config) {
c.Set("scan_request", scanReq) c.Set("scan_request", scanReq)
return h.ScanLibrary(c) return h.ScanLibrary(c)
}) })
adminLibrary.GET("/:id/media-items", func(c echo.Context) error { adminLibrary.GET("/:id/media-items", func(c *echo.Context) error {
libraryID := c.Param("id") libraryID := c.Param("id")
c.QueryParams().Set("library_id", libraryID) c.QueryParams().Set("library_id", libraryID)
return cfg.MediaHandler.ListMediaItems(c) return cfg.MediaHandler.ListMediaItems(c)
+10 -10
View File
@@ -21,9 +21,9 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
echojwt "github.com/labstack/echo-jwt/v4" echojwt "github.com/labstack/echo-jwt/v5"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
echomiddleware "github.com/labstack/echo/v4/middleware" echomiddleware "github.com/labstack/echo/v5/middleware"
) )
// CustomValidator wraps the go-playground validator // CustomValidator wraps the go-playground validator
@@ -71,7 +71,7 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
SigningKey: []byte(cfg.Cfg.JWTSecret), SigningKey: []byte(cfg.Cfg.JWTSecret),
ContextKey: "user", ContextKey: "user",
TokenLookup: "cookie:token,header:Authorization:Bearer ", TokenLookup: "cookie:token,header:Authorization:Bearer ",
SuccessHandler: func(c echo.Context) { SuccessHandler: func(c *echo.Context) error {
token := c.Get("user").(*jwt.Token) token := c.Get("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)
c.Set("user_id", claims["user_id"]) c.Set("user_id", claims["user_id"])
@@ -83,8 +83,7 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
userIDStr, _ := claims["user_id"].(string) userIDStr, _ := claims["user_id"].(string)
userUUID, err := uuid.Parse(userIDStr) userUUID, err := uuid.Parse(userIDStr)
if err != nil { if err != nil {
c.Error(echo.NewHTTPError(http.StatusBadRequest, "invalid user ID in token")) return echo.NewHTTPError(http.StatusBadRequest, "invalid user ID in token")
return
} }
c.Set("user", database.Users{ c.Set("user", database.Users{
@@ -93,8 +92,9 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
Username: claims["user_username"].(string), Username: claims["user_username"].(string),
Role: claims["user_role"].(string), Role: claims["user_role"].(string),
}) })
return nil
}, },
ErrorHandler: func(c echo.Context, err error) error { ErrorHandler: func(c *echo.Context, err error) error {
// Check if this is a page request (browser navigation) // Check if this is a page request (browser navigation)
if wantsHTML(c.Request().Header) { if wantsHTML(c.Request().Header) {
// Page request → Redirect to login with message // Page request → Redirect to login with message
@@ -114,7 +114,7 @@ func createJWTMiddleware(cfg *Config) echo.MiddlewareFunc {
// ensureUserExistsMiddleware checks if the authenticated user still exists in the database // ensureUserExistsMiddleware checks if the authenticated user still exists in the database
func ensureUserExistsMiddleware(cfg *Config) echo.MiddlewareFunc { func ensureUserExistsMiddleware(cfg *Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
userIDStr, ok := c.Get("user_id").(string) userIDStr, ok := c.Get("user_id").(string)
if !ok { if !ok {
return next(c) return next(c)
@@ -184,7 +184,7 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
e.Validator = &CustomValidator{validator: v} e.Validator = &CustomValidator{validator: v}
// Global middleware // Global middleware
e.Use(echomiddleware.Logger()) e.Use(echomiddleware.RequestLogger())
e.Use(echomiddleware.Recover()) e.Use(echomiddleware.Recover())
e.Use(echomiddleware.CORS()) e.Use(echomiddleware.CORS())
e.Use(ratelimit.RequestTracingMiddleware(cfg.Cfg)) e.Use(ratelimit.RequestTracingMiddleware(cfg.Cfg))
@@ -227,7 +227,7 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
e.Static("/static", "web/static") e.Static("/static", "web/static")
// Catch-all 404 handler - must be last // Catch-all 404 handler - must be last
e.GET("/*", func(c echo.Context) error { e.GET("/*", func(c *echo.Context) error {
var buf bytes.Buffer var buf bytes.Buffer
err := templates.ErrorPage("Page not found", "404").Render(c.Request().Context(), &buf) err := templates.ErrorPage("Page not found", "404").Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
+2 -1
View File
@@ -2,7 +2,8 @@ package router
import ( import (
"bookhoard/internal/handlers" "bookhoard/internal/handlers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
) )
// registerScannerRoutes registers all scanner-related endpoints // registerScannerRoutes registers all scanner-related endpoints
+2 -2
View File
@@ -43,6 +43,6 @@ func registerSyncRoutes(cfg *Config) {
func registerWebSocketRoutes(cfg *Config) { func registerWebSocketRoutes(cfg *Config) {
e := cfg.Echo e := cfg.Echo
// WebSocket endpoint for real-time sync wsGroup := e.Group("/ws/sync")
e.GET("/ws/sync", cfg.WSHandler.HandleWebSocket) wsGroup.GET("", cfg.WSHandler.HandleWebSocket)
} }