Files
HIPCTF2/docs/api/setup.md
T
2026-07-21 16:49:46 +00:00

81 lines
3.6 KiB
Markdown
Raw Blame History

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.
---
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-21T16:48:20Z
---
# 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)
- [Setup Form Field Errors](/guides/setup-field-errors.md)
- [Backend Module Map](/architecture/backend-modules.md)
- [Bootstrap Service](/architecture/frontend-structure.md)