API Documents
Version 1 · Initial API integration doc based on codebase exploration
QDG CMS — API Integration
QDG CMS is a pure API-consuming frontend. All data access goes through the QDG CMS API backend (see the qdg-cms-api KB project) or, for raw file bytes, directly to Wasabi object storage. There is no server-side code in this repository.
HTTP client
src/config/axiosInstance.ts exports a single shared Axios instance used by all backend calls:
const instance = axios.create({
baseURL: config.apiBaseUrl,
headers: { 'Content-Type': 'application/json' },
responseType: 'json',
withCredentials: true,
})
- A request interceptor waits for an initial "bootstrap ready" promise (resolved once the startup
GET /Auth/mesession-restore call settles) before sending any non-auth request. - A response interceptor dispatches a global
auth:logoutbrowser event on any401from a non-auth endpoint, which drives the app back to a logged-out state.
Base URL / environment config
Configured via Vite env vars, read in src/config/envConfig.ts:
| File | VITE_API_BASE_URL |
|---|---|
.env / .env.development |
http://localhost:5087/api/v1 |
.env.production |
http://138.226.220.168:8014/api/v1 |
All three env files also set VITE_CENTRAL_AUTH_LOGIN_URL=https://auth.qdatasite.com/login/.
Authentication
No JWT/tokens are stored client-side. The API issues an HttpOnly session cookie (qdg_session); the browser sends it automatically because every request uses withCredentials: true. Login and 2FA happen entirely outside this app, on the centralized auth portal.
Flow:
- User is redirected to
https://auth.qdatasite.com/login/. - After authenticating, the portal redirects back with a
?token=...query param. AuthContextexchanges it viaPOST /Auth/exchange-token, which sets theqdg_sessioncookie.localStoragekeyqdg_session_expires_at(src/config/sessionExpiry.ts) holds a non-authoritative expiry timestamp, used only to decide whether it's worth calling/Auth/meon load and to drive a client-side auto-logout timer — the server session is always the source of truth.
authService.ts endpoints:
POST /Auth/exchange-tokenGET /Auth/mePOST /Auth/logoutPOST /Auth/create-user
(Older /Auth/login, /Auth/totp-verify endpoints are dead code, commented out — in-app login was replaced by the centralized auth redirect.)
Media API (live)
src/services/api/mediaService.ts — backs Image Gallery, Video Gallery, Video Uploader, and the Image Editor's save path:
GET /Media— list, paginated and filterable bytype=photo|video.GET /Media/{id}?type=DELETE /Media/{id}?type=GET /Media/upload-url?type=&fileName=&contentType=— returns a presigned Wasabi PUT URL and object key.POST /Media— create a media record.PUT /Media/{id}— update a media record.
Upload flow (presigned URL, two-phase)
This is a deliberate architecture decision to keep raw file bytes off the CMS API entirely:
- Frontend calls
GET /Media/upload-urlto get a short-lived presigned PUT URL + object key from Wasabi. - Frontend
PUTs the raw file bytes directly to Wasabi with that URL, via a separateaxios.put(presignedUrl, file, ...)call that deliberately bypasses the sharedaxiosInstance(no cookies/credentials should be sent to Wasabi). - Frontend calls
POST/PUT /Media[/{id}]with the resulting object key plus metadata to persist the record.
Photos additionally get 5 size-variant object keys — smallObjectKey, mediumObjectKey, hdObjectKey, hiresObjectKey, largeObjectKey (see src/types/media.ts) — each uploaded independently through the same flow. Videos have no variants.
Download/preview URLs are also presigned Wasabi URLs with a short TTL (~5 minutes); the app refetches them just before use rather than caching them.
Story API (live)
src/services/api/storyService.ts:
GET /Story— listGET /Story/{id}DELETE /Story/{id}GET /Story/link-search?query=&siteIds=GET /Story/{id}/childrenGET /Story/{id}/parentPOST /StoryPUT /Story/{id}
src/utils/storyMappings.ts maps between the wire DTOs (StoryReadDto, wire DTO for create/update) and the internal form record shape used by the Create/Edit Story page.
Lookup API (live)
src/services/api/lookupService.ts — reference/autocomplete data:
GET /Lookup/bootstrap?keys=, /Lookup/sites, /Lookup/authors, /Lookup/contributors, /Lookup/news-types?siteIds=, /Lookup/horses?query=, /Lookup/trainers?query=, /Lookup/jockeys?query=, /Lookup/courses, /Lookup/races?courseId=.
Mock/legacy layers (not fully live)
imageService.tsandvideoService.ts— mostlyUSE_MOCK = true, representing planned/legacy endpoints (/Image,/Image/filter-options,/Image/editor,/Image/{id}/download-url,/Video,/Video/filter-options,/Video/{id}/file,/Video/{id}/download-url) markedTODO(backend). The real Image/Video Gallery listing now goes throughmediaServiceinstead.storyFormService.ts— explicitly deprecated/mock-only; kept for reference. Real Create/Edit Story now usesstoryService+mediaServicedirectly.
Error handling
A 401 on any non-auth call triggers a global auth:logout event (see above), which the app listens for to drop the user back to a logged-out state — there is no per-call retry/refresh logic since the session is cookie-based and refresh happens via the centralized auth portal.