7.1 KiB
7.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||
|---|---|---|---|---|---|---|---|
| architecture | Frontend Structure | Angular routes, components, services, guards, and interceptors. |
|
2026-07-21T15:05:00Z |
Routes
Routes live in frontend/src/app/app.routes.ts:
| Path | Component | Guard | Notes |
|---|---|---|---|
/bootstrap |
SetupCreateAdminComponent |
— | Rendered only when initialized === false. Non-dismissible modal overlay. |
/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 (non-dismissible, typed reactive forms). |
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). |
AdminService |
frontend/src/app/core/services/admin.service.ts |
GET /api/v1/admin/users (typed AdminUser[]). |
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
AppComponent.ngOnInit()→BootstrapService.load()thenAuthService.restoreSession(). Both are deduplicated so concurrent callers share a single in-flight promise (loadPromise/hydrateWaiters).BootstrapServicecallsGET /api/v1/bootstrap(public, withcredentials: 'include').- The payload sets
initialized(true iff any admin user exists),pageTitle,logo,welcomeMarkdown, the activetheme, and the default challenge IP. - Theme tokens are written to CSS custom properties on
document.documentElement(--color-primary,--font-family,--radius-*, etc.) consumed byfrontend/src/styles.css. AuthService.restoreSession()rehydrates the in-memory signals fromsessionStorage(keyhipctf.auth.v1) and POSTs/api/v1/auth/refreshwithwithCredentials: trueto mint a fresh access token. On success it persists the new token; on failure it clears storage. Either way it flipshydrated()so the guards can unblock.- The
authGuard(andadminGuard)awaitBootstrapService.ready()andAuthService.waitUntilHydrated()before readinginitialized()/isAuthenticated(), which prevents a flash of/bootstrapor/loginon a hard refresh of a deep link.
Home shell flow
After successful auth, HomeComponent renders a thin shell layout:
- The header (
shell-header) shows the page title and signed-in identity. - When
shouldShowAdminNav({isAuthenticated, role})returnstrue(seefrontend/src/app/features/home/home.shell.ts) the admin nav link (data-testid="nav-admin") is rendered. - Child routes (e.g.
/admin→AdminUsersComponent) render into the shell's<router-outlet>. AdminUsersComponentcallsAdminService.listUsers()on init; loading, error, and the rendered list are exposed as Angular signals.