--- 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-21T15:05:00Z --- # 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 route becomes a 404 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 `404 NOT_FOUND` once any admin row exists, so the flow is safe even if the SPA is bypassed. # 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. Helper text is shown via `data-testid="setup-username-error"` when the pattern fails. - **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 (verified by the group-level `passwordMatchValidator`; mismatch shows `data-testid="setup-confirm-error"`). 5. Click **Create admin** (`data-testid="setup-submit"`). # 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. | | Confirm input | `[data-testid="setup-password-confirm"]` | Reactive control. | | Submit button | `[data-testid="setup-submit"]` | Disabled while submitting or invalid. | | 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`)