Files
Piik/src/client/lib/api.ts
T

201 lines
5.1 KiB
TypeScript

import { say } from "../ui/copy";
import {
createRoomResponseSchema,
roomAccessUpdateResponseSchema,
type CreateRoomResponse,
type CodeEntryPolicy,
type RoomAccessUpdateRequest,
type RoomAccessUpdateResponse,
} from "../../shared/protocol";
export interface SiteAccessStatus {
required: boolean;
authenticated: boolean;
}
export class ApiError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message);
this.name = "ApiError";
}
}
async function responseBody(response: Response): Promise<unknown> {
try {
return await response.json();
} catch {
throw new ApiError(say("host.err.serverError"), response.status);
}
}
function parseSiteAccessStatus(value: unknown): SiteAccessStatus {
if (!value || typeof value !== "object") {
throw new ApiError(say("host.err.serverError"), 502);
}
const record = value as Record<string, unknown>;
if (
typeof record.required !== "boolean" ||
typeof record.authenticated !== "boolean"
) {
throw new ApiError(say("host.err.serverError"), 502);
}
return {
required: record.required,
authenticated: record.authenticated,
};
}
export async function getSiteAccess(): Promise<SiteAccessStatus> {
const response = await fetch("/api/site-access", {
headers: { Accept: "application/json" },
});
const body = await responseBody(response);
if (!response.ok) {
throw new ApiError(
response.status === 401
? say("gate.expired")
: say("gate.serviceUnavailable", {
status: String(response.status),
}),
response.status,
);
}
return parseSiteAccessStatus(body);
}
export async function authenticateSiteAccess(
password: string,
): Promise<SiteAccessStatus> {
const response = await fetch("/api/site-access", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${password.trim()}`,
},
});
const body = await responseBody(response);
if (!response.ok) {
throw new ApiError(
response.status === 401
? say("gate.wrong")
: say("gate.serviceUnavailable", {
status: String(response.status),
}),
response.status,
);
}
return parseSiteAccessStatus(body);
}
export async function createRoom(
codeEntryPolicy: CodeEntryPolicy,
roomPassword: string | null,
preferredRoomId: string | null = null,
): Promise<CreateRoomResponse> {
const response = await fetch("/api/rooms", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
codeEntryPolicy,
...(roomPassword === null ? {} : { roomPassword }),
...(preferredRoomId === null ? {} : { preferredRoomId }),
}),
});
const body = await responseBody(response);
if (!response.ok) {
throw new ApiError(
response.status === 401
? say("gate.expired")
: say("host.err.createRoomStatus", {
status: String(response.status),
}),
response.status,
);
}
const parsed = createRoomResponseSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(say("host.err.serverError"), 502);
}
return parsed.data;
}
export async function replaceOwnedRoom(
roomId: string,
hostToken: string,
codeEntryPolicy: CodeEntryPolicy,
roomPassword: string | null,
): Promise<CreateRoomResponse> {
const response = await fetch(`/api/rooms/${roomId}/replacement`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${hostToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
codeEntryPolicy,
...(roomPassword === null ? {} : { roomPassword }),
}),
});
const body = await responseBody(response);
if (!response.ok) {
throw new ApiError(
response.status === 404
? say("viewer.msg.notFound")
: say("host.err.replaceRoomStatus", {
status: String(response.status),
}),
response.status,
);
}
const parsed = createRoomResponseSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(say("host.err.serverError"), 502);
}
return parsed.data;
}
export async function updateRoomAccess(
roomId: string,
hostToken: string,
request: RoomAccessUpdateRequest,
): Promise<RoomAccessUpdateResponse> {
const response = await fetch(`/api/rooms/${roomId}/access`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${hostToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
const body = await responseBody(response);
if (!response.ok) {
throw new ApiError(
response.status === 401
? say("gate.expired")
: response.status === 404
? say("viewer.msg.notFound")
: say("host.err.updateRoomStatus", {
status: String(response.status),
}),
response.status,
);
}
const parsed = roomAccessUpdateResponseSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(say("host.err.serverError"), 502);
}
return parsed.data;
}