103 lines
7.4 KiB
Markdown
103 lines
7.4 KiB
Markdown
---
|
|
type: architecture
|
|
title: Frontend Structure
|
|
description: Angular routes, components, services, guards, and interceptors.
|
|
tags: [architecture, frontend, angular]
|
|
timestamp: 2026-07-21T16:48:20Z
|
|
---
|
|
|
|
# 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` | `LoginComponent` | — | Standard login form. |
|
|
| `/` | `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` | `frontend/src/app/features/auth/login.component.ts` | Username/password form. |
|
|
| `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](/guides/setup-field-errors.md)), 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` and exposes `restoreSession()` + `waitUntilHydrated()`. |
|
|
| `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. |
|
|
| `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}`. |
|
|
| `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. `/admin` → `AdminUsersComponent`) 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
|
|
|
|
- [System Overview](/architecture/overview.md)
|
|
- [Backend Module Map](/architecture/backend-modules.md)
|
|
- [First-Run Bootstrap](/guides/bootstrap.md)
|
|
- [Admin Shell & User Management](/guides/admin-shell.md)
|