package admin import ( "context" "fmt" "sync" "sync/atomic" ) // cacheEntry pairs the admin aggregate with its bcrypt hash. The // hash is private to the admin package: handlers receive only the // Admin shape, and Verify consumes the hash directly off the cache. type cacheEntry struct { admin Admin passwordHash []byte } // Cache is the in-memory write-through projection of the rows in // `backend.admin_accounts`. Reads (Get) are RLocked; writes (Put, // Remove) are Locked. // // The cache mirrors the `auth.Cache` and `user.Cache` idioms: callers // commit to Postgres first, then update the cache. A commit failure // leaves the cache untouched, matching the previous DB state. type Cache struct { mu sync.RWMutex byName map[string]cacheEntry ready atomic.Bool } // NewCache constructs an empty Cache. The cache reports Ready() == // false until Warm completes successfully. func NewCache() *Cache { return &Cache{ byName: make(map[string]cacheEntry), } } // Warm replaces the cache contents with every row loaded from store. // It is intended to be called exactly once at process boot before the // HTTP listener accepts traffic; successful completion flips Ready to // true. Subsequent calls re-warm the cache (useful in tests). func (c *Cache) Warm(ctx context.Context, store *Store) error { if c == nil { return nil } admins, hashes, err := store.ListAll(ctx) if err != nil { return fmt.Errorf("admin cache warm: %w", err) } c.mu.Lock() defer c.mu.Unlock() c.byName = make(map[string]cacheEntry, len(admins)) for i, a := range admins { c.byName[a.Username] = cacheEntry{ admin: a, passwordHash: hashes[i], } } c.ready.Store(true) return nil } // Ready reports whether Warm has completed at least once. The HTTP // readiness probe wires through this method together with the auth // and user caches so `/readyz` only flips to 200 after every cache is // hydrated. func (c *Cache) Ready() bool { if c == nil { return false } return c.ready.Load() } // Size returns the number of cached admin accounts. Useful for the // startup log line and tests. func (c *Cache) Size() int { if c == nil { return 0 } c.mu.RLock() defer c.mu.RUnlock() return len(c.byName) } // Get returns the cached entry for username and a presence flag. // Misses always return the zero entry and false. func (c *Cache) Get(username string) (Admin, []byte, bool) { if c == nil { return Admin{}, nil, false } c.mu.RLock() defer c.mu.RUnlock() entry, ok := c.byName[username] if !ok { return Admin{}, nil, false } return entry.admin, entry.passwordHash, true } // Put stores admin and its bcrypt hash in the cache. It is safe to // call on an existing entry — the value is overwritten with the // latest snapshot. The slice is stored by reference; callers must not // mutate it after handing it to Put. func (c *Cache) Put(admin Admin, passwordHash []byte) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() c.byName[admin.Username] = cacheEntry{ admin: admin, passwordHash: passwordHash, } } // Remove evicts the entry for username. Calling Remove on a missing // entry is a no-op. The current implementation ships no Delete operation; the helper // exists for symmetry with `auth.Cache` / `user.Cache` and for any // future hard-delete flow. func (c *Cache) Remove(username string) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() delete(c.byName, username) }