Give a cashier their own till login, not just a PIN behind a supervisor

A PIN cannot open a closed terminal. The PIN route needs a session that already
exists, so a PIN-only account works only while somebody else is standing there
to unlock the till first. For a supervisor that was an outright deadlock and was
fixed last commit. For a cashier it is subtler and just as wrong: the shop
cannot open until two people have arrived, and whoever gets in at seven is as
often the cashier as the supervisor.

So every till account now gets a username and a password, and the role decides
the shell rather than the credential deciding it. A cashier signs in exactly the
way a supervisor does and is still held to billing only, because that comes from
roleid 8 and not from how they got in.

The earlier reasoning — that a second password is one more credential to leak
for no capability gained — was measuring the wrong thing. It counted the cost of
the credential and not the cost of the shop that cannot open without one.

CreatePosUser generates both when the request omits them, so provisioning is one
call per person and nobody has to invent a naming scheme. An explicit value
always wins. A generated name that collides walks to the next free one, because
a second cashier at one counter is ordinary rather than an error; a name the
caller supplied is refused instead, because silently signing somebody in as
another person's address is worse than a message. Uniqueness is checked against
authname and email together, since the insert writes the same value to both and
app_users_email_unique would otherwise fail the transaction rather than return
something anyone can act on.

The password comes back exactly once, in the creation response. Listing till
users still reports only has_password, so an admin who loses it reissues rather
than looks it up — the right shape even while the column behind it is plaintext.

The domain is deliberately unroutable. These are till credentials, never a
mailbox, and an address that looks deliverable invites somebody to try sending a
reset to it.

Verified against live rows by scratch/posseparation, which now checks the
cashier path too: cashier.1185@pos.nearle.in opens a closed terminal alone and
comes back can_manage_staff=false. All five outlets that stock products have
both accounts, each proved by an actual sign-in.

Also drops a stray `print(queryBuilder.String())` from GetAllUsers, which was
writing the whole SQL statement to stderr on every call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-07 12:04:24 +05:30
parent c0a7fbc1b1
commit 9e9401215d
7 changed files with 222 additions and 90 deletions

View File

@@ -258,15 +258,44 @@ login, it is simply not found.
one, 22 live accounts have it including a delivery rider, and it grants nothing
on either side.
### A Supervisor needs a password, a Cashier does not
### Every till account gets its own username and password
A PIN cannot open a *closed* terminal — `/pos/login/pin` requires a session that
already exists. So a Supervisor is provisioned with an `authname` **and** a
`password` as well as a PIN, and a Cashier gets only a PIN: a cashier signs on
at a counter a Supervisor has already opened, so a second password would be one
more credential to leak for no capability gained.
Both roles. A PIN cannot open a *closed* terminal — `/pos/login/pin` requires a
session that already exists — so a PIN-only account works only while somebody
else is standing there to unlock the till first. For a Supervisor that was an
outright deadlock; for a Cashier it meant a shop that could not open until two
people had arrived, and whoever gets in at seven is as often the cashier as the
supervisor.
Provisioning a Cashier and nobody else leaves an outlet with no way in at all.
So a Cashier signs in exactly like a Supervisor does, and the *role* decides
what they get — not which credential they used:
```
POST /v1/pos/login supervisor.1185@pos.nearle.in -> full shell
POST /v1/pos/login cashier.1185@pos.nearle.in -> billing only
```
`POST /pos/users` generates both when the request omits them, and returns the
password **once**, in the creation response only:
```json
{ "user_id": 1452, "role": "Cashier",
"authname": "cashier.1185@pos.nearle.in",
"password": "9tWx2KUJksM5Rm", "pin": "4513", "has_password": true }
```
`GET /pos/users` never returns a password, only `has_password`. An admin who
loses it reissues rather than looks it up.
Send `authname` and `password` explicitly if the shop wants its people signing
in as themselves. A generated name that collides — a second cashier at one
outlet — becomes `cashier2.1185@pos.nearle.in`; a name **you** supplied is never
adjusted, it is refused, because silently signing somebody in as another
person's address is worse than an error.
The PIN stays optional. It switches operator at an open counter, which not every
shop does, and it is the one credential the till holds in plaintext to hand
around — so it is set deliberately, never by default.
---
@@ -321,11 +350,16 @@ own store id.
|---|---|
| `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 |
| `pin` | optional, 4 digits. See the rules below |
| `authname` | optional. **Generated if omitted**`cashier.1185@pos.nearle.in`, or `cashier2.…` if that is taken |
| `password` | optional. **Generated if omitted**, and returned once in this response |
**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.
**Everyone gets a username and a password, cashiers included**, because a PIN
cannot open a closed terminal. Omit both fields and they are generated for you,
so provisioning a shop is one call per person.
The response is the only time the password is returned; `GET /pos/users` reports
`has_password` and nothing more.
:warning: **PIN rules, and why**