277 lines
6.5 KiB
TypeScript
277 lines
6.5 KiB
TypeScript
import { appConfig } from "@/config/env";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import { ApiError } from "@/lib/errors";
|
|
import type { z } from "zod";
|
|
import {
|
|
BillingStatusSchema,
|
|
CheckoutSessionResponseSchema,
|
|
DepotObjectSchema,
|
|
FirebaseTokenResponseSchema,
|
|
GetLivekitTokenResponseSchema,
|
|
HumanSchema,
|
|
ListInvitationsResponseSchema,
|
|
ListNetworksResponseSchema,
|
|
NetworkSchema,
|
|
NetworkUsageSchema,
|
|
PortalSessionResponseSchema,
|
|
PrepareUploadResponseSchema,
|
|
SignInResponseSchema,
|
|
} from "./types";
|
|
import type {
|
|
AcceptInvitationRequest,
|
|
AddMembersRequest,
|
|
BillingCadence,
|
|
CreateNetworkRequest,
|
|
PrepareUploadRequest,
|
|
RequestCodeRequest,
|
|
RevokeInvitationRequest,
|
|
SignInRequest,
|
|
} from "./types";
|
|
import type { LinkMetadata } from "@/lib/link-metadata";
|
|
|
|
interface ApiClientConfig {
|
|
baseUrl: string;
|
|
getToken: () => string | null;
|
|
onUnauthorized: () => void;
|
|
}
|
|
|
|
class ApiClient {
|
|
private config: ApiClientConfig;
|
|
|
|
constructor(config: ApiClientConfig) {
|
|
this.config = config;
|
|
}
|
|
|
|
private async fetch(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<Response> {
|
|
const headers: Record<string, string> = {};
|
|
|
|
if (body) {
|
|
headers["Content-Type"] = "application/json";
|
|
}
|
|
|
|
const token = this.config.getToken();
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(`${this.config.baseUrl}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
this.config.onUnauthorized();
|
|
throw new ApiError(401, "Unauthorized");
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => "Unknown error");
|
|
throw new ApiError(response.status, text);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
private async request<T>(
|
|
schema: z.ZodType<T>,
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<T> {
|
|
const response = await this.fetch(method, path, body);
|
|
const json = await response.json();
|
|
return schema.parse(json);
|
|
}
|
|
|
|
private async requestVoid(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<void> {
|
|
await this.fetch(method, path, body);
|
|
}
|
|
|
|
// --- Auth ---
|
|
|
|
async requestCode(data: RequestCodeRequest): Promise<void> {
|
|
await this.requestVoid("POST", "/auth/request-code", data);
|
|
}
|
|
|
|
async signIn(data: SignInRequest) {
|
|
return this.request(SignInResponseSchema, "POST", "/auth/sign-in", data);
|
|
}
|
|
|
|
async me() {
|
|
return this.request(HumanSchema, "GET", "/auth/me");
|
|
}
|
|
|
|
async signOut(): Promise<void> {
|
|
await this.requestVoid("POST", "/auth/sign-out");
|
|
}
|
|
|
|
async getFirebaseToken() {
|
|
return this.request(
|
|
FirebaseTokenResponseSchema,
|
|
"POST",
|
|
"/auth/firebase-token",
|
|
);
|
|
}
|
|
|
|
// TODO: security: require passing in the particle id once api deprecates this
|
|
async getParticleDownloadUrl(objectId: string): Promise<string> {
|
|
const response = await this.fetch(
|
|
"GET",
|
|
`/particles/${objectId}/download`,
|
|
);
|
|
const data = await response.json();
|
|
return data.url;
|
|
}
|
|
|
|
// --- Settings ---
|
|
|
|
async updateSettings(data: { email_notifications_enabled?: boolean }): Promise<void> {
|
|
await this.requestVoid("PATCH", "/humans/me/settings", data);
|
|
}
|
|
|
|
// --- Depot ---
|
|
|
|
async prepareUpload(data: PrepareUploadRequest) {
|
|
return this.request(
|
|
PrepareUploadResponseSchema,
|
|
"POST",
|
|
"/depot/upload",
|
|
data,
|
|
);
|
|
}
|
|
|
|
async confirmUpload(objectId: string) {
|
|
return this.request(
|
|
DepotObjectSchema,
|
|
"POST",
|
|
`/depot/objects/${objectId}/confirm`,
|
|
);
|
|
}
|
|
|
|
|
|
// --- Networks ---
|
|
|
|
async listNetworks() {
|
|
return this.request(
|
|
ListNetworksResponseSchema,
|
|
"GET",
|
|
"/networks",
|
|
);
|
|
}
|
|
|
|
async createNetwork(data: CreateNetworkRequest) {
|
|
return this.request(NetworkSchema, "POST", "/networks", data);
|
|
}
|
|
|
|
async getNetwork(id: string) {
|
|
return this.request(NetworkSchema, "GET", `/networks/${id}`);
|
|
}
|
|
|
|
async addMembers(networkId: string, data: AddMembersRequest): Promise<void> {
|
|
await this.requestVoid(
|
|
"POST",
|
|
`/networks/${networkId}/members`,
|
|
data,
|
|
);
|
|
}
|
|
|
|
async removeMember(networkId: string, humanId: string): Promise<void> {
|
|
await this.requestVoid(
|
|
"DELETE",
|
|
`/networks/${networkId}/members/${humanId}`,
|
|
);
|
|
}
|
|
|
|
// --- Invitations ---
|
|
|
|
async listNetworkInvitations(networkId: string) {
|
|
return this.request(
|
|
ListInvitationsResponseSchema,
|
|
"GET",
|
|
`/networks/${networkId}/invitations`,
|
|
);
|
|
}
|
|
|
|
async listMyInvitations() {
|
|
return this.request(
|
|
ListInvitationsResponseSchema,
|
|
"GET",
|
|
"/invitations",
|
|
);
|
|
}
|
|
|
|
async acceptInvitation(data: AcceptInvitationRequest): Promise<void> {
|
|
await this.requestVoid("POST", "/invitations/accept", data);
|
|
}
|
|
|
|
async revokeInvitation(networkId: string, data: RevokeInvitationRequest): Promise<void> {
|
|
await this.requestVoid("DELETE", `/networks/${networkId}/invitations`, data);
|
|
}
|
|
|
|
// --- LiveKit ---
|
|
|
|
async getLivekitToken(networkId: string, streamId: string) {
|
|
return this.request(GetLivekitTokenResponseSchema, "POST", "/livekit/token", { network_id: networkId, stream_id: streamId });
|
|
}
|
|
|
|
// --- Billing (network admin only) ---
|
|
|
|
async getNetworkBilling(networkId: string) {
|
|
return this.request(
|
|
BillingStatusSchema,
|
|
"GET",
|
|
`/networks/${networkId}/billing`,
|
|
);
|
|
}
|
|
|
|
async createCheckoutSession(networkId: string, cadence: BillingCadence) {
|
|
return this.request(
|
|
CheckoutSessionResponseSchema,
|
|
"POST",
|
|
`/networks/${networkId}/billing/checkout-session`,
|
|
{ cadence },
|
|
);
|
|
}
|
|
|
|
async createPortalSession(networkId: string) {
|
|
return this.request(
|
|
PortalSessionResponseSchema,
|
|
"POST",
|
|
`/networks/${networkId}/billing/portal-session`,
|
|
);
|
|
}
|
|
|
|
async getNetworkUsage(networkId: string) {
|
|
return this.request(
|
|
NetworkUsageSchema,
|
|
"GET",
|
|
`/networks/${networkId}/usage`,
|
|
);
|
|
}
|
|
|
|
// --- Link metadata ---
|
|
|
|
async getLinkMetadata(url: string): Promise<LinkMetadata | null> {
|
|
const response = await this.fetch(
|
|
"GET",
|
|
`/metadata?url=${encodeURIComponent(url)}`,
|
|
);
|
|
return (await response.json()) as LinkMetadata | null;
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient({
|
|
baseUrl: appConfig.orionUrl,
|
|
getToken: () => useSessionStore.getState().token,
|
|
onUnauthorized: () => useSessionStore.getState().clearToken(),
|
|
});
|