The platform admin's last shell-only jobs are endpoints

Suspend or reinstate a company (PATCH /api/admin/clients/{id}), reset
its owner's password (shown once), and delete it - and an owner can
remove a shop opened by mistake (DELETE /api/sites/{site}, empty only).

Suspension ends every session the company holds in the same
transaction: login and ingest already refused an inactive client, but a
live access token would have kept reading for up to twelve hours, so
'suspend' would have meant 'suspend some time tomorrow'. Deletion is
deliberately two steps - the company must already be suspended and the
request repeats the slug - because the data under it is biometric.
Face images go first (a storage failure aborts with nothing touched),
then the broker logins, then the rows by cascade.

Exercised against the local Postgres and broker: create, open a shop,
remove it (two plugin commands), refuse delete while active, suspend
(owner's token 401 immediately), reset, delete, zero rows left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-19 12:36:21 +05:30
parent c93fbff31f
commit 8786a5b0b4
8 changed files with 686 additions and 9 deletions

View File

@@ -76,7 +76,7 @@ func (s *Store) CreateClientWithOwner(ctx context.Context, in api.NewClientInput
// in whichever direction the operator's eye went first.
func (s *Store) ListClients(ctx context.Context) ([]api.ClientRow, error) {
rows, err := s.pool.Query(ctx, `
SELECT c.id::text, c.slug, c.name, c.created_at,
SELECT c.id::text, c.slug, c.name, c.active, c.created_at,
(SELECT count(*) FROM sites si WHERE si.client_id = c.id),
(SELECT count(*) FROM app_users au WHERE au.client_id = c.id)
FROM clients c
@@ -90,7 +90,7 @@ func (s *Store) ListClients(ctx context.Context) ([]api.ClientRow, error) {
for rows.Next() {
var c api.ClientRow
var at time.Time
if err := rows.Scan(&c.ID, &c.Slug, &c.Name, &at, &c.Sites, &c.Users); err != nil {
if err := rows.Scan(&c.ID, &c.Slug, &c.Name, &c.Active, &at, &c.Sites, &c.Users); err != nil {
return nil, err
}
c.CreatedAt = at.UTC().Format(time.RFC3339)