114 lines
6.9 KiB
Markdown
114 lines
6.9 KiB
Markdown
---
|
|
type: architecture
|
|
title: Frontend Structure
|
|
description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport.
|
|
tags: [architecture, frontend, angular, shell, sse]
|
|
timestamp: 2026-07-22T10:32:00Z
|
|
---
|
|
|
|
# Routes
|
|
|
|
Routes live in `frontend/src/app/app.routes.ts`:
|
|
|
|
| Path | Component | Guard | Notes |
|
|
|---|---|---|---|
|
|
| `/bootstrap` | `SetupCreateAdminComponent` | — | First-admin creation flow. |
|
|
| `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. |
|
|
| `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. |
|
|
| `/challenges` | `ChallengesPage` | inherited | Placeholder challenges page. |
|
|
| `/scoreboard` | `ScoreboardPage` | inherited | Placeholder scoreboard page. |
|
|
| `/blog` | `BlogPage` | inherited | Placeholder blog page. |
|
|
| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. |
|
|
|
|
# Components and services
|
|
|
|
| Symbol | Path | Responsibility |
|
|
|---|---|---|
|
|
| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Smart shell; owns navigation signals, user hydration, change-password flow, event-stream lifecycle, and peer-invalidation subscription. |
|
|
| `ShellHeaderComponent` | `frontend/src/app/features/shell/header/shell-header.component.ts` | Renders title, active section, event LED/countdown, and user menu; contains component-scoped LED styling. |
|
|
| `QuickTabsComponent` | `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Emits navigation events for Challenges, Scoreboard, and Blog. |
|
|
| `EventStatusStore` | `frontend/src/app/core/services/event-status.store.ts` | Applies event state frames and computes countdown text; re-exports the pure LED color mapping; wires an optional `onUnauthorized` callback for the new SSE `401`/`403` event. |
|
|
| `AuthenticatedEventSourceService` | `frontend/src/app/core/services/authenticated-event-source.service.ts` | Opens authenticated SSE over `fetch`, parsing streamed frames into `EventSourceLike` events; dispatches a separate `'unauthorized'` event for `401`/`403`. |
|
|
| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Stores the access token used by the authenticated SSE transport; owns the cross-tab invalidation `BroadcastChannel` + `storage`-event fallback and exposes `clearAndBroadcast` / `onPeerInvalidation`. |
|
|
| `UserStore` | `frontend/src/app/core/services/user.store.ts` | Hydrates the signed-in user and rank data. |
|
|
| `auth-session-events.pure.ts` | `frontend/src/app/core/services/auth-session-events.pure.ts` | Pure cross-tab message encoding/validation and the namespaced channel / storage-key constants. |
|
|
|
|
# Authenticated SSE wiring
|
|
|
|
`HomeComponent.ngOnInit()` calls `EventStatusStore.start()` with a factory for
|
|
`AuthenticatedEventSourceService.open('/api/v1/events/status')`. The transport:
|
|
|
|
1. Reads the access token from `AuthService`.
|
|
2. Sends `Accept: text/event-stream`, optional `Authorization: Bearer <token>`,
|
|
`credentials: 'include'`, and an `AbortSignal`.
|
|
3. Reads the response body with a stream reader and decodes UTF-8 chunks.
|
|
4. Splits complete SSE records on blank lines, joins repeated `data:` lines, and
|
|
dispatches `MessageEvent` objects.
|
|
5. Buffers records received before the message listener is attached.
|
|
6. Inspects the response status *before* the `ok`/`body` check; on `401` or
|
|
`403` dispatches a separate `'unauthorized'` event and returns without
|
|
reading the body. Generic transport failures still dispatch `'error'`.
|
|
7. Aborts and clears listeners when `close()` is called.
|
|
|
|
`EventStatusStore.start(createSource, onUnauthorized?)` parses each message as
|
|
`EventStatePayload`, updates its signals, runs a one-second local timer, and —
|
|
when `onUnauthorized` is provided — invokes it on `'unauthorized'` events.
|
|
`HomeComponent` passes an `onUnauthorized` callback that delegates to its
|
|
shared `handleSessionInvalidated('sse-unauthorized')` flow and stops the store
|
|
on destruction.
|
|
|
|
# Key integration files
|
|
|
|
| File | Responsibility |
|
|
|---|---|
|
|
| `frontend/src/app/app.routes.ts` | Registers shell and child routes. |
|
|
| `frontend/src/app/core/interceptors/auth.interceptor.ts` | Adds Bearer authentication to Angular HTTP requests. |
|
|
| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Adds Bearer authentication to the browser SSE request, which does not use the Angular HTTP interceptor chain. |
|
|
| `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. |
|
|
| `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. |
|
|
|
|
# Event-status pure helpers
|
|
|
|
`frontend/src/app/core/services/event-status.pure.ts` owns the small,
|
|
no-Angular types and pure functions consumed by the event status pipeline.
|
|
`EventStatusStore` re-exports them so application code can import from a
|
|
single path.
|
|
|
|
| Export | Purpose |
|
|
|---|---|
|
|
| `EventState` | Union `'running' \| 'countdown' \| 'stopped' \| 'unconfigured'`. |
|
|
| `EventStatePayload` | Server-pushed event window payload shape. |
|
|
| `EventSourceLike` | Minimal interface for any SSE transport the store can drive. |
|
|
| `formatDdHhMm(seconds)` | Pure `DD:HH:mm` formatter. |
|
|
| `deriveCountdownText(state, start, end)` | Pure mapping from event window state to countdown label. |
|
|
| `LED_COLOR_BY_STATE` | `Readonly<Record<EventState, string>>` mapping each state to its theme CSS variable (`var(--color-success)`, `--color-warning`, `--color-danger`, `--color-secondary`). |
|
|
| `ledBackgroundColor(state)` | Pure accessor returning the `background-color` value for the LED element. |
|
|
|
|
`ShellHeaderComponent` exposes a `computed` signal `ledColor()` that calls
|
|
`ledBackgroundColor(this.eventState())` and binds it to the LED via
|
|
`[style.background-color]`. The component's `.led` base rule is therefore
|
|
shape-only (size, radius, `inline-block`, vertical alignment); the visible
|
|
color always comes from `LED_COLOR_BY_STATE` and is guaranteed to be a
|
|
non-transparent theme token. The four `led-<state>` classes remain on the
|
|
element as selectors/test hooks but no longer own the color.
|
|
|
|
# Cross-tab session invalidation
|
|
|
|
`AuthService` (constructed once at root) installs a single
|
|
`BroadcastChannel('hipctf.auth.v1')` and a `localStorage` `storage`-event
|
|
fallback. The shape of the broadcast payload is defined in
|
|
`auth-session-events.pure.ts` and contains only `{ type, origin, ts }` — no
|
|
tokens or user data. `clearAndBroadcast()` is invoked from `logout()` and
|
|
from the SSE unauthorized callback; peer listeners clear their local state
|
|
without rebroadcasting. `HomeComponent` subscribes via
|
|
`auth.onPeerInvalidation(...)`, resets `UserStore`, closes any open shell
|
|
overlays, and navigates to `/login`. See
|
|
[Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md)
|
|
for the full contract and tester matrix.
|
|
|
|
# See also
|
|
|
|
- [Authenticated Shell](/guides/authenticated-shell.md)
|
|
- [Event Window](/guides/event-window.md)
|
|
- [System Overview](/architecture/overview.md)
|