11 KiB
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. |
|
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)
- Ensure the instance is initialized
(
GET /api/v1/bootstrap→initialized: true). If not, follow First-Run Bootstrap. - Sign in via
/login(admin or player). - 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
- On
ngOnInitHomeComponentopensnew EventSource('/api/v1/events/status', { withCredentials: true })viaEventStatusStore.start(...). - The first frame is the current state; subsequent frames arrive on transitions and on the server's 60-second tick.
- The store runs a local 1-second tick so the displayed countdown advances between server pushes, using the stored clock-skew anchor.
- On
ngOnDestroy(and the store'sDestroyRef.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
- Configure
eventStartUtcso the event is currently incountdown: the LED should show the countdown colour, the countdown text should read00:00:NNand decrement each second. - Move
eventStartUtcto a past timestamp andeventEndUtcto a future timestamp. The LED switches to therunningclass and the countdown now readsDD:HH:mmto end. - Move
eventEndUtcto a past timestamp. The LED switches to thestoppedclass and the countdown text readsEvent ended. - Clear one of the two
settingrows. The LED switches tounconfiguredand the countdown text is empty.
Change-password modal (happy path)
- Sign in as a player (or admin).
- Open the user menu and click Change password.
- Enter the current password, a new password that meets the policy (length + mixed-case), and the same new password in confirmation.
- Click OK → the modal closes. Other tabs that were signed in to
the same account will be forced to
/loginbecause all refresh tokens for the user were revoked server-side. - See Change Password Guide for the error branches (wrong-old / mismatched / weak / unauth).
Admin-only entries
- Sign in as an admin and confirm the Admin area entry appears in
the user menu. Clicking it navigates to
/adminand renders the existingAdminUsersComponentinside the shell body. - Sign in as a player and confirm the Admin area entry is hidden.
Navigating directly to
/adminis intercepted byadminGuardand redirects back to/.
Notes
- The shell deliberately keeps
HomeComponentas 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 infrontend/src/app/features/home/home.shell.tsand are exported so they can be unit-tested without Angular DI. Seetests/frontend/shell-active-section.spec.tsandtests/frontend/admin-shell.spec.ts. - The change-password modal renders
mode='self'from the shell; themode='admin-reset'variant (whereoldPasswordis hidden) is wired by the future admin-reset Job.