Files
HIPCTF2/docs/api/setup.md
T
2026-07-21 14:45:52 +00:00

3.5 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
api Setup Endpoint Dedicated POST /api/v1/setup/create-admin endpoint used only while no administrator exists.
api
setup
bootstrap
first-admin
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)

{
  "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:

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