AI Implementation feature(821): First-Start Create Admin Modal (#2)

This commit was merged in pull request #2.
This commit is contained in:
2026-07-21 14:45:55 +00:00
parent 03bcb6b156
commit 9f67ec8c50
30 changed files with 1306 additions and 141 deletions
+80
View File
@@ -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 | 332 chars, regex `^[a-zA-Z0-9_.-]+$` |
| `password` | string | 1256 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)