AI Implementation feature(841): First-Start Create Admin Modal 1.00 (#3)
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
---
|
||||
type: guide
|
||||
title: Admin Shell & User Management
|
||||
description: How an authenticated admin navigates the post-login shell and reaches the admin user-management area.
|
||||
tags: [guide, admin, shell, navigation, tester]
|
||||
timestamp: 2026-07-21T15:05:00Z
|
||||
---
|
||||
|
||||
# When this view is available
|
||||
|
||||
The admin shell is the post-login UI for users with `role === 'admin'`.
|
||||
It is gated by:
|
||||
|
||||
| Layer | File | Check |
|
||||
|------------------|--------------------------------------------------------------------------------------------|------------------------------------|
|
||||
| Client route | `frontend/src/app/app.routes.ts` | `/admin` uses `adminGuard`. |
|
||||
| Client nav link | `frontend/src/app/features/home/home.component.ts` | Renders only when `showAdminNav()` is true. |
|
||||
| Client predicate | `frontend/src/app/features/home/home.shell.ts` (`shouldShowAdminNav`) | `isAuthenticated && role === 'admin'`. |
|
||||
| Server route | `backend/src/modules/admin/admin.controller.ts` (mounted under `@UseGuards(AdminGuard)` + `@Roles('admin')`) | Requires admin JWT. |
|
||||
|
||||
A non-admin (or unauthenticated) request to `/admin` is intercepted by
|
||||
the client guard *before* any HTTP call is made, so the page never
|
||||
renders.
|
||||
|
||||
# How to access (tester steps)
|
||||
|
||||
1. Ensure the instance is initialized (`/api/v1/bootstrap` returns
|
||||
`initialized: true`). If not, follow [First-Run Bootstrap](/guides/bootstrap.md).
|
||||
2. Sign in as a user with `role === 'admin'` via `/login`.
|
||||
3. The browser lands on `/` and renders the **Home shell** with a
|
||||
header containing the page title and a sign-in status line.
|
||||
4. Click the **Admin** nav link (`data-testid="nav-admin"`) in the
|
||||
shell header, or navigate directly to `/admin`.
|
||||
5. The admin area renders inside the shell:
|
||||
- **Heading:** "Admin area"
|
||||
- **Loading state:** `Loading users...` (`data-testid="admin-loading"`)
|
||||
while the GET is in flight.
|
||||
- **User list:** an unordered list (`data-testid="admin-user-list"`)
|
||||
showing one `<li>` per user with the username in bold and the role
|
||||
in parentheses, e.g. `root (admin)`.
|
||||
- **Error state:** a red message (`data-testid="admin-error"`) when
|
||||
the request fails (e.g. backend down or auth expired).
|
||||
|
||||
# Expected behavior
|
||||
|
||||
| User role | Visiting `/admin` |
|
||||
|----------------------|---------------------------------------------------------|
|
||||
| Unauthenticated | Redirected to `/login` by `adminGuard`. |
|
||||
| Authenticated player | Redirected to `/` by `adminGuard`. |
|
||||
| Authenticated admin | Page renders, fetches `GET /api/v1/admin/users`, lists users. |
|
||||
|
||||
* When `BootstrapService.initialized()` is still `false` the guard
|
||||
redirects to `/bootstrap` instead of `/login`, so the first admin
|
||||
flow is honored.
|
||||
* The Admin nav link is hidden for non-admin users — there is no
|
||||
visible UI affordance to reach `/admin` without the role.
|
||||
* The list endpoint uses the existing `authInterceptor` + `csrfInterceptor`,
|
||||
so no extra plumbing is required.
|
||||
|
||||
# Visual elements
|
||||
|
||||
| Element | Selector | Purpose |
|
||||
|----------------------|-----------------------------------|----------------------------------------------|
|
||||
| Shell header | `.shell-header` | Page title + signed-in identity. |
|
||||
| Admin nav link | `[data-testid="nav-admin"]` | RouterLink to `/admin`; shown to admins only. |
|
||||
| Shell body | `.shell-body` | Holds `<router-outlet>` for child routes. |
|
||||
| Admin heading | `h2` inside `.admin-area` | "Admin area". |
|
||||
| Loading message | `[data-testid="admin-loading"]` | "Loading users...". |
|
||||
| Error message | `[data-testid="admin-error"]` | Red text; renders `error().error.message` or fallback. |
|
||||
| User list | `[data-testid="admin-user-list"]` | Renders one `<li>` per `AdminUser`. |
|
||||
|
||||
# Architecture map
|
||||
|
||||
| Step | Where | What happens |
|
||||
|------|----------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| 1 | `frontend/src/app/app.routes.ts` | `/` is a parent route with `authGuard`; `/admin` child uses `adminGuard` + lazy-loads `AdminUsersComponent`. |
|
||||
| 2 | `frontend/src/app/core/guards/admin.guard.ts` | `adminGuard` calls `decideAdminGuard(...)` with bootstrap + auth state. |
|
||||
| 3 | `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure function returning `{kind: 'allow'}` or `{kind: 'redirect', path}`. |
|
||||
| 4 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
|
||||
| 5 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
|
||||
| 6 | `frontend/src/app/features/admin/admin-users.component.ts` | On init, calls `AdminService.listUsers()` and populates signals (`loading`, `error`, `users`). |
|
||||
| 7 | `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` (credentials included); typed as `AdminUser[]`. |
|
||||
| 8 | `backend/src/modules/admin/admin.controller.ts` | `AdminController.list` returns `AdminService.listUsers({limit, cursor, role})`. |
|
||||
|
||||
# Notes
|
||||
|
||||
* The shell deliberately keeps `HomeComponent` as a thin layout owner —
|
||||
child routes (currently just `/admin`) render into its
|
||||
`<router-outlet>`. New admin sub-pages can be added as additional
|
||||
children without changing the shell.
|
||||
* The guard decision function (`decideAdminGuard`) is a pure module so
|
||||
it is straightforward to unit-test without Angular DI. See
|
||||
`tests/frontend/admin-shell.spec.ts` and `tests/frontend/admin-navigation.spec.ts`.
|
||||
|
||||
# See also
|
||||
|
||||
- [First-Run Bootstrap](/guides/bootstrap.md)
|
||||
- [Frontend Structure](/architecture/frontend-structure.md)
|
||||
- [REST API Overview](/api/rest-overview.md)
|
||||
- [Admin Endpoints](/api/admin.md)
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
timestamp: 2026-07-21T15:05:00Z
|
||||
---
|
||||
|
||||
# When this flow runs
|
||||
@@ -46,7 +46,7 @@ bypassed.
|
||||
|
||||
* On success the modal disappears, the app stores the access token via
|
||||
`AuthService.setSession`, calls `BootstrapService.markInitialized()`,
|
||||
and navigates to `/challenges`.
|
||||
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.
|
||||
@@ -84,7 +84,7 @@ The modal **cannot** be dismissed by the user:
|
||||
| 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`. |
|
||||
| 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
|
||||
@@ -94,8 +94,10 @@ Once the first admin exists, `/api/v1/bootstrap` returns
|
||||
|
||||
* Skips `/bootstrap` entirely.
|
||||
* Routes unauthenticated users to `/login`.
|
||||
* Routes authenticated users to `/` (Home) which itself redirects to
|
||||
`/challenges` once admin features are wired.
|
||||
* 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user