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

60
API.md
View File

@@ -49,13 +49,13 @@ user; the tenant is always taken from the session and never from the request.
| `DELETE /api/visitors/{id}` — erasure | manager |
| `GET /api/sites` · `GET /api/sites/{site}/check` · `GET /api/cameras` · `GET /api/cameras/{id}/snapshot.jpg` · `GET /api/cameras/{id}/live` | authed |
| `POST /api/sites/{site}/cameras` · `PATCH` / `DELETE /api/cameras/{id}` · `POST /api/cameras/{id}/check` | manager |
| `POST /api/sites` — open a shop | owner |
| `POST /api/sites` — open a shop · `DELETE /api/sites/{site}` — remove an empty one | owner |
| `POST /api/sites/{site}/enrolment-code` | manager |
| `GET /api/team` | authed (tenant users only) |
| `POST /api/team/members` · `POST /api/team/{id}/password` · `PATCH /api/team/{id}` · `/api/team/invitations*` | manager |
| `GET /api/reports/footfall` · `GET /api/reports/conversion` | authed |
| `POST /api/assistant` | authed |
| `GET` / `POST /api/admin/clients` | **platform admin** |
| `GET` / `POST /api/admin/clients` · `PATCH /api/admin/clients/{id}` · `POST /api/admin/clients/{id}/owner-password` · `DELETE /api/admin/clients/{id}` | **platform admin** |
| `/api/agent/*` | **shop PC token** — never a user |
A role that may not call something gets **403 `forbidden`** with a message
@@ -102,6 +102,9 @@ travels over chat.
```
GET /api/admin/clients → every merchant, with site and user counts
PATCH /api/admin/clients/{id} {"active": false} → suspend (or true: reinstate)
POST /api/admin/clients/{id}/owner-password → new owner password, shown once
DELETE /api/admin/clients/{id} {"confirm": "<slug>"} → delete a SUSPENDED company
```
### Tier 2 — the merchant owner registers sales staff
@@ -218,9 +221,10 @@ somebody else's account.
- **There is no mobile app in this repository.** Tier 3 is a complete API with
no client yet. Everything above is what that app will call.
- **The admin cannot reset a merchant owner's password over HTTP**, nor suspend
or delete a merchant. Today that is `behavision-server provision` on the
server.
- **A shop with visit history cannot be deleted**, only its cameras removed.
`DELETE /api/sites/{site}` is for the shop opened by mistake (no visits, no
cameras); taking away footfall and faces is an erasure decision, and there
is no endpoint for it yet.
---
@@ -261,10 +265,13 @@ GET /api/reports/footfall?from=&to= → the numbers, with their confidenc
POST /api/auth/login (an account with no company)
GET /api/admin/clients → every company
POST /api/admin/clients → create one, with its owner
PATCH /api/admin/clients/{id} → suspend / reinstate
POST /api/admin/clients/{id}/owner-password → reset the owner's password
DELETE /api/admin/clients/{id} → delete, once suspended
```
That is the whole admin surface today. Everything inside a company is the
company's own business and is reached by signing in as one of its users.
That is the whole admin surface. Everything inside a company is the company's
own business and is reached by signing in as one of its users.
---
@@ -1045,6 +1052,45 @@ in as the company's owner, or by `behavision-server provision` on the server.
---
### `PATCH /api/admin/clients/{id}` — suspend or reinstate
```
{ "active": false }
→ 200 { "client": { "id": "…", "slug": "acme", "active": false, "sites": 2, "users": 5, … },
"sessions_revoked": 3 }
```
Suspension is complete the moment it returns: the company's users cannot sign
in, every session they hold is revoked in the same transaction (so a live
access token stops working now, not at expiry), and visits from its shop PCs
are dropped at ingest. `{"active": true}` reinstates; sessions are not
restored — people sign in again.
### `POST /api/admin/clients/{id}/owner-password` — reset the owner's password
```
{ "email": "owner@acme.com" } ← optional when the company has exactly one owner
→ 200 { "email": "owner@acme.com", "password": "n7xw…" } ← shown ONCE
```
For the owner who has locked themselves out with nobody above them. Generated,
never chosen; every session that owner held is revoked. With several owners
and no `email`, 400 listing them.
### `DELETE /api/admin/clients/{id}` — delete a company
```
{ "confirm": "acme" }
→ 200 { "deleted": "acme", "images_deleted": 12 }
```
Irreversible, and the data is biometric, so it is a two-step decision: the
company must already be **suspended** (`409 still_active` otherwise) and the
body must repeat its slug. Stored face images are deleted from object storage
first — a failure there is `502 storage_error` and nothing else is touched —
then the shop PCs' broker logins, then every row (templates, visits, users,
sessions, cameras) by cascade.
## 12. Errors
```json