Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.

This commit is contained in:
Codex Agent
2025-10-12 10:32:37 +02:00
parent d04e234ca0
commit 9394c3171e
73 changed files with 3277 additions and 911 deletions

View File

@@ -126,6 +126,20 @@ export type EventMember = {
type EventListResponse = { data?: TenantEvent[] };
type EventResponse = { data: TenantEvent };
export type EventJoinToken = {
id: number;
token: string;
url: string;
label: string | null;
usage_limit: number | null;
usage_count: number;
expires_at: string | null;
revoked_at: string | null;
is_active: boolean;
created_at: string | null;
metadata: Record<string, unknown>;
};
type CreatedEventResponse = { message: string; data: TenantEvent; balance: number };
type PhotoResponse = { message: string; data: TenantPhoto };
@@ -256,6 +270,22 @@ function normalizeMember(member: JsonValue): EventMember {
};
}
function normalizeJoinToken(raw: JsonValue): EventJoinToken {
return {
id: Number(raw.id ?? 0),
token: String(raw.token ?? ''),
url: String(raw.url ?? ''),
label: raw.label ?? null,
usage_limit: raw.usage_limit ?? null,
usage_count: Number(raw.usage_count ?? 0),
expires_at: raw.expires_at ?? null,
revoked_at: raw.revoked_at ?? null,
is_active: Boolean(raw.is_active),
created_at: raw.created_at ?? null,
metadata: (raw.metadata ?? {}) as Record<string, unknown>,
};
}
function eventEndpoint(slug: string): string {
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
}
@@ -337,9 +367,40 @@ export async function getEventStats(slug: string): Promise<EventStats> {
};
}
export async function createInviteLink(slug: string): Promise<{ link: string; token: string }> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/invites`, { method: 'POST' });
return jsonOrThrow<{ link: string; token: string }>(response, 'Failed to create invite');
export async function getEventJoinTokens(slug: string): Promise<EventJoinToken[]> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`);
const payload = await jsonOrThrow<{ data: JsonValue[] }>(response, 'Failed to load join tokens');
const list = Array.isArray(payload.data) ? payload.data : [];
return list.map(normalizeJoinToken);
}
export async function createInviteLink(
slug: string,
payload?: { label?: string; usage_limit?: number; expires_at?: string }
): Promise<EventJoinToken> {
const body = JSON.stringify(payload ?? {});
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create invite');
return normalizeJoinToken(data.data ?? {});
}
export async function revokeEventJoinToken(
slug: string,
tokenId: number,
reason?: string
): Promise<EventJoinToken> {
const options: RequestInit = { method: 'DELETE' };
if (reason) {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify({ reason });
}
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens/${tokenId}`, options);
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to revoke join token');
return normalizeJoinToken(data.data ?? {});
}
export type Package = {