The terminal shipped with three names and three PINs compiled into it. Same three on every install, readable by anyone with the APK, and permanent — nothing anywhere could replace them. `/pos/staff` answers with the people the back office says may ring a bill at an outlet, and the same list rides down with the session so a till is ready to trade the moment it signs in. The terminal writes them over its own and deactivates whatever it had, which is what actually kills the seeded logins. Two sources are unioned because the schema has two and neither is complete. `tenantstaffs` is the table built for this and holds 12 rows on the entire platform; `app_users.locationid` is where staff actually ended up. Either alone returns nothing for almost every shop. The endpoint takes no location parameter. The answer carries PINs, so the outlet comes from the caller's token and a request without one is refused whatever POS_AUTH_REQUIRED says — a till must not be able to ask who works at the shop next door. Rows with no PIN are dropped rather than sent: a name on screen nobody can sign in as reads as a broken terminal rather than as an unfinished setup. Duplicate PINs are dropped too, keeping the first — live data has 1234 on eleven accounts and 1111 on nine, and two people sharing one would make the till attribute a bill to whichever row it checked first. The PIN travels in the clear over TLS, deliberately. Four digits are brute-forceable in microseconds however they are wrapped, so hashing here would buy the appearance of strength and not the substance — while costing something real, since the terminal salts every PIN with its own salt before storing it and could never verify a hash computed here. A PIN is shift attribution, not a security boundary; the boundary is the session token. Verified against live data, and it says the fallback still matters: outlet 1135 — the one the POS actually uses — has zero staff, and the only staff row found anywhere is a delivery rider on PIN 1111. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.8 KiB
Go
66 lines
2.8 KiB
Go
package routes
|
|
|
|
import (
|
|
"nearle/facade"
|
|
"nearle/middleware"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// Routes for the Nearle POS terminal.
|
|
//
|
|
// The paths are fixed by the till, which appends `/orders`, `/customers` and
|
|
// `/catalogue` to whatever base URL a shop enters in Settings. Set that base to
|
|
// this group — `https://your-host/live/api/v1/pos` — and the three line up.
|
|
//
|
|
// Kept in their own group rather than folded into the order routes because a
|
|
// terminal authenticates as a device, not as a signed-in user, and because
|
|
// these answer with a bare ack rather than the web app's response envelope.
|
|
func RegisterPosRoutes(api fiber.Router, f *facade.Facade) {
|
|
|
|
pos := api.Group("/v1/pos")
|
|
|
|
// Sign-in, and the only route on this group that runs before the guard —
|
|
// it is where a session comes from. A till posts the same `app_users`
|
|
// credentials the web console takes, and gets back a token plus the outlet
|
|
// that account is entitled to. The store id it will bill under is decided
|
|
// here, from the user's record, instead of being typed into Settings and
|
|
// taken on trust.
|
|
pos.Post("/login", f.PosController.Login)
|
|
|
|
// Everything past this point carries the session.
|
|
//
|
|
// The guard verifies the token and refuses a request naming an outlet the
|
|
// token's tenant does not own. Until `POS_AUTH_REQUIRED=true` is set it
|
|
// lets an unauthenticated request through, so the terminals already
|
|
// trading do not stop the day this deploys — see middleware.PosAuth.
|
|
pos.Use(middleware.PosAuth(f.PosService()))
|
|
|
|
pos.Get("/session", f.PosController.Session)
|
|
|
|
// Who may ring a bill here. Deliberately takes no location parameter — the
|
|
// answer carries PINs, so the outlet comes from the caller's own token.
|
|
pos.Get("/staff", f.PosController.Staff)
|
|
|
|
pos.Post("/orders", f.PosController.IngestOrders)
|
|
pos.Post("/customers", f.PosController.IngestCustomers)
|
|
pos.Get("/catalogue", f.PosController.Catalogue)
|
|
|
|
// The 30-second heartbeat, for tills on the HTTP route. The broker carries
|
|
// the same payload for tills on MQTT; both land in the same Redis record,
|
|
// so the fleet board cannot tell them apart and does not need to.
|
|
pos.Post("/health", f.PosController.IngestHealth)
|
|
|
|
// Counter sales, read back out. The ingest above only ever writes; without
|
|
// these a committed bill is unreachable from every screen in the product.
|
|
pos.Get("/sales", f.PosController.GetSales)
|
|
pos.Get("/sales/detail", f.PosController.GetSaleDetail)
|
|
pos.Get("/sales/summary", f.PosController.GetSalesSummary)
|
|
|
|
// Terminal presence, read from Redis. What the rider app's POS board and a
|
|
// support call both hit — the tills themselves publish health over the
|
|
// broker rather than posting it here.
|
|
pos.Get("/health/terminal", f.PosController.TerminalHealth)
|
|
pos.Get("/health/location", f.PosController.LocationHealth)
|
|
}
|