179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
# 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 `<div class="container">` with no nav bar, no sidebar, no menu, and no `routerLink` to any admin area.
|
|
- `frontend/src/app/app.component.ts` only renders `<router-outlet></router-outlet>`; 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<Array<{ id: string; username: string; role: 'admin' | 'player' }>>` 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<string|null>(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 `<router-outlet></router-outlet>` 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 <b>{{ username }}</b> ({{ role }})`).
|
|
- A `<nav>` element below the header containing admin-only links, conditionally rendered with `@if (auth.currentUser()?.role === 'admin')`. Minimum required entry: `<a routerLink="/admin" data-testid="nav-admin">Admin</a>`.
|
|
- The existing welcome / Sign-in block for unauthenticated viewers stays.
|
|
- `<router-outlet />` below the nav so the new `/admin` child route renders.
|
|
- Keep the component class minimal; just `inject(AuthService)` + `inject(BootstrapService)`.
|
|
|
|
**3.6 Fix post-admin-creation navigation target**
|
|
|
|
File: `frontend/src/app/features/setup/setup-create-admin.component.ts:105`
|
|
|
|
- Change `await this.router.navigateByUrl('/challenges');` → `await this.router.navigateByUrl('/admin');`. The current `/challenges` target is dead (no route registered). `/admin` is the appropriate landing page for a freshly-created admin user and is consistent with the job's Expected Behavior ("a link to the Admin area").
|
|
- The bootstrap flow doc (`docs/guides/bootstrap.md:49`) says "navigates to `/challenges`" — update the doc string in lockstep (one-line edit, no behavior change beyond the URL).
|
|
|
|
### 4. `/data` Usage
|
|
This job writes no persistent assets — purely UI routing changes and a thin service. No `/data` directory writes required.
|
|
|
|
## 4. Test Strategy
|
|
|
|
Tests live in the existing `tests/frontend/` folder and run via `npm test` (which invokes `jest --config tests/jest.config.js`). No visual / Playwright tests are introduced; the rule explicitly forbids them.
|
|
|
|
### 4.1 `tests/frontend/admin-shell.spec.ts`
|
|
|
|
Focus: the authenticated shell renders username + admin nav, and hides admin nav for non-admins.
|
|
|
|
- Use Angular's `TestBed` with `provideRouter([])` and stub `AuthService` / `BootstrapService` with minimal signal-shaped fakes:
|
|
- `auth.currentUser = signal({ id: 'u1', username: 'firstadmin', role: 'admin' })`
|
|
- `auth.isAuthenticated = computed(() => true)`
|
|
- `bootstrap.initialized = signal(true)`, `bootstrap.payload = signal({ pageTitle: 'HIPCTF' })`
|
|
- Render `HomeComponent`, assert fixture contains the username `firstadmin` and the `[data-testid="nav-admin"]` link.
|
|
- Re-render with `role: 'player'` and assert the admin nav link is **not** present.
|
|
|
|
Mocking strategy: do **not** mock `HttpClient`. `HomeComponent` template is static (no HTTP); no network is touched. `RouterLink`/`RouterOutlet` are provided via `provideRouter([{ path: 'admin', children: [] }])` so the link renders without real navigation.
|
|
|
|
### 4.2 `tests/frontend/admin-navigation.spec.ts`
|
|
|
|
Focus: `adminGuard` redirect logic.
|
|
|
|
- Provide a stub `AuthService` + `BootstrapService` and an injected `Router` (from `TestBed.inject(Router)`).
|
|
- Use `TestBed.runInInjectionContext(() => adminGuard({}, {} as any))` to invoke the guard.
|
|
- Cases:
|
|
- Uninitialized → returns `UrlTree('/bootstrap')`.
|
|
- Unauthenticated → returns `UrlTree('/login')`.
|
|
- Authenticated non-admin → returns `UrlTree('/')`.
|
|
- Authenticated admin → returns `true`.
|
|
|
|
Mocking strategy: pure DI stubs; no HTTP, no DOM navigation.
|
|
|
|
### 4.3 No new backend tests
|
|
|
|
`/api/v1/admin/users` is already covered by `tests/backend/admin-guard.spec.ts` (200 for admin, 403 for player, 401 unauth). This job introduces no backend surface area.
|
|
|
|
## 5. Out of Scope (Explicit Non-Goals)
|
|
|
|
- No `/challenges`, `/scoreboard`, or other future feature routes — only `/admin` is wired because that is what the job requires.
|
|
- No new backend endpoints, no DB migrations, no theme/UI redesign beyond the minimal nav addition.
|
|
- No Playwright/visual tests (explicitly forbidden by the test rule).
|