Files
familienarchiv/frontend/src/routes/admin/invites/+page.server.ts
Marcel 08eec086a9 refactor(admin/invites): migrate to createApiClient; fix Sentry mock event
Replace manual fetch(${apiUrl}/api/...) calls in load, create, and revoke
with createApiClient(fetch) so auth injection is handled by handleFetch
and the typed API contract is enforced at compile time.

Also fix pre-existing load test failures caused by Sentry's load wrapper
reading event.request.method (add request to the mock event object).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:46:28 +02:00

94 lines
3.3 KiB
TypeScript

import { fail } from '@sveltejs/kit';
import { createApiClient } from '$lib/shared/api.server';
import type { Actions, PageServerLoad } from './$types';
import type { components } from '$lib/generated/api';
// The spec marks shareableUrl optional but the backend always populates it.
// Keeping the required shape here avoids null-guarding throughout the page component.
export interface InviteListItem {
id: string;
code: string;
displayCode: string;
label?: string;
useCount: number;
maxUses?: number;
expiresAt?: string;
revoked: boolean;
status: string;
createdAt: string;
shareableUrl: string;
}
export type UserGroup = components['schemas']['UserGroup'];
export const load: PageServerLoad = async ({ url, fetch }) => {
const status = url.searchParams.get('status') ?? 'active';
const api = createApiClient(fetch);
const [invitesResult, groupsResult] = await Promise.all([
api.GET('/api/invites', { params: { query: { status } } }),
api.GET('/api/groups')
]);
let invites: InviteListItem[] = [];
let loadError: string | null = null;
if (!invitesResult.response.ok) {
const code = (invitesResult.error as unknown as { code?: string })?.code;
loadError = code ?? 'INTERNAL_ERROR';
} else {
invites = (invitesResult.data ?? []) as unknown as InviteListItem[];
}
let groups: UserGroup[] = [];
let groupsLoadError: string | null = null;
if (!groupsResult.response.ok) {
const code = (groupsResult.error as unknown as { code?: string })?.code;
groupsLoadError = code ?? 'INTERNAL_ERROR';
} else {
const raw = groupsResult.data ?? [];
groups = [...raw].sort((a, b) => a.name.localeCompare(b.name));
}
return { invites, status, loadError, groups, groupsLoadError };
};
export const actions = {
create: async ({ request, fetch }) => {
const formData = await request.formData();
const label = (formData.get('label') as string) || undefined;
const maxUsesRaw = formData.get('maxUses') as string;
const maxUses = maxUsesRaw ? parseInt(maxUsesRaw, 10) : undefined;
const prefillFirstName = (formData.get('prefillFirstName') as string) || undefined;
const prefillLastName = (formData.get('prefillLastName') as string) || undefined;
const prefillEmail = (formData.get('prefillEmail') as string) || undefined;
const expiresAt = (formData.get('expiresAt') as string) || undefined;
const groupIds = formData.getAll('groupIds') as string[];
const api = createApiClient(fetch);
const result = await api.POST('/api/invites', {
body: { label, maxUses, prefillFirstName, prefillLastName, prefillEmail, expiresAt, groupIds }
});
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { createError: code ?? 'INTERNAL_ERROR' });
}
return { created: result.data! as unknown as InviteListItem };
},
revoke: async ({ request, fetch }) => {
const formData = await request.formData();
const id = formData.get('id') as string;
const api = createApiClient(fetch);
const result = await api.DELETE('/api/invites/{id}', { params: { path: { id } } });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { revokeError: code ?? 'INTERNAL_ERROR' });
}
return { revoked: id };
}
} satisfies Actions;