9.1 KiB
9.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||
|---|---|---|---|---|---|---|---|
| architecture | Frontend Structure | Angular routes, components, services, guards, and interceptors. |
|
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
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.