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)
119 lines
2.5 KiB
Go
119 lines
2.5 KiB
Go
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
|
|
})
|
|
}
|