SlotBattle Admin API
The browser-facing API behind the operations console: authentication, operators, casinos, roles, API keys, settings, signing keys, battles and games.
Authentication is a cookie, not a bearer token
Signing in sets an HttpOnly cookie scoped to /admin, and the token never
appears in a response body. Authorization: Bearer is not accepted here — a
credential within JavaScript's reach is exactly what this design avoids.
What the sign-in body carries instead is expires_in: the console cannot read
an HttpOnly cookie, and still has to know when to call /admin/auth/refresh.
The cookie is SameSite=Strict, which reasons about the registrable domain.
The console and this API therefore answer on one domain. A client served
from another one fails every authenticated request silently — the browser
simply never attaches the cookie, so the symptom is a 401 on a request that
looks correct on the wire.
There is no registration route, by design. Operators are created by other
operators, through POST /admin/users.
The active casino travels in a header
X-Tenant-Id names the casino a request applies to. It is a header rather than
a claim in the session so one operator can hold access to several casinos and
switch without signing in again. Membership is verified on every request — the
header is a request, not an assertion.
Nothing under /admin/auth takes it: authentication is about the person, and
casino scope starts at the resource routes. Each route below states whether it
needs the header.
GET /admin/tenants is the one collection reachable without it: it is what
draws the casino switcher, so requiring a casino would make choosing one depend
on having chosen one.
Four other routes take no casino because they are about no casino: registering and deleting one, and the two signing-key routes. All four are super-user only, and each says so.
Requiring a custom header is also the third leg of the CSRF defence: an HTML form cannot set one, so the request is forced through a preflight, and the preflight meets the origin allowlist.
Order of checks
Authenticate, resolve X-Tenant-Id, then load the role's permissions — always
in that order. Checking a permission before the membership would tell someone
with no link to the casino that the permission exists.
Two consequences are worth reading off that order. A caller with no membership
on the casino they named gets the same 403 as one whose role is short a
permission. And a failed read of the access catalog denies: treating an
unreadable table as "allow" would turn a database blip into an open console
with no symptom.
Error envelope
The same shape as the server-to-server API — the console is another client of this service, not a second API:
{ "error": { "code": "invalid_credentials", "message": "invalid credentials" } }
code is stable and safe to branch on; message is for humans and may be
reworded.
Request bodies are decoded strictly
Bodies are capped at 1 MiB, reject unknown fields, and reject trailing
content after the JSON value. Naming an actor or a casino in a body is a 400,
not a silently ignored override: the actor comes from the session and the
casino from X-Tenant-Id, always.
A settings document is the one exception, and only in shape: its fields are checked against the category's own key set instead, which rejects unknown keys and wrong types.
What never comes back
A secret is returned by nothing on this surface. A settings document answers
"<field>Set": true|false in place of each one, the signing keys answer no
private key at all, and an API key answers only its prefix. The single
exception is the moment of creation: POST /admin/tenants/{id}/keys carries the
plaintext key once, and POST /admin/auth/totp/activate the recovery codes
once. Neither is recoverable afterwards.
Authentication
- API Key: SessionCookie
HttpOnly, Path=/admin, SameSite=Strict. Set by sign-in and never readable from JavaScript. Authorization: Bearer is not accepted here. Because the cookie is SameSite=Strict, the client and this API must answer on the same registrable domain — otherwise the browser never attaches it and every authenticated request fails silently.
Security Scheme Type: | apiKey |
|---|---|
Cookie parameter name: | sb_admin_session |