import { useSessionStore } from "@/stores/session-store"; import type { z } from "zod"; import { DepotObjectSchema, HumanSchema, ListNetworksResponseSchema, NetworkSchema, PrepareUploadResponseSchema, SignInResponseSchema, } from "./types"; import type { AddMembersRequest, CreateNetworkRequest, PrepareUploadRequest, RequestCodeRequest, SignInRequest, } from "./types"; export class ApiError extends Error { constructor( public status: number, message: string, ) { super(message); this.name = "ApiError"; } } 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 { const headers: Record = {}; 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( schema: z.ZodType, method: string, path: string, body?: unknown, ): Promise { 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 { await this.fetch(method, path, body); } // --- Auth --- async requestCode(data: RequestCodeRequest): Promise { 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 { await this.requestVoid("POST", "/auth/sign-out"); } // TODO: security: require passing in the particle id once api deprecates this async getParticleDownloadUrl(objectId: string): Promise { const response = await this.fetch( "GET", `/particles/${objectId}/download`, ); const data = await response.json(); return data.url; } // --- 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 { await this.requestVoid( "POST", `/networks/${networkId}/members`, data, ); } async removeMember(networkId: string, email: string): Promise { await this.requestVoid( "DELETE", `/networks/${networkId}/members/${email}`, ); } } export const apiClient = new ApiClient({ baseUrl: "https://orion.dev.flowy.live", getToken: () => useSessionStore.getState().token, onUnauthorized: () => useSessionStore.getState().clearToken(), });