feat(router): add setup redirect middleware and setup routes
Add setupRedirectMiddleware that checks the setup_complete system setting on every request. If setup is incomplete, all non-setup requests are redirected to /setup so the wizard is the first thing new users see. The check uses an in-memory cache (10s TTL) to avoid hitting the database on every request, with cache invalidation on setup completion. The middleware skips /setup, /api/*, /static/*, /health, and /favicon.ico so the wizard page, API calls, and static assets load normally during setup. Register two new routes: - GET /setup: renders the setup wizard SSR template - PUT /api/setup/complete: marks setup as complete (JWT-protected, requires an authenticated admin user created in step 1)
This commit is contained in:
@@ -188,6 +188,9 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
|
|||||||
}
|
}
|
||||||
e.Validator = &CustomValidator{validator: v}
|
e.Validator = &CustomValidator{validator: v}
|
||||||
|
|
||||||
|
// Setup redirect middleware - must run before all routes
|
||||||
|
e.Pre(setupRedirectMiddleware(cfg))
|
||||||
|
|
||||||
// Rate limiter
|
// Rate limiter
|
||||||
rateLimiterConfig := ratelimit.RateLimiterConfig{
|
rateLimiterConfig := ratelimit.RateLimiterConfig{
|
||||||
Enabled: cfg.Cfg.RateLimitEnabled,
|
Enabled: cfg.Cfg.RateLimitEnabled,
|
||||||
@@ -206,6 +209,7 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
|
|||||||
cfg.ScannerHandler = scannerHandler
|
cfg.ScannerHandler = scannerHandler
|
||||||
|
|
||||||
// Register route groups
|
// Register route groups
|
||||||
|
registerSetupRoutes(cfg)
|
||||||
registerAuthRoutes(cfg, rateLimitMiddleware)
|
registerAuthRoutes(cfg, rateLimitMiddleware)
|
||||||
registerLibraryRoutes(cfg)
|
registerLibraryRoutes(cfg)
|
||||||
registerDeviceRoutes(cfg)
|
registerDeviceRoutes(cfg)
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bookhoard/templates"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
setupCacheMu sync.RWMutex
|
||||||
|
setupCacheComplete bool = true
|
||||||
|
setupCacheExpiry time.Time
|
||||||
|
setupCacheTTL = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func isSetupComplete(cfg *Config) bool {
|
||||||
|
setupCacheMu.RLock()
|
||||||
|
if time.Now().Before(setupCacheExpiry) {
|
||||||
|
complete := setupCacheComplete
|
||||||
|
setupCacheMu.RUnlock()
|
||||||
|
return complete
|
||||||
|
}
|
||||||
|
setupCacheMu.RUnlock()
|
||||||
|
|
||||||
|
val, err := cfg.Queries.GetSystemSetting(context.Background(), "setup_complete")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
setupCacheMu.Lock()
|
||||||
|
setupCacheComplete = false
|
||||||
|
setupCacheExpiry = time.Now().Add(setupCacheTTL)
|
||||||
|
setupCacheMu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
complete, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
complete = false
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCacheMu.Lock()
|
||||||
|
setupCacheComplete = complete
|
||||||
|
setupCacheExpiry = time.Now().Add(setupCacheTTL)
|
||||||
|
setupCacheMu.Unlock()
|
||||||
|
return complete
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidateSetupCache() {
|
||||||
|
setupCacheMu.Lock()
|
||||||
|
setupCacheComplete = true
|
||||||
|
setupCacheExpiry = time.Time{}
|
||||||
|
setupCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRedirectMiddleware(cfg *Config) echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c *echo.Context) error {
|
||||||
|
path := c.Request().URL.Path
|
||||||
|
|
||||||
|
if path == "/setup" || path == "/setup/" {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(path, "/api/") {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(path, "/static/") || path == "/health" || path == "/favicon.ico" {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSetupComplete(cfg) {
|
||||||
|
return c.Redirect(http.StatusFound, "/setup")
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerSetupRoutes(cfg *Config) {
|
||||||
|
e := cfg.Echo
|
||||||
|
|
||||||
|
e.GET("/setup", func(c *echo.Context) error {
|
||||||
|
if isSetupComplete(cfg) {
|
||||||
|
return c.Redirect(http.StatusFound, "/")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := templates.Setup().Render(c.Request().Context(), &buf); err != nil {
|
||||||
|
log.Printf("Failed to render setup template: %v", err)
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Failed to render setup page")
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
jwtMiddleware := createJWTMiddleware(cfg)
|
||||||
|
protected := e.Group("/api/setup", jwtMiddleware)
|
||||||
|
protected.PUT("/complete", func(c *echo.Context) error {
|
||||||
|
err := cfg.SystemSettingsHandler.SetSetupComplete(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
invalidateSetupCache()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user