Files
HIPCTF2/.kilo/plans/841.md
T
2026-07-21 15:07:42 +00:00

11 KiB

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

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:

{
  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).