diff --git a/docs/api/auth.md b/docs/api/auth.md index c6bb0a6..0aff1d9 100644 --- a/docs/api/auth.md +++ b/docs/api/auth.md @@ -1,7 +1,7 @@ --- type: api title: Auth Endpoints -description: Login, refresh, logout, public self-registration, CSRF, and first-admin registration. +description: Login, refresh, logout, public self-registration, CSRF, first-admin registration, and registration throttling. tags: [api, auth, login, register, refresh, csrf] timestamp: 2026-07-21T18:28:00Z --- @@ -51,18 +51,27 @@ The refresh token is set as an `HttpOnly` cookie via `setRefreshCookie(res, sess | 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. | +| 429 | `RATE_LIMITED` | The per-IP registration limiter has reached 10 attempts in the rolling 60-second window. | ## Wiring -* `AuthService.registerPlayer(dto, ip)` runs inside a single - `DataSource.transaction`: - 1. `RegistrationRateLimitService.isAllowed(ip)` check. +* `AuthService.registerPlayer(dto, ip)` runs inside a single `DataSource.transaction`: + 1. `RegistrationRateLimitService.tryConsume(ip)` permits at most 10 attempts per IP in a rolling 60-second window. 2. `SettingsService.get(SETTINGS_KEYS.REGISTRATIONS_ENABLED, 'false')` check. 3. Username uniqueness lookup → `USERNAME_TAKEN` on collision. 4. Argon2id hash + insert `UserEntity({ role: 'player', status: 'enabled' })`. - 5. `RegistrationRateLimitService.record(ip)`. - 6. `mintSession(manager, user)` → access JWT + refresh row. + 5. `mintSession(manager, user)` → access JWT + refresh row. +* `AuthService.registerFirstAdmin(username, password, ip)` uses the same limiter before checking whether an administrator already exists. +* The limiter is an in-memory Nest provider in `backend/src/common/services/registration-rate-limit.service.ts`; counters are process-local and reset when the API process restarts. + +# Key Files + +| File | Responsibility | +|------|----------------| +| `backend/src/modules/auth/auth.service.ts` | Validates registration policy, applies the limiter, creates users, and mints sessions. | +| `backend/src/common/services/registration-rate-limit.service.ts` | Tracks timestamps and enforces the 10-per-minute per-IP limit. | +| `backend/src/modules/auth/auth.controller.ts` | Exposes public registration and authentication routes. | +| `backend/src/modules/auth/dto/auth.dto.ts` | Validates registration request fields. | # Frontend consumer @@ -70,18 +79,22 @@ The refresh token is set as an `HttpOnly` cookie via `setRefreshCookie(res, sess `register()`. Both: 1. Call `ensureCsrf()` → `GET /api/v1/auth/csrf` to materialise the `csrf` - cookie if it isn't set yet (failures are swallowed because the - `CsrfMiddleware` will mint the cookie on the next response anyway). + cookie if it isn't set yet. 2. `POST` with `{ withCredentials: true, observe: 'response' }`. -3. Return a discriminated union: - * `{ ok: true, accessToken, expiresIn, user }` on success. - * `{ ok: false, status, code, message }` on failure, built by the - private `mapAuthError()` helper from the standard - [error envelope](/api/rest-overview.md). +3. Return success or the standard [error envelope](/api/rest-overview.md). -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](/guides/landing-page.md)). +The `LandingComponent` consumes both methods and maps failures to user-facing +copy (see [Landing Page](/guides/landing-page.md)). + +# Examples + +## Tester flow: registration rate limit + +1. Enable registrations in the admin settings. +2. Open the public registration form from the landing page. +3. Submit attempts from the same client IP. +4. The first 10 attempts are processed normally; the 11th within 60 seconds returns HTTP `429` with code `RATE_LIMITED`. +5. After the rolling window expires, a new attempt is accepted if other requirements pass. # See also diff --git a/docs/index.md b/docs/index.md index 9b55fb0..66acf78 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin controls. The docs below are organized by purpose so agents can pull just the slice -they need. Last regenerated 2026-07-21T18:45:54Z. +they need. Last regenerated 2026-07-21T21:18:00Z. # Architecture