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:105already callsthis.router.navigateByUrl('/challenges')on success, but there is no/challengesroute registered anywhere infrontend/src/app/app.routes.ts(onlybootstrap,login, and''→HomeComponent).frontend/src/app/features/home/home.component.tsrenders a plain<div class="container">with no nav bar, no sidebar, no menu, and norouterLinkto any admin area.frontend/src/app/app.component.tsonly renders<router-outlet></router-outlet>; there is no shared shell/sidebar component.frontend/src/app/app.routes.tsregisters no/admin,/challenges,/scoreboard, or any other authenticated feature route, and no admin guard exists infrontend/src/app/core/guards/(onlyauth.guard.ts).- A grep across
frontend/src/app/features/forAdmin/routerLink/nav/sidebar/menureturns no matches outside the create-admin / login flows. - The backend admin surface exists (
backend/src/modules/admin/admin.controller.tsexposing/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(), functionalCanActivateFnguards. No NgModules. Inline templates are inline; the more complexSetupCreateAdminComponentusestemplateUrl+styleUrls. - Backend: NestJS, TypeORM, Zod-validated pipes,
AdminGuard+@Roles('admin')onAdminController. REST surface at/api/v1/.... - Test style: Jest via
tests/jest.config.jswith two projects (backendNode,frontendjsdom). Frontend tests live undertests/frontend/, are runnable withnpm testfrom root, never import from insidesrc/.
- Frontend: Angular 20 standalone components, signals (
- Data Layer: TypeORM with SQLite (file DB at
DATABASE_PATH,:memory:in tests). No schema change is required for this job —user.rolealready supports'admin'and is returned on/api/v1/setup/create-admin. - Test Framework & Structure: Jest. Tests MUST live under
tests/(never insidefrontend/src/). Run withnpm testfrom repo root. - Required Tools & Dependencies: None new. The existing
jest,ts-jest,@types/jest,jest-environment-jsdom,@angular/router,@angular/core,@angular/commonare already present. Nosetup.shchange 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 (/adminis the natural target since this user is the freshly-created admin).
To Create
frontend/src/app/core/guards/admin.guard.ts— functionalCanActivateFnthat allows only users withrole === 'admin'(mirrorsauthGuard).frontend/src/app/core/services/admin.service.ts— thin wrapper over the existing/api/v1/admin/usersREST 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 callingAdminService.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 aplayerrole 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' }>>callsGET /api/v1/admin/usersviaHttpClientwithwithCredentials: 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, callsAdminService.listUsers()and stores the result. - Renders a heading ("Admin area") plus a list of users with
usernameandrole. - Uses inline template (no extra
.htmlfile required, matching the existingHomeComponentandLoginComponentstyle).
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/adminchild route renders.
- A header bar with the page title (
- 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/challengestarget is dead (no route registered)./adminis 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
TestBedwithprovideRouter([])and stubAuthService/BootstrapServicewith 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 usernamefirstadminand 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+BootstrapServiceand an injectedRouter(fromTestBed.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.
- Uninitialized → returns
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/adminis 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).