All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m15s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m29s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 59s
- Add load() unit tests for admin/users/[id] (permission gate, 404, success) - Rename .test.ts → .spec.ts for consistency with rest of suite - Add @Schema(requiredMode=REQUIRED) to InviteListItem.shareableUrl - Add client-side allowlist for invite status query param Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.5 KiB
TypeScript
100 lines
3.5 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'];
|
|
|
|
const VALID_STATUSES = ['ACTIVE', 'REVOKED', 'EXPIRED'] as const;
|
|
type InviteStatus = (typeof VALID_STATUSES)[number];
|
|
|
|
export const load: PageServerLoad = async ({ url, fetch }) => {
|
|
const rawStatus = url.searchParams.get('status');
|
|
const status: InviteStatus | 'active' = VALID_STATUSES.includes(rawStatus as InviteStatus)
|
|
? (rawStatus as InviteStatus)
|
|
: '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;
|