Opening a shop is an API call; the broker learns of it in the same request

The last step of onboarding that needed a shell: provision site printed
a broker password and a person typed it into Mosquitto's passwd file on
the host - mounted read-only in the container, so the first attempt
failed silently and the password was re-rolled. No tenant could open a
second branch without us.

The server now drives Mosquitto's dynamic-security plugin over its own
broker login: POST /api/sites (owner) writes the row and the sealed
password, registers the login and a per-site role with literal topics
(the 2.0 plugin does not substitute %u - measured), and removes the row
again if the broker refuses, so a shop cannot exist in the database and
not on the broker. provision site goes through the same path. The
head-office Shops screen gets 'Open a new shop'.

broker-init converts the existing passwd file into the plugin's store
with every hash intact - PBKDF2-SHA512 both sides - so the cutover
re-claims no shop PC. Rehearsed locally: old logins keep working,
isolation holds, the health probe works, and a PC claiming a shop opened
through the API connects as that shop. run-local.sh now brings the
broker up the same way.

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 11:55:26 +05:30
parent 5f83a1077d
commit 4c750cb2ac
21 changed files with 1310 additions and 54 deletions

29
API.md
View File

@@ -49,6 +49,7 @@ 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/{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 |
@@ -164,9 +165,9 @@ PATCH /api/team/{id} { "role": "manager" } → promote
PATCH /api/team/{id} { "active": false } → they have left; signs them out now
```
The owner also sets the shop up from the same login — `POST
/api/sites/{site}/enrolment-code` for the shop PC, `POST
/api/sites/{site}/cameras` for cameras — see §8.
The owner also opens shops and sets them up from the same login — `POST
/api/sites` to open one, `POST /api/sites/{site}/enrolment-code` for its
shop PC, `POST /api/sites/{site}/cameras` for cameras — see §8.
### Tier 3 — the salesperson gets their mobile login
@@ -718,6 +719,28 @@ unplugged PC.
footfall lost because that queue overflowed. Non-zero `dropped` is a report
that is wrong in a way the report itself cannot show.
### `POST /api/sites` — open a shop (owner)
```
{ "name": "TeNext Bengaluru", "slug": "bengaluru", "timezone": "Asia/Kolkata" }
→ 201 { "site_id": "…", "slug": "bengaluru", "name": "TeNext Bengaluru",
"timezone": "Asia/Kolkata", "broker_username": "tenext-retail.bengaluru" }
```
`slug` and `timezone` are optional: the slug is made from the name (lower-case,
digits and dashes, 3–32 characters) and the timezone defaults to Asia/Kolkata.
The slug is the shop PC's identity and an MQTT topic segment; it **cannot be
changed afterwards**. The server registers the shop's broker login with
Mosquitto in the same request, so the next step is simply
`POST /api/sites/{slug}/enrolment-code` for the PC.
| status | code | meaning |
|---|---|---|
| 403 | `forbidden` | not the owner |
| 409 | `conflict` | a shop with that slug exists |
| 502 | `broker_unavailable` | the broker did not accept the login; **nothing was created** — try again |
| 503 | `broker_unavailable` / `no_encryption_key` | this server cannot create shops; contact support |
### `GET /api/sites/{site}/check`
Five ordered steps that answer *is this shop working*, assembled from what head