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:
+191
-64
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user