feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -1,98 +1,103 @@
export async function login(email: string, password: string): Promise<{ token: string }> {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const res = await fetch('/api/v1/tenant/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken || '',
},
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Login failed');
return res.json();
import { authorizedFetch } from './auth/tokens';
type JsonValue = Record<string, any>;
async function jsonOrThrow<T>(response: Response, message: string): Promise<T> {
if (!response.ok) {
const body = await safeJson(response);
console.error('[API]', message, response.status, body);
throw new Error(message);
}
return (await response.json()) as T;
}
async function safeJson(response: Response): Promise<JsonValue | null> {
try {
return (await response.clone().json()) as JsonValue;
} catch {
return null;
}
}
export async function getEvents(): Promise<any[]> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch('/api/v1/tenant/events', {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Failed to load events');
const json = await res.json();
return json.data ?? [];
const response = await authorizedFetch('/api/v1/tenant/events');
const data = await jsonOrThrow<{ data?: any[] }>(response, 'Failed to load events');
return data.data ?? [];
}
export async function createEvent(payload: { name: string; slug: string; date?: string; is_active?: boolean }): Promise<number> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch('/api/v1/tenant/events', {
const response = await authorizedFetch('/api/v1/tenant/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Failed to create event');
const json = await res.json();
return json.id;
const data = await jsonOrThrow<{ id: number }>(response, 'Failed to create event');
return data.id;
}
export async function updateEvent(id: number, payload: Partial<{ name: string; slug: string; date?: string; is_active?: boolean }>): Promise<void> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}`, {
export async function updateEvent(
id: number,
payload: Partial<{ name: string; slug: string; date?: string; is_active?: boolean }>
): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/events/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Failed to update event');
if (!response.ok) {
await safeJson(response);
throw new Error('Failed to update event');
}
}
export async function getEventPhotos(id: number): Promise<any[]> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}/photos`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error('Failed to load photos');
const json = await res.json();
return json.data ?? [];
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/photos`);
const data = await jsonOrThrow<{ data?: any[] }>(response, 'Failed to load photos');
return data.data ?? [];
}
export async function featurePhoto(id: number) {
const token = localStorage.getItem('ta_token') || '';
await fetch(`/api/v1/tenant/photos/${id}/feature`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
export async function featurePhoto(id: number): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}/feature`, { method: 'POST' });
if (!response.ok) {
await safeJson(response);
throw new Error('Failed to feature photo');
}
}
export async function unfeaturePhoto(id: number) {
const token = localStorage.getItem('ta_token') || '';
await fetch(`/api/v1/tenant/photos/${id}/unfeature`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
export async function unfeaturePhoto(id: number): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}/unfeature`, { method: 'POST' });
if (!response.ok) {
await safeJson(response);
throw new Error('Failed to unfeature photo');
}
}
export async function getEvent(id: number): Promise<any> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error('Failed to load event');
return res.json();
const response = await authorizedFetch(`/api/v1/tenant/events/${id}`);
return jsonOrThrow<any>(response, 'Failed to load event');
}
export async function toggleEvent(id: number): Promise<boolean> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}/toggle`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error('Failed to toggle');
const json = await res.json();
return !!json.is_active;
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/toggle`, { method: 'POST' });
const data = await jsonOrThrow<{ is_active: boolean }>(response, 'Failed to toggle event');
return !!data.is_active;
}
export async function getEventStats(id: number): Promise<{ total: number; featured: number; likes: number }> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}/stats`, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error('Failed to load stats');
return res.json();
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/stats`);
return jsonOrThrow<{ total: number; featured: number; likes: number }>(response, 'Failed to load stats');
}
export async function createInviteLink(id: number): Promise<string> {
const token = localStorage.getItem('ta_token') || '';
const res = await fetch(`/api/v1/tenant/events/${id}/invites`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error('Failed to create invite');
const json = await res.json();
return json.link as string;
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/invites`, { method: 'POST' });
const data = await jsonOrThrow<{ link: string }>(response, 'Failed to create invite');
return data.link;
}
export async function deletePhoto(id: number) {
const token = localStorage.getItem('ta_token') || '';
await fetch(`/api/v1/tenant/photos/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
export async function deletePhoto(id: number): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}`, { method: 'DELETE' });
if (!response.ok) {
await safeJson(response);
throw new Error('Failed to delete photo');
}
}