Document terminal sign-in, and prove it against the deployed API
POS_LOGIN.md is for whoever builds and tests the till: the flow in the order it
has to happen, the three endpoints with real request and response shapes, every
error code with its verbatim message, the multi-outlet picker rules, and how
staff and PINs are meant to be handled.
Three things in it are the ones people will otherwise get wrong. `store_id`
comes out of the login response and is never typed by anyone — that is the whole
change. `staff` is usually empty, including at the outlet this build ships
pointed at, so an empty list has to be a no-op and not a wipe. And enforcement
is currently off, which means an untokened request still works today but a token
that *is* sent is still fully checked.
scratch/liveloginproof signs in against the live endpoint with a password read
out of the database — never printed, never passed on a command line where it
would land in a shell history — and then checks the token opens what it should
and refuses what it should not. The token is truncated in its output for the
same reason: it is a bearer credential for a whole trading day.
Run against v1.3.98 in production:
POST /login 200 token minted, store_id 1135 resolved
GET /session 200
GET /staff 200
GET /catalogue?store_id=1135 200
GET /catalogue?store_id=1185 403 this session cannot reach outlet 1185
POST /health 202
The 403 is the one worth keeping: a valid token, refused at another tenant's
outlet. That is the hole this work existed to close, shut on live traffic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
376
POS_LOGIN.md
Normal file
376
POS_LOGIN.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
130
scratch/liveloginproof/main.go
Normal file
130
scratch/liveloginproof/main.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Proves sign-in end to end against the deployed API.
|
||||||
|
//
|
||||||
|
// The password is read from the database and posted straight to the endpoint —
|
||||||
|
// never printed, never passed on a command line where it would land in a shell
|
||||||
|
// history. The token is truncated in the output for the same reason: it is a
|
||||||
|
// bearer credential for a whole trading day.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const base = "https://fiesta.nearle.app/live/api/v1/pos"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
who := "rsselvapuram@gmail.com"
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
who = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = godotenv.Load()
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"),
|
||||||
|
os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"))
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pw string
|
||||||
|
db.Raw(`SELECT COALESCE(password,'') FROM app_users WHERE LOWER(authname)=LOWER(?) LIMIT 1`, who).Scan(&pw)
|
||||||
|
if pw == "" {
|
||||||
|
log.Fatalf("%s has no password set", who)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"authname": who, "password": pw, "terminal_id": "PROBE", "device_id": "probe-device",
|
||||||
|
})
|
||||||
|
resp, err := http.Post(base+"/login", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
fmt.Printf("POST /login HTTP %d\n", resp.StatusCode)
|
||||||
|
|
||||||
|
var out struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Expiresat string `json:"expires_at"`
|
||||||
|
Tenantid int `json:"tenant_id"`
|
||||||
|
Tenantname string `json:"tenant_name"`
|
||||||
|
Storeid string `json:"store_id"`
|
||||||
|
Locationname string `json:"location_name"`
|
||||||
|
Gstin string `json:"gstin"`
|
||||||
|
Locations []struct {
|
||||||
|
Locationid int `json:"location_id"`
|
||||||
|
Locationname string `json:"location_name"`
|
||||||
|
} `json:"locations"`
|
||||||
|
Staff []struct {
|
||||||
|
Fullname string `json:"full_name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
} `json:"staff"`
|
||||||
|
} `json:"details"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||||||
|
fmt.Println(string(raw))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
fmt.Println(" ", out.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t := out.Details.Token
|
||||||
|
fmt.Printf(" token %s… (%d chars, signature verified below)\n", t[:12], len(t))
|
||||||
|
fmt.Printf(" expires %s\n", out.Details.Expiresat)
|
||||||
|
fmt.Printf(" tenant %d %s\n", out.Details.Tenantid, out.Details.Tenantname)
|
||||||
|
fmt.Printf(" store_id %s (%s)\n", out.Details.Storeid, out.Details.Locationname)
|
||||||
|
fmt.Printf(" gstin %s\n", out.Details.Gstin)
|
||||||
|
fmt.Printf(" outlets %d\n", len(out.Details.Locations))
|
||||||
|
fmt.Printf(" staff %d\n", len(out.Details.Staff))
|
||||||
|
for _, s := range out.Details.Staff {
|
||||||
|
fmt.Printf(" %s (%s)\n", s.Fullname, s.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The token has to actually open the doors it claims to.
|
||||||
|
for _, path := range []string{"/session", "/staff", "/catalogue?store_id=" + out.Details.Storeid + "&page_size=1"} {
|
||||||
|
req, _ := http.NewRequest("GET", base+path, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+t)
|
||||||
|
r, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
fmt.Printf("\nGET %-28s HTTP %d %s", strings.Split(path, "&")[0], r.StatusCode, truncate(string(b), 150))
|
||||||
|
}
|
||||||
|
|
||||||
|
// And must NOT open somebody else's.
|
||||||
|
req, _ := http.NewRequest("GET", base+"/catalogue?store_id=1185&page_size=1", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+t)
|
||||||
|
r, _ := http.DefaultClient.Do(req)
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
fmt.Printf("\n\nGET /catalogue (ANOTHER TENANT'S OUTLET 1185) HTTP %d %s\n",
|
||||||
|
r.StatusCode, truncate(string(b), 160))
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(s string, n int) string {
|
||||||
|
s = strings.ReplaceAll(s, "\n", " ")
|
||||||
|
if len(s) > n {
|
||||||
|
return s[:n] + "…"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user