feat: add protected frontend SSR routes

- Add frontendProtected group for authenticated pages
- Add /dashboard, /collections, /progress, /devices, /conflicts, /analytics routes
- Add /admin, /admin/, /admin/profile, /admin/library routes
- Keep legacy /api/devices-page and /api/conflicts-page for backward compatibility
- All routes use JWT middleware for authentication
This commit is contained in:
2026-02-15 21:36:36 -05:00
parent 1803ac2ee7
commit 6b3ccdfc55
+191 -64
View File
@@ -10,8 +10,10 @@ import (
"bookhoard/templates" "bookhoard/templates"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo-jwt/v4" "github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/google/uuid"
) )
func registerFrontendRoutes(cfg *Config) { func registerFrontendRoutes(cfg *Config) {
@@ -24,23 +26,16 @@ func registerFrontendRoutes(cfg *Config) {
// All authenticated frontend routes use the jwtMiddleware to validate tokens. // All authenticated frontend routes use the jwtMiddleware to validate tokens.
// ============================================================================ // ============================================================================
// JWT middleware for protected routes // Use existing JWT middleware (sets database user object in context)
jwtMiddleware := echojwt.WithConfig(echojwt.Config{ jwtMiddleware := createJWTMiddleware(cfg)
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 route group for API routes
protected := e.Group("/api", jwtMiddleware) protected := e.Group("/api", jwtMiddleware)
// ============================================================================
// PUBLIC FRONTEND ROUTES (No authentication required)
// ============================================================================
// 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
@@ -90,70 +85,83 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
}) })
// Public redirect routes // ============================================================================
e.GET("/bookshelf", func(c echo.Context) error { // PROTECTED FRONTEND ROUTES (Authentication required)
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf") // ============================================================================
// 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())
}) })
e.GET("/dashboard", func(c echo.Context) error { // Collections page
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf") 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())
}) })
// Admin routes // Progress page
e.GET("/admin", handlers.AdminMiddleware(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 c.HTML(http.StatusInternalServerError, "Error loading user") 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 var buf bytes.Buffer
err = templates.Admin(user).Render(c.Request().Context(), &buf) err = templates.Progress(user, progressData).Render(c.Request().Context(), &buf)
if err != nil { if err != nil {
return err return err
} }
return c.HTML(http.StatusOK, buf.String()) 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 // Devices page
protected.GET("/devices-page", 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 c.HTML(http.StatusInternalServerError, "Error loading user") return c.HTML(http.StatusInternalServerError, "Error loading user")
@@ -176,6 +184,121 @@ func registerFrontendRoutes(cfg *Config) {
}) })
// Conflicts page // 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 { 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 {
@@ -193,6 +316,10 @@ func registerFrontendRoutes(cfg *Config) {
return c.HTML(http.StatusOK, buf.String()) return c.HTML(http.StatusOK, buf.String())
}) })
// ============================================================================
// HEALTH CHECK
// ============================================================================
// Health check // Health check
e.GET("/health", func(c echo.Context) error { e.GET("/health", func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second) ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)