// Package setupstatus reports whether the application's initial setup has been // completed. Setup is considered complete when at least one admin user exists // AND a non-empty base_url has been configured, regardless of how those were // 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) } // BaseURLGetter returns the configured base_url value from the database, or an // error if it cannot be read. Defined as a function type (not an interface) so // it can be satisfied by a closure wrapping *database.Queries.GetSystemConfig // without importing the database package. type BaseURLGetter func(ctx context.Context) (string, 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 AND base_url is configured. 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, baseURLGetter BaseURLGetter) bool { cacheMu.RLock() if time.Now().Before(cacheExpiry) { complete := cacheComplete cacheMu.RUnlock() return complete } cacheMu.RUnlock() complete := true count, err := q.CountAdmins(ctx) if err == nil { complete = count > 0 } if complete && baseURLGetter != nil { baseURL, err := baseURLGetter(ctx) if err == nil { complete = baseURL != "" } } 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) or // the base_url configuration. func Invalidate() { cacheMu.Lock() cacheComplete = true cacheExpiry = time.Time{} cacheMu.Unlock() }