Allow public leaderboard API reads

This commit is contained in:
2026-10-05 16:46:44 -04:00
parent bd10c1c5d2
commit 083d29323f
5 changed files with 83 additions and 37 deletions
+17 -34
View File
@@ -157,35 +157,18 @@ sources are the [HTTP handler](../src/routes/api/leaderboard/+server.ts),
### Authentication and requests
`GET /api/leaderboard` is a session-authenticated JSON endpoint. A **viewer**,
operator or administrator account can read it after any required temporary
password change. There is no API-key or bearer-token authentication for this
endpoint; telemetry and host-agent credentials do not grant access.
`GET /api/leaderboard` is a public JSON endpoint; `HEAD` is public too. A tool
does not need an account, session cookie, API key or authorization header to
read it. Reads also work before administrator setup and with an expired session
or a session awaiting a password change. This exception applies only to reads
of this exact API route. Leaderboard/profile HTML pages, match-history APIs and
management routes still require a session; see [Users and access](authentication.md).
A tool can sign in with `POST /api/auth/login`, sending a JSON object with
`username` and `password`, `Content-Type: application/json`, and an `Origin`
header exactly matching the instance's origin (scheme, hostname and port, with
no trailing slash). Preserve the returned `__Host-ec_session` cookie and send it
on subsequent requests. Login returns `{ "user": ... }`; the session token is
in `Set-Cookie`, not the JSON body. Sessions expire after 12 hours or 30 minutes
of inactivity, and can be revoked earlier. Handle `401` by signing in again.
See [Users and access](authentication.md) for setup and session rules.
For example, with an existing account's credentials in a private `login.json`
file containing `{"username":"reader","password":"..."}`:
For example:
```sh
EC_BASE_URL='https://ec.example.com'
umask 077
curl --fail-with-body --silent --show-error \
--cookie-jar ec-cookies.txt \
--header "Origin: $EC_BASE_URL" \
--header 'Content-Type: application/json' \
--data-binary @login.json \
"$EC_BASE_URL/api/auth/login"
curl --fail-with-body --silent --show-error \
--cookie ec-cookies.txt \
--get "$EC_BASE_URL/api/leaderboard" \
--data-urlencode 'game=t6' \
--data-urlencode 'metric=kills' \
@@ -194,8 +177,9 @@ curl --fail-with-body --silent --show-error \
```
HTTPS is required. Local HTTP development requires `EC_AUTH_ALLOW_HTTP=true`
and a loopback hostname, and uses the separate `ec_session_dev` cookie. The
read request does not require an `Origin` header. Successful responses use
and a loopback hostname. The read request does not require an `Origin` header.
This endpoint does not add cross-origin browser access (CORS) headers; the
example above works for command-line or server-side clients. Successful responses use
`Content-Type: application/json`, `Cache-Control: no-store` and `Vary: Cookie`.
### Query parameters
@@ -308,14 +292,13 @@ strings such as `2026-10-05T12:00:00.000Z`, not reporter gameplay-clock times.
### Errors, pagination and freshness
Invalid query values return HTTP `400` with
`{ "ok": false, "kind": "bad-request", "error": "..." }`. Authentication errors
have a different shape: HTTP `401` returns `{ "error": "...", "setupRequired":
false }` (`true` before initial administrator setup); a required password change
returns HTTP `403` with `{ "error": "...", "mustChangePassword": true }`.
Host/enrollment bearer credentials return `403`. Disallowed HTTP transport
`{ "ok": false, "kind": "bad-request", "error": "..." }`. Public reads do not
return `401` for missing/expired sessions or `403` for a required password change.
Sending host/enrollment bearer credentials still returns `403` because those
credentials are restricted to the host-agent API; omit them. Disallowed HTTP transport
returns `426` with an `error` string. Report-service failures can produce `5xx`;
check status and content type before assuming the success schema or a uniform
error body. Failed/rate-limited login requests use their own auth error responses.
error body.
For successive pages, retain the same filters and increase `offset` by `limit`
until `offset + entries.length >= total` or the page is empty. There is no cursor,
@@ -335,8 +318,8 @@ an identity rename can change results without changing those timestamps.
### Related routes and current limits
`GET /api/matches` and `GET /api/servers/{name}/matches` return JSON match
summaries as `{ "matches": [...], "total": number }`, under the same session
authentication. Both accept only `limit` (default 25, range 1–10,000); there is
summaries as `{ "matches": [...], "total": number }`. These routes still require
session authentication. Both accept only `limit` (default 25, range 1–10,000); there is
no offset pagination. Results are newest first by first observation, then match
ID, and include active/interrupted matches that reached round 2. Their
[ServerMatch fields](../src/lib/match-history.ts) differ from leaderboard entries