114 lines
6.8 KiB
Markdown
114 lines
6.8 KiB
Markdown
---
|
||
type: guide
|
||
title: First-Run Bootstrap
|
||
description: How a fresh HIPCTF instance is initialized by the very first administrator.
|
||
tags: [guide, bootstrap, first-admin, onboarding, tester]
|
||
timestamp: 2026-07-21T17:18:08Z
|
||
---
|
||
|
||
# When this flow runs
|
||
|
||
A brand-new deployment shows a non-dismissible modal asking the operator
|
||
to create the very first admin user. This is gated by the
|
||
`initialized` flag returned from `GET /api/v1/bootstrap`:
|
||
|
||
| `initialized` | User experience |
|
||
|---------------|---------------------------------------------------------------|
|
||
| `false` | Every route is redirected to `/bootstrap` until an admin is created. |
|
||
| `true` | The setup endpoint becomes a 409 `SYSTEM_INITIALIZED` and the modal can never appear again. |
|
||
|
||
The `authGuard` enforces the redirect on the client side; the backend
|
||
independently rejects `/api/v1/setup/create-admin` with
|
||
`409 SYSTEM_INITIALIZED` once any admin row exists, so the flow is safe
|
||
even if the SPA is bypassed. Concurrent first-admin requests are
|
||
serialized inside the Nest process so that two simultaneous submissions
|
||
cannot both succeed — the loser receives the same `SYSTEM_INITIALIZED`
|
||
409 (see [Setup Endpoint — Concurrency](/api/setup.md)).
|
||
|
||
# How to access (tester steps)
|
||
|
||
1. Start the stack against an empty database:
|
||
```
|
||
npm run db:reset && npm run dev
|
||
```
|
||
2. Open the browser at `http://localhost:3000/`.
|
||
3. The app redirects to `/bootstrap` and shows the
|
||
**"Welcome to HIPCTF — Create the first administrator"** modal.
|
||
4. Fill in the three fields and submit:
|
||
- **Username** — 3–32 chars, letters/digits/`.`/`-`/`_` only. The
|
||
username field displays inline validation after it is touched.
|
||
- **Password** — must satisfy the policy shown directly below the
|
||
field (default: 12 chars with upper, lower, digit, symbol).
|
||
- **Confirm Password** — must equal the password above; mismatch displays
|
||
an inline error.
|
||
5. Click **Create admin** (`data-testid="setup-submit"`). The button is
|
||
disabled only while a submission is in progress and shows **Creating...**
|
||
with a spinner during that request.
|
||
6. To verify client-side validation, submit with empty or partial fields. The
|
||
overlay stays open, the relevant field errors appear, and no administrator
|
||
is created or setup request is sent.
|
||
|
||
# Expected behavior
|
||
|
||
* On success the modal disappears, the app stores the access token via
|
||
`AuthService.setSession`, calls `BootstrapService.markInitialized()`,
|
||
and navigates to `/admin` (see [Admin Shell & User Management](/guides/admin-shell.md)).
|
||
* On `USERNAME_TAKEN` the form shows a red alert:
|
||
`"Username already exists"`. The **Retry** button is intentionally
|
||
hidden because the username must be changed first.
|
||
* On any other failure (network, `WEAK_PASSWORD`, `VALIDATION_FAILED`,
|
||
`RATE_LIMITED`) the alert shows `"Setup failed, please retry"` and a
|
||
**Retry** link (`data-testid="setup-retry"`) re-submits the same
|
||
payload.
|
||
|
||
The modal **cannot** be dismissed by the user:
|
||
|
||
* Clicking the backdrop is a no-op (`swallowBackdrop`).
|
||
* Pressing **Esc** is swallowed by the `keydown.escape` host listener.
|
||
|
||
# Visual elements
|
||
|
||
| Element | Selector | Purpose |
|
||
|--------------------------------|----------------------------------|-----------------------------------------------|
|
||
| Modal overlay | `.overlay` | Full-screen backdrop. |
|
||
| Modal card | `.modal[role="dialog"]` | Contains the form; stops click propagation. |
|
||
| Username input | `[data-testid="setup-username"]` | Reactive control with length/pattern rules. |
|
||
| Password input | `[data-testid="setup-password"]` | Reactive control; shows inline error on touch when empty. |
|
||
| Password error | `[data-testid="setup-password-error"]` | Renders `passwordError()` (currently `required`). |
|
||
| Confirm input | `[data-testid="setup-password-confirm"]` | Reactive control. |
|
||
| Submit button | `[data-testid="setup-submit"]` | Triggers validation even when invalid; disabled only while submitting. |
|
||
| Server error alert | `[data-testid="setup-server-error"]` | Displays `serverError()`. |
|
||
| Retry button | `[data-testid="setup-retry"]` | Re-submits; absent when the error is non-retryable. |
|
||
| Password policy hint | `.hint` | Live description from `BootstrapService.passwordPolicy()`. |
|
||
|
||
# Behind the scenes (architecture map)
|
||
|
||
| Step | Where | What happens |
|
||
|------|--------------------------------------------------------|-------------------------------------------------------------------------|
|
||
| 1 | `BootstrapService.load()` | Calls `GET /api/v1/bootstrap`, sets `initialized = false`. |
|
||
| 2 | `authGuard` | Sees `initialized() === false`, redirects to `/bootstrap`. |
|
||
| 3 | `SetupCreateAdminComponent` | Renders the modal; reactive form enforces username + password policy + passwordMatch. |
|
||
| 4 | `SetupCreateAdminService.ensureCsrf()` | Preflights `GET /api/v1/auth/csrf` so the response sets the `csrf` cookie. |
|
||
| 5 | `POST /api/v1/setup/create-admin` | `SetupService.createAdmin` runs in a TypeORM transaction; creates the user with `role='admin'`; mints session via `AuthService.createSession`. |
|
||
| 6 | `setRefreshCookie` | Sets the `rt` HttpOnly cookie. |
|
||
| 7 | Component on success | `AuthService.setSession`, `BootstrapService.markInitialized()`, navigate to `/admin`. |
|
||
| 8 | Subsequent `authGuard` checks | `initialized === true`; require auth instead. |
|
||
|
||
# What the operator sees after init
|
||
|
||
Once the first admin exists, `/api/v1/bootstrap` returns
|
||
`initialized: true`. The app now:
|
||
|
||
* Skips `/bootstrap` entirely.
|
||
* Routes unauthenticated users to `/login`.
|
||
* Routes authenticated admins to `/` (the Home shell) and renders the
|
||
admin nav link. The first admin lands directly on the user list at
|
||
`/admin` (see [Admin Shell & User Management](/guides/admin-shell.md)).
|
||
* Routes authenticated players to `/` without the admin nav link.
|
||
|
||
# See also
|
||
|
||
- [Setup Endpoint](/api/setup.md)
|
||
- [Frontend Structure](/architecture/frontend-structure.md)
|
||
- [Backend Module Map](/architecture/backend-modules.md)
|
||
- [Bootstrap payload source of truth](`/architecture/overview.md`) |