feat: backend service
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user