// Package setupstatus reports whether the application's initial setup has been // completed. Setup is considered complete as soon as at least one admin user // exists, regardless of how that user was created (setup wizard, API, or a // future CLI). This keeps the setup gate a derived property of real data // rather than a manually-flipped flag that can drift out of sync. package setupstatus import ( "context" "sync" "time" ) // AdminCounter is satisfied by *database.Queries. It is defined as an interface // here so this package does not import the database package, keeping the // dependency graph flat and avoiding import cycles. type AdminCounter interface { CountAdmins(ctx context.Context) (int64, error) } var ( cacheMu sync.RWMutex cacheComplete bool = true cacheExpiry time.Time cacheTTL = 10 * time.Second ) // IsSetupComplete reports whether setup is complete. Setup is complete when at // least one admin user exists. A short in-memory cache avoids hammering the // database on every request. On a database error the function fails open // (returns true) so a transient outage does not lock users out of the app. func IsSetupComplete(ctx context.Context, q AdminCounter) bool { cacheMu.RLock() if time.Now().Before(cacheExpiry) { complete := cacheComplete cacheMu.RUnlock() return complete } cacheMu.RUnlock() count, err := q.CountAdmins(ctx) complete := true if err == nil { complete = count > 0 } cacheMu.Lock() cacheComplete = complete cacheExpiry = time.Now().Add(cacheTTL) cacheMu.Unlock() return complete } // Invalidate clears the cached setup status so the next call to IsSetupComplete // re-reads from the database. Call this after any write that could change the // admin user count (user creation, role promotion/demotion, user deletion). func Invalidate() { cacheMu.Lock() cacheComplete = true cacheExpiry = time.Time{} cacheMu.Unlock() }