feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+128
View File
@@ -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)
}