From a3ea62640d2e81a1e80269ab893f9737b7cdfa0e Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Tue, 21 Jul 2026 15:07:41 +0000 Subject: [PATCH 1/2] docs: update documentation to OKF v0.1 format --- docs/architecture/frontend-structure.md | 26 +++++- docs/architecture/key-files.md | 13 ++- docs/architecture/overview.md | 4 +- docs/guides/admin-shell.md | 100 ++++++++++++++++++++++++ docs/guides/bootstrap.md | 12 +-- docs/index.md | 2 + 6 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 docs/guides/admin-shell.md diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md index 5b3e3d8..3b70e5b 100644 --- a/docs/architecture/frontend-structure.md +++ b/docs/architecture/frontend-structure.md @@ -3,7 +3,7 @@ type: architecture title: Frontend Structure description: Angular routes, components, services, guards, and interceptors. tags: [architecture, frontend, angular] -timestamp: 2026-07-21T14:43:00Z +timestamp: 2026-07-21T15:05:00Z --- # Routes @@ -14,7 +14,8 @@ Routes live in `frontend/src/app/app.routes.ts`: |--------------|----------------------------------|---------------|----------------------------------------| | `/bootstrap` | `SetupCreateAdminComponent` | — | Rendered only when `initialized === false`. Non-dismissible modal overlay. | | `/login` | `LoginComponent` | — | Standard login form. | -| `/` | `HomeComponent` | `authGuard` | Requires auth + initialized. | +| `/` | `HomeComponent` (shell) | `authGuard` | Requires auth + initialized. Hosts a `` for child routes. | +| `/admin` | `AdminUsersComponent` | `adminGuard` | Child of `/`; requires `role === 'admin'`. | | `**` | Redirect to `/` | — | Wildcard fallback. | # Components @@ -25,7 +26,8 @@ All components are standalone (no NgModules). Each component imports | Component | Path | Purpose | |------------------------|-----------------------------------------------------------------|-------------------------------------------------| | `AppComponent` | `frontend/src/app/app.component.ts` | Root; renders `` and calls `BootstrapService.load()`. | -| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Landing page shown after auth. | +| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Authenticated shell with header, conditional admin nav, and `` for children. | +| `AdminUsersComponent` | `frontend/src/app/features/admin/admin-users.component.ts` | Admin-only user list rendered inside the home shell. | | `LoginComponent` | `frontend/src/app/features/auth/login.component.ts` | Username/password form. | | `SetupCreateAdminComponent` | `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin bootstrap modal (non-dismissible, typed reactive forms). | @@ -35,12 +37,15 @@ All components are standalone (no NgModules). Each component imports |---------------------|---------------------------------------------------------------|-----------------------------------------------------------| | `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Signal-backed access token + current user. | | `BootstrapService` | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap`, applies theme tokens to CSS. | +| `AdminService` | `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` (typed `AdminUser[]`). | # Guards and interceptors | Symbol | Path | Purpose | |---------------------|---------------------------------------------------------------|-----------------------------------------------------------| | `authGuard` | `frontend/src/app/core/guards/auth.guard.ts` | Redirects to `/bootstrap` when uninitialized, `/login` when not authenticated. | +| `adminGuard` | `frontend/src/app/core/guards/admin.guard.ts` | Delegates to `decideAdminGuard` (pure). Redirects to `/bootstrap`, `/login`, or `/` based on state; allows only admins. | +| `decideAdminGuard` | `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure decision function: returns `{kind:'allow'}` or `{kind:'redirect', path}`. | | `authInterceptor` | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches `Authorization: Bearer ` header. | | `csrfInterceptor` | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` header on POST/PUT/PATCH/DELETE. | @@ -62,8 +67,23 @@ Both interceptors are wired in `frontend/src/main.ts` via 5. The `authGuard` reads `BootstrapService.initialized()` and either allows navigation or redirects to `/bootstrap`. +# Home shell flow + +After successful auth, `HomeComponent` renders a thin shell layout: + +1. The header (`shell-header`) shows the page title and signed-in + identity. +2. When `shouldShowAdminNav({isAuthenticated, role})` returns `true` + (see `frontend/src/app/features/home/home.shell.ts`) the admin nav + link (`data-testid="nav-admin"`) is rendered. +3. Child routes (e.g. `/admin` → `AdminUsersComponent`) render into the + shell's ``. +4. `AdminUsersComponent` calls `AdminService.listUsers()` on init; + loading, error, and the rendered list are exposed as Angular signals. + # See also - [System Overview](/architecture/overview.md) - [Backend Module Map](/architecture/backend-modules.md) - [First-Run Bootstrap](/guides/bootstrap.md) +- [Admin Shell & User Management](/guides/admin-shell.md) diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index 26feb6a..ab34784 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -3,7 +3,7 @@ type: architecture title: Key Files Index description: One-line responsibility for every important source file in the repository. tags: [architecture, index, key-files] -timestamp: 2026-07-21T14:43:00Z +timestamp: 2026-07-21T15:05:00Z --- # Backend @@ -107,17 +107,22 @@ timestamp: 2026-07-21T14:43:00Z | `frontend/src/index.html` | Root HTML shell. | | `frontend/src/styles.css` | Global styles consuming theme CSS custom properties. | | `frontend/src/app/app.component.ts` | Root component; triggers bootstrap on init. | -| `frontend/src/app/app.routes.ts` | Angular route table. | +| `frontend/src/app/app.routes.ts` | Angular route table (declares `/` shell with `adminGuard`-gated `/admin` child). | | `frontend/src/app/core/services/auth.service.ts` | Signal store for access token + current user. | | `frontend/src/app/core/services/bootstrap.service.ts` | Fetches `/api/v1/bootstrap` and applies theme tokens to CSS. | +| `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` returning `AdminUser[]`. | | `frontend/src/app/core/guards/auth.guard.ts` | `CanActivateFn` checking init + auth. | +| `frontend/src/app/core/guards/admin.guard.ts` | `CanActivateFn` for admin-only routes; delegates to `decideAdminGuard`. | +| `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure decision function for the admin guard (returns `allow` / `redirect`). | | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Attaches Bearer header. | | `frontend/src/app/core/interceptors/csrf.interceptor.ts` | Attaches `X-CSRF-Token` on unsafe methods. | | `frontend/src/app/features/auth/login.component.ts` | Login form. | +| `frontend/src/app/features/admin/admin-users.component.ts` | Admin-only user list (signals: `loading`, `error`, `users`). | | `frontend/src/app/features/setup/setup-create-admin.component.ts` | First-admin modal overlay (non-dismissible; typed reactive form). | | `frontend/src/app/features/setup/setup-create-admin.service.ts` | POSTs to `/api/v1/setup/create-admin` and tags the result with the error code. | | `frontend/src/app/features/setup/setup-create-admin.validators.ts` | Standalone `passwordMatchValidator` group-level reactive form validator. | -| `frontend/src/app/features/home/home.component.ts` | Landing page after auth. | +| `frontend/src/app/features/home/home.component.ts` | Authenticated shell: header + conditional admin nav + ``. | +| `frontend/src/app/features/home/home.shell.ts` | Pure `shouldShowAdminNav({isAuthenticated, role})` predicate (unit-tested). | # Tests @@ -125,6 +130,8 @@ timestamp: 2026-07-21T14:43:00Z |------------------------------------------------|--------------------------------------------------------------------------------| | `tests/backend/*.spec.ts` (18 suites) | Jest tests for backend modules, CSRF, OpenAPI 3.1, migrations, rate limits. | | `tests/frontend/theme.spec.ts` | Jest test for theme token application. | +| `tests/frontend/admin-shell.spec.ts` | Unit tests for `shouldShowAdminNav` predicate. | +| `tests/frontend/admin-navigation.spec.ts` | Tests for admin guard decision + nav-link rendering. | | `tests/backend/csrf-client.ts` | Test helper for CSRF flow. | | `tests/backend/db-helper.ts` | Test helper for spinning up an isolated DB. | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index f28b399..b1d1439 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -3,7 +3,7 @@ type: architecture title: System Overview description: High-level layout of the HIPCTF monorepo (NestJS API + Angular SPA) and how they communicate. tags: [architecture, backend, frontend, overview] -timestamp: 2026-07-21T14:18:00Z +timestamp: 2026-07-21T15:05:00Z --- # Overview @@ -73,7 +73,7 @@ Browser ──HTTPS──▶ NestJS process (PORT, default 3000) | Concern | Backend | Frontend | |----------------|------------------------------------------------------|---------------------------------------------| | Authentication | `JwtAuthGuard` (global) + `AuthGuard('jwt')` strategy | `AuthService` signal + `authInterceptor` | -| Authorization | `AdminGuard` + `@Roles('admin')` decorator | `authGuard` route guard | +| Authorization | `AdminGuard` + `@Roles('admin')` decorator | `authGuard` for `/`, `adminGuard` for `/admin` (delegates to pure `decideAdminGuard`) | | CSRF | `CsrfMiddleware` (skips `/api/v1/auth/login` and `/api/v1/auth/register-first-admin`) | `csrfInterceptor` reads `csrf` cookie | | Errors | `ApiError` → `GlobalExceptionFilter` returns `{code, message, details, path, timestamp}` | Components display `error?.error?.message` | | Config | `envSchema` (zod) + `validateEnv` | None (consumed via bootstrap) | diff --git a/docs/guides/admin-shell.md b/docs/guides/admin-shell.md new file mode 100644 index 0000000..d539dff --- /dev/null +++ b/docs/guides/admin-shell.md @@ -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 `
  • ` 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 `` 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 `
  • ` 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 ``. | +| 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 + ``. 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) \ No newline at end of file diff --git a/docs/guides/bootstrap.md b/docs/guides/bootstrap.md index 2e5b4d8..12c2cba 100644 --- a/docs/guides/bootstrap.md +++ b/docs/guides/bootstrap.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 diff --git a/docs/index.md b/docs/index.md index 6a007f0..490aa84 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,8 @@ they need. * [First-Run Bootstrap](/guides/bootstrap.md) - How a fresh instance is initialized by the very first admin via the non-dismissible modal. +* [Admin Shell & User Management](/guides/admin-shell.md) - How admins + navigate the post-login shell and reach the user-management area. * [Theming](/guides/theming.md) - How themes are loaded and applied. * [Event Window](/guides/event-window.md) - How the event window and live countdown work. -- 2.52.0 From b315f8f7fc0d3d7a362ffbe5328552fd7eab066b Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Tue, 21 Jul 2026 15:07:42 +0000 Subject: [PATCH 2/2] feat: First-Start Create Admin Modal 1.00 --- .kilo/plans/821-followup-build-fix.md | 86 --------- .kilo/plans/841.md | 178 ++++++++++++++++++ frontend/src/app/app.routes.ts | 11 +- .../app/core/guards/admin.guard.decision.ts | 14 ++ frontend/src/app/core/guards/admin.guard.ts | 23 +++ .../src/app/core/services/admin.service.ts | 20 ++ .../features/admin/admin-users.component.ts | 48 +++++ .../src/app/features/home/home.component.ts | 43 +++-- frontend/src/app/features/home/home.shell.ts | 6 + .../setup/setup-create-admin.component.ts | 2 +- tests/frontend/admin-navigation.spec.ts | 27 +++ tests/frontend/admin-shell.spec.ts | 15 ++ 12 files changed, 374 insertions(+), 99 deletions(-) delete mode 100644 .kilo/plans/821-followup-build-fix.md create mode 100644 .kilo/plans/841.md create mode 100644 frontend/src/app/core/guards/admin.guard.decision.ts create mode 100644 frontend/src/app/core/guards/admin.guard.ts create mode 100644 frontend/src/app/core/services/admin.service.ts create mode 100644 frontend/src/app/features/admin/admin-users.component.ts create mode 100644 frontend/src/app/features/home/home.shell.ts create mode 100644 tests/frontend/admin-navigation.spec.ts create mode 100644 tests/frontend/admin-shell.spec.ts diff --git a/.kilo/plans/821-followup-build-fix.md b/.kilo/plans/821-followup-build-fix.md deleted file mode 100644 index 2ca06dd..0000000 --- a/.kilo/plans/821-followup-build-fix.md +++ /dev/null @@ -1,86 +0,0 @@ -# Implementation Plan: Fix Angular build error — Property 'code' does not exist on type 'CreateAdminResult' - -## 1. Architectural Reconnaissance - -- **Codebase style & conventions:** Angular 17 standalone components, signals, typed reactive forms. `frontend/tsconfig.json` uses `strict: false` and `strictTemplates: false`. The project still uses `@angular/compiler` (ngc) which performs its own type checking on top of TS narrowing rules. -- **Data Layer:** N/A — this is a frontend-only type-narrowing fix. -- **Test Framework & Structure:** `tests/jest.config.js`. Tests live under `tests/frontend/`. All compile via `npm test`. -- **Required Tools & Dependencies:** No new dependencies. The fix is purely TypeScript / component-internal. - -## 2. Impacted Files - -- **To Modify:** `/repo/frontend/src/app/features/setup/setup-create-admin.component.ts` — fix the type-narrowing that Angular's compiler refuses to accept. -- **No new files, no test changes, no docs changes.** - -## 3. Proposed Changes - -### Root cause - -In `setup-create-admin.component.ts:88-117` the `submit()` method awaits the service, then reads `result.ok`. After the `if (result.ok) { ... return; }` branch, TS should narrow `result` to `CreateAdminFailure` (which has `code`). However Angular's compiler (used by `ng build`) reports: - -``` -TS2339: Property 'code' does not exist on type 'CreateAdminResult'. - Property 'code' does not exist on type 'CreateAdminSuccess'. -``` - -at the two reads `result.code` on lines 109 and 116. With `strict: false` + `strictTemplates: false`, narrowing across an awaited union *should* succeed, but Angular's compiler's narrowing model is stricter in some code paths (notably after `await` of an external service) and conservatively widens back to the full union. The error message proves it: type is `CreateAdminResult`, not `CreateAdminFailure`. - -### Minimal fix - -Replace the discriminated-union narrowing with an explicit type guard in the service and an `else` branch in the component. This avoids relying on the compiler's narrowing at all. - -### Step-by-step - -1. **Add a type predicate to the service** (`setup-create-admin.service.ts`): - - Add `export function isCreateAdminFailure(r: CreateAdminResult): r is CreateAdminFailure { return !r.ok; }`. - - No behaviour change. - -2. **Refactor `submit()`** in `setup-create-admin.component.ts`: - - Replace the `if (result.ok) { ... return; } ... if (result.code === ...) ...` sequence with an `if (result.ok) { ... return; } const failure = result;` pattern. Because `result.ok` is `true | false`, the negation narrows the local copy identically. The safest cross-compiler approach is: - ```ts - if (result.ok) { - this.auth.setSession(result.accessToken, result.user); - this.bootstrap.markInitialized(); - await this.router.navigateByUrl('/challenges'); - return; - } - const failure: CreateAdminFailure = result; // explicit local of narrowed type - if (failure.code === 'USERNAME_TAKEN') { - this.serverError.set('Username already exists'); - this.serverErrorCode.set('USERNAME_TAKEN'); - return; - } - this.serverError.set('Setup failed, please retry'); - this.serverErrorCode.set(failure.code); - ``` - - Import `CreateAdminFailure` from `./setup-create-admin.service`. - - The cast `const failure: CreateAdminFailure = result` after the `return` is safe (TS will assert assignability; if the union ever changes it becomes a compile error here rather than two ambiguous ones). - - Alternatively, an explicit cast: `const failure = result as CreateAdminFailure;`. Either is acceptable; the `as`-cast is one token shorter and more idiomatic. Choose: - - ```ts - const failure = result as CreateAdminFailure; - ``` - -3. **No HTML/CSS/service file changes.** - -## 4. Test Strategy - -- **Target Unit Test File:** no changes. The existing `tests/frontend/setup-create-admin.spec.ts` already covers the validator and rules; the build error is a compile-time-only issue that the existing tests do not exercise. -- **Mocking Strategy:** N/A. -- **Verification:** - 1. `npm --workspace frontend run build` must complete without `TS2339` errors. - 2. `npm test` must still pass (20 suites, 97 tests). -- **Do not** touch any test file. The fix is internal to the component. - -## 5. Risk / Non-Goals - -- **No behaviour change.** The runtime control flow is identical; the change only adds an explicit local that the compiler can definitively narrow. -- **No public API change.** `CreateAdminResult`, `CreateAdminSuccess`, `CreateAdminFailure` shapes remain as defined. -- **No docs change required** — implementation detail only. - -## 6. Execution Checklist - -1. Edit `setup-create-admin.component.ts`: import `CreateAdminFailure`, refactor lines 102-116 to use `const failure = result as CreateAdminFailure;` after the success branch returns. -2. Run `npm --workspace frontend run build` and confirm 0 errors. -3. Run `npm test` and confirm `Test Suites: 20 passed, 20 total`. diff --git a/.kilo/plans/841.md b/.kilo/plans/841.md new file mode 100644 index 0000000..2e612ec --- /dev/null +++ b/.kilo/plans/841.md @@ -0,0 +1,178 @@ +# Implementation Plan: Job 841 — First-Start Create Admin Modal: Authenticated Shell & Admin Navigation + +## 0. Already-Implemented Check + +The job is **NOT** fully implemented. Specifically: + +- `frontend/src/app/features/setup/setup-create-admin.component.ts:105` already calls `this.router.navigateByUrl('/challenges')` on success, but there is **no `/challenges` route registered** anywhere in `frontend/src/app/app.routes.ts` (only `bootstrap`, `login`, and `''` → `HomeComponent`). +- `frontend/src/app/features/home/home.component.ts` renders a plain `
    ` with no nav bar, no sidebar, no menu, and no `routerLink` to any admin area. +- `frontend/src/app/app.component.ts` only renders ``; there is no shared shell/sidebar component. +- `frontend/src/app/app.routes.ts` registers **no** `/admin`, `/challenges`, `/scoreboard`, or any other authenticated feature route, and **no admin guard** exists in `frontend/src/app/core/guards/` (only `auth.guard.ts`). +- A grep across `frontend/src/app/features/` for `Admin` / `routerLink` / `nav` / `sidebar` / `menu` returns **no** matches outside the create-admin / login flows. +- The backend admin surface exists (`backend/src/modules/admin/admin.controller.ts` exposing `/api/v1/admin/users`), but the frontend never exposes a link to it. + +Result: after a successful `POST /api/v1/setup/create-admin`, the operator is dropped on `HomeComponent` which shows only "HIPCTF" + the username, with **no way to reach any Admin functionality**. + +The remediation below is the minimal, focused change required to satisfy the job's Expected Behavior: authenticated shell must display the created username **and** provide admin-only navigation entries (including a link to the Admin area). + +## 1. Architectural Reconnaissance + +- **Codebase style & conventions:** + - Frontend: Angular 20 standalone components, signals (`signal`/`computed`), `inject()`, functional `CanActivateFn` guards. No NgModules. Inline templates are inline; the more complex `SetupCreateAdminComponent` uses `templateUrl` + `styleUrls`. + - Backend: NestJS, TypeORM, Zod-validated pipes, `AdminGuard` + `@Roles('admin')` on `AdminController`. REST surface at `/api/v1/...`. + - Test style: Jest via `tests/jest.config.js` with two projects (`backend` Node, `frontend` jsdom). Frontend tests live under `tests/frontend/`, are runnable with `npm test` from root, never import from inside `src/`. +- **Data Layer:** TypeORM with SQLite (file DB at `DATABASE_PATH`, `:memory:` in tests). No schema change is required for this job — `user.role` already supports `'admin'` and is returned on `/api/v1/setup/create-admin`. +- **Test Framework & Structure:** Jest. Tests **MUST** live under `tests/` (never inside `frontend/src/`). Run with `npm test` from repo root. +- **Required Tools & Dependencies:** None new. The existing `jest`, `ts-jest`, `@types/jest`, `jest-environment-jsdom`, `@angular/router`, `@angular/core`, `@angular/common` are already present. No `setup.sh` change is required. + +## 2. Impacted Files + +### To Modify + +- `frontend/src/app/app.routes.ts` — register the new admin-protected child route(s) and the admin guard. +- `frontend/src/app/features/home/home.component.ts` — replace plain container with a small shell that renders username and admin-only nav links (or host the new shell component). +- `frontend/src/app/features/setup/setup-create-admin.component.ts` — fix the post-success navigation target so the operator lands somewhere valid (`/admin` is the natural target since this user is the freshly-created admin). + +### To Create + +- `frontend/src/app/core/guards/admin.guard.ts` — functional `CanActivateFn` that allows only users with `role === 'admin'` (mirrors `authGuard`). +- `frontend/src/app/core/services/admin.service.ts` — thin wrapper over the existing `/api/v1/admin/users` REST endpoint (so the Admin area page can list users immediately, exercising the admin nav link end-to-end). +- `frontend/src/app/features/admin/admin-users.component.ts` — minimal standalone "Admin area" landing page reachable via the new nav link. Renders a list of users by calling `AdminService.list()`. Lazy-loaded. +- `tests/frontend/admin-navigation.spec.ts` — focused unit tests for the new guard and the nav-link rendering logic. +- `tests/frontend/admin-shell.spec.ts` — focused unit test verifying the authenticated shell shows username + admin nav and that a `player` role does not see admin nav. + +## 3. Proposed Changes + +### 1. Schema / Data +No DB migration required. `user.role` already supports `'admin'`, and `CurrentUser.role` is already returned by `/api/v1/setup/create-admin` (see `frontend/src/app/features/setup/setup-create-admin.service.ts:15`). + +### 2. Backend Logic & APIs +No backend changes required. The existing `AdminController` (`backend/src/modules/admin/admin.controller.ts`) at `/api/v1/admin/users` is sufficient for the Admin area page to display something real. + +### 3. Frontend UI Integration + +**3.1 Add `adminGuard` (functional guard)** + +File: `frontend/src/app/core/guards/admin.guard.ts` + +```ts +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { BootstrapService } from '../services/bootstrap.service'; + +export const adminGuard: CanActivateFn = () => { + const auth = inject(AuthService); + const bootstrap = inject(BootstrapService); + const router = inject(Router); + + if (!bootstrap.initialized()) return router.createUrlTree(['/bootstrap']); + if (!auth.isAuthenticated()) return router.createUrlTree(['/login']); + if (auth.currentUser()?.role !== 'admin') return router.createUrlTree(['/']); + return true; +}; +``` + +Rationale: mirrors `authGuard` (`frontend/src/app/core/guards/auth.guard.ts`) and reuses the same signal-driven `AuthService` + `BootstrapService`. + +**3.2 Add `AdminService` (thin HTTP wrapper)** + +File: `frontend/src/app/core/services/admin.service.ts` + +- `listUsers(): Promise>` calls `GET /api/v1/admin/users` via `HttpClient` with `withCredentials: true`. +- Single-purpose; no caching layer needed for this job. + +**3.3 Add `AdminUsersComponent` (lazy-loaded Admin landing page)** + +File: `frontend/src/app/features/admin/admin-users.component.ts` + +- Standalone, `ChangeDetectionStrategy.OnPush`, signals (`users = signal<...>([])`, `loading = signal(false)`, `error = signal(null)`). +- On `ngOnInit`, calls `AdminService.listUsers()` and stores the result. +- Renders a heading ("Admin area") plus a list of users with `username` and `role`. +- Uses inline template (no extra `.html` file required, matching the existing `HomeComponent` and `LoginComponent` style). + +**3.4 Wire routes** + +File: `frontend/src/app/app.routes.ts` + +Add an authenticated `admin` child under the `''` route (still `authGuard`-protected) so the admin nav link resolves: + +```ts +{ + path: '', + loadComponent: () => import('./features/home/home.component').then(m => m.HomeComponent), + canActivate: [authGuard], + children: [ + { + path: 'admin', + canActivate: [adminGuard], + loadComponent: () => import('./features/admin/admin-users.component').then(m => m.AdminUsersComponent), + }, + ], +}, +``` + +And add `` inside `HomeComponent`'s template so the child route renders below the nav. + +**3.5 Authenticated shell with admin nav** + +File: `frontend/src/app/features/home/home.component.ts` + +- Replace the current plain template with a shell that always renders: + - A header bar with the page title (`bootstrap.payload()?.pageTitle ?? 'HIPCTF'`) on the left. + - On the right: the signed-in username + role (`Signed in as {{ username }} ({{ role }})`). + - A `