From 0126f6ad391615a9f85a6bea65be907488abeeb2 Mon Sep 17 00:00:00 2001 From: OpenVelo Agent Date: Tue, 21 Jul 2026 18:34:42 +0000 Subject: [PATCH] feat: Landing Page and Login/Register Modal --- .kilo/plans/823.md | 351 ++++++++++++++++++ .kilo/plans/848.md | 148 -------- backend/src/app.module.ts | 2 + backend/src/common/errors/error-codes.ts | 1 + .../src/common/middleware/csrf.middleware.ts | 1 - backend/src/modules/auth/auth.controller.ts | 25 +- backend/src/modules/auth/auth.module.ts | 2 + backend/src/modules/auth/auth.service.ts | 51 ++- backend/src/modules/auth/dto/auth.dto.ts | 16 + backend/src/modules/blog/blog.controller.ts | 19 + backend/src/modules/blog/blog.module.ts | 12 + backend/src/modules/blog/blog.service.ts | 26 ++ backend/src/modules/blog/dto/blog.dto.ts | 14 + frontend/package.json | 2 + frontend/src/app/app.routes.ts | 5 +- .../app/core/guards/landing.guard.decision.ts | 19 + frontend/src/app/core/guards/landing.guard.ts | 25 ++ .../src/app/core/services/auth.service.ts | 102 ++++- .../src/app/core/services/markdown.pure.ts | 12 + .../src/app/core/services/markdown.service.ts | 12 + .../src/app/features/auth/login.component.ts | 49 --- .../features/landing/landing.component.css | 169 +++++++++ .../features/landing/landing.component.html | 180 +++++++++ .../app/features/landing/landing.component.ts | 192 ++++++++++ .../app/features/landing/landing.service.ts | 36 ++ .../features/landing/login-modal.service.ts | 46 +++ package-lock.json | 30 ++ tests/backend/admin-guard.spec.ts | 5 + tests/backend/admin-validation.spec.ts | 5 + tests/backend/auth-refresh.spec.ts | 26 +- tests/backend/auth-register.spec.ts | 148 ++++++++ tests/backend/blog-public.spec.ts | 81 ++++ tests/backend/csrf-protected-routes.spec.ts | 70 ++++ tests/backend/uploads.spec.ts | 5 + tests/frontend/landing-guard.spec.ts | 27 ++ tests/frontend/landing-markdown.spec.ts | 34 ++ tests/frontend/landing-modal.spec.ts | 53 +++ 37 files changed, 1791 insertions(+), 210 deletions(-) create mode 100644 .kilo/plans/823.md delete mode 100644 .kilo/plans/848.md create mode 100644 backend/src/modules/blog/blog.controller.ts create mode 100644 backend/src/modules/blog/blog.module.ts create mode 100644 backend/src/modules/blog/blog.service.ts create mode 100644 backend/src/modules/blog/dto/blog.dto.ts create mode 100644 frontend/src/app/core/guards/landing.guard.decision.ts create mode 100644 frontend/src/app/core/guards/landing.guard.ts create mode 100644 frontend/src/app/core/services/markdown.pure.ts create mode 100644 frontend/src/app/core/services/markdown.service.ts delete mode 100644 frontend/src/app/features/auth/login.component.ts create mode 100644 frontend/src/app/features/landing/landing.component.css create mode 100644 frontend/src/app/features/landing/landing.component.html create mode 100644 frontend/src/app/features/landing/landing.component.ts create mode 100644 frontend/src/app/features/landing/landing.service.ts create mode 100644 frontend/src/app/features/landing/login-modal.service.ts create mode 100644 tests/backend/auth-register.spec.ts create mode 100644 tests/backend/blog-public.spec.ts create mode 100644 tests/backend/csrf-protected-routes.spec.ts create mode 100644 tests/frontend/landing-guard.spec.ts create mode 100644 tests/frontend/landing-markdown.spec.ts create mode 100644 tests/frontend/landing-modal.spec.ts diff --git a/.kilo/plans/823.md b/.kilo/plans/823.md new file mode 100644 index 0000000..38849bb --- /dev/null +++ b/.kilo/plans/823.md @@ -0,0 +1,351 @@ +# Implementation Plan: Job 823 — Landing Page and Login/Register Modal + +## 0. Already-Implemented Status + +**Not yet implemented.** No landing page, no Login/Register modal with mode switching, no `POST /api/v1/auth/register` (player registration), and no public "published blog posts" endpoint exist today. Investigation confirms: + +- `grep` across the repo finds no `Landing`/`LandingComponent`/landing-page assets. +- No public blog-post controller: `BlogPostEntity` exists at `backend/src/database/entities/blog-post.entity.ts`, but is only referenced by the migration (`1700000000000-InitSchema.ts`) and seed/docs. No `BlogController`/`BlogService` exists. +- The existing `LoginComponent` (`frontend/src/app/features/auth/login.component.ts`) is a bare full-page form, not the modal overlay described in the Job. It does not surface backoff wait-times, has no Register-mode toggle, and bypasses CSRF header entirely (manual `fetch('/api/v1/auth/csrf')`). +- Backend `AuthController.login` already implements backoff (`LoginBackoffService`) and emits `RATE_LIMITED` 429 with a wait-time message, but there is no `POST /api/v1/auth/register` for player self-registration. `AuthService.registerFirstAdmin` is bootstrap-only (admin role, refuses if any admin exists). +- CSRF skip-list (`backend/src/common/middleware/csrf.middleware.ts:11`) currently includes `/api/v1/auth/login` and `/api/v1/setup/create-admin`. Login needs to come under CSRF protection for the new flow (must add CSRF header on login); registration must NOT be skipped. + +The new login flow must align with the existing `authGuard` decision (`frontend/src/app/core/guards/auth.guard.decision.ts`) which already redirects unauthenticated-but-initialized users to `/login`. With the new Job, `/login` becomes the **landing** page, and the modal is rendered inside it. See Section 3 for the routing change. + +## 1. Architectural Reconnaissance + +- **Codebase style & conventions:** + - Backend: NestJS 10 standalone modules, TypeORM with `better-sqlite3`, zod-validated DTOs via `ZodValidationPipe`, `ApiError` → `GlobalExceptionFilter` envelope, `@Public()` decorator + global `JwtAuthGuard`, CSRF double-submit via `CsrfMiddleware` + `setOrGetCsrfToken`. + - Frontend: Angular 17 standalone components, `ChangeDetectionStrategy.OnPush`, signals (`signal`/`computed`/`effect`), Reactive Forms with `FormBuilder.nonNullable`, control-flow `@if`/`@for` (per `setup-create-admin.component.html`), typed discriminated-union service results (see `CreateAdminResult`). + - Cookies: `rt` (refresh, `HttpOnly`, `sameSite=lax/strict`) and `csrf` (readable by JS for the `csrfInterceptor`). + - Session restore on hard-refresh already exists (`AuthService.restoreSession()` → `POST /api/v1/auth/refresh`). +- **Data Layer:** SQLite + TypeORM. `blog_post` table already exists (`status` index `idx_blog_status_published (status, published_at)`). `user` table supports `role='player'` and `status='enabled'`. `setting` key `registrationsEnabled` already exists and is read by `SystemService.bootstrap`. +- **Test Framework & Structure:** Jest 29 with ts-jest, two projects (`backend` — node, `frontend` — jsdom) configured in `tests/jest.config.js`. Run all tests with `npm test` from repo root. New tests MUST live under `tests/backend/*.spec.ts` or `tests/frontend/*.spec.ts`. +- **Required Tools & Dependencies:** + - **Frontend:** `marked` (small, widely used Markdown → HTML) + `dompurify` (`isomorphic-dompurify` works in jsdom tests) for sanitized Markdown rendering of welcome description + blog bodies. Both have minimal footprint, no extra peer-deps, tree-shakeable. + - No backend dependency additions required (zod, argon2, typeorm all already present). + - `package.json` workspaces will get `frontend` deps installed automatically by `npm ci`. No `setup.sh` change needed beyond ensuring `npm ci` runs. + +## 2. Impacted Files + +### To Modify + +| Path | Reason | +|---|---| +| `backend/src/modules/auth/auth.controller.ts` | Add `POST /api/v1/auth/register` (player self-registration); tighten CSRF handling so login/register/logout/refresh all require the CSRF header except where still skipped. | +| `backend/src/modules/auth/auth.service.ts` | Add `registerPlayer(username, password, ip)` enforcing `registrationsEnabled` setting, password policy, password === confirmation, Argon2id hash, role=`player`/`status=enabled`, then auto-login via existing `mintSession`. | +| `backend/src/modules/auth/dto/auth.dto.ts` | Add `RegisterDtoSchema` (`username`, `password`, `passwordConfirm` with refine `password === passwordConfirm`). | +| `backend/src/common/middleware/csrf.middleware.ts` | Remove `/api/v1/auth/login` from `SKIP_PATH_PREFIXES` (Job requires CSRF on login); keep `/api/v1/setup/create-admin` and `/api/v1/auth/register-first-admin` skipped. | +| `backend/src/app.module.ts` | Register the new `BlogModule` (see Created files). | +| `frontend/src/app/app.routes.ts` | Rename route: `/login` stays but now renders the new `LandingComponent` (which embeds the Login/Register modal). Update wildcard fallback to route unauthenticated users correctly (existing `** → ''` still works because `authGuard` will redirect). | +| `frontend/src/app/core/guards/auth.guard.decision.ts` | No change required: it already redirects to `/login` when initialized && !authenticated. Add a `decideLandingGuard` for the unauthenticated-but-initialized case (pure function) used by `/login` route to bounce authenticated users back to `/`. | +| `frontend/src/app/features/auth/login.component.ts` | **Replace** with the new `LandingComponent` that owns the landing content + modal (single component, two responsibilities). | +| `frontend/src/app/core/services/auth.service.ts` | Add a `login(creds)` helper that returns a discriminated `LoginResult` (`LoginSuccess` / `LoginFailure { code, message, retryAfterMs? }`) so the modal can surface backoff messages. Keep existing `setSession`/`restoreSession` API. | +| `frontend/src/app/features/setup/setup-create-admin.service.ts` | Reference only — copy the discriminated-union shape into a new `auth.service.ts` helper / new `auth-modal.service.ts`. No changes required here. | +| `frontend/src/main.ts` | No code change; interceptors already cover POST/PUT/PATCH/DELETE. The new login modal will rely on the existing `csrfInterceptor`. | +| `package.json` (root) | No new root deps — frontend deps are workspace-scoped. | +| `frontend/package.json` | Add `marked` and `dompurify` (with `@types/dompurify` if needed). | +| `kilo.json` / `AGENTS.md` | No edits required. | +| `docs/architecture/frontend-structure.md`, `docs/architecture/key-files.md`, `docs/architecture/backend-modules.md`, `docs/api/rest-overview.md` | Optional doc updates to list new module/components. | + +### To Create + +| Path | Purpose | +|---|---| +| `backend/src/modules/blog/blog.module.ts` | NestJS module wiring `BlogController` + `BlogService` + `BlogPostEntity`. | +| `backend/src/modules/blog/blog.controller.ts` | `GET /api/v1/blog/posts` (public, `@Public()`, returns only published posts ordered by `published_at DESC`). | +| `backend/src/modules/blog/blog.service.ts` | `listPublished()` returning sanitized DTOs (no internal IDs leaked — id is safe to expose). | +| `backend/src/modules/blog/dto/blog.dto.ts` | Zod schema for the public response shape (`id`, `title`, `bodyHtml?`, `publishedAt`). | +| `frontend/src/app/features/landing/landing.component.ts` | Landing page + Login/Register modal. Standalone, `OnPush`, signals. Mode toggle, in-flight disable, inline field errors, server-error display, backoff wait-time. | +| `frontend/src/app/features/landing/landing.component.html` | Template for the centered landing content + modal overlay. | +| `frontend/src/app/features/landing/landing.component.css` | Scoped styles for landing layout + modal. | +| `frontend/src/app/features/landing/landing.service.ts` | Calls `POST /api/v1/blog/posts`, returns `BlogPost[]`. Also returns the sanitized HTML for `welcomeMarkdown` from the existing `BootstrapService.payload().welcomeMarkdown` (no new endpoint needed). | +| `frontend/src/app/features/landing/login-modal.service.ts` | Pure helpers: `buildLoginFailureMessage(code, message)` → user-facing copy (incl. backoff wait). Mirrors the `setup-create-admin.field-errors.ts` style. | +| `frontend/src/app/core/services/markdown.service.ts` | `renderMarkdown(md: string): SafeHtml` using `marked` + `DOMPurify.sanitize`. Used by the landing for welcome description + blog bodies. | +| `frontend/src/app/core/guards/landing.guard.ts` + `landing.guard.decision.ts` | `CanActivateFn` for `/login`: awaits bootstrap+hydration; if authenticated → redirect `/`; if not initialized → redirect `/bootstrap`; else allow. Pure decision function for unit test. | +| `tests/backend/blog-public.spec.ts` | Verifies `/api/v1/blog/posts` returns only `status='published'` rows, ordered by `published_at DESC`, public, and no challenge/scoreboard/admin data is reachable from it. | +| `tests/backend/auth-register.spec.ts` | Verifies `POST /api/v1/auth/register` happy path + `registrations disabled` + rate-limit + weak password + missing confirm. | +| `tests/backend/csrf-protected-routes.spec.ts` | Verifies login now requires CSRF header (after the skip-list change). | +| `tests/frontend/landing-markdown.spec.ts` | Pure-Jest tests for `MarkdownService.renderMarkdown` (sanitizes `').toString()` does NOT contain `safe'); + expect(out).not.toContain('