Some checks are pending
CI / validate (push) Waiting to run
Cards-Web läuft jetzt auch über die 15min-JWT-TTL hinaus weiter: - Login schickt credentials:'include' → SSO-Cookie auf .mana.how wird gesetzt (mana-auth-Standard). - tryRefresh() ruft mana-auth POST /api/v1/auth/refresh mit Cookie- Auth, holt frischen accessToken, legt ihn in localStorage. Multi- Caller werden zu einer Promise gecoalesced (kein Refresh-Storm). - ensureFreshToken() prüft beim Konstruktor + vor jedem API-Request, ob der Token noch ≥60s gültig ist. Wenn nicht, proaktiver Refresh. - API-Client: 401 → tryRefresh() → exactly-once Retry. Erst wenn das auch 401 gibt, wird die Session lokal geleert (führt User zurück auf /). - uploadMedia (multipart) parallel mit demselben Pattern. - Logout ruft mana-auth /logout mit Cookie-Auth, damit das SSO-Cookie für andere *.mana.how-Apps auch weg ist (best-effort, nicht-fatal). - Boot-Pfad: bei abgelaufenem Token im localStorage behalten wir das User-Profil temporär und versuchen einen still-Refresh — User sieht beim Reload kein Login-Flackern, wenn die Cookie-Session noch lebt. svelte-check 384 files 0 errors, 7 Web-Tests grün, prod-Build sauber. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
1.6 KiB
TypeScript
59 lines
1.6 KiB
TypeScript
import { API_BASE, ApiError } from './client.ts';
|
|
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
|
|
|
export interface MediaUploadResult {
|
|
id: string;
|
|
url: string;
|
|
mime_type: string;
|
|
kind: 'image' | 'audio' | 'video' | 'other';
|
|
size_bytes: number;
|
|
original_filename: string | null;
|
|
}
|
|
|
|
/**
|
|
* Lädt ein einzelnes File via multipart/form-data hoch. Anders als der
|
|
* Standard-API-Helper geht das nicht über `Content-Type: application/json`,
|
|
* deshalb hier ein eigener Pfad mit FormData + manuellem fetch.
|
|
*/
|
|
export async function uploadMedia(file: File | Blob, filename?: string): Promise<MediaUploadResult> {
|
|
const form = new FormData();
|
|
const wrapped = file instanceof File ? file : new File([file], filename ?? 'upload.bin');
|
|
form.append('file', wrapped);
|
|
|
|
await devUser.ensureFreshToken();
|
|
|
|
const buildHeaders = (): Record<string, string> => {
|
|
const h: Record<string, string> = {};
|
|
if (devUser.token) h['Authorization'] = `Bearer ${devUser.token}`;
|
|
else if (devUser.stubId) h['X-User-Id'] = devUser.stubId;
|
|
return h;
|
|
};
|
|
|
|
let res = await fetch(`${API_BASE}/api/v1/media/upload`, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: buildHeaders(),
|
|
});
|
|
|
|
if (res.status === 401 && devUser.token) {
|
|
const refreshed = await devUser.tryRefresh();
|
|
if (refreshed) {
|
|
res = await fetch(`${API_BASE}/api/v1/media/upload`, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: buildHeaders(),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let body: unknown = null;
|
|
try {
|
|
body = await res.json();
|
|
} catch {
|
|
body = await res.text().catch(() => null);
|
|
}
|
|
throw new ApiError(res.status, body, `media upload failed: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|