Files
HIPCTF2/docs/architecture/frontend-structure.md
T

9.1 KiB

type, title, description, tags, timestamp
type title description tags timestamp
architecture Frontend Structure Angular routes, components, services, guards, and interceptors.
architecture
frontend
angular
2026-07-21T18:28:00Z

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 a <router-outlet> for child routes.
/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 Authenticated shell with header, conditional admin nav, and <router-outlet> for children.
AdminUsersComponent frontend/src/app/features/admin/admin-users.component.ts Admin-only user list rendered inside the home shell.
LoginComponent (removed) — replaced by LandingComponent.
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.
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.

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() (each calls ensureCsrf() then posts, returning a discriminated LoginResult / RegisterResult).
BootstrapService frontend/src/app/core/services/bootstrap.service.ts Fetches /api/v1/bootstrap, applies theme tokens to CSS; exposes ready() that deduplicates the in-flight load.
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.

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, and the default challenge IP.
  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.

Home shell flow

After successful auth, HomeComponent renders a thin shell layout:

  1. The header (shell-header) shows the page title and signed-in identity.
  2. When shouldShowAdminNav({isAuthenticated, role}) returns true (see frontend/src/app/features/home/home.shell.ts) the admin nav link (data-testid="nav-admin") is rendered.
  3. Child routes (e.g. /adminAdminUsersComponent) render into the shell's <router-outlet>.
  4. AdminUsersComponent calls AdminService.listUsers() on init; loading, error, and the rendered list are exposed as Angular signals.

See also