stage 1 of oauth removal, switch to sanctum pat tokens

This commit is contained in:
Codex Agent
2025-11-06 20:35:58 +01:00
parent c9783bd57b
commit 776da57ca9
47 changed files with 1571 additions and 2555 deletions

View File

@@ -838,10 +838,17 @@ function eventEndpoint(slug: string): string {
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
}
export async function getEvents(): Promise<TenantEvent[]> {
const response = await authorizedFetch('/api/v1/tenant/events');
const data = await jsonOrThrow<EventListResponse>(response, 'Failed to load events');
return (data.data ?? []).map(normalizeEvent);
export async function getEvents(options?: { force?: boolean }): Promise<TenantEvent[]> {
return cachedFetch(
CacheKeys.events,
async () => {
const response = await authorizedFetch('/api/v1/tenant/events');
const data = await jsonOrThrow<EventListResponse>(response, 'Failed to load events');
return (data.data ?? []).map(normalizeEvent);
},
DEFAULT_CACHE_TTL,
options?.force === true,
);
}
export async function createEvent(payload: EventSavePayload): Promise<{ event: TenantEvent; balance: number }> {
@@ -851,7 +858,9 @@ export async function createEvent(payload: EventSavePayload): Promise<{ event: T
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<CreatedEventResponse>(response, 'Failed to create event');
return { event: normalizeEvent(data.data), balance: data.balance };
const result = { event: normalizeEvent(data.data), balance: data.balance };
invalidateTenantApiCache([CacheKeys.events, CacheKeys.dashboard]);
return result;
}
export async function updateEvent(slug: string, payload: Partial<EventSavePayload>): Promise<TenantEvent> {
@@ -861,7 +870,9 @@ export async function updateEvent(slug: string, payload: Partial<EventSavePayloa
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<EventResponse>(response, 'Failed to update event');
return normalizeEvent(data.data);
const event = normalizeEvent(data.data);
invalidateTenantApiCache([CacheKeys.events, CacheKeys.dashboard]);
return event;
}
export async function getEvent(slug: string): Promise<TenantEvent> {
@@ -1130,38 +1141,52 @@ async function fetchTenantPackagesEndpoint(): Promise<Response> {
return first;
}
export async function getDashboardSummary(): Promise<DashboardSummary | null> {
const response = await authorizedFetch('/api/v1/tenant/dashboard');
if (response.status === 404) {
return null;
}
if (!response.ok) {
const payload = await safeJson(response);
const fallbackMessage = i18n.t('dashboard:errors.loadFailed', 'Dashboard konnte nicht geladen werden.');
emitApiErrorEvent({ message: fallbackMessage, status: response.status });
console.error('[API] Failed to load dashboard', response.status, payload);
throw new Error(fallbackMessage);
}
const json = (await response.json()) as JsonValue;
return normalizeDashboard(json);
export async function getDashboardSummary(options?: { force?: boolean }): Promise<DashboardSummary | null> {
return cachedFetch(
CacheKeys.dashboard,
async () => {
const response = await authorizedFetch('/api/v1/tenant/dashboard');
if (response.status === 404) {
return null;
}
if (!response.ok) {
const payload = await safeJson(response);
const fallbackMessage = i18n.t('dashboard:errors.loadFailed', 'Dashboard konnte nicht geladen werden.');
emitApiErrorEvent({ message: fallbackMessage, status: response.status });
console.error('[API] Failed to load dashboard', response.status, payload);
throw new Error(fallbackMessage);
}
const json = (await response.json()) as JsonValue;
return normalizeDashboard(json);
},
DEFAULT_CACHE_TTL,
options?.force === true,
);
}
export async function getTenantPackagesOverview(): Promise<{
export async function getTenantPackagesOverview(options?: { force?: boolean }): Promise<{
packages: TenantPackageSummary[];
activePackage: TenantPackageSummary | null;
}> {
const response = await fetchTenantPackagesEndpoint();
if (!response.ok) {
const payload = await safeJson(response);
const fallbackMessage = i18n.t('common:errors.generic', 'Etwas ist schiefgelaufen. Bitte versuche es erneut.');
emitApiErrorEvent({ message: fallbackMessage, status: response.status });
console.error('[API] Failed to load tenant packages', response.status, payload);
throw new Error(fallbackMessage);
}
const data = (await response.json()) as TenantPackagesResponse;
const packages = Array.isArray(data.data) ? data.data.map(normalizeTenantPackage) : [];
const activePackage = data.active_package ? normalizeTenantPackage(data.active_package) : null;
return { packages, activePackage };
return cachedFetch(
CacheKeys.packages,
async () => {
const response = await fetchTenantPackagesEndpoint();
if (!response.ok) {
const payload = await safeJson(response);
const fallbackMessage = i18n.t('common:errors.generic', 'Etwas ist schiefgelaufen. Bitte versuche es erneut.');
emitApiErrorEvent({ message: fallbackMessage, status: response.status });
console.error('[API] Failed to load tenant packages', response.status, payload);
throw new Error(fallbackMessage);
}
const data = (await response.json()) as TenantPackagesResponse;
const packages = Array.isArray(data.data) ? data.data.map(normalizeTenantPackage) : [];
const activePackage = data.active_package ? normalizeTenantPackage(data.active_package) : null;
return { packages, activePackage };
},
DEFAULT_CACHE_TTL * 5,
options?.force === true,
);
}
export type NotificationPreferenceResponse = {
@@ -1651,3 +1676,67 @@ export async function removeEventMember(eventIdentifier: number | string, member
throw new Error('Failed to remove member');
}
}
type CacheEntry<T> = {
value?: T;
expiresAt: number;
promise?: Promise<T>;
};
const tenantApiCache = new Map<string, CacheEntry<unknown>>();
const DEFAULT_CACHE_TTL = 60_000;
const CacheKeys = {
dashboard: 'tenant:dashboard',
events: 'tenant:events',
packages: 'tenant:packages',
} as const;
function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = DEFAULT_CACHE_TTL,
force = false,
): Promise<T> {
if (force) {
tenantApiCache.delete(key);
}
const now = Date.now();
const existing = tenantApiCache.get(key) as CacheEntry<T> | undefined;
if (!force && existing) {
if (existing.promise) {
return existing.promise;
}
if (existing.value !== undefined && existing.expiresAt > now) {
return Promise.resolve(existing.value);
}
}
const promise = fetcher()
.then((value) => {
tenantApiCache.set(key, { value, expiresAt: Date.now() + ttl });
return value;
})
.catch((error) => {
tenantApiCache.delete(key);
throw error;
});
tenantApiCache.set(key, { value: existing?.value, expiresAt: existing?.expiresAt ?? 0, promise });
return promise;
}
export function invalidateTenantApiCache(keys?: string | string[]): void {
if (!keys) {
tenantApiCache.clear();
return;
}
const entries = Array.isArray(keys) ? keys : [keys];
for (const key of entries) {
tenantApiCache.delete(key);
}
}