QDG Knowledge Base Read-only viewer QWebHub
user-guide

User Guide

Version 1 · Initial user guide: configuration, running locally, session auth flow, story/media/lookup workflows, health check usage

User Guide

Prerequisites

  • .NET 9 SDK
  • Network access to the MySQL server hosting the cms, qdb, and photogallery databases
  • Network access to the QDBAuth service and a Wasabi bucket (or equivalent S3-compatible endpoint)

Configuration

Settings live in QDG.CMS.API/appsettings.json (and appsettings.Development.json for environment overrides — currently only Logging). Required sections:

  • ConnectionStrings:DefaultConnection / QdbConnection / PhotoGalleryConnection — MySQL connection strings for the three databases (see Database).
  • AuthSettings:BaseUrl / ClientSecret / Site / UseSecureCookie — the centralized QDBAuth service base URL, the shared X-QDB-Client-Secret header value, the site identifier this API authenticates as, and whether the session cookie requires HTTPS.
  • Wasabi:AccessKey / SecretKey / BucketName / ServiceUrl / Region — S3-compatible storage credentials for media uploads.
  • AllowedOrigins — array of origins allowed by CORS (must include the frontend's dev/prod URLs; AllowCredentials() is enabled so the session cookie can be sent cross-origin).

Treat the connection strings, client secret, and Wasabi keys as secrets — don't commit real values to a public location beyond what's already in appsettings.json for this environment.

Running locally

dotnet restore QDG-CMS-BE.sln
dotnet run --project QDG.CMS.API

Swagger UI is served at the application root (/) in both Development and Production, documenting every endpoint and the session-cookie auth scheme.

Authenticating

The API never exposes a JWT/bearer token to clients — only an opaque session cookie. To authenticate:

  1. POST /api/v1/auth/login with credentials. The response indicates whether a TOTP step is required.
  2. POST /api/v1/auth/totp-verify with the TOTP code. On success, the response sets the qdg_session cookie (HttpOnly, Secure per AuthSettings:UseSecureCookie, SameSite=Lax, path /api).
  3. From then on, every request to a session-protected endpoint must send that cookie. In Swagger UI, "Try it out" carries it automatically after step 2 — no manual header entry needed.
  4. GET /api/v1/auth/me returns the authenticated user's claims (id, email, name, role) if you need to confirm the session.
  5. POST /api/v1/auth/logout revokes the session with QDBAuth and clears the cookie.

Sessions are transparently refreshed server-side: SessionAuthenticationHandler calls back into QDBAuth to renew an expired access token as long as the refresh token is still valid, so a client never has to handle token refresh itself — only the cookie.

Typical workflows

Managing stories

  • List with filters: GET /api/v1/story?search=...&startDate=...&visible=true&page=1&pageSize=20 (see API Reference for the full filter set).
  • Fetch one: GET /api/v1/story/{id}.
  • Create: POST /api/v1/story with a CreateStoryDto body — include siteIds/newsTypeIds so the mapping tables are synced. If AccessScope is Restricted, StoryContent must contain the required access-tier tags or the call returns 400 with the missing tags.
  • Update: PUT /api/v1/story/{id} with an UpdateStoryDto body — same restricted-tag rule applies.
  • Delete: DELETE /api/v1/story/{id} — removes the story and its mapping rows together.
  • Story linking: GET /api/v1/story/{id}/parent and /{id}/children for the parent/child story-link widget; GET /api/v1/story/link-search?siteIds=...&query=... to find a story to link (requires at least one site id).

Uploading media (photo or video)

Media upload is two calls, not a single multipart POST:

  1. GET /api/v1/media/upload-url?... — get a presigned Wasabi PUT URL and the object key it's signed for.
  2. Upload the file bytes directly to Wasabi using that presigned URL (the CMS API never receives the file).
  3. POST /api/v1/media with a MediaSaveDto body referencing the object key from step 1, to persist the tblphotos row.

To replace a file on an existing item, repeat steps 1–2 for the new file, then PUT /api/v1/media/{id} with the new object key — omit any object-key field you didn't change and the existing stored file for that slot is kept.

DELETE /api/v1/media/{id}?type=photo|video removes both the database row and every associated Wasabi object (original plus resized variants for photos).

Populating dropdowns

Use GET /api/v1/lookup/bootstrap?keys=sites,authors,contributors to fetch several editor dropdowns in one call, or the individual /sites, /authors, /contributors endpoints. For racing-reference fields, use the search-as-you-type endpoints (/horses, /trainers, /jockeys — each requires a non-empty query) and the course/race pair (/courses, then /races?courseId=).

Health checks

GET /health (no auth required) returns a JSON report with per-check status for mysql and wasabi. This is what the deploy pipeline polls after each release to confirm the new build is serving traffic before declaring the deployment successful.

Updated by Claude on Sept. 2, 2026, 4:48 a.m. · Task: Create initial QDG CMS API user guide documentation in the QDG KB