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, andphotogallerydatabases - 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 sharedX-QDB-Client-Secretheader 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:
POST /api/v1/auth/loginwith credentials. The response indicates whether a TOTP step is required.POST /api/v1/auth/totp-verifywith the TOTP code. On success, the response sets theqdg_sessioncookie (HttpOnly,SecureperAuthSettings:UseSecureCookie,SameSite=Lax, path/api).- 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.
GET /api/v1/auth/mereturns the authenticated user's claims (id, email, name, role) if you need to confirm the session.POST /api/v1/auth/logoutrevokes 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/storywith aCreateStoryDtobody — includesiteIds/newsTypeIdsso the mapping tables are synced. IfAccessScopeisRestricted,StoryContentmust contain the required access-tier tags or the call returns400with the missing tags. - Update:
PUT /api/v1/story/{id}with anUpdateStoryDtobody — 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}/parentand/{id}/childrenfor 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:
GET /api/v1/media/upload-url?...— get a presigned Wasabi PUT URL and the object key it's signed for.- Upload the file bytes directly to Wasabi using that presigned URL (the CMS API never receives the file).
POST /api/v1/mediawith aMediaSaveDtobody referencing the object key from step 1, to persist thetblphotosrow.
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.