Let a shop run its own counter: supervisor and cashier, created from the till
A shop had no way to add the people who work in it. The terminal fell back to
three names and three PINs compiled into the app — the same three on every
install — because there was nothing for it to fall back *from*.
Two roles now exist in `app_roles`: Supervisor (7) runs the terminal and creates
staff, Cashier (8) bills. Fixed ids, written by hand, because that table has no
sequence and every id in it was assigned the same way. configid is left NULL
rather than duplicated per portal: a till is a till whichever portal a tenant
uses, and Admin already appears twice in that table for exactly that reason.
`/pos/users` is CRUD over them, and `/pos/login/pin` signs a cashier on at a
terminal a supervisor has already opened.
The rule every one of these follows: **tenant and outlet come from the caller's
token, never from the request.** There is no location field on the create body
to get wrong. A supervisor at Selvapuram cannot create staff at R mart, for the
same reason a till cannot bill into another shop's books — it is the same
inversion applied to people instead of sales.
PIN sign-in is deliberately behind the guard. Four digits is ten thousand
guesses, which is no barrier to an anonymous caller; requiring a session means a
real password opened the terminal first and the guesses are confined to one
outlet's own staff. The session it mints is fresh rather than derived, so a
cashier taking over from a supervisor drops their permissions instead of
inheriting them.
Three things the schema forced:
- A PIN cannot start with zero. `app_users.pin` is a bigint, so "0451" stores as
451 and reads back as three digits — a cashier would type four and be refused
for ever. Live data already holds one such account. Rendering refuses to show
a PIN it cannot represent, rather than showing a short one nobody can type.
- `app_users` has no sequence either, so the next id is read and written inside
one transaction behind an advisory lock. Two supervisors creating staff at the
same moment would otherwise compute the same id and one insert would lose.
- 1234, 1111 and friends are refused outright. Live data has 1234 on eleven
accounts and 1111 on nine.
Proven against outlet 1135, which had zero staff and was the reason the built-in
PINs were still load-bearing:
created 9188 Store Supervisor Supervisor can_manage_staff=true
created 9189 Counter Cashier Cashier can_manage_staff=false
/pos/staff now returns 2 an unknown PIN is refused
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
117
POS_LOGIN.md
117
POS_LOGIN.md
@@ -208,6 +208,123 @@ door. A request without a token is refused whatever the enforcement setting is.
|
||||
|
||||
---
|
||||
|
||||
## Roles
|
||||
|
||||
Two POS roles, added to `app_roles`:
|
||||
|
||||
| roleid | Role | Can |
|
||||
|---|---|---|
|
||||
| `7` | **Supervisor** | everything a till does, **plus** creating and editing counter staff |
|
||||
| `8` | **Cashier** | billing only |
|
||||
|
||||
The session carries both, so the terminal never has to map role ids itself:
|
||||
|
||||
```json
|
||||
{ "role_id": 7, "role": "Supervisor", "can_manage_staff": true }
|
||||
```
|
||||
|
||||
Branch on `can_manage_staff`, not on the number. `app_roles` holds six rows for
|
||||
four back-office roles (Admin is both 3 and 5, Manager both 4 and 6) and most
|
||||
accounts carry an id that is not in the table at all — any mapping written on
|
||||
the terminal would be wrong.
|
||||
|
||||
Back-office roles 1–6 also count as supervisors: somebody who already
|
||||
administers the shop from a browser is not made less privileged by standing at
|
||||
the counter. **`role_id` 0 is not a role** — it is what an account carries when
|
||||
nobody set one, and it grants nothing.
|
||||
|
||||
---
|
||||
|
||||
## `POST /pos/login/pin` — signing on at an open terminal
|
||||
|
||||
For a cashier taking over a counter a supervisor has already opened.
|
||||
|
||||
**Requires an existing valid token.** That is the security model, not an
|
||||
oversight: four digits is ten thousand guesses, which is no barrier at all to an
|
||||
anonymous caller. Tying it to a session means a supervisor has opened the
|
||||
terminal with a real password first, and the guesses are confined to that one
|
||||
outlet's staff.
|
||||
|
||||
```bash
|
||||
curl -s -X POST $BASE/login/pin \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"pin":"1602"}'
|
||||
```
|
||||
|
||||
Returns a **new** session, with the same shape as `/login`. New rather than
|
||||
reused, because the token carries the role — a cashier taking over from a
|
||||
supervisor must drop their permissions, not inherit them.
|
||||
|
||||
`401` if the PIN is not recognised. `400` if two people at the outlet share it,
|
||||
which creation refuses but older data may contain.
|
||||
|
||||
---
|
||||
|
||||
## `/pos/users` — the shop's own counter staff
|
||||
|
||||
A supervisor creates their own cashiers, from the terminal.
|
||||
|
||||
**The outlet is never in the request.** Tenant and location come from the
|
||||
caller's token, so a supervisor at Selvapuram cannot create staff at R mart by
|
||||
sending a different number — the same inversion that stopped a till naming its
|
||||
own store id.
|
||||
|
||||
### `POST /pos/users`
|
||||
|
||||
```json
|
||||
{
|
||||
"full_name": "Asha Kumar",
|
||||
"role": "cashier",
|
||||
"pin": "4821",
|
||||
"authname": "asha@shop.test",
|
||||
"password": "…"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| `full_name` | required; split across `firstname`/`lastname` |
|
||||
| `role` | `"supervisor"` or `"cashier"`. Anything else is refused — never defaulted |
|
||||
| `pin` | 4 digits. See the rules below |
|
||||
| `password` + `authname` | optional; for someone who also signs the terminal in |
|
||||
|
||||
**At least one of `pin` or `password` is required.** Creating a person who can
|
||||
sign in by neither would look like it worked right up until somebody tried.
|
||||
|
||||
:warning: **PIN rules, and why**
|
||||
|
||||
- **Exactly 4 digits, and cannot start with `0`.** `app_users.pin` is a
|
||||
`bigint`, so `"0451"` would be stored as `451` and read back as three digits —
|
||||
a cashier would type four and be refused for ever. One such account already
|
||||
exists in live data.
|
||||
- **`1234`, `1111`, `2345`, `4321`, `9999`, `2222`, `3456`, `0000` are refused.**
|
||||
Live data has `1234` on eleven accounts and `1111` on nine.
|
||||
- **Unique within the outlet**, not globally. A PIN only distinguishes people at
|
||||
one counter; making it platform-unique would exhaust the space fast.
|
||||
|
||||
Answers `201` with the created user. Every failure is a `400` carrying the
|
||||
reason, because all of them are things the caller can fix.
|
||||
|
||||
### `GET /pos/users`
|
||||
|
||||
Readable by anyone signed in — the terminal needs it to show who is on shift.
|
||||
**A cashier gets the list with `pin` blanked**; only somebody who could set a
|
||||
PIN gets to see one. `?include_inactive=true` to see leavers.
|
||||
|
||||
### `PUT /pos/users`
|
||||
|
||||
Same fields plus `user_id`. Send only what changes. Supervisor only.
|
||||
|
||||
### `DELETE /pos/users?user_id=9189`
|
||||
|
||||
Deactivates — never deletes, because bills carry the cashier's name and shifts
|
||||
settle against it. Supervisor only, and you cannot deactivate the account you
|
||||
are signed in as: otherwise the last supervisor at a shop can lock everyone out
|
||||
with one tap.
|
||||
|
||||
---
|
||||
|
||||
## Using the token
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user