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
+105
View File
@@ -0,0 +1,105 @@
---
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-21T14:43: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** — 332 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 `/challenges`.
* 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 `/challenges`. |
| 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 users to `/` (Home) which itself redirects to
`/challenges` once admin features are wired.
# 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`)