- load() fetches /api/groups in parallel with /api/invites; returns sorted groups array and groupsLoadError for partial failures - create action forwards groupIds[] to POST /api/invites so invited users are placed in the selected groups on registration - +page.svelte: group checkboxes via UserGroupsSection inside the form; amber warning banner when groups could not be loaded - page.svelte.test.ts: groups checkboxes + warning banner tests - page.server.test.ts: parallel fetch, sorting, error fallback, groupIds in POST body Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import { fail } from '@sveltejs/kit';
|
|
import { env } from '$env/dynamic/private';
|
|
import { parseBackendError } from '$lib/shared/errors';
|
|
import type { Actions, PageServerLoad } from './$types';
|
|
import type { components } from '$lib/generated/api';
|
|
|
|
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 apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
|
|
const [invitesRes, groupsRes] = await Promise.all([
|
|
fetch(`${apiUrl}/api/invites?status=${encodeURIComponent(status)}`),
|
|
fetch(`${apiUrl}/api/groups`)
|
|
]);
|
|
|
|
let invites: InviteListItem[] = [];
|
|
let loadError: string | null = null;
|
|
if (!invitesRes.ok) {
|
|
const backendError = await parseBackendError(invitesRes);
|
|
loadError = backendError?.code ?? 'INTERNAL_ERROR';
|
|
} else {
|
|
invites = await invitesRes.json();
|
|
}
|
|
|
|
let groups: UserGroup[] = [];
|
|
let groupsLoadError: string | null = null;
|
|
if (!groupsRes.ok) {
|
|
const backendError = await parseBackendError(groupsRes);
|
|
groupsLoadError = backendError?.code ?? 'INTERNAL_ERROR';
|
|
} else {
|
|
const raw: UserGroup[] = await groupsRes.json();
|
|
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 apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
const res = await fetch(`${apiUrl}/api/invites`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
label,
|
|
maxUses,
|
|
prefillFirstName,
|
|
prefillLastName,
|
|
prefillEmail,
|
|
expiresAt,
|
|
groupIds
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const backendError = await parseBackendError(res);
|
|
return fail(res.status, { createError: backendError?.code ?? 'INTERNAL_ERROR' });
|
|
}
|
|
|
|
const created: InviteListItem = await res.json();
|
|
return { created };
|
|
},
|
|
|
|
revoke: async ({ request, fetch }) => {
|
|
const formData = await request.formData();
|
|
const id = formData.get('id') as string;
|
|
|
|
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
const res = await fetch(`${apiUrl}/api/invites/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const backendError = await parseBackendError(res);
|
|
return fail(res.status, { revokeError: backendError?.code ?? 'INTERNAL_ERROR' });
|
|
}
|
|
|
|
return { revoked: id };
|
|
}
|
|
} satisfies Actions;
|