5.1 KiB
5.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| api | Auth Endpoints | Login, refresh, logout, public self-registration, CSRF, and first-admin registration. |
|
2026-07-21T18:28:00Z |
Endpoints
| Method | Path | Auth | CSRF | Rate limited | Source |
|---|---|---|---|---|---|
POST |
/api/v1/auth/register |
@Public() |
Enforced — caller must first call GET /api/v1/auth/csrf (the SPA's AuthService.ensureCsrf() does this automatically). |
Yes — RegistrationRateLimitService per IP |
backend/src/modules/auth/auth.controller.ts |
POST |
/api/v1/auth/login |
@Public() |
Enforced — same pattern as register. CSRF skip was removed in this change. |
Yes — LoginBackoffService per IP + username |
backend/src/modules/auth/auth.controller.ts |
POST |
/api/v1/auth/refresh |
Public route, requires a valid refresh cookie. | Skipped (the request is a POST carrying an HttpOnly cookie, not a state-changing user action). |
No | backend/src/modules/auth/auth.controller.ts |
POST |
/api/v1/auth/logout |
Authenticated (JWT). | Enforced. | No | backend/src/modules/auth/auth.controller.ts |
GET |
/api/v1/auth/csrf |
@Public() |
n/a (GET). |
No | backend/src/modules/auth/auth.controller.ts |
POST |
/api/v1/auth/register-first-admin |
@Public() |
Skipped (path is in CsrfMiddleware.SKIP_PATH_PREFIXES). |
Yes | backend/src/modules/users/users.controller.ts |
Public player self-registration — POST /api/v1/auth/register
New endpoint, gated by the registrationsEnabled setting
(backend/src/config/env.schema.ts → SETTINGS_KEYS.REGISTRATIONS_ENABLED,
default "false").
| Field | Type | Constraints |
|---|---|---|
username |
string | 3–32 chars, regex ^[a-zA-Z0-9_.-]+$ |
password |
string | 1–256 chars; also passes validatePassword (Argon2 policy). |
passwordConfirm |
string | Must equal password (zod refine → VALIDATION_FAILED on passwordConfirm). |
Validated by backend/src/modules/auth/dto/auth.dto.ts → RegisterDtoSchema.
Successful response (201)
{
"accessToken": "<jwt>",
"expiresIn": 900,
"user": { "id": "<uuid>", "username": "player1", "role": "player" }
}
The refresh token is set as an HttpOnly cookie via setRefreshCookie(res, session.refreshToken, config). The new user row has role='player' and status='enabled'.
Error envelope
| HTTP | code |
When |
|---|---|---|
| 400 | VALIDATION_FAILED |
zod body validation failed. |
| 400 | WEAK_PASSWORD |
Argon2 policy rejects the password. |
| 403 | REGISTRATIONS_DISABLED |
setting.registrationsEnabled !== 'true'. |
| 409 | USERNAME_TAKEN |
A user with that username already exists. |
| 429 | RATE_LIMITED |
RegistrationRateLimitService blocked the IP. |
Wiring
AuthService.registerPlayer(dto, ip)runs inside a singleDataSource.transaction:RegistrationRateLimitService.isAllowed(ip)check.SettingsService.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false')check.- Username uniqueness lookup →
USERNAME_TAKENon collision. - Argon2id hash + insert
UserEntity({ role: 'player', status: 'enabled' }). RegistrationRateLimitService.record(ip).mintSession(manager, user)→ access JWT + refresh row.
Frontend consumer
frontend/src/app/core/services/auth.service.ts exposes login() and
register(). Both:
- Call
ensureCsrf()→GET /api/v1/auth/csrfto materialise thecsrfcookie if it isn't set yet (failures are swallowed because theCsrfMiddlewarewill mint the cookie on the next response anyway). POSTwith{ withCredentials: true, observe: 'response' }.- Return a discriminated union:
{ ok: true, accessToken, expiresIn, user }on success.{ ok: false, status, code, message }on failure, built by the privatemapAuthError()helper from the standard error envelope.
The LandingComponent (frontend/src/app/features/landing/landing.component.ts)
consumes both methods and maps the code to user-facing copy via
buildLoginFailureMessage (see Landing Page).