Files
bookhoard/internal/router/frontend.go
T
john-okeefe 2a64ca423f Refactor: Eliminate duplicate types - Use handler types directly
- Deleted templates.CollectionDetailData - using templates.CollectionData everywhere
- Deleted templates.BookData - using handlers.BookInfo everywhere
- Deleted templates.DeviceData - using handlers.DeviceInfo everywhere
- Deleted templates.ProgressItemData - using handlers.ProgressWithMedia everywhere
- Deleted templates.convertDevices() helper - Use handlers types directly in templates
- Enhanced handlers.ProgressWithMedia with device metadata fields
- Added handlers.getDeviceIcon() helper
- Updated all templates to import handlers package
- Cleaned up unused imports

This aligns codebase with templ's design philosophy (use Go types directly, no parallel type system)
2026-02-12 19:48:07 -05:00

213 lines
6.1 KiB
Go

package router
import (
"bytes"
"context"
"net/http"
"time"
"bookhoard/internal/handlers"
"bookhoard/templates"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
)
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.
// ============================================================================
// 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"])
},
})
// Protected route group
protected := e.Group("/api", jwtMiddleware)
// Public routes for login and registration pages
e.GET("/login", func(c echo.Context) error {
var buf bytes.Buffer
err := templates.Login().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())
})
// Public redirect routes
e.GET("/bookshelf", func(c echo.Context) error {
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
})
e.GET("/dashboard", func(c echo.Context) error {
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
})
// Admin routes
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/", 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 {
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).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Conflicts page
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
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",
})
})
}