app_users is the only thing the two products have in common, and the code was treating it as though it were the whole relationship. Both directions leaked. Back-office roles were leaking into the till. PosRoleCanManageStaff returned true for roleid 1 to 6, on the reasoning that somebody who already administers a shop from a browser is not made less privileged by standing at the counter. That sounds fine and is wrong: measured against live data it handed till-supervisor powers to 68 accounts, 59 of them Nearle Daily Super admins, not one of whom is the administrator of anybody's POS. Meanwhile the actual shop accounts carry roleid 0 and were refused, so the mapping was backwards from intent in both halves at once. Till accounts were leaking into the application. GetStaffs is WHERE tenantid with no role filter, so a Counter Cashier appeared in the tenant staff list beside the delivery riders — a row every action on that page would fail against, since a cashier has no app login, no rider shift and no back-office screen. So: eligibility for a till is now granted explicitly by provisioning a Supervisor or a Cashier, never inherited from a back-office role, and roles 7 and 8 are excluded from every Nearle Daily lookup. The exclusion lives in the queries rather than in a check after them, because a check bolted on afterwards has to be repeated at six call sites and is one edit away from being forgotten at one of them — and that one would be the hole. A till account is not rejected by the app login; it is not found. Two things this surfaced that were not visible before. A Supervisor could not open a till. PIN sign-in needs a session that already exists, so once back-office roles were refused, an outlet whose only POS accounts were PIN-only had no way in at all. Supervisors are now provisioned with a username and password as well as a PIN; cashiers deliberately get neither, because they sign on at a counter somebody has already opened and a second password would be one more credential to leak for no capability gained. UpdatePosUser silently dropped authname. It wrote the password, reported success, and left the account unreachable by either lookup — the failure surfaced at a counter as "not recognised" rather than on the screen that caused it. Contactno had the same gap. Verified against live rows rather than asserted, by scratch/posseparation: a provisioned supervisor signs in and gets the supervisor shell; five real back-office accounts including Super admins are refused; the supervisor is invisible to applogin, tenant weblogin and the password-setup lookup; and no till account appears in getallusers, while asking for role 7 by name still returns them so the console can read its own people. All five outlets that stock products now have a Supervisor and a Cashier. Also moves the loose markdown into docs/, which was already staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
592 lines
20 KiB
Markdown
592 lines
20 KiB
Markdown
# Nearle POS — Terminal Sign-In
|
||
|
||
How a till authenticates, and how it finds out which shop it belongs to.
|
||
|
||
**Base URL** `https://fiesta.nearle.app/live/api/v1/pos`
|
||
**Live since** 6 Aug 2026, `v1.3.98`
|
||
|
||
---
|
||
|
||
## What changed, and why it matters
|
||
|
||
A terminal used to hold a store id typed into Settings and a password compiled
|
||
into the app. That made the store id a **claim** rather than a fact: any till
|
||
could name any outlet and be believed, so changing one number on one screen
|
||
moved a terminal into another tenant's books. The password was identical on
|
||
every install of a build.
|
||
|
||
Now a person signs in with their own back-office account, and the outlet
|
||
arrives **as a consequence** — sealed inside a signed token the terminal cannot
|
||
edit, and re-checked by the server on every request.
|
||
|
||
The rule to hold onto: **the till no longer decides which shop it is. It is
|
||
told.**
|
||
|
||
---
|
||
|
||
## Quickstart
|
||
|
||
```bash
|
||
BASE=https://fiesta.nearle.app/live/api/v1/pos
|
||
|
||
# 1. Sign in
|
||
curl -s -X POST $BASE/login \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"authname":"rsselvapuram@gmail.com","password":"…","terminal_id":"T5EDD"}'
|
||
|
||
# 2. Use the token on everything else
|
||
curl -s $BASE/session -H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
---
|
||
|
||
## The flow
|
||
|
||
These steps are in order, and the order matters.
|
||
|
||
**1. Sign in.** `POST /login` with the operator's own credentials — the same
|
||
`app_users` account they use for the web console. There is no separate POS
|
||
password.
|
||
|
||
**2. Read `store_id` out of the response.** Do not ask anyone to type it. It is
|
||
whatever the back office says that account's outlet is.
|
||
|
||
**3. If `locations` has more than one entry, ask which one.** Only then. A
|
||
single-outlet account gets a list of one and must never see a picker.
|
||
|
||
**4. Save the token.** Platform keystore, not a plain file or SQLite — it is a
|
||
bearer credential for a whole trading day. Restore it on launch **before** any
|
||
upload or catalogue pull runs.
|
||
|
||
**5. Send it on every request** as `Authorization: Bearer <token>`.
|
||
|
||
**6. Import `staff`.** Replace the till's local staff with what came down, and
|
||
deactivate anything that wasn't in the list. That is what retires the built-in
|
||
PINs.
|
||
|
||
---
|
||
|
||
## `POST /login`
|
||
|
||
The only unauthenticated route. It is where a token comes from.
|
||
|
||
### Request
|
||
|
||
```json
|
||
{
|
||
"authname": "rsselvapuram@gmail.com",
|
||
"password": "…",
|
||
"terminal_id": "T5EDD",
|
||
"device_id": "a5f3…",
|
||
"location_id": 1135,
|
||
"configid": 1
|
||
}
|
||
```
|
||
|
||
| Field | Required | Notes |
|
||
|---|---|---|
|
||
| `authname` | yes* | Email. **Or** send `contactno` instead. |
|
||
| `contactno` | yes* | Mobile number, as an alternative to `authname`. |
|
||
| `password` | yes | |
|
||
| `terminal_id` | no | This till's short code, e.g. `T5EDD`. Recorded on the session. |
|
||
| `device_id` | no | The device's stable UUID. |
|
||
| `location_id` | no | **Only** meaningful for a multi-outlet account. A request, not an assertion — it is checked against what the account may reach. |
|
||
| `configid` | no | Inferred when absent. Send it only if you get the ambiguity error below. |
|
||
|
||
\* one of `authname` or `contactno`.
|
||
|
||
### Response — `200`
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"status": true,
|
||
"message": "Login successful",
|
||
"details": {
|
||
"token": "eyJ1aWQiOjEy….K3p9",
|
||
"expires_at": "2026-09-05T10:51:17Z",
|
||
|
||
"user_id": 1229,
|
||
"full_name": "Selvapuram",
|
||
"email": "rsselvapuram@gmail.com",
|
||
"role_id": 0,
|
||
|
||
"tenant_id": 1087,
|
||
"tenant_name": "Ragul Stores",
|
||
|
||
"store_id": "1135",
|
||
"location_id": 1135,
|
||
"location_name": "Ragul stores Selvapuram",
|
||
"gstin": "123456",
|
||
"address": "…",
|
||
"phone": "…",
|
||
|
||
"locations": [
|
||
{ "location_id": 1135, "location_name": "Ragul stores Selvapuram",
|
||
"address": "", "city": "", "status": "Active" }
|
||
],
|
||
|
||
"staff": [
|
||
{ "user_id": 1148, "full_name": "Ragul Kannan",
|
||
"role": "Super admin", "pin": "1111", "status": "Active" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### The fields that matter
|
||
|
||
**`store_id`** — a string, because that is the shape every uplink already
|
||
sends. Use it verbatim as the `store_id` on `/orders`, `/customers` and
|
||
`/catalogue`. It is the same value as `location_id`, handed back in the form it
|
||
will be replayed in.
|
||
|
||
**`token`** — **opaque**. Do not parse it, do not read anything out of it, do
|
||
not trust anything it appears to say. Its only correct use is to hand it back.
|
||
|
||
**`expires_at`** — 30 days out. Long on purpose: a shop signs a terminal in once
|
||
and expects it to keep working. Forcing a re-login mid-shift means a queue of
|
||
customers waiting while somebody finds the manager.
|
||
|
||
**`gstin` / `address` / `phone`** — print these on the receipt. They are a legal
|
||
requirement on a GST invoice and they used to be compile-time constants, so a
|
||
shop correcting its GSTIN had to wait for a rebuild. Write them locally on
|
||
sign-in.
|
||
|
||
**`locations`** — every outlet this account may open a till at. Length 1 is the
|
||
normal case.
|
||
|
||
**`staff`** — see [Staff and PINs](#staff-and-pins). **Often empty.**
|
||
|
||
---
|
||
|
||
## `GET /session`
|
||
|
||
Answers who the caller is, per their token. What a till calls on launch to
|
||
check whether yesterday's session is still good, without making a real request
|
||
and interpreting the failure.
|
||
|
||
Requires the token. Returns `401` when there isn't one.
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"status": true,
|
||
"details": {
|
||
"user_id": 1229,
|
||
"tenant_id": 1087,
|
||
"location_id": 1135,
|
||
"store_id": "1135",
|
||
"role_id": 0,
|
||
"terminal_id": "PROBE",
|
||
"expires_at": "2026-09-05T10:51:17Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## `GET /staff`
|
||
|
||
Who may ring a bill at this terminal's outlet. For pulling down somebody hired
|
||
mid-shift without signing the terminal out.
|
||
|
||
**Takes no parameters.** The answer carries PINs, so the outlet comes from the
|
||
caller's own token — a till must not be able to ask who works at the shop next
|
||
door. A request without a token is refused whatever the enforcement setting is.
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"status": true,
|
||
"details": {
|
||
"location_id": 1135,
|
||
"staff": []
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
### The till and Nearle Daily do not share accounts
|
||
|
||
`app_users` is the only thing the two products have in common. An account
|
||
belongs to one or the other, never to both:
|
||
|
||
| | Nearle Daily app + console | POS terminal |
|
||
|---|---|---|
|
||
| roles | `1`–`6` — Super admin, Operations, Admin, Manager | `7` Supervisor, `8` Cashier |
|
||
| `/applogin`, `/tenant/weblogin`, `/tenant/login` | yes | **not found** |
|
||
| `POST /v1/pos/login` | **403** | yes |
|
||
| listed by `/getallusers`, `/getstaffs` | yes | **hidden** |
|
||
|
||
A Nearle Daily **Super admin is not the administrator of anybody's POS.** The
|
||
back office reaches a till by *provisioning* a Supervisor from the console; it
|
||
never becomes one by signing in.
|
||
|
||
This was the other way round until it was measured. Roles 1–6 counted as
|
||
supervisors, on the reasoning that somebody who already administers a shop from
|
||
a browser is not made less privileged by standing at the counter. That handed
|
||
till-supervisor powers to **68 live accounts, 59 of them platform Super
|
||
admins**, while the actual shop accounts carry `roleid 0` and were refused.
|
||
|
||
Both directions are now closed in the queries themselves rather than in a check
|
||
each call site has to remember — a till account is not *rejected* by the app
|
||
login, it is simply not found.
|
||
|
||
**`role_id` 0 is not a role.** It is what an account carries when nobody set
|
||
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
|
||
|
||
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.
|
||
|
||
Provisioning a Cashier and nobody else leaves an outlet with no way in at all.
|
||
|
||
---
|
||
|
||
## `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.
|
||
|
||
---
|
||
|
||
## Creating staff from the web console
|
||
|
||
The same staff management, for the screen an admin actually uses. Registered
|
||
under both `/v1/web/tenants` and `/v1/mob/tenants`.
|
||
|
||
```
|
||
GET /v1/web/tenants/posroles
|
||
GET /v1/web/tenants/getposusers?tenantid=1087&locationid=1135
|
||
POST /v1/web/tenants/createposuser
|
||
PUT /v1/web/tenants/updateposuser
|
||
DELETE /v1/web/tenants/deleteposuser?tenantid=1087&locationid=1135&userid=9189
|
||
```
|
||
|
||
`createposuser` takes the same body as `/pos/users`, plus the outlet — the
|
||
console has no session token, so it has to name one:
|
||
|
||
```json
|
||
{
|
||
"tenantid": 1087,
|
||
"locationid": 1135,
|
||
"full_name": "Asha Kumar",
|
||
"role": "cashier",
|
||
"pin": "4821"
|
||
}
|
||
```
|
||
|
||
**These run the same service calls as `/pos/users`.** A supervisor created from
|
||
a browser is the same row, with the same rules applied, as one created at a
|
||
counter — same PIN validation, same duplicate check, same identity-column
|
||
allocation. That is the point of them: two paths writing one table is how the
|
||
two stop matching.
|
||
|
||
`configid` is never asked for. It is inferred from whichever value the tenant's
|
||
existing accounts carry — a number nobody looks up, that varies per tenant (1087
|
||
is spread across 1, 6 and 15), and that silently creates an account nobody can
|
||
find if it is wrong.
|
||
|
||
`GET /posroles` returns the two roles with their ids and labels, so a console
|
||
offering the choice never has to know that supervisor is `7`.
|
||
|
||
### :red_circle: These are unauthenticated
|
||
|
||
Like every other route in the `/v1/web` and `/v1/mob` groups — there is no auth
|
||
middleware anywhere on the web API. The outlet is checked against the tenant
|
||
before anything is written, so a caller cannot create staff at a shop that is
|
||
not theirs *given a tenant id* — but nothing proves the caller is that tenant.
|
||
|
||
So this mints till credentials on an unauthenticated request. It is consistent
|
||
with the rest of the platform, and it is still the weakest point in this design.
|
||
They should move behind a session guard as soon as the console can hold one.
|
||
|
||
The terminal routes are not affected: `/pos/users` proves its outlet with a
|
||
signed token.
|
||
|
||
---
|
||
|
||
## Using the token
|
||
|
||
```
|
||
Authorization: Bearer eyJ1aWQiOjEy….K3p9
|
||
```
|
||
|
||
`X-Pos-Token: <token>` is accepted as a fallback, because some shop routers
|
||
strip `Authorization` headers over plain HTTP. A bare token with no `Bearer `
|
||
prefix is tolerated too.
|
||
|
||
Send it on **every** POS call: `/orders`, `/customers`, `/catalogue`, `/health`,
|
||
`/sales*`, `/session`, `/staff`.
|
||
|
||
### What the server checks
|
||
|
||
1. The token verifies against our signing key and has not expired.
|
||
2. The outlet named in the request belongs to the token's tenant.
|
||
|
||
The second is the one that matters. A valid token is a licence to name **your**
|
||
outlets, not any outlet. The outlet is read from the query string *and* from the
|
||
JSON body, because `/orders` and `/customers` carry `store_id` in the batch and
|
||
never in the URL.
|
||
|
||
```
|
||
GET /catalogue?store_id=1135 → 200 your outlet
|
||
GET /catalogue?store_id=1185 → 403 {"message":"this session cannot reach outlet 1185"}
|
||
```
|
||
|
||
---
|
||
|
||
## Errors
|
||
|
||
### Sign-in
|
||
|
||
| Code | Meaning | What the till should do |
|
||
|---|---|---|
|
||
| `400` | Body unreadable, or neither `authname` nor `contactno` sent | Fix the request |
|
||
| `401` | `those sign-in details were not recognised` | Ask them to re-type. **Wrong email and wrong password give the same message** — deliberately, so the endpoint isn't a directory of who banks here |
|
||
| `403` | Real account, but it can't open this till | Show the message; re-typing won't help |
|
||
|
||
The `403` messages, verbatim:
|
||
|
||
- `this account is not set up for the till; ask your store admin to add you as a Supervisor or Cashier in the web console`
|
||
- `this account is inactive; contact your administrator`
|
||
- `this account has no password set; set one in the web console first`
|
||
- `this account is not attached to a tenant and cannot open a till`
|
||
- `no active outlet is registered for this account`
|
||
- `this account cannot open a till at outlet 1185`
|
||
- `more than one account uses these sign-in details; ask your administrator for the configid and send it with the login`
|
||
|
||
That last one is real, not theoretical: `authname` is not unique in this schema.
|
||
Live data has the same address twice. We refuse rather than pick one, because
|
||
picking wrong means billing into another tenant's books.
|
||
|
||
The **first** one is the common case now, and it is deliberately specific where a
|
||
bad password is deliberately vague. By the time it fires the caller has already
|
||
proved the credential, so naming the reason leaks nothing they did not just
|
||
demonstrate — and the vague answer would send a shop owner hunting for a
|
||
password that was never wrong.
|
||
|
||
### Authenticated routes
|
||
|
||
| Code | Meaning |
|
||
|---|---|
|
||
| `401` | No token, malformed token, bad signature, or expired — sign in again |
|
||
| `403` | Valid token naming an outlet the tenant doesn't own |
|
||
|
||
---
|
||
|
||
## Multi-outlet accounts
|
||
|
||
An account pinned to one location gets that location. An account with no
|
||
location — a proprietor with several shops — gets all of the tenant's active
|
||
outlets.
|
||
|
||
```
|
||
rsselvapuram@gmail.com → 1 outlet (1135, Selvapuram)
|
||
raguladmin@gmail.com → 6 outlets (1097, 1135, 1137, 1138, 1139, 885536644)
|
||
```
|
||
|
||
When `locations.length > 1`:
|
||
|
||
1. Show a picker. **Don't make it dismissable** — a terminal has to be standing
|
||
somewhere, and silently defaulting to the first outlet is how a day's takings
|
||
get filed against the wrong shop.
|
||
2. Sign in **again** with `location_id` set to their choice.
|
||
|
||
Re-signing-in is not laziness. The outlet is inside the signed token, so only
|
||
the server can issue one for a different shop — and re-checking entitlement at
|
||
that moment is the point.
|
||
|
||
---
|
||
|
||
## Staff and PINs
|
||
|
||
Two different credentials, easily confused:
|
||
|
||
| | Says | Checked by |
|
||
|---|---|---|
|
||
| **Sign-in** (email + password) | which **shop** this terminal is | the server |
|
||
| **PIN** | which **person** rang this bill | the terminal, offline |
|
||
|
||
The PIN stamps `cashiername` and is what shifts settle against. It is **shift
|
||
attribution, not a security boundary** — the boundary is the token.
|
||
|
||
### The PIN comes down in the clear
|
||
|
||
Over TLS, and that's considered rather than sloppy. Four digits are
|
||
brute-forceable in microseconds whatever they're wrapped in, so hashing
|
||
server-side would buy the appearance of strength and not the substance — while
|
||
costing something real, because the terminal salts every PIN with its own random
|
||
salt before storing it and could never verify a hash computed on the server.
|
||
|
||
**Store it hashed on the device.** It arrives in the clear; it must not sit that
|
||
way.
|
||
|
||
### Importing
|
||
|
||
Write everyone in `staff`, keyed on `user_id` so a re-sync updates rather than
|
||
duplicates. Then **deactivate everything you didn't just import** — that is what
|
||
kills the built-in PINs. Deactivate, never delete: bills carry the cashier's
|
||
name.
|
||
|
||
### :warning: `staff` is usually empty today
|
||
|
||
Only 116 of 596 accounts on the platform have a PIN set. Outlet 1135 — the one
|
||
the terminal ships pointed at — has **zero**.
|
||
|
||
So:
|
||
|
||
- **An empty list is not a failure.** Do nothing and leave the till exactly as
|
||
it was.
|
||
- **A list where every PIN is unusable** (`0`, blank) must behave the same way.
|
||
Deactivating the local accounts because the back office isn't filled in yet
|
||
would leave a counter nobody can sign in to.
|
||
|
||
The terminal still ships with three seeded logins for exactly this reason. They
|
||
retire automatically the moment real staff exist. Filling in real PINs in the
|
||
back office is what makes that happen.
|
||
|
||
---
|
||
|
||
## Current state
|
||
|
||
| | |
|
||
|---|---|
|
||
| Endpoints | live on `v1.3.98`, all three pods |
|
||
| Signing key | set in `app-secrets` |
|
||
| **Enforcement** | **OFF** — `POS_AUTH_REQUIRED` is unset |
|
||
|
||
Enforcement being off means a request carrying **no** token is still allowed
|
||
through, so terminals already trading don't stop the day this ships. It does
|
||
**not** mean tokens are ignored:
|
||
|
||
- a token that's present and invalid is **always** refused;
|
||
- a valid token naming another tenant's outlet is **always** refused.
|
||
|
||
Once the fleet is on a build that signs in, `POS_AUTH_REQUIRED=true` closes the
|
||
door on untokened requests.
|
||
|
||
---
|
||
|
||
## Known limitations
|
||
|
||
- **Passwords are stored in plaintext** across the whole platform, not just
|
||
here. Fixing it is a migration touching every login path.
|
||
- **No role check.** Any active account with a tenant, a password and an active
|
||
outlet can open a till — including `roleid 0`, which isn't in `app_roles` at
|
||
all and currently includes a delivery rider. The damage is bounded by the
|
||
token: they can only reach their own tenant's books.
|
||
- **`1135` means two different things.** It's a *location* (Ragul stores
|
||
Selvapuram, under tenant 1087) and separately a *tenant* (Suriya Store). Same
|
||
number, different tables. Watch for it in logs.
|