Files
HIPCTF2/docs/guides/authenticated-shell.md
T
2026-07-21 22:26:44 +00:00

11 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Authenticated Shell How a signed-in user experiences the post-login shell — header, LED + countdown, quick tabs, user menu, and change-password modal.
guide
shell
header
sse
navigation
tester
2026-07-21T22:19:08Z

When this view is available

The authenticated shell is the post-login UI rendered by HomeComponent (frontend/src/app/features/home/home.component.ts). It is gated by:

Layer File Check
Client route frontend/src/app/app.routes.ts '' uses authGuard; child routes inherit it.
Server route backend/src/modules/system/system.controller.ts (@Sse('events/status')) JWT-required (global JwtAuthGuard). 401 on missing/expired token.

A user who is not signed in (or whose session has expired) is sent to /login by authGuard before the shell ever mounts; the SSE stream will not be opened until a valid JWT is present.

How to access (tester steps)

  1. Ensure the instance is initialized (GET /api/v1/bootstrapinitialized: true). If not, follow First-Run Bootstrap.
  2. Sign in via /login (admin or player).
  3. The browser lands on / (which redirects to /challenges) and renders the full authenticated shell.

Expected behavior

Header (ShellHeaderComponent)

Region / element Selector Expected
Page title (clickable) [data-testid="shell-title"] Shows the bootstrap pageTitle (default HIPCTF). Clicking it navigates to /challenges.
Active section label [data-testid="shell-active-section"] Challenges / Scoreboard / Blog / Admin based on the URL (deriveActiveSectionFromUrl).
Event status LED [data-testid="shell-led"] One of led-running, led-countdown, led-stopped, led-unconfigured classes. aria-label="Event status: <state>".
Countdown text [data-testid="shell-countdown"] DD:HH:mm while countdown / running, "Event ended" while stopped, empty while unconfigured. Updates every second.
User menu trigger [data-testid="user-menu-trigger"] Shows the current username + . Toggles the inline dropdown.
User menu — rank entry [data-testid="user-menu-rank"] "Rank: <r> - Points: <p>" (or empty while rank unknown / 0 points). Clicking navigates to /scoreboard.
User menu — change password [data-testid="user-menu-change-password"] Opens the change-password modal.
User menu — admin area [data-testid="user-menu-admin"] Visible only when canAccessAdmin() is true (admin role). Navigates to /admin.
User menu — logout [data-testid="user-menu-logout"] Logs out and navigates to /login. See the auth.md logout flow.

Quick tabs (QuickTabsComponent)

Region / element Selector Expected
Tab strip [data-testid="quick-tabs"] Three buttons in order: Challenges, Scoreboard, Blog.
Each tab button [data-testid="quick-tab-<id>"] class.active matches the current active() tab.
Tab click emits tabChange: {id} Parent navigates to /{id} (or /admin when id === 'admin').

The default child route is '' → challenges, so visiting / lands on the Challenges page with the Challenges tab highlighted.

Body

Region Notes
<router-outlet> Renders the active child route (ChallengesPage, ScoreboardPage, BlogPage, AdminUsersComponent).

Live event window

  1. On ngOnInit HomeComponent opens new EventSource('/api/v1/events/status', { withCredentials: true }) via EventStatusStore.start(...).
  2. The first frame is the current state; subsequent frames arrive on transitions and on the server's 60-second tick.
  3. The store runs a local 1-second tick so the displayed countdown advances between server pushes, using the stored clock-skew anchor.
  4. On ngOnDestroy (and the store's DestroyRef.onDestroy) the source is closed and the tick is cleared.

If the user logs out, the next refresh tick or route change tears the SSE down. If the SSE errors out, the store simply stops applying frames; the header LED retains its last class until a new connect attempt lands a frame.

Visual elements

Element Selector Purpose
Shell header bar [data-testid="shell-header"] Holds the title, active section, LED + countdown, and user menu.
Tab strip [data-testid="quick-tabs"] Top-level navigation between the three main pages.
Shell body .shell-body Hosts <router-outlet> for child routes.

Architecture map

Step Where What happens
1 frontend/src/app/app.routes.ts / is a parent route with authGuard; child routes challenges/scoreboard/blog/admin (admin uses adminGuard).
2 frontend/src/app/core/guards/auth.guard.ts authGuard waits on bootstrap + auth hydration and delegates to decideAuthRedirect.
3 frontend/src/app/features/home/home.component.ts Smart shell: instantiates EventStatusStore, UserStore, AuthService; manages the SSE lifecycle.
4 frontend/src/app/core/services/event-status.store.ts start(createSource) opens the SSE connection, parses each message frame into applyServerStatus(), and ticks secondsToStart/secondsToEnd locally.
5 frontend/src/app/core/services/user.store.ts loadMe() calls AuthService.me() (GET /api/v1/auth/me) → UsersRankService.rankOfUser for rank + points.
6 frontend/src/app/features/shell/header/shell-header.component.ts Dumb presentational header rendering title, active section, LED, countdown, and user menu.
7 frontend/src/app/features/shell/tabs/quick-tabs.component.ts Dumb tab strip. Click emits tabChange; parent navigates.
8 frontend/src/app/features/shell/change-password/change-password-modal.component.ts Dumb reactive-form modal opened via the changePasswordClick output. See Change Password Guide.
9 backend/src/modules/auth/auth.controller.ts GET /api/v1/auth/me returns {id, username, role, rank, points}.
10 backend/src/modules/users/users-rank.service.ts rankOfUser(manager, userId){rank, points} over the solve table.
11 backend/src/modules/system/system.controller.ts GET /api/v1/events/status (JWT) → SSE emitting the full EventStatePayload.

Tester flows

Live event window

  1. Configure eventStartUtc so the event is currently in countdown: the LED should show the countdown colour, the countdown text should read 00:00:NN and decrement each second.
  2. Move eventStartUtc to a past timestamp and eventEndUtc to a future timestamp. The LED switches to the running class and the countdown now reads DD:HH:mm to end.
  3. Move eventEndUtc to a past timestamp. The LED switches to the stopped class and the countdown text reads Event ended.
  4. Clear one of the two setting rows. The LED switches to unconfigured and the countdown text is empty.

Change-password modal (happy path)

  1. Sign in as a player (or admin).
  2. Open the user menu and click Change password.
  3. Enter the current password, a new password that meets the policy (length + mixed-case), and the same new password in confirmation.
  4. Click OK → the modal closes. Other tabs that were signed in to the same account will be forced to /login because all refresh tokens for the user were revoked server-side.
  5. See Change Password Guide for the error branches (wrong-old / mismatched / weak / unauth).

Admin-only entries

  1. Sign in as an admin and confirm the Admin area entry appears in the user menu. Clicking it navigates to /admin and renders the existing AdminUsersComponent inside the shell body.
  2. Sign in as a player and confirm the Admin area entry is hidden. Navigating directly to /admin is intercepted by adminGuard and redirects back to /.

Notes

  • The shell deliberately keeps HomeComponent as a smart container with dumb children (ShellHeaderComponent, QuickTabsComponent, ChangePasswordModalComponent) so they can be unit-tested in isolation.
  • The active-section predicate (deriveActiveSectionFromUrl) and the admin-nav predicate (shouldShowAdminNav) live in frontend/src/app/features/home/home.shell.ts and are exported so they can be unit-tested without Angular DI. See tests/frontend/shell-active-section.spec.ts and tests/frontend/admin-shell.spec.ts.
  • The change-password modal renders mode='self' from the shell; the mode='admin-reset' variant (where oldPassword is hidden) is wired by the future admin-reset Job.

See also