2 API Reference
Emily edited this page 2026-11-04 23:27:23 +01:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

API Reference

Base URL

https://account.example.com

All endpoints are under /api/v1/ unless noted. Requests and responses are JSON. The JSON body limit is 16 KB.

Authentication

Protected routes require:

Authorization: Bearer <jwt>

Tokens are HS256 JWTs with the issuer universal-gaming and audience universal-gaming-app. They contain:

Claim Meaning
sub Account UUID
username Username at issue time
ver Session version (auth_version)
exp Expiry

A password reset bumps auth_version, so every older token returns 401 Session expired. Sign in again.

Errors

Errors use an error string:

{ "error": "Invalid username, e-mail or password." }
Status Meaning
400 Invalid input
401 Missing/invalid session, wrong password
403 Feature disabled, registration closed
404 Unknown route, account, match or release
409 Conflict: duplicate username/e-mail, stale match, wrong turn
413 Request body too large
429 Rate limit exceeded
502 Upstream release feed or e-mail delivery failed
503 SMTP not configured

Service endpoints

Method Path Auth Description
GET / No Service name and version
GET /healthz No Health plus database kind
GET /api/v1/meta No Feature flags and version

Example:

curl https://account.example.com/healthz
{
  "status": "ok",
  "service": "universal-gaming-accounts",
  "version": "1.4.0",
  "database": "postgres"
}

/api/v1/meta:

{
  "name": "Universal Gaming Accounts",
  "version": "1.4.0",
  "registrationEnabled": true,
  "updateProxyEnabled": true,
  "leaderboardEnabled": true,
  "chessEnabled": true,
  "emailConfirmationEnabled": true,
  "emailDeliveryConfigured": true,
  "managementEnabled": true
}

Account object

{
  "id": "9f1c2f7e-3d1a-4e7a-9d55-2a0b6c8e4f10",
  "username": "player",
  "email": "player@example.com",
  "emailVerified": true,
  "createdAt": "2026-01-01T12:00:00.000Z",
  "updatedAt": "2026-01-02T12:00:00.000Z"
}

email is null when none is assigned. The UUID never changes when the username changes.

Registration and login

POST /api/v1/accounts

Creates an account. Rate limited with the auth limiter.

{
  "username": "player",
  "password": "correct horse battery staple",
  "email": "player@example.com"
}
  • Username: 3–32 characters, letters, numbers, ., _, -.
  • Password: 8–128 characters.
  • E-mail: optional, at most 254 characters.
  • Returns 201 with { token, user }.
  • Returns 403 when registration is disabled.
  • Returns 409 for a duplicate username or e-mail.

If an e-mail is supplied, a confirmation mail is sent when SMTP and e-mail confirmation are enabled. The account is usable immediately either way.

POST /api/v1/auth/login

{
  "identifier": "player",
  "password": "correct horse battery staple"
}

identifier may be a username or e-mail. username is accepted as a legacy alias. Returns { token, user }.

Failed logins log only the client IP. The response is always:

{ "error": "Invalid username, e-mail or password." }

GET /api/v1/accounts/check-username?username=player

Public availability check. Rate limited.

{ "username": "player", "available": false }

Profile

GET /api/v1/accounts/me

Returns { user } for the current token.

PATCH /api/v1/accounts/me

Rename and/or change e-mail:

{
  "username": "new-name",
  "email": "new@example.com"
}

At least one field is required. Changing the e-mail clears the verification state, clears any pending password-reset token and sends a new confirmation mail when enabled. Changing to null removes the address.

Returns { user }.

POST /api/v1/accounts/me/resend-verification

Resends the confirmation mail. Requires an assigned, unverified address, e-mail confirmation enabled and SMTP configured.

Status Meaning
200 { "sent": true }
400 No e-mail assigned
403 Confirmation disabled
409 Already verified
502 SMTP delivery failed
503 SMTP not configured

POST /api/v1/accounts/verify-email

{ "token": "..." }

Returns { user } with emailVerified: true. Tokens are single-use and expire after 48 hours.

GET /verify-email?token=...

Browser-facing confirmation page. Returns HTML, not JSON. Marks the address verified and tells the user to return to the app.

DELETE /api/v1/accounts/me

Requires password confirmation:

{ "password": "correct horse battery staple" }

Deletes the account, leaderboard rows and chess matches through database cascades. Returns 204.

Password reset

POST /api/v1/auth/forgot-password

{ "email": "player@example.com" }
  • Requires SMTP; otherwise 503.
  • Only accounts with an assigned and confirmed address receive mail.
  • Always returns 202 { "sent": true }, whether or not the address exists, to prevent account enumeration.
  • Tokens are 32 random bytes, stored only as SHA-256 hashes.
  • Link format: ${APP_PUBLIC_URL}/account?reset=<token>.
  • Default lifetime: 30 minutes (PASSWORD_RESET_TTL_MINUTES).

POST /api/v1/auth/reset-password

{
  "token": "...",
  "password": "new password"
}

Returns { "reset": true }. The token is cleared, all existing sessions are invalidated, and the user must sign in again.

Leaderboard

Ranked games:

sudoku, killer-sudoku, mahjong, minesweeper, klondike, classic-solitaire, spider, freecell, pyramid, tripeaks.

Chess is not ranked.

GET /api/v1/leaderboards

Query parameters:

Name Default Description
gameId all Optional game filter
limit 5 Entries per mode, 1–50

GET /api/v1/leaderboards/:gameId

Same payload, scoped to one game.

{
  "updatedAt": "2026-01-02T12:00:00.000Z",
  "limit": 5,
  "games": [
    {
      "gameId": "sudoku",
      "modes": [
        {
          "mode": "Expert",
          "entries": [
            {
              "rank": 1,
              "username": "player",
              "seconds": 412,
              "moves": 87,
              "detail": "No hints",
              "achievedAt": "2026-01-02T11:00:00.000Z"
            }
          ]
        }
      ]
    }
  ]
}

Ranking order: seconds, then moves, then achievedAt, then user_id.

POST /api/v1/leaderboards/:gameId/scores

Requires authentication. Rate limited to 30 submissions per minute.

{
  "mode": "Expert",
  "seconds": 412,
  "moves": 87,
  "detail": "No hints"
}
  • mode: 1–32 characters, letters, numbers, spaces, ., _, +, -.
  • seconds: 0 to 30 days.
  • moves: 0 to 1,000,000.
  • detail: optional, at most 120 characters.

Only one score is stored per user, game and mode. A worse score does not replace the existing best.

{
  "improved": true,
  "rank": 1,
  "score": {
    "gameId": "sudoku",
    "mode": "Expert",
    "username": "player",
    "seconds": 412,
    "moves": 87,
    "detail": "No hints",
    "achievedAt": "2026-01-02T11:00:00.000Z"
  }
}

Returns 403 when LEADERBOARD_ENABLED=false.

Chess

All chess routes require authentication and return 403 when CHESS_ENABLED=false. Rate limit: 120 requests per minute.

The server replays the complete move history with chess.js. The client is never trusted for legality, turn order or results.

Match object

{
  "id": "uuid",
  "status": "active",
  "moves": ["e2e4", "e7e5"],
  "turn": "w",
  "result": null,
  "version": 2,
  "createdAt": "2026-01-01T12:00:00.000Z",
  "updatedAt": "2026-01-01T12:01:00.000Z",
  "finishedAt": null,
  "lastMoveAt": "2026-01-01T12:01:00.000Z",
  "white": { "username": "alice" },
  "black": { "username": "bob" },
  "you": "white",
  "yourTurn": true
}

Statuses: pending, active, declined, cancelled, finished.

moves are UCI strings (e2e4, e7e8q). result is white, black or draw.

Routes

Method Path Description
GET /api/v1/chess/matches Up to 30 matches involving the user
GET /api/v1/chess/matches/:id One match
POST /api/v1/chess/challenges Challenge a username
POST /api/v1/chess/challenges/:id/accept Accept as the challenged player
POST /api/v1/chess/challenges/:id/decline Decline as the challenged player
POST /api/v1/chess/challenges/:id/cancel Cancel as the challenger
POST /api/v1/chess/matches/:id/moves Submit a move
POST /api/v1/chess/matches/:id/resign Resign

Create a challenge:

{ "opponentUsername": "bob" }

The challenger plays White. Returns 201 with { match }.

Submit a move:

{
  "from": "e2",
  "to": "e4",
  "promotion": "q"
}

promotion is optional and must be q, r, b or n.

{
  "match": { "...": "..." },
  "move": {
    "uci": "e2e4",
    "san": "e4",
    "check": false,
    "checkmate": false,
    "draw": false
  }
}

Move conflicts return 409:

  • Not your turn.
  • Match not active.
  • The match changed while saving (optimistic version check).

Resigning returns { match } with status: "finished" and the opponent as the winner.

Release proxy

The app uses these routes to avoid browser CORS restrictions. The backend caches the upstream feed and sanitizes every field.

Method Path Description
GET /api/v1/app/releases Up to 20 releases, newest first
GET /api/v1/app/latest Newest non-draft, non-prerelease release

A release contains:

{
  "tag_name": "v2.9.0",
  "name": "Universal Gaming 2.9.0",
  "body": "Release notes…",
  "html_url": "https://git.emilyh.de/Emily/UniversalGamingApp/releases/tag/v2.9.0",
  "published_at": "2026-01-01T12:00:00Z",
  "created_at": "2026-01-01T11:00:00Z",
  "draft": false,
  "prerelease": false,
  "assets": [
    {
      "name": "UniversalGaming-2.9.0-android.apk",
      "browser_download_url": "https://…",
      "size": 59600000
    }
  ]
}

Returns 404 when UPDATE_ENABLED=false and 502 when the upstream feed fails.

Management panel

GET /management returns a read-only HTML dashboard with counts and recent activity. It uses HTTP Basic authentication.

  • Disabled when MANAGEMENT_PASSWORD is empty.
  • Username: MANAGEMENT_USER (default admin).
  • Cache-Control: no-store.

It is an operator convenience, not a public API. Restrict it at the reverse proxy where possible.

Rate limits

Limiter Window Limit Applies to
Auth 15 min 30 Registration, login, reset-password
Username check 1 min 60 Availability checks
E-mail 15 min 8 Forgot-password, resend verification, verify-email
Chess 1 min 120 All chess routes
Leaderboard read 1 min 120 Leaderboard GETs
Leaderboard write 1 min 30 Score submissions

Rate limits use the client IP derived from the trusted-proxy configuration.