diff --git a/.kilo/plans/823.md b/.kilo/plans/823.md deleted file mode 100644 index 38849bb..0000000 --- a/.kilo/plans/823.md +++ /dev/null @@ -1,351 +0,0 @@ -# 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 `\n\n[bad](javascript:alert(1))', + }); + expect(html).toContain('

Hello

'); + expect(html).not.toContain('