refactor(setup): derive setup-complete status from admin user count

Setup completion was previously tracked by a manually-flipped setup_complete row in system_settings, written via a JWT-protected PUT /api/setup/complete endpoint. This meant any admin user created outside the setup wizard (future CLI, seed scripts, direct DB inserts) would not flip the switch, leaving the app stuck redirecting to /setup.

The trigger is now derived from real data: setup is complete iff at least one admin user exists. This is self-correcting regardless of how users are created, and re-engages setup automatically if all admins are ever removed.

Changes:
- Add internal/setupstatus package with IsSetupComplete() (queries CountAdmins, 10s in-memory cache, fails open on DB error) and Invalidate() to clear the cache. Uses an AdminCounter interface to avoid importing the database package.
- Add CountAdmins sqlc query (SELECT COUNT(*) FROM users WHERE role = 'admin') and regenerate.
- Rewire router/setup.go isSetupComplete() to delegate to setupstatus; drop the old setup_complete setting read, cache vars, and the PUT /api/setup/complete route.
- Call setupstatus.Invalidate() in the auth handler after CreateUser, UpdateUserRole, and DeleteUser so the cache reflects admin-count changes immediately.
- Align first-user promotion in Register to key off !adminExists instead of len(users) == 0, so the two checks cannot diverge.
- Remove the now-dead SetSetupComplete/GetSetupStatus handlers.
- Drop the setup_complete seed row from schema.sql.
- Remove the apiPut('/setup/complete') call from the setup wizard finishSetup(); the admin account created in submitAdmin already marks setup complete server-side.
This commit is contained in:
2026-07-29 11:08:18 -04:00
parent acfb298b74
commit 980aaee0d9
9 changed files with 111 additions and 110 deletions
+2 -60
View File
@@ -3,65 +3,18 @@ package router
import (
"bytes"
"context"
"errors"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"bookhoard/internal/setupstatus"
"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()
return setupstatus.IsSetupComplete(context.Background(), cfg.Queries)
}
func setupRedirectMiddleware(cfg *Config) echo.MiddlewareFunc {
@@ -104,15 +57,4 @@ func registerSetupRoutes(cfg *Config) {
}
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
})
}