Files
HIPCTF2/docs/guides/landing-page.md
T
2026-07-21 18:46:12 +00:00

123 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: guide
title: Landing Page
description: How the public landing page renders, how the login + registration modal is opened, and what each form does.
tags: [guide, landing, login, register, ui]
timestamp: 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:
1. **Logo**`<img data-testid="landing-logo">` rendered from
`BootstrapService.payload().logo` (empty string → the `<img>` is not
rendered).
2. **Page title**`<h1 data-testid="landing-title">` rendered from
`BootstrapService.payload().pageTitle` (defaults to `HIPCTF`).
3. **Welcome card**`<article data-testid="landing-welcome">` whose
`innerHTML` is bound to the computed signal `welcomeHtml()`, i.e. the
sanitized HTML render of
`BootstrapService.payload().welcomeMarkdown` (via
`MarkdownService.render``renderMarkdownToHtml``marked` +
`DOMPurify`). The binding **must** invoke the signal (`welcomeHtml()`),
not the signal object — see [Key Files Index](/architecture/key-files.md)
for the `landing.component.html` entry that pins this.
4. **Login button**`<button data-testid="landing-open-login">`.
5. **Blog list**`<section data-testid="landing-blog">` listing every
row returned by `GET /api/v1/blog/posts` (only `status='published'`
rows), or the text `No 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:
1. `submitLogin()``AuthService.login({ username, password })`.
2. On `ok: true`, `AuthService.setSession(...)` is called and the user
is navigated to `/`.
3. On `ok: false`, the modal renders the mapped failure text and a
`(Ns remaining)` hint when `code === '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` | 332 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.
# See also
- [Auth Endpoints](/api/auth.md)
- [Database Schema Overview](/database/schema.md) (`blog_post`)
- [Frontend Structure](/architecture/frontend-structure.md)
- [First-Run Bootstrap](/guides/bootstrap.md)