AI Implementation feature(869): Admin Area Blog and Blog Page (#58)
This commit was merged in pull request #58.
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
- [Challenges Endpoints](/api/challenges.md)
|
||||
- [Blog Endpoints](/api/blog.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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+14
-12
@@ -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 `<li>` 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 `<router-outlet>`. |
|
||||
| 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 `<router-outlet>`. |
|
||||
| 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
|
||||
`<router-outlet>`. 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 `<router-outlet>`. 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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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** — `<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.
|
||||
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)
|
||||
|
||||
+7
-4
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user