diff --git a/docs/api/blog.md b/docs/api/blog.md new file mode 100644 index 0000000..e52369c --- /dev/null +++ b/docs/api/blog.md @@ -0,0 +1,71 @@ +--- +type: api +title: Blog Endpoints +description: Public blog listing and administrator-only post management endpoints. +tags: [api, blog, admin, markdown] +timestamp: 2026-07-23T10:12:24Z +--- + +# Endpoints + +| Method | Path | Access | Behavior | +|---|---|---|---| +| `GET` | `/api/v1/blog/posts` | Public | Returns published posts only, newest publication first. | +| `GET` | `/api/v1/admin/blog/posts` | Admin | Returns drafts and published posts, ordered by most recently updated. | +| `GET` | `/api/v1/admin/blog/posts/:id` | Admin | Returns one post or `404 NOT_FOUND`. | +| `POST` | `/api/v1/admin/blog/posts` | Admin + CSRF | Creates a draft or published post; defaults to draft. | +| `PUT` | `/api/v1/admin/blog/posts/:id` | Admin + CSRF | Partially updates title, Markdown body, or status. | +| `DELETE` | `/api/v1/admin/blog/posts/:id` | Admin + CSRF | Deletes a post and returns `204`; missing posts return `404 NOT_FOUND`. | + +The public route is marked `@Public()`. Admin routes require a Bearer JWT with the administrator role, and unsafe requests use the standard CSRF cookie/header contract from [REST API Overview](/api/rest-overview.md). + +# Schema + +## Public list response + +```json +{ + "posts": [ + { + "id": "post-id", + "title": "Event update", + "publishedAt": "2026-07-23T10:00:00.000Z", + "bodyMd": "# Welcome" + } + ] +} +``` + +Drafts and administrative fields such as `status`, `createdAt`, and `updatedAt` are excluded. + +## Admin post + +| Field | Type | Description | +|---|---|---| +| `id` | string | Server-generated UUID. | +| `title` | string | Trimmed title, required, maximum 200 characters. | +| `bodyMd` | string | Markdown body, maximum 200,000 characters. | +| `status` | `draft` or `published` | Visibility state. | +| `createdAt` | ISO 8601 string | Creation time. | +| `updatedAt` | ISO 8601 string | Last persistence update. | +| `publishedAt` | ISO 8601 string or null | First publication time. | + +`POST` accepts `title`, optional `bodyMd`, and optional `status`. `PUT` accepts any subset of those fields. Validation failures use HTTP 400 with code `VALIDATION_FAILED`. + +# Publication behavior + +* Creating with `status: "published"` sets `publishedAt`; omitted status creates a draft. +* Publishing a never-published draft sets `publishedAt` once. +* Editing a published post does not change `publishedAt`. +* Moving a post back to draft preserves `publishedAt`; republishing retains the original timestamp. +* Only rows currently marked `published` are returned by the public endpoint. + +# Wiring + +`BlogModule` registers `BlogController`/`BlogService` for the public list and `AdminBlogController`/`AdminBlogService` for management. Both services access `BlogPostEntity` through `TypeOrmModule.forFeature([BlogPostEntity])`. The Angular `BlogApiService` calls these routes for the admin page, authenticated Blog page, and landing-page post list. + +# See also + +- [Blog Publishing and Reading](/guides/blog.md) +- [Blog Post Table](/database/blog-posts.md) +- [REST API Overview](/api/rest-overview.md) diff --git a/docs/api/rest-overview.md b/docs/api/rest-overview.md index 27cda81..eaca903 100644 --- a/docs/api/rest-overview.md +++ b/docs/api/rest-overview.md @@ -3,7 +3,7 @@ type: api title: REST API Overview description: Base URL, versioning, auth, CSRF, and the standard error envelope. tags: [api, rest, overview, csrf] -timestamp: 2026-07-23T00:10:00Z +timestamp: 2026-07-23T10:12:24Z --- # Base URL & versioning @@ -98,4 +98,5 @@ statuses and the canonical messages produced by - [Setup Endpoint](/api/setup.md) - [System Endpoints](/api/system.md) - [Uploads Endpoints](/api/uploads.md) -- [Challenges Endpoints](/api/challenges.md) \ No newline at end of file +- [Challenges Endpoints](/api/challenges.md) +- [Blog Endpoints](/api/blog.md) \ No newline at end of file diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md index fa103e3..db816cb 100644 --- a/docs/architecture/backend-modules.md +++ b/docs/architecture/backend-modules.md @@ -3,7 +3,7 @@ type: architecture title: Backend Module Map description: NestJS modules, controllers, services, and how they are wired together. tags: [architecture, backend, nestjs, modules] -timestamp: 2026-07-23T00:10:00Z +timestamp: 2026-07-23T10:12:24Z --- # Module Map @@ -29,7 +29,7 @@ also registers two global providers: | `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. | | `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). | | `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. | -| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). | +| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController`/`BlogService` expose published posts at `GET /api/v1/blog/posts`; `AdminBlogController`/`AdminBlogService` provide guarded CRUD at `/api/v1/admin/blog/posts` using `BlogPostEntity`. | | `ChallengesModule` | `backend/src/modules/challenges/challenges.module.ts` | `ChallengesController` (`/api/v1/challenges/{board,status,:id,:id/solves}`) + `ChallengesEventsController` (authenticated SSE `/api/v1/events`) + `ChallengesService` (board, detail, submit, scoring util). | | `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. | @@ -47,6 +47,7 @@ also registers two global providers: | `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` | | `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` | | `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` | +| `AdminBlogController` | `/api/v1/admin/blog/posts` | Admin only | `backend/src/modules/blog/admin-blog.controller.ts` | | `ChallengesController` | `/api/v1/challenges` | Authenticated | `backend/src/modules/challenges/challenges.controller.ts` | | `ChallengesEventsController` | `/api/v1/events` | Authenticated (SSE) | `backend/src/modules/challenges/events.controller.ts` | @@ -89,3 +90,4 @@ also registers two global providers: - [System Overview](/architecture/overview.md) - [Key Files Index](/architecture/key-files.md) - [REST API Overview](/api/rest-overview.md) +- [Blog API](/api/blog.md) diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md index 898a870..125abc2 100644 --- a/docs/architecture/frontend-structure.md +++ b/docs/architecture/frontend-structure.md @@ -1,9 +1,9 @@ --- type: architecture title: Frontend Structure -description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport. -tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications] -timestamp: 2026-07-23T05:10:00Z +description: Angular routes, components, services, guards, interceptors, authenticated SSE transport, and blog management and presentation wiring. +tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications, blog] +timestamp: 2026-07-23T10:12:24Z --- # Routes @@ -17,9 +17,10 @@ Routes live in `frontend/src/app/app.routes.ts`: | `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. | | `/challenges` | `ChallengesPage` | inherited | Full challenges board (category columns, modal, flag submit, live SSE). | | `/scoreboard` | `ScoreboardPage` | inherited | Live scoreboard page with 4 tabs (Ranking / Matrix / Event Log / Score Graph), live SSE `solve` updates. | -| `/blog` | `BlogPage` | inherited | Placeholder blog page. | -| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. | +| `/blog` | `BlogPage` | inherited | Loads and renders published posts with loading, error, and empty states. | +| `/admin` | `AdminShellComponent` | `adminGuard` | Admin-only layout; redirects to `/admin/general`. | | `/admin/challenges` | `AdminChallengesComponent` | inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. | +| `/admin/blog` | `AdminBlogComponent` | inherited | Admin post table and create/edit/draft/publish/delete workflow. | # Components and services @@ -45,6 +46,11 @@ Routes live in `frontend/src/app/app.routes.ts`: | `RankingComponent` / `MatrixComponent` / `EventLogComponent` / `ScoreGraphComponent` | `frontend/src/app/features/scoreboard/` | Presentational components for the 4 tabs; each binds a single `@Input()` from `ScoreboardStore` and uses `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` to color each player consistently. | | `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. | | `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that pushes a friendly message into `NotificationService` for every `HttpErrorResponse`; suppresses duplicate toasts for endpoints with their own error UI (login form, `/challenges/status` snapshot). | +| `BlogApiService` | `frontend/src/app/core/services/blog.service.ts` | Promise-based HTTP client for the published list and admin CRUD routes. | +| `BlogPage` | `frontend/src/app/features/blog/blog.page.ts` | Smart `/blog` page that loads published posts and derives loading, error, empty, and list states. | +| `BlogPresenterComponent` | `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared title/date/sanitized-Markdown renderer used by `/blog` and `/login`. | +| `AdminBlogComponent` | `frontend/src/app/features/admin/blog/blog.component.ts` | Smart `/admin/blog` page coordinating list refresh and create/edit/delete modal state. | +| `BlogFormModalComponent` / `BlogDeleteModalComponent` | `frontend/src/app/features/admin/blog/` | Admin Markdown editor with live preview and destructive-action confirmation. | # Authenticated SSE wiring @@ -81,6 +87,9 @@ on destruction. | `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. | | `frontend/src/app/features/challenges/challenges.page.ts` | `/challenges` smart page; owns gate logic, modal lifecycle, SSE wiring, and the countdown-zero reload. | | `frontend/src/app/features/challenges/challenges.store.ts` | Signal store backing `/challenges`: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, public `reset()`, and per-user `setMyUserId` flush. | +| `frontend/src/app/core/services/blog.service.ts` | Calls public and admin blog endpoints with typed payloads. | +| `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared sanitized Markdown post rendering for authenticated and landing views. | +| `frontend/src/app/features/admin/blog/blog.component.ts` | Coordinates administrator blog CRUD UI and table refreshes. | | `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. | # Event-status pure helpers @@ -129,4 +138,5 @@ for the full contract and tester matrix. - [Challenges Board](/guides/challenges-board.md) - [Scoreboard Page](/guides/scoreboard-page.md) - [Notifications](/guides/notifications.md) +- [Blog Publishing and Reading](/guides/blog.md) - [System Overview](/architecture/overview.md) diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index 1378131..08f7bcc 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -1,9 +1,9 @@ --- type: architecture title: Key Files Index -description: One-line responsibility for important source and contract-test files, including strict event-window validation, the public bootstrap SSE listener, the challenges full-replace import flow, the authenticated player-facing challenges board, and the per-user `solvedByMe` reset on the session boundary. -tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session] -timestamp: 2026-07-23T04:05:00Z +description: One-line responsibility for important source and contract-test files, including event-window, challenge, scoreboard, session, notification, and blog flows. +tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session, blog] +timestamp: 2026-07-23T10:12:24Z --- # Backend @@ -41,6 +41,11 @@ timestamp: 2026-07-23T04:05:00Z | `backend/src/modules/challenges/dto/challenges.dto.ts` | zod contracts (`SolveSubmitBodySchema`, `BoardQuerySchema`, `ChallengeIdParamSchema`) + DTO types for the player-facing board. | | `backend/src/modules/challenges/scoring.util.ts` | Pure `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, `computeAwardedPoints` (base + rank bonus), `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` (FNV-1a → palette index), and constant-time `safeEqualString` for flag comparison. | | `backend/src/common/errors/error-codes.ts` | Canonical error code map (now includes `EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`). | +| `backend/src/modules/blog/blog.module.ts` | Wires the public and admin blog controllers/services to `BlogPostEntity`. | +| `backend/src/modules/blog/admin-blog.controller.ts` | Registers administrator CRUD routes at `/api/v1/admin/blog/posts`. | +| `backend/src/modules/blog/admin-blog.service.ts` | Lists, creates, updates, publishes/drafts, and deletes posts while preserving first-publication time. | +| `backend/src/modules/blog/dto/blog.dto.ts` | Public/admin response contracts and zod validation for title, body, status, and post id. | +| `tests/backend/blog-admin.spec.ts` | Admin authorization, CRUD, validation, publication-timestamp, deletion, and public draft-hiding contracts. | # Frontend @@ -94,6 +99,16 @@ timestamp: 2026-07-23T04:05:00Z | `frontend/src/app/features/scoreboard/scoreboard.page.ts` | `/scoreboard` smart page (also listed above): gates the four tabs, wires `store.loadAll()` and `store.wireSse(...)` on init, and tears them down via `store.stop()` on destroy. | | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. | | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Translates `HttpErrorResponse` into friendly messages and pushes them to `NotificationService` (with suppression for `/api/v1/auth/{login,csrf,register}` and `/api/v1/challenges/status`). | +| `frontend/src/app/core/services/blog.service.ts` | Typed Promise-based public blog list and admin CRUD HTTP client. | +| `frontend/src/app/features/blog/blog.page.ts` | `/blog` smart page with loading, error, empty, and published-list states. | +| `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared sanitized Markdown post renderer used by the Blog and landing pages. | +| `frontend/src/app/features/blog/blog.pure.ts` | Pure blog-list state derivation. | +| `frontend/src/app/features/admin/blog/blog.component.ts` | `/admin/blog` post table and create/edit/delete workflow coordinator. | +| `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` | Reactive title/Markdown editor with live preview and draft/publish actions. | +| `frontend/src/app/features/admin/blog/blog-delete-modal.component.ts` | Post deletion confirmation and inline failure UI. | +| `frontend/src/app/features/admin/blog/blog-form.pure.ts` | Admin form validation, form synchronization, request cleanup, and status display helpers. | +| `tests/frontend/blog-page.spec.ts` | Blog page-state and shared sanitized Markdown rendering contracts. | +| `tests/frontend/blog-admin-form.spec.ts` | Admin form validation, prefill/reset, payload, and status-label contracts. | | `tests/frontend/challenges.pure.spec.ts` | Pure helpers: sorting, parsers, friendly error mapping, `formatDdHhMm` (`DD:HH:mm:ss`). | | `tests/frontend/challenges.service.spec.ts` | HTTP-service contract: `getDetail` URL-encodes the id, attaches `?include=solvers`, and forwards `withCredentials: true`. | | `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, `stop()`. The "marks solvedByMe" and "does not double-count" tests now call `setMyUserId('me-1')` *before* `load()` to exercise the realistic page-mount sequence. | @@ -118,3 +133,5 @@ timestamp: 2026-07-23T04:05:00Z - [Scoreboard Page Guide](/guides/scoreboard-page.md) - [Challenges Board](/guides/challenges-board.md) - [Notifications](/guides/notifications.md) +- [Blog Publishing and Reading](/guides/blog.md) +- [Blog API](/api/blog.md) diff --git a/docs/database/blog-posts.md b/docs/database/blog-posts.md index 71df2e8..c30ef97 100644 --- a/docs/database/blog-posts.md +++ b/docs/database/blog-posts.md @@ -3,7 +3,7 @@ type: database title: Blog Post Table description: blog_post table — draft/published Markdown posts surfaced on the public landing page. tags: [database, blog, markdown] -timestamp: 2026-07-21T18:28:00Z +timestamp: 2026-07-23T10:12:24Z --- # Schema @@ -27,12 +27,18 @@ timestamp: 2026-07-21T18:28:00Z # Behavior * Only rows with `status = 'published'` are exposed through the public - endpoint `GET /api/v1/blog/posts` (see - [REST API Overview](/api/rest-overview.md)). Drafts are hidden. + endpoint `GET /api/v1/blog/posts` (see [Blog API](/api/blog.md)). Drafts + are hidden from the landing page and authenticated `/blog` page. +* The admin CRUD endpoints under `/api/v1/admin/blog/posts` expose both + statuses and all timestamps to administrators. +* First publication sets `published_at`. Later edits, demotion to draft, + and republication preserve that original timestamp. * `BlogService.listPublished()` orders by `published_at DESC`, falling back to `created_at` when `published_at` is null. # See also - [Database Schema Overview](/database/schema.md) +- [Blog API](/api/blog.md) +- [Blog Publishing and Reading](/guides/blog.md) - [Landing Page Guide](/guides/landing-page.md) diff --git a/docs/guides/admin-shell.md b/docs/guides/admin-shell.md index f032bcb..16b3fff 100644 --- a/docs/guides/admin-shell.md +++ b/docs/guides/admin-shell.md @@ -1,9 +1,9 @@ --- type: guide title: Admin Shell & Side Navigation -description: How an authenticated admin navigates the post-login admin area, the side-nav layout, and how the General and Categories pages are reached. +description: How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages. tags: [guide, admin, shell, navigation, tester] -timestamp: 2026-07-22T12:00:00Z +timestamp: 2026-07-23T10:12:24Z --- # When this view is available @@ -47,9 +47,9 @@ The side-nav is defined as a static `ENTRIES` array in |-------------|-----------------------|---------|----------------------------------------------------------------------------------------------| | General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). | | Challenges | `/admin/challenges` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. | -| Players | `/admin/players` | no | Greyed-out placeholder. No route registered. | -| Blog | `/admin/blog` | no | Greyed-out placeholder. No route registered. | -| System | `/admin/system` | no | Greyed-out placeholder. No route registered. | +| Players | `/admin/players` | yes | User management page. | +| Blog | `/admin/blog` | yes | [Blog Publishing and Reading](/guides/blog.md) — post list with draft/publish, edit, preview, and delete workflows. | +| System | `/admin/system` | no | Greyed-out placeholder. No route registered. | Each `
  • ` carries `data-testid="admin-nav-{id}"`. Disabled entries have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled @@ -95,17 +95,18 @@ current URL starts with the entry's path. General is the default child | 4 | `frontend/src/app/features/admin/admin-shell.component.ts` | Renders the aside + body; `ENTRIES` is the static side-nav list. | | 5 | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` (default `/admin/general`); embeds `AdminCategoriesComponent`. | | 6 | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent` (`/admin/categories`); also rendered inside the General page. | -| 7 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). | -| 8 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and ``. | -| 9 | `frontend/src/app/core/services/admin.service.ts` | Typed wrappers for `/api/v1/admin/users`, `/admin/general/*`, `/admin/categories/*`, `/uploads/{logo,category-icon}`. | -| 10 | `backend/src/modules/admin/admin.module.ts` | Registers `AdminController`, `AdminGeneralController`, `AdminCategoriesController` + their services. Imports `AuthModule`, `UsersModule`, `SettingsModule`, `CommonModule`, and `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`. | +| 7 | `frontend/src/app/features/admin/blog/blog.component.ts` | `AdminBlogComponent` (`/admin/blog`); coordinates post listing, editing, publication, and deletion. | +| 8 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). | +| 9 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and ``. | +| 10 | `frontend/src/app/core/services/blog.service.ts` | Typed wrappers for public blog listing and `/api/v1/admin/blog/posts` CRUD. | +| 11 | `backend/src/modules/blog/blog.module.ts` | Registers public and admin blog controllers/services with the blog repository. | # Notes * The shell deliberately keeps `AdminShellComponent` as a thin layout - owner — child routes (`general`, `categories`) render into its - ``. New admin sub-pages can be added by enabling - entries in `ENTRIES` and registering a child route in `app.routes.ts`. + owner — enabled child routes render into its ``. New admin + sub-pages require both an enabled `ENTRIES` item and a child route in + `app.routes.ts`. * The guard decision function (`decideAdminGuard`) is a pure module so it is straightforward to unit-test without Angular DI. See `tests/frontend/admin-shell.spec.ts` and @@ -124,5 +125,6 @@ current URL starts with the entry's path. General is the default child - [Change Password](/guides/change-password.md) - [Admin — General Settings](/guides/admin-general-settings.md) - [Admin — Categories](/guides/admin-categories.md) +- [Blog Publishing and Reading](/guides/blog.md) - [REST API Overview](/api/rest-overview.md) - [Admin Endpoints](/api/admin.md) diff --git a/docs/guides/blog.md b/docs/guides/blog.md new file mode 100644 index 0000000..ba0e626 --- /dev/null +++ b/docs/guides/blog.md @@ -0,0 +1,71 @@ +--- +type: guide +title: Blog Publishing and Reading +description: How administrators create, edit, publish, and delete Markdown posts and how users read published posts. +tags: [guide, blog, admin, markdown, tester] +timestamp: 2026-07-23T10:12:24Z +--- + +# User views + +Published posts are available in two places: + +| View | Access | Expected content | +|---|---|---| +| `/login` | Unauthenticated users | The landing-page blog section lists published posts below the Login button. | +| `/blog` | Signed-in users | The Blog page loads and lists all published posts. | + +Both views use `BlogPresenterComponent` to show the post title, publication date, and sanitized Markdown body. Draft posts never appear in either public list. The `/blog` page shows `Loading blog posts…` while loading, `No announcements yet.` when no posts are published, and an error message if loading fails. + +# Admin workflow + +1. Sign in as an administrator. +2. Open **Admin**, then select **Blog**, or navigate directly to `/admin/blog`. +3. Confirm the page displays a table with **Title**, **Status**, **Created**, **Updated**, **Published**, and **Actions** columns. If no posts exist, it displays `No posts yet.`. +4. Click **+ New post**. +5. Enter a required title and an optional Markdown body. The live preview updates beside the editor. +6. Click **Save draft** to keep the post hidden from users, or **Publish** to expose it in `/blog` and on `/login`. +7. Confirm a success banner displays `Draft saved.` or `Post published.`, and the refreshed table contains the post with the correct status badge. + +# Editing and deleting + +* Click the pencil action on a row to open the prefilled **Edit post** modal. It shows created, updated, and publication timestamps where available. +* Saving as draft changes the status to **Draft** and hides the post from public lists. Republishing preserves the post's original publication timestamp. +* Click the trash action to open **Delete post**. The confirmation includes the title and warns that deletion cannot be undone. +* Click **Delete** to remove the post. The table refreshes and displays `Post deleted.`. Click **Cancel** or the modal backdrop to leave the post unchanged. + +# Validation and expected errors + +| Input or operation | Expected behavior | +|---|---| +| Empty or whitespace-only title | `Title is required`; save and publish remain unavailable. | +| Title over 200 characters | `Title must be at most 200 characters`. | +| Markdown body over 200,000 characters | `Body must be at most 200000 characters`. | +| API list failure | An error banner replaces the table state. | +| Create or update failure | The form stays open and displays the server message. | +| Delete failure | The confirmation stays open and displays the server message. | + +# Architecture map + +| Layer | File | Responsibility | +|---|---|---| +| Routes | `frontend/src/app/app.routes.ts` | Registers authenticated `/blog` and admin-only `/admin/blog`. | +| Admin page | `frontend/src/app/features/admin/blog/blog.component.ts` | Loads all posts and coordinates create, edit, publish, draft, and delete operations. | +| Admin form | `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` | Reactive title/body editor with sanitized live Markdown preview. | +| Admin form logic | `frontend/src/app/features/admin/blog/blog-form.pure.ts` | Validation, form synchronization, request cleanup, and status labels. | +| Delete confirmation | `frontend/src/app/features/admin/blog/blog-delete-modal.component.ts` | Confirms destructive deletion and displays failures. | +| User page | `frontend/src/app/features/blog/blog.page.ts` | Loads published posts and selects loading, error, empty, or list state. | +| Shared presenter | `frontend/src/app/features/blog/blog-presenter.component.ts` | Renders a published post consistently on `/blog` and `/login`. | +| HTTP client | `frontend/src/app/core/services/blog.service.ts` | Calls public list and admin CRUD endpoints. | +| Backend wiring | `backend/src/modules/blog/blog.module.ts` | Registers public and admin controllers and services with the blog repository. | +| Admin API | `backend/src/modules/blog/admin-blog.controller.ts` | Registers guarded CRUD routes under `/api/v1/admin/blog/posts`. | +| Persistence logic | `backend/src/modules/blog/admin-blog.service.ts` | Queries and mutates `blog_post` rows and controls publication timestamps. | + +The Angular client uses HTTP JSON calls. Existing auth and CSRF interceptors attach the administrator JWT and CSRF token to unsafe requests. The backend controller is protected by `AdminGuard` and `@Roles('admin')`, then delegates to `AdminBlogService`, which uses the TypeORM `BlogPostEntity` repository. + +# See also + +- [Blog API](/api/blog.md) +- [Blog Post Table](/database/blog-posts.md) +- [Landing Page](/guides/landing-page.md) +- [Admin Shell & Side Navigation](/guides/admin-shell.md) diff --git a/docs/guides/landing-page.md b/docs/guides/landing-page.md index 608b812..5e73670 100644 --- a/docs/guides/landing-page.md +++ b/docs/guides/landing-page.md @@ -3,7 +3,7 @@ type: guide title: Landing Page description: How the public landing page renders, how the login + registration modal is opened, what each form does, and how the modal stays in sync with admin `registrationsEnabled` changes via SSE. tags: [guide, landing, login, register, ui, sse, bootstrap] -timestamp: 2026-07-22T16:15:00Z +timestamp: 2026-07-23T10:12:24Z --- # What you see @@ -31,6 +31,8 @@ On a fresh visit the page shows, in order: 5. **Blog list** — `
    ` 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. + Each row is rendered by the shared `BlogPresenterComponent` with its + title, publication date, and sanitized Markdown body, matching `/blog`. # Guard @@ -144,4 +146,5 @@ response to the unsafe call itself. - [Auth Endpoints](/api/auth.md) - [Database Schema Overview](/database/schema.md) (`blog_post`) - [Frontend Structure](/architecture/frontend-structure.md) +- [Blog Publishing and Reading](/guides/blog.md) - [First-Run Bootstrap](/guides/bootstrap.md) diff --git a/docs/index.md b/docs/index.md index c0db5e0..2b8c2de 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,10 +8,11 @@ HIPCTF is a single-tenant CTF (Capture-The-Flag) platform built as a Node.js monorepo containing a NestJS REST API and an Angular single-page application. It supports user registration, authentication, challenge management, a live scoreboard, an event window with a public countdown, theming, and admin -controls. +controls, administrator-managed Markdown blog publishing, and public and +signed-in blog views. The docs below are organized by purpose so agents can pull just the slice -they need. Last regenerated 2026-07-23T08:42:24Z. +they need. Last regenerated 2026-07-23T10:12:24Z. # Architecture @@ -61,6 +62,7 @@ they need. Last regenerated 2026-07-23T08:42:24Z. streams, public event-window settings, and the authenticated `/events/status` SSE stream. * [Uploads Endpoints](/api/uploads.md) - Admin-only multipart uploads for category icons, challenge files, and validated site logos. +* [Blog Endpoints](/api/blog.md) - Public published-post listing and admin-only post CRUD, validation, and publication behavior. # Guides @@ -70,8 +72,9 @@ they need. Last regenerated 2026-07-23T08:42:24Z. helpers that translate reactive-form state into the inline validation messages on the first-admin modal. * [Admin Shell & Side Navigation](/guides/admin-shell.md) - How admins - navigate the post-login admin area side-nav (General, Challenges, - Players, Blog, System) and reach the enabled child pages. + navigate the post-login admin area side-nav and reach General, + Challenges, Players, and Blog pages. +* [Blog Publishing and Reading](/guides/blog.md) - How administrators create, preview, draft, publish, edit, and delete Markdown posts and how users read them. * [Admin — General Settings](/guides/admin-general-settings.md) - How an admin edits platform-wide settings, including the required and strictly ordered event window, at `/admin/general`.