103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
import { getDeviceId } from '../lib/device';
|
|
|
|
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++) {
|
|
let 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) {
|
|
const errorText = await res.text();
|
|
if (res.status === 419) {
|
|
throw new Error('CSRF Token mismatch. This usually means:\n\n' +
|
|
'1. The page needs to be refreshed\n' +
|
|
'2. Check if <meta name="csrf-token"> is present in HTML source\n' +
|
|
'3. API routes might need CSRF exemption in VerifyCsrfToken middleware');
|
|
}
|
|
throw new Error(`Like failed: ${res.status} - ${errorText}`);
|
|
}
|
|
|
|
const json = await res.json();
|
|
return json.likes_count ?? json.data?.likes_count ?? 0;
|
|
}
|
|
|
|
export async function uploadPhoto(slug: 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/${slug}/upload`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: formData,
|
|
// Don't set Content-Type for FormData - let browser handle it with boundary
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
if (res.status === 419) {
|
|
throw new Error('CSRF Token mismatch during upload.\n\n' +
|
|
'This usually means:\n' +
|
|
'1. API routes need CSRF exemption in VerifyCsrfToken middleware\n' +
|
|
'2. Check if <meta name="csrf-token"> is present in page source\n' +
|
|
'3. The page might need to be refreshed');
|
|
}
|
|
throw new Error(`Upload failed: ${res.status} - ${errorText}`);
|
|
}
|
|
|
|
const json = await res.json();
|
|
return json.photo_id ?? json.id ?? json.data?.id ?? 0;
|
|
}
|
|
|