6.3 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| guide | Landing Page | How the public landing page renders, how the login + registration modal is opened, and what each form does. |
|
2026-07-21T18:45:54Z |
What you see
LandingComponent (frontend/src/app/features/landing/landing.component.{ts,html,css})
is the route mounted at /login (the path is historic — the page is now
the public landing page, not a bare login form).
On a fresh visit the page shows, in order:
- Logo —
<img data-testid="landing-logo">rendered fromBootstrapService.payload().logo(empty string → the<img>is not rendered). - Page title —
<h1 data-testid="landing-title">rendered fromBootstrapService.payload().pageTitle(defaults toHIPCTF). - Welcome card —
<article data-testid="landing-welcome">whoseinnerHTMLis bound to the computed signalwelcomeHtml(), i.e. the sanitized HTML render ofBootstrapService.payload().welcomeMarkdown(viaMarkdownService.render→renderMarkdownToHtml→marked+DOMPurify). The binding must invoke the signal (welcomeHtml()), not the signal object — see Key Files Index for thelanding.component.htmlentry that pins this. - Login button —
<button data-testid="landing-open-login">. - Blog list —
<section data-testid="landing-blog">listing every row returned byGET /api/v1/blog/posts(onlystatus='published'rows), or the textNo announcements yet.when the list is empty.
Guard
/login is gated by landingGuard
(frontend/src/app/core/guards/landing.guard.ts):
initialized |
isAuthenticated |
Result |
|---|---|---|
false |
any | Redirect to /bootstrap (first-admin modal). |
true |
true |
Redirect to / (already signed in). |
true |
false |
Render the landing page. |
Login modal
Clicking the Login button sets mode = 'login', which renders an
overlay containing:
| Field | Selector (data-testid) |
Notes |
|---|---|---|
| Username | login-username |
autocomplete="username" |
| Password | login-password |
autocomplete="current-password" |
| Field error | login-field-error |
Inline required message. |
| Server error | login-server-error |
Mapped via buildLoginFailureMessage. |
| Submit | login-submit |
Disabled while submitting() is true. |
| Switch | login-switch-btn |
Visible only when registrationsEnabled is true; opens the registration modal. |
Submission flow:
submitLogin()→AuthService.login({ username, password }).- On
ok: true,AuthService.setSession(...)is called and the user is navigated to/. - On
ok: false, the modal renders the mapped failure text and a(Ns remaining)hint whencode === 'RATE_LIMITED'and the server message contains a wait-seconds value.
Registration modal
When bootstrap.payload().registrationsEnabled === true, the Login
modal shows a "Register here" link (login-switch-btn) that swaps the
form to mode = 'register'. The registration form contains:
| Field | Selector (data-testid) |
Notes |
|---|---|---|
| Username | register-username |
3–32 chars, regex ^[a-zA-Z0-9_.-]+$. |
| Password | register-password |
Argon2id policy enforced server-side. |
| Confirm password | register-confirm |
Must match (group-level passwordMatchValidator). |
| Field error | register-field-error |
Inline message covering required / minLength / maxLength / pattern / mismatch. |
| Server error | register-server-error |
Warn-styled when code is RATE_LIMITED or REGISTRATIONS_DISABLED. |
| Submit | register-submit |
Disabled while submitting. |
| Switch | register-switch-btn |
Goes back to the login form. |
Submission calls AuthService.register(...) which posts to
POST /api/v1/auth/register. On ok: true the user is signed in
immediately and navigated to /.
The modal is dismissible via the × button (landing-modal-close),
clicking the overlay backdrop, or pressing Escape. Submit-in-flight
dismissal is suppressed.
Server error code → user copy
buildLoginFailureMessage (frontend/src/app/features/landing/login-modal.service.ts)
maps the API error envelope into modal copy:
| Code | Rendered text |
|---|---|
INVALID_CREDENTIALS |
Invalid username or password. |
RATE_LIMITED |
Please wait N seconds before trying again. + (Ns remaining) hint when N is found in the server message. |
CSRF_INVALID |
Session expired. Please reload the page and try again. |
USERNAME_TAKEN |
Username already exists. |
WEAK_PASSWORD |
Password does not meet the security policy. |
REGISTRATIONS_DISABLED |
Registrations are currently disabled. |
VALIDATION_FAILED |
Please check the form fields and try again. |
| other / default | Raw message (or Something went wrong. Please try again. if empty). |
CSRF
AuthService.ensureCsrf() (frontend/src/app/core/services/auth.service.ts)
calls GET /api/v1/auth/csrf before both login() and register() to
materialise the csrf cookie if it isn't already set. The call swallows
errors because the CSRF middleware also mints the cookie on the
response to the unsafe call itself.