Authentication
The session is a cookie
Signing in sets sb_admin_session: HttpOnly, Path=/admin, SameSite=Strict.
The token never appears in a response body, and Authorization: Bearer is not accepted on
this surface. An XSS that can read a token can impersonate an operator indefinitely; one that
can only use a cookie is bounded by the browser.
SameSite=Strict reasons about the registrable domain, not the origin.
admin.slotbattle.example.com and api.slotbattle.example.com are cross-origin but same-site,
and work. console.example.net and api.example.com do not.
Get it wrong and every authenticated request fails silently: the browser never attaches the cookie, nothing logs an error, and the requests come back unauthenticated.
Decide this before buying domains. It cannot be worked around from application code.
CORS matters here too. Credentials are only allowed for a named origin, never for *, and an
empty origin allowlist means the browser refuses to send the cookie at all.
Three ways in
Password, optionally with a TOTP code when the account has two-factor enabled.
Magic link, a single-use code emailed to the address.
Password reset, the same mechanism with a different outcome.
202 unconditionallyThey answer 202 whether or not the address exists, which is what stops them being used to
enumerate accounts. It also means undelivered mail is invisible from the API: if mail is
unconfigured, the route still answers 202 and nothing arrives.
slotbattle:config-check is what catches that. The API will not.
Two-factor
Enrol, then activate with a code. Activation returns recovery codes, shown once, under the same rule as API keys.
Disabling TOTP is a DELETE with a body carrying the password. The alternative,
POST /totp/disable, describes the effect less accurately.
invalid_code means three things at once
Wrong, expired and already-used all answer invalid_code. There is no code_expired.
Distinguishing them would tell someone probing a magic link that they had the right code but were too late.
Switching casinos
The active casino travels in X-Tenant-Id, not in the session token, which is what lets an
operator with access to several casinos switch without re-authenticating.
Membership is checked on every request, so the header states an intent rather than an entitlement. Naming a casino you are not a member of is refused.
GET /admin/tenants is the one collection reachable without the header: it draws the casino
switcher, so requiring a casino would make choosing one depend on having chosen one.
Throttling
Every unauthenticated route is throttled inside its handler. Sustained failures answer
too_many_requests.
Common failures
| Symptom | Cause |
|---|---|
Every login answers 500, correct password included | The JWT secret is empty |
| Login succeeds, every later request is unauthenticated | Console and API on different registrable domains |
| Browser refuses to send the cookie | Origin allowlist empty, or the cookie's secure flag set over plain HTTP |
| Magic links never arrive | Mail unconfigured. The API cannot report this, so run the config check |