QDG Knowledge Base Read-only viewer QWebHub
general

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/me session-restore call settles) before sending any non-auth request.
  • A response interceptor dispatches a global auth:logout browser event on any 401 from 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:

  1. User is redirected to https://auth.qdatasite.com/login/.
  2. After authenticating, the portal redirects back with a ?token=... query param.
  3. AuthContext exchanges it via POST /Auth/exchange-token, which sets the qdg_session cookie.
  4. localStorage key qdg_session_expires_at (src/config/sessionExpiry.ts) holds a non-authoritative expiry timestamp, used only to decide whether it's worth calling /Auth/me on 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-token
  • GET /Auth/me
  • POST /Auth/logout
  • POST /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 by type=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:

  1. Frontend calls GET /Media/upload-url to get a short-lived presigned PUT URL + object key from Wasabi.
  2. Frontend PUTs the raw file bytes directly to Wasabi with that URL, via a separate axios.put(presignedUrl, file, ...) call that deliberately bypasses the shared axiosInstance (no cookies/credentials should be sent to Wasabi).
  3. 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 — list
  • GET /Story/{id}
  • DELETE /Story/{id}
  • GET /Story/link-search?query=&siteIds=
  • GET /Story/{id}/children
  • GET /Story/{id}/parent
  • POST /Story
  • PUT /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.ts and videoService.ts — mostly USE_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) marked TODO(backend). The real Image/Video Gallery listing now goes through mediaService instead.
  • storyFormService.ts — explicitly deprecated/mock-only; kept for reference. Real Create/Edit Story now uses storyService + mediaService directly.

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.

Updated by Claude on Sept. 2, 2026, 6:21 a.m.