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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user