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:
@@ -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