Files
fotospiel-app/resources/js/guest/services/photosApi.ts
2025-11-01 13:19:07 +01:00

137 lines
3.7 KiB
TypeScript

import { getDeviceId } from '../lib/device';
export type UploadError = Error & {
code?: string;
status?: number;
meta?: Record<string, unknown>;
};
function getCsrfToken(): string | null {
// Method 1: Meta tag (preferred for SPA)
const metaToken = document.querySelector('meta[name="csrf-token"]');
if (metaToken) {
return (metaToken as HTMLMetaElement).getAttribute('content') || null;
}
// Method 2: XSRF-TOKEN cookie (Sanctum fallback)
const name = 'XSRF-TOKEN=';
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
const c = ca[i].trimStart();
if (c.startsWith(name)) {
const token = c.substring(name.length);
try {
// Decode base64 if needed
return decodeURIComponent(atob(token));
} catch {
return token;
}
}
}
console.warn('No CSRF token found - API requests may fail');
return null;
}
function getCsrfHeaders(): Record<string, string> {
const token = getCsrfToken();
const headers: Record<string, string> = {
'X-Device-Id': getDeviceId(),
'Accept': 'application/json',
};
if (token) {
headers['X-CSRF-TOKEN'] = token;
headers['X-XSRF-TOKEN'] = token;
}
return headers;
}
export async function likePhoto(id: number): Promise<number> {
const headers = getCsrfHeaders();
const res = await fetch(`/api/v1/photos/${id}/like`, {
method: 'POST',
credentials: 'include',
headers: {
...headers,
'Content-Type': 'application/json',
},
});
if (!res.ok) {
let payload: any = null;
try {
payload = await res.clone().json();
} catch {}
if (res.status === 419) {
const error: UploadError = new Error('CSRF token mismatch. Please refresh the page and try again.');
error.code = 'csrf_mismatch';
error.status = res.status;
throw error;
}
const error: UploadError = new Error(
payload?.error?.message ?? `Like failed: ${res.status}`
);
error.code = payload?.error?.code ?? 'like_failed';
error.status = res.status;
if (payload?.error?.meta) {
error.meta = payload.error.meta as Record<string, unknown>;
}
throw error;
}
const json = await res.json();
return json.likes_count ?? json.data?.likes_count ?? 0;
}
export async function uploadPhoto(eventToken: string, file: File, taskId?: number, emotionSlug?: string): Promise<number> {
const formData = new FormData();
formData.append('photo', file, `photo-${Date.now()}.jpg`);
if (taskId) formData.append('task_id', taskId.toString());
if (emotionSlug) formData.append('emotion_slug', emotionSlug);
formData.append('device_id', getDeviceId());
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/upload`, {
method: 'POST',
credentials: 'include',
body: formData,
// Don't set Content-Type for FormData - let browser handle it with boundary
});
if (!res.ok) {
let payload: any = null;
try {
payload = await res.clone().json();
} catch {}
if (res.status === 419) {
const csrfError: UploadError = new Error(
'CSRF token mismatch during upload. Please refresh the page and try again.'
);
csrfError.code = 'csrf_mismatch';
csrfError.status = res.status;
throw csrfError;
}
const error: UploadError = new Error(
payload?.error?.message ?? `Upload failed: ${res.status}`
);
error.code = payload?.error?.code ?? 'upload_failed';
error.status = res.status;
if (payload?.error?.meta) {
error.meta = payload.error.meta as Record<string, unknown>;
}
throw error;
}
const json = await res.json();
return json.photo_id ?? json.id ?? json.data?.id ?? 0;
}