Support cross-origin frontends alongside same-origin setups

- django-cors-headers with env-driven CORS_ALLOWED_ORIGINS,
  CORS_ALLOW_ALL_ORIGINS, CORS_ALLOW_CREDENTIALS and CSRF_TRUSTED_ORIGINS;
  same-origin traffic is unaffected and a disallowed origin gets no CORS
  headers. Token auth needs no cookies, so credentials stay off by default.
- TRUST_PROXY_HEADERS=true lets a TLS-terminating proxy supply
  X-Forwarded-Proto/Host for correct absolute URLs.
- API media URLs (raw/thumbnail/upload/similarity/staged previews) are now
  absolute, built from the request host, so <img>/<video>/fetch() keep
  working when the SPA is served from another origin. Signed URLs are still
  per-user; nothing is stored in the DB.
- The SPA gains VITE_API_BASE (build-time, empty = same-origin) applied by
  a small apiUrl() helper used for XHR/fetch and the few URL fallbacks.

Verified with a throwaway instance: preflight and GET responses carry the
allowed origin, foreign origins get nothing, media GETs include CORS for
cross-origin fetch(), and payload URLs use the request host (dev :8000
unchanged).
This commit is contained in:
2026-09-17 22:50:12 -05:00
parent 98a674d55e
commit 16907c39ca
16 changed files with 139 additions and 52 deletions
+15 -2
View File
@@ -2,6 +2,19 @@ import type { TempUpload } from "@/lib/types";
const TOKEN_KEY = "j621.token";
/**
* Backend origin when the SPA is served from somewhere else (build-time
* `VITE_API_BASE`, e.g. `https://j621-api.example.com`). Empty means
* same-origin, which is the default and needs no configuration.
*/
const API_BASE = String(import.meta.env.VITE_API_BASE ?? "").replace(/\/+$/, "");
/** Absolutise a backend path; external/absolute URLs pass through. */
export function apiUrl(path: string): string {
if (!path.startsWith("/")) return path;
return `${API_BASE}${path}`;
}
export function getToken(): string | null {
return window.localStorage.getItem(TOKEN_KEY);
}
@@ -69,7 +82,7 @@ export async function api<T>(path: string, options: ApiOptions = {}): Promise<T>
const token = getToken();
if (token) finalHeaders.set("Authorization", `Token ${token}`);
const response = await fetch(path, { ...rest, headers: finalHeaders, body });
const response = await fetch(apiUrl(path), { ...rest, headers: finalHeaders, body });
if (!response.ok) {
let data: unknown = null;
try {
@@ -99,7 +112,7 @@ export function uploadFile<T = TempUpload>(
}
const request = new XMLHttpRequest();
request.open("POST", path);
request.open("POST", apiUrl(path));
const token = getToken();
if (token) request.setRequestHeader("Authorization", `Token ${token}`);
request.upload.addEventListener("progress", (event) => {