Files
HIPCTF2/docs/architecture/frontend-structure.md
T
2026-07-21 22:26:44 +00:00

15 KiB

type, title, description, tags, timestamp
type title description tags timestamp
architecture Frontend Structure Angular routes, components, services, guards, and interceptors.
architecture
frontend
angular
shell
sse
2026-07-21T22:19:08Z

Routes

Routes live in frontend/src/app/app.routes.ts:

Path Component Guard Notes
/bootstrap SetupCreateAdminComponent Lazy-loaded first-admin creation route. Non-dismissible modal overlay while initialized === false; successful creation navigates to /admin.
/login LandingComponent landingGuard Public landing page that hosts the login (and registration, when enabled) modal. The landingGuard redirects to /bootstrap until the instance is initialized and to / if the user is already authenticated.
/ HomeComponent (shell) authGuard Requires auth + initialized. Hosts the header (LED + countdown + user menu), quick-jump tabs (Challenges/Scoreboard/Blog), and a <router-outlet> for child routes. Default child route redirects to /challenges.
/challenges ChallengesPage inherits authGuard Placeholder child of /; renders an empty page until the challenges Job lands.
/scoreboard ScoreboardPage inherits authGuard Placeholder child of /; renders an empty page until the scoreboard Job lands.
/blog BlogPage inherits authGuard Placeholder child of /; renders an empty page until the blog Job lands.
/admin AdminUsersComponent adminGuard Child of /; requires role === 'admin'.
** Redirect to / Wildcard fallback.

Components

All components are standalone (no NgModules). Each component imports FormsModule directly and uses Angular signals for state.

Component Path Purpose
AppComponent frontend/src/app/app.component.ts Root; renders <router-outlet>, calls BootstrapService.load() then AuthService.restoreSession().
HomeComponent frontend/src/app/features/home/home.component.ts Smart authenticated shell. Hosts the ShellHeaderComponent, QuickTabsComponent, the ChangePasswordModalComponent, and the <router-outlet> for child routes. Owns the active section / active tab signals and the SSE lifecycle for EventStatusStore.
ShellHeaderComponent frontend/src/app/features/shell/header/shell-header.component.ts Dumb presentational header. Inputs: pageTitle, activeSection, eventState, countdownText, username, rankText, canAccessAdmin, userMenuOpen. Outputs: titleClick, usernameMenuToggle, rankClick, changePasswordClick, adminClick, logoutClick. Renders the LED (state class), DD:HH:mm countdown, and an inline user menu dropdown (Change password, optional Admin area, Logout).
QuickTabsComponent frontend/src/app/features/shell/tabs/quick-tabs.component.ts Dumb presentational tab strip. Inputs: tabs: ShellTab[], active: string. Output: tabChange: {id}.
ChangePasswordModalComponent frontend/src/app/features/shell/change-password/change-password-modal.component.ts Dumb reactive-form modal with three fields (oldPassword only in mode='self'), passwordMatchValidator group validation, policy hint from BootstrapService.passwordPolicy(), and errorMessage/submitting inputs. Emits submit(payload) and cancel(). Closes on Escape and on overlay click.
ChallengesPage frontend/src/app/features/challenges/challenges.page.ts Placeholder child route.
ScoreboardPage frontend/src/app/features/scoreboard/scoreboard.page.ts Placeholder child route.
BlogPage frontend/src/app/features/blog/blog.page.ts Placeholder child route.
AdminUsersComponent frontend/src/app/features/admin/admin-users.component.ts Admin-only user list rendered inside the home shell.
LandingComponent frontend/src/app/features/landing/landing.component.{ts,html,css} Public landing page: logo, page title, rendered welcomeMarkdown, "Login" button, blog post list, and the modal that contains both the login and registration forms (mode toggled by a single signal).
LandingService frontend/src/app/features/landing/landing.service.ts Fetches GET /api/v1/blog/posts, exposes posts/loading/error signals, refresh().
LoginModalService (pure helpers) frontend/src/app/features/landing/login-modal.service.ts Pure buildLoginFailureMessage that maps an API error envelope into user-facing copy (and extracts a retryAfterSeconds for RATE_LIMITED).
SetupCreateAdminComponent frontend/src/app/features/setup/setup-create-admin.component.ts First-admin bootstrap modal with typed reactive form, inline validation messages (via the field-error helpers), retry handling, session initialization, and redirect to /admin.
HomeShell (pure helpers) frontend/src/app/features/home/home.shell.ts Pure shouldShowAdminNav({isAuthenticated, role}) + deriveActiveSectionFromUrl(url) predicates (unit-tested).

Services

Service Path Purpose
AuthService frontend/src/app/core/services/auth.service.ts Signal-backed access token + current user; persists session to sessionStorage, exposes restoreSession() + waitUntilHydrated(), plus login() / register() / logout() / me() / changePassword() (each calls ensureCsrf() first). Returns discriminated LoginResult / RegisterResult / ChangePasswordResult for component-level error handling.
BootstrapService frontend/src/app/core/services/bootstrap.service.ts Fetches /api/v1/bootstrap, applies theme tokens to CSS, exposes a passwordPolicy computed signal derived from the bootstrap payload; load() / ready() deduplicate the in-flight fetch.
EventStatusStore frontend/src/app/core/services/event-status.store.ts Signal store for the live event window. Owns state/serverNowUtc/eventStartUtc/eventEndUtc, derives a countdownText computed signal (DD:HH:mm or "Event ended"), and exposes start(createSource) / stop() so HomeComponent can drive a 1-second tick that re-derives the seconds-to-start/end locally between SSE pushes.
EventStatusPure frontend/src/app/core/services/event-status.pure.ts Pure helpers consumed by the store and by tests: deriveCountdownText(state, sStart, sEnd), formatDdHhMm(totalSeconds), EventSourceLike interface, and the EventState/EventStatePayload types.
UserStore frontend/src/app/core/services/user.store.ts Signal store for the authenticated user (id/username/role/rank/points + loading/error). hydrateFromAuth() mirrors AuthService.currentUser(); loadMe() calls AuthService.me() and merges rank/points. Exposes rankText ("Rank: <r> - Points: <p>").
MarkdownService frontend/src/app/core/services/markdown.service.ts Wraps DomSanitizer.bypassSecurityTrustHtml around the pure renderMarkdownToHtml helper. Used to render welcomeMarkdown and blog post bodies.
AuthSessionStorage (helpers) frontend/src/app/core/services/auth.session-storage.ts Pure readStoredSession / writeStoredSession / clearStoredSession against sessionStorage (key hipctf.auth.v1).
SetupCreateAdminService frontend/src/app/features/setup/setup-create-admin.service.ts Preflights the CSRF cookie, calls POST /api/v1/setup/create-admin, and normalizes success/failure responses.
ChangePassword (pure helpers) frontend/src/app/features/shell/change-password/password-feedback.ts formatChangePasswordError({code, message}) maps the API error code (INVALID_OLD_PASSWORD / PASSWORDS_DO_NOT_MATCH / PASSWORD_POLICY / UNAUTHORIZED) to user-facing copy.
ChangePassword (validators) frontend/src/app/features/shell/change-password/change-password-modal.validators.ts passwordMatchValidator (group-level reactive-forms validator) + PasswordPolicy zod schema used by the modal.

Guards and interceptors

Symbol Path Purpose
authGuard frontend/src/app/core/guards/auth.guard.ts Async CanActivateFn; awaits BootstrapService.ready() and AuthService.waitUntilHydrated(), then delegates to decideAuthRedirect.
decideAuthRedirect frontend/src/app/core/guards/auth.guard.decision.ts Pure decision function mirroring decideAdminGuard; redirects to /bootstrap or /login, returns true otherwise.
adminGuard frontend/src/app/core/guards/admin.guard.ts Async CanActivateFn; awaits bootstrap + auth hydration, then delegates to decideAdminGuard (pure). Redirects to /bootstrap, /login, or / based on state; allows only admins.
decideAdminGuard frontend/src/app/core/guards/admin.guard.decision.ts Pure decision function: returns {kind:'allow'} or {kind:'redirect', path}.
landingGuard frontend/src/app/core/guards/landing.guard.ts Async CanActivateFn for /login; awaits bootstrap + auth hydration, then delegates to the pure decideLandingGuard. Redirects to /bootstrap (not initialized) or / (already authenticated).
decideLandingGuard frontend/src/app/core/guards/landing.guard.decision.ts Pure decision function: returns true to render the landing page, or UrlTree to /bootstrap / /.
authInterceptor frontend/src/app/core/interceptors/auth.interceptor.ts Attaches Authorization: Bearer <accessToken> header.
csrfInterceptor frontend/src/app/core/interceptors/csrf.interceptor.ts Attaches X-CSRF-Token header on POST/PUT/PATCH/DELETE.

Both interceptors are wired in frontend/src/main.ts via provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor])) (notably CSRF runs first so the token is attached before the auth header).

Bootstrap flow

  1. AppComponent.ngOnInit()BootstrapService.load() then AuthService.restoreSession(). Both are deduplicated so concurrent callers share a single in-flight promise (loadPromise / hydrateWaiters).
  2. BootstrapService calls GET /api/v1/bootstrap (public, with credentials: 'include').
  3. The payload sets initialized (true iff any admin user exists), pageTitle, logo, welcomeMarkdown, the active theme, the defaultChallengeIp, the registrationsEnabled flag, and the passwordPolicy descriptor consumed by the change-password modal.
  4. Theme tokens are written to CSS custom properties on document.documentElement (--color-primary, --font-family, --radius-*, etc.) consumed by frontend/src/styles.css.
  5. AuthService.restoreSession() rehydrates the in-memory signals from sessionStorage (key hipctf.auth.v1) and POSTs /api/v1/auth/refresh with withCredentials: true to mint a fresh access token. On success it persists the new token; on failure it clears storage. Either way it flips hydrated() so the guards can unblock.
  6. The authGuard (and adminGuard) await BootstrapService.ready() and AuthService.waitUntilHydrated() before reading initialized() / isAuthenticated(), which prevents a flash of /bootstrap or /login on a hard refresh of a deep link.

Authenticated shell flow

After successful auth, HomeComponent renders a full shell layout:

  1. ShellHeaderComponent is fed (signal inputs) the page title from bootstrap, the active section label (computed from the current router URL), the EventStatusStore.state() (used to colour the LED — running / countdown / stopped / unconfigured), and the countdownText() (DD:HH:mm or "Event ended"). The user menu trigger opens an inline dropdown with Rank: <r> - Points: <p>, Change password, optional Admin area (visible only when canAccessAdmin() is true), and Logout.
  2. QuickTabsComponent renders Challenges / Scoreboard / Blog tabs. The active input is computed from the current URL (HomeComponent.activeTabId()). Clicking a tab emits tabChange and the parent calls router.navigateByUrl('/' + id) (or /admin for id === 'admin').
  3. The <router-outlet> renders whichever child route is active (ChallengesPage, ScoreboardPage, BlogPage, or AdminUsersComponent).
  4. On ngOnInit, HomeComponent calls UserStore.hydrateFromAuth() + UserStore.loadMe() (which calls AuthService.me()GET /api/v1/auth/me) and starts the SSE consumer via EventStatusStore.start(() => new EventSource('/api/v1/events/status', { withCredentials: true })). The store parses each message frame, applies the state via applyServerStatus(payload), and ticks the local secondsToStart / secondsToEnd signals every second.
  5. On ngOnDestroy (and via DestroyRef.onDestroy for safety), HomeComponent calls EventStatusStore.stop() to close the SSE source and clear the tick interval.
  6. Clicking Change password in the header opens the ChangePasswordModalComponent (mode 'self'). Submitting posts /api/v1/auth/change-password via AuthService.changePassword(); on success the modal closes and the user's existing refresh tokens are revoked server-side. See the Change Password Guide for the full tester flow.
  7. Clicking Logout calls AuthService.logout() (which POSTs /api/v1/auth/logout), resets the UserStore, and navigates to /login.

See also