Files
HIPCTF2/docs/guides/admin-shell.md
T

8.9 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Admin Shell & Side Navigation How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages.
guide
admin
shell
navigation
tester
2026-07-23T10:12:24Z

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 parent uses adminGuard; children inherit it.
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 + admin-general.controller.ts + admin-categories.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.
  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 open the username menu and choose Admin area, or navigate directly to /admin.
  5. The admin area renders inside the shell:
    • Aside (data-testid="admin-aside"): heading "Admin area" and a vertical side-nav (data-testid="admin-nav").
    • Body (data-testid="admin-body"): holds the <router-outlet /> that renders the active child page.

Side-nav entries

The side-nav is defined as a static ENTRIES array in frontend/src/app/features/admin/admin-shell.component.ts:

Label Path Enabled Renders
General /admin/general yes Admin — General Settings (default — /admin redirects here).
Challenges /admin/challenges yes Admin — Challenges — sortable list, search, Add/Edit modal, file staging, import/export.
Players /admin/players yes User management page.
Blog /admin/blog yes Blog Publishing and Reading — post list with draft/publish, edit, preview, and delete workflows.
System /admin/system no Greyed-out placeholder. No route registered.

Each <li> carries data-testid="admin-nav-{id}". Disabled entries have cursor: not-allowed and opacity: 0.45. Clicking a disabled entry is a no-op (go() returns early when !e.enabled).

The active entry is highlighted by [class.active] whenever the current URL starts with the entry's path. General is the default child (/admin/admin/general).

Expected behavior

User role Visiting /admin
Unauthenticated Redirected to /login by adminGuard.
Authenticated player Redirected to / by adminGuard.
Authenticated admin Page renders, defaults to /admin/general.
  • 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 in the shell header is hidden for non-admin users — there is no visible UI affordance to reach /admin without the role.
  • All admin HTTP requests use the existing authInterceptor + csrfInterceptor, so no extra plumbing is required.

Visual elements

Element Selector Purpose
Shell aside [data-testid="admin-aside"] Side-nav container. Heading "Admin area".
Side-nav list [data-testid="admin-nav"] <ul> of all five entries.
Side-nav entry [data-testid="admin-nav-{id}"] One <li> per entry (General / Challenges / Players / Blog / System).
Shell body [data-testid="admin-body"] Wraps <router-outlet /> for the active child page.

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 AdminShellComponent with its own child routes.
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/admin/admin-shell.component.ts Renders the aside + body; ENTRIES is the static side-nav list.
5 frontend/src/app/features/admin/general.component.ts AdminGeneralComponent (default /admin/general); embeds AdminCategoriesComponent.
6 frontend/src/app/features/admin/categories/categories.component.ts AdminCategoriesComponent (/admin/categories); also rendered inside the General page.
7 frontend/src/app/features/admin/blog/blog.component.ts AdminBlogComponent (/admin/blog); coordinates post listing, editing, publication, and deletion.
8 frontend/src/app/features/home/home.shell.ts shouldShowAdminNav({isAuthenticated, role}) predicate (exported and unit-tested).
9 frontend/src/app/features/home/home.component.ts Shell template renders header, conditional nav (*ngIf="showAdminNav()"), and <router-outlet>.
10 frontend/src/app/core/services/blog.service.ts Typed wrappers for public blog listing and /api/v1/admin/blog/posts CRUD.
11 backend/src/modules/blog/blog.module.ts Registers public and admin blog controllers/services with the blog repository.

Notes

  • The shell deliberately keeps AdminShellComponent as a thin layout owner — enabled child routes render into its <router-outlet>. New admin sub-pages require both an enabled ENTRIES item and a child route in app.routes.ts.
  • 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.
  • The Categories management page is intentionally embedded inside the General settings page (see Admin — General Settings) so an admin can adjust platform settings and challenge taxonomy in one place.

See also