Stage 10: admin console & dictionary ops (complaint review, hot-reload, broadcasts)
Server-rendered admin console in the backend at /_gm (internal/adminconsole), fronted on the gateway's public listener by Basic-Auth + a verbatim reverse proxy (mounted on the edge mux below the h2c wrap). A same-origin check guards its POSTs; no operator identity is tracked. This supersedes the Stage 6 gateway-fronts- /api/v1/admin model: GATEWAY_ADMIN_ADDR and the backend /api/v1/admin ping are dropped and gateway/internal/admin is repurposed to the verbatim proxy. - Complaints: migration 00008 (+ jetgen) adds disposition/resolution_note/ resolved_at/applied_in_version + the deferred status CHECK; resolution feeds a query-derived pending dictionary-change pipeline (marked applied after a reload). - Dictionary hot-reload: per-version subdir BACKEND_DICT_DIR/<version>/ via the new Registry.LoadAvailable; engine.OpenWithVersions restores resident versions on restart. Partially addresses TODO-2. - Broadcasts: a backend Telegram-connector client (internal/connector, BACKEND_CONNECTOR_ADDR) for SendToUser / SendToGameChannel (discharges the Stage 9 forward-note). - Admin reads: account.ListAccounts/CountAccounts/Identities and game.ListGames/CountGames/GameByID/ListComplaints/GetComplaint/CountComplaints/ ResolveComplaint/DictionaryChanges/MarkChangesApplied. - Tests: adminconsole render, engine reload, same-origin guard, gateway verbatim proxy + h2c console mount, inttest complaint pipeline + list/count + /_gm console. - Docs: PLAN (Stage 10 done + refinements + TODO-2), ARCHITECTURE §1/§5/§6/§12/§13, FUNCTIONAL (+_ru), TESTING, backend/gateway READMEs.
This commit is contained in:
@@ -67,6 +67,16 @@ type Account struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Identity is one of an account's platform/email identities, surfaced on the
|
||||
// admin account-detail view. ExternalID is the platform user id (or the email
|
||||
// address for an email identity); Confirmed tracks the email confirm-code flow.
|
||||
type Identity struct {
|
||||
Kind string
|
||||
ExternalID string
|
||||
Confirmed bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Store is the Postgres-backed query surface for accounts and identities.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
@@ -187,6 +197,54 @@ func (s *Store) IdentityExternalID(ctx context.Context, accountID uuid.UUID, kin
|
||||
return row.ExternalID, nil
|
||||
}
|
||||
|
||||
// Identities returns the account's platform/email identities, oldest first, for
|
||||
// the admin account-detail view.
|
||||
func (s *Store) Identities(ctx context.Context, accountID uuid.UUID) ([]Identity, error) {
|
||||
stmt := postgres.SELECT(table.Identities.AllColumns).
|
||||
FROM(table.Identities).
|
||||
WHERE(table.Identities.AccountID.EQ(postgres.UUID(accountID))).
|
||||
ORDER_BY(table.Identities.CreatedAt.ASC())
|
||||
var rows []model.Identities
|
||||
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
|
||||
return nil, fmt.Errorf("account: list identities %s: %w", accountID, err)
|
||||
}
|
||||
out := make([]Identity, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, Identity{Kind: r.Kind, ExternalID: r.ExternalID, Confirmed: r.Confirmed, CreatedAt: r.CreatedAt})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListAccounts returns accounts for the admin user list, newest first, paginated
|
||||
// by limit and offset.
|
||||
func (s *Store) ListAccounts(ctx context.Context, limit, offset int) ([]Account, error) {
|
||||
stmt := postgres.SELECT(table.Accounts.AllColumns).
|
||||
FROM(table.Accounts).
|
||||
ORDER_BY(table.Accounts.CreatedAt.DESC()).
|
||||
LIMIT(int64(limit)).
|
||||
OFFSET(int64(offset))
|
||||
var rows []model.Accounts
|
||||
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
|
||||
return nil, fmt.Errorf("account: list accounts: %w", err)
|
||||
}
|
||||
out := make([]Account, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, modelToAccount(r))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountAccounts returns the total number of accounts, for admin-list pagination.
|
||||
func (s *Store) CountAccounts(ctx context.Context) (int, error) {
|
||||
stmt := postgres.SELECT(postgres.COUNT(table.Accounts.AccountID).AS("count")).
|
||||
FROM(table.Accounts)
|
||||
var dest struct{ Count int64 }
|
||||
if err := stmt.QueryContext(ctx, s.db, &dest); err != nil {
|
||||
return 0, fmt.Errorf("account: count accounts: %w", err)
|
||||
}
|
||||
return int(dest.Count), nil
|
||||
}
|
||||
|
||||
// findByIdentity joins identities to accounts and returns the matching account,
|
||||
// or ErrNotFound.
|
||||
func (s *Store) findByIdentity(ctx context.Context, kind, externalID string) (Account, error) {
|
||||
|
||||
Reference in New Issue
Block a user