A merchant can create a salesperson's login and hand it over
The flow this product is sold on is three tiers: the platform admin
registers a merchant, the merchant registers their sales staff, the
staff sign in on a phone. Tier 1 handed the new owner a password. Tier 2
could not - a manager could only mint an invitation code, which the
salesperson had to redeem themselves, on their own phone, choosing their
own password. Good practice, and no use to a manager setting somebody up
before their first shift with a card and a pen.
POST /api/team/members mirrors POST /api/admin/clients: generated
password unless one is given, returned exactly once, bcrypt-hashed on
the way in and not recoverable after. Same permission shape as an
invitation - manager and above, only an owner mints an owner, admin
refused - so a manager cannot do through one door what they are refused
at the other. The invitation path stays; it is the better one whenever
the salesperson has their phone.
POST /api/team/{id}/password is the everyday case on a shop floor:
they forgot it. It sets a new one AND revokes every session they hold,
in one transaction, because the other reason a manager resets a
password is a lost phone, and a reset that left that phone signed in
would look complete while fixing nothing. Tenant-scoped in the UPDATE
itself; another company's user id is 404, never 403. No self-service
and no reset-by-email, deliberately: a floor account often has no
mailbox anyone checks, and the person who can vouch for the salesperson
standing in front of them is their manager.
RandomPassword moves from a private helper in the store to auth, so the
admin path, the merchant path and the reset all mint the same 80-bit
credential - rather than someone later writing a shorter one for the
"less important" account.
Verified: eight handler tests, and two against a real Postgres for the
things a fake cannot see - the RETURNING list scans on a row with no
last_login_at, the tenant scope holds, and the sessions row is actually
revoked. The tenant cleanup from yesterday held throughout.
API.md now documents the chain with both paths, and the note saying a
merchant could not create a login directly is gone because it is no
longer true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
102
API.md
102
API.md
@@ -25,7 +25,7 @@ everything the one below can:
|
||||
| role | can additionally |
|
||||
|---|---|
|
||||
| `staff` | see arrivals, search customers, edit a customer's profile, record a purchase |
|
||||
| `manager` | manage cameras, invite and remove team members, issue shop-PC installation codes, erase a customer |
|
||||
| `manager` | manage cameras, create / invite / reset / remove team members, issue shop-PC installation codes, erase a customer |
|
||||
| `owner` | promote somebody to owner |
|
||||
|
||||
A platform admin has `role: "admin"` **and an empty `client_id`** — both
|
||||
@@ -51,7 +51,7 @@ user; the tenant is always taken from the session and never from the request.
|
||||
| `POST /api/sites/{site}/cameras` · `PATCH` / `DELETE /api/cameras/{id}` · `POST /api/cameras/{id}/check` | manager |
|
||||
| `POST /api/sites/{site}/enrolment-code` | manager |
|
||||
| `GET /api/team` | authed (tenant users only) |
|
||||
| `PATCH /api/team/{id}` · `/api/team/invitations*` | manager |
|
||||
| `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** |
|
||||
@@ -69,12 +69,14 @@ Three tiers. Each one creates the login for the next, and nobody ever creates
|
||||
their own from nothing.
|
||||
|
||||
```
|
||||
┌──────────────────┐ creates ┌──────────────────┐ invites ┌──────────────────┐
|
||||
┌──────────────────┐ creates ┌──────────────────┐ creates ┌──────────────────┐
|
||||
│ Platform admin │ ───────────▶ │ Merchant owner │ ───────────▶ │ Sales staff │
|
||||
│ (web) │ company + │ (web) │ code, │ (mobile) │
|
||||
│ (web) │ company + │ (web) │ login, pw │ (mobile) │
|
||||
│ │ owner login │ │ shown once │ │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
POST /api/admin/clients POST /api/team/invitations POST /api/auth/register
|
||||
POST /api/admin/clients POST /api/team/members POST /api/auth/login
|
||||
(or /api/team/invitations → (or /api/auth/register
|
||||
a code they redeem themselves) with the code)
|
||||
```
|
||||
|
||||
### Tier 1 — the platform admin registers a merchant
|
||||
@@ -103,12 +105,33 @@ GET /api/admin/clients → every merchant, with site and user counts
|
||||
|
||||
### Tier 2 — the merchant owner registers sales staff
|
||||
|
||||
Signed in as the owner (or any manager).
|
||||
Signed in as the owner (or any manager). Two ways to do it; use whichever fits
|
||||
the moment.
|
||||
|
||||
**Directly — create the login and hand it over.** For a salesperson being set
|
||||
up before their first shift, without a phone in hand. Exactly how the admin
|
||||
created the merchant in Tier 1.
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
{ "email": "suriya@tenext.in", "password": "xK9…", "device": "Head office" }
|
||||
|
||||
POST /api/team/members
|
||||
{ "email": "priya@tenext.in", "full_name": "Priya R", "role": "staff" }
|
||||
→ 201 { "id": "…", "email": "priya@tenext.in", "full_name": "Priya R",
|
||||
"role": "staff", "active": true, "created_at": "…",
|
||||
"password": "m4kq…" } ← shown ONCE
|
||||
```
|
||||
|
||||
Leave `password` out and one is generated; give one and it is used (8
|
||||
characters minimum). Either way it is returned exactly once — write it on the
|
||||
card now. The salesperson signs in on their phone with that email and
|
||||
password, and the merchant login is done.
|
||||
|
||||
**By invitation — the salesperson chooses their own password.** Better when
|
||||
they have their phone: the merchant never sees or handles a staff password.
|
||||
|
||||
```
|
||||
POST /api/team/invitations
|
||||
{ "email": "priya@tenext.in", "full_name": "Priya R", "role": "staff",
|
||||
"expires_in_days": 7 }
|
||||
@@ -117,9 +140,19 @@ POST /api/team/invitations
|
||||
```
|
||||
|
||||
`code` is shown **once** and is what the merchant gives the salesperson —
|
||||
read aloud, WhatsApp, printed on a card. It is single-use and expires. The
|
||||
salesperson chooses their own password when they redeem it (Tier 3), so the
|
||||
merchant never knows or handles a staff password.
|
||||
read aloud, WhatsApp, printed on a card. Single-use, expires. They redeem it in
|
||||
Tier 3 and pick a password there.
|
||||
|
||||
**When they forget it** — which is the everyday case on a shop floor:
|
||||
|
||||
```
|
||||
POST /api/team/{id}/password { } or { "password": "chosen" }
|
||||
→ 200 { "password": "n7xw…" } ← shown ONCE
|
||||
```
|
||||
|
||||
Resets the password **and signs them out of every device** in one step,
|
||||
because the other reason a manager resets a password is a lost phone, and a
|
||||
reset that left that phone signed in would look complete while fixing nothing.
|
||||
|
||||
Managing the team afterwards:
|
||||
|
||||
@@ -137,7 +170,9 @@ The owner also sets the shop up from the same login — `POST
|
||||
|
||||
### Tier 3 — the salesperson gets their mobile login
|
||||
|
||||
Not signed in yet. They have the code.
|
||||
If the merchant created the login directly, they already have an email and
|
||||
password: skip straight to `POST /api/auth/login` below. Otherwise they have a
|
||||
code.
|
||||
|
||||
```
|
||||
GET /api/auth/invitation?code=LQOUHR-AYYTPE-7Q756N-PGAAN6 (no auth)
|
||||
@@ -180,13 +215,6 @@ somebody else's account.
|
||||
|
||||
### What does not exist, stated plainly
|
||||
|
||||
- **The merchant cannot create a staff login directly** with a password of
|
||||
their choosing. The only path is an invitation code the staff member redeems
|
||||
themselves. This is deliberate — it keeps staff passwords out of the
|
||||
merchant's hands and off chat — but it means a salesperson without a phone in
|
||||
hand cannot be set up *for* them. If that friction is real, a
|
||||
`POST /api/team/members` that mirrors `POST /api/admin/clients` (generated
|
||||
password, shown once) is a small addition.
|
||||
- **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
|
||||
@@ -443,6 +471,46 @@ deactivate them (§4); that revokes every session they hold.
|
||||
"last_login_at": "…", "created_at": "…" }]
|
||||
```
|
||||
|
||||
### `POST /api/team/members` — manager or owner
|
||||
|
||||
Create a login directly and hand it over. The alternative to an invitation
|
||||
(§2) for somebody without a phone in hand.
|
||||
|
||||
```json
|
||||
{ "email": "priya@tenext.in", "full_name": "Priya R", "role": "staff",
|
||||
"password": "" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "id": "…", "email": "priya@tenext.in", "full_name": "Priya R",
|
||||
"role": "staff", "active": true, "last_login_at": "", "created_at": "…",
|
||||
"password": "m4kq…" }
|
||||
```
|
||||
|
||||
- **`password` is returned once** and is not recoverable. Leave it empty in
|
||||
the request and one is generated; supply one and it must be 8+ characters.
|
||||
- `role` is `staff` (default), `manager` or `owner`. Only an owner may create
|
||||
an owner; `admin` is refused.
|
||||
- **409 `conflict`** if that email already has an account anywhere.
|
||||
|
||||
### `POST /api/team/{id}/password` — manager or owner
|
||||
|
||||
```json
|
||||
{ } or { "password": "chosen-one" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "password": "n7xw…" }
|
||||
```
|
||||
|
||||
Sets a new password (generated unless given) and **revokes every session the
|
||||
member holds**, in one transaction. Returns the new password once. A member of
|
||||
another company is **404**, never 403.
|
||||
|
||||
There is deliberately no self-service reset and no reset-by-email: a shop-floor
|
||||
account often has no mailbox anyone checks, and the person who can vouch for
|
||||
the salesperson standing in front of them is their manager.
|
||||
|
||||
### `PATCH /api/team/{id}` — manager or owner
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user