AI Implementation feature(821): First-Start Create Admin Modal (#2)
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
---
|
||||
type: api
|
||||
title: REST API Overview
|
||||
description: Base URL, versioning, auth, CSRF, and the standard error envelope.
|
||||
tags: [api, rest, overview, csrf]
|
||||
timestamp: 2026-07-21T14:43:00Z
|
||||
---
|
||||
|
||||
# Base URL & versioning
|
||||
|
||||
All endpoints live under `/api/v1`. The Angular SPA uses the same
|
||||
prefix; the SPA fallback only kicks in for non-API routes.
|
||||
|
||||
| Path prefix | Purpose |
|
||||
|-----------------|--------------------------------------------------------|
|
||||
| `/api/v1/*` | Versioned JSON REST endpoints. |
|
||||
| `/api/docs` | Swagger UI (OpenAPI 3.1). |
|
||||
| `/api/docs-json`| Raw OpenAPI 3.1 document. |
|
||||
| `/uploads/*` | Static uploads directory (`UPLOAD_DIR`). |
|
||||
| `/*` | SPA fallback (`FRONTEND_DIST/index.html`). |
|
||||
|
||||
# Authentication
|
||||
|
||||
* `JwtAuthGuard` is registered as `APP_GUARD` and runs on every request
|
||||
unless the handler is marked with `@Public()` from
|
||||
`backend/src/common/decorators/public.decorator.ts`.
|
||||
* Successful auth produces an access JWT in the response body and an
|
||||
`HttpOnly` refresh cookie named `rt` (see `backend/src/modules/auth/cookie.ts`).
|
||||
* `Authorization: Bearer <accessToken>` is attached by the
|
||||
`authInterceptor` on the frontend.
|
||||
|
||||
# CSRF
|
||||
|
||||
* `CsrfMiddleware` (registered globally) mints a `csrf` cookie on every
|
||||
response and, for unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`),
|
||||
compares the cookie against the `x-csrf-token` header using
|
||||
`crypto.timingSafeEqual`.
|
||||
* The frontend's `csrfInterceptor` runs *before* `authInterceptor` so
|
||||
the header is attached on every unsafe call.
|
||||
* Skip list (`SKIP_PATH_PREFIXES` in
|
||||
`backend/src/common/middleware/csrf.middleware.ts`):
|
||||
- `/api/v1/auth/register-first-admin`
|
||||
- `/api/v1/auth/login`
|
||||
- `/api/v1/setup/create-admin`
|
||||
|
||||
# Error envelope
|
||||
|
||||
Every error flows through `GlobalExceptionFilter` and is shaped as:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "USERNAME_TAKEN",
|
||||
"message": "Username already exists",
|
||||
"details": null,
|
||||
"path": "/api/v1/setup/create-admin",
|
||||
"timestamp": "2026-07-21T14:43:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------------|---------------------|--------------------------------------------------------|
|
||||
| `code` | string | One of `ERROR_CODES` (`backend/src/common/errors/error-codes.ts`). |
|
||||
| `message` | string | Human-readable summary. |
|
||||
| `details` | unknown \| null | Optional structured details (e.g. zod field errors). |
|
||||
| `path` | string | Original request path. |
|
||||
| `timestamp` | ISO 8601 string | When the filter produced the response. |
|
||||
|
||||
# Canonical error codes
|
||||
|
||||
`USERNAME_TAKEN` is the most recently added code (used by the setup
|
||||
endpoint). Full list lives in `backend/src/common/errors/error-codes.ts`:
|
||||
|
||||
`VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`,
|
||||
`CONFLICT`, `LAST_ADMIN`, `SYSTEM_INITIALIZED`, `RATE_LIMITED`,
|
||||
`CSRF_INVALID`, `WEAK_PASSWORD`, `USERNAME_TAKEN`,
|
||||
`INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`, `INTERNAL`.
|
||||
|
||||
# See also
|
||||
|
||||
- [Auth Endpoints](/api/auth.md)
|
||||
- [Admin Endpoints](/api/admin.md)
|
||||
- [Setup Endpoint](/api/setup.md)
|
||||
- [System Endpoints](/api/system.md)
|
||||
- [Uploads Endpoints](/api/uploads.md)
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
type: api
|
||||
title: Setup Endpoint
|
||||
description: Dedicated POST /api/v1/setup/create-admin endpoint used only while no administrator exists.
|
||||
tags: [api, setup, bootstrap, first-admin]
|
||||
timestamp: 2026-07-21T14:43:00Z
|
||||
---
|
||||
|
||||
# Endpoint
|
||||
|
||||
| Method | Path | Auth | CSRF | Rate limited |
|
||||
|--------|-------------------------------|------|--------|--------------|
|
||||
| `POST` | `/api/v1/setup/create-admin` | `@Public()` | Skipped (path is in `CsrfMiddleware.SKIP_PATH_PREFIXES`) | Yes (per-IP via `RegistrationRateLimitService`) |
|
||||
|
||||
# Request body
|
||||
|
||||
Validated by `backend/src/modules/setup/dto/create-admin.dto.ts` (zod):
|
||||
|
||||
| Field | Type | Constraints |
|
||||
|-------------------|--------|------------------------------------------------------------------------------|
|
||||
| `username` | string | 3–32 chars, regex `^[a-zA-Z0-9_.-]+$` |
|
||||
| `password` | string | 1–256 chars; additionally passes `validatePassword` (Argon2 policy). |
|
||||
| `passwordConfirm` | string | Must equal `password` (zod refine → `VALIDATION_FAILED`, `passwordConfirm`).|
|
||||
|
||||
# Successful response (201)
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "<jwt>",
|
||||
"expiresIn": 900,
|
||||
"user": { "id": "<uuid>", "username": "first.admin", "role": "admin" }
|
||||
}
|
||||
```
|
||||
|
||||
The refresh token is set as an `HttpOnly` cookie via
|
||||
`setRefreshCookie(res, session.refreshToken, config)` so the SPA can keep
|
||||
refreshing without storing the raw token in JS.
|
||||
|
||||
# Error envelope
|
||||
|
||||
| HTTP | `code` | When |
|
||||
|------|---------------------|----------------------------------------------------------------------|
|
||||
| 400 | `VALIDATION_FAILED` | zod body validation failed (length, regex, or `passwordConfirm`). |
|
||||
| 400 | `WEAK_PASSWORD` | Argon2 policy rejects the password (length / mixed-case requirement). |
|
||||
| 404 | `NOT_FOUND` | An admin already exists — the route is hidden once initialized. |
|
||||
| 409 | `USERNAME_TAKEN` | Username collision (reachable only during a race before init). |
|
||||
| 429 | `RATE_LIMITED` | `RegistrationRateLimitService` blocked the IP. |
|
||||
|
||||
# Wiring
|
||||
|
||||
* Registered in `backend/src/app.module.ts` via `SetupModule`.
|
||||
* `SetupController` (`backend/src/modules/setup/setup.controller.ts`) is
|
||||
`@Public()` and uses `ZodValidationPipe(CreateAdminDtoSchema)`.
|
||||
* `SetupService.createAdmin` runs everything inside a single
|
||||
`DataSource.transaction`: admin count check → username uniqueness check
|
||||
→ Argon2id hash → insert `UserEntity(role='admin', status='enabled')` →
|
||||
mint session via the static `AuthService.createSession`.
|
||||
* CSRF is bypassed for `/api/v1/setup/create-admin` so the first caller
|
||||
(before any cookie exists) is not blocked. The middleware still mints
|
||||
a `csrf` cookie on the response so subsequent calls are covered.
|
||||
|
||||
# Frontend consumer
|
||||
|
||||
`frontend/src/app/features/setup/setup-create-admin.service.ts` calls
|
||||
this endpoint and normalizes the response into the discriminated union
|
||||
`CreateAdminResult`:
|
||||
|
||||
```ts
|
||||
type CreateAdminResult = CreateAdminSuccess | CreateAdminFailure;
|
||||
```
|
||||
|
||||
`CreateAdminFailure` exposes `{ ok: false, status, code, message }` so
|
||||
the component can switch on `code === 'USERNAME_TAKEN'` to surface a
|
||||
non-retryable inline error and offer a Retry button for everything else.
|
||||
|
||||
# See also
|
||||
|
||||
- [First-Run Bootstrap Guide](/guides/bootstrap.md)
|
||||
- [Backend Module Map](/architecture/backend-modules.md)
|
||||
- [Bootstrap Service](/architecture/frontend-structure.md)
|
||||
Reference in New Issue
Block a user