import type z from "zod"; import { AppError, AuthenticationError, AuthorizationError, NotFoundError, } from "../error"; export type RequestOptions = { method: "GET" | "POST" | "PUT" | "DELETE"; headers?: Record; contentType?: "application/json" | "application/x-www-form-urlencoded"; signal?: AbortSignal; body?: any; responseType?: "json" | "text" | "blob" | "arrayBuffer"; // Add more response types as needed // Add more options as needed }; class BaseApiService { bearerToken: string; baseUrl: string; noAuthEndpoints: string[] = [ "/auth/jwt/login", "/auth/register", "/auth/refresh", ]; // Add more endpoints as needed constructor(bearerToken: string, baseUrl: string) { this.bearerToken = bearerToken; this.baseUrl = baseUrl; } setBearerToken(bearerToken: string) { this.bearerToken = bearerToken; } async request( url: string, responseSchema?: z.ZodSchema, options?: RequestOptions ): Promise { try { const defaultOptions: RequestOptions = { headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.bearerToken || ""}`, }, method: "GET", responseType: "json", }; const mergedOptions: RequestOptions = { ...defaultOptions, ...(options ?? {}), headers: { ...defaultOptions.headers, ...(options?.headers ?? {}), }, }; if (!this.baseUrl) { throw new AppError("Base URL is not set."); } if (!this.bearerToken && !this.noAuthEndpoints.includes(url)) { throw new AuthenticationError( "You are not authenticated. Please login again." ); } const fullUrl = new URL(url, this.baseUrl).toString(); const response = await fetch(fullUrl, mergedOptions); if (!response.ok) { // biome-ignore lint/suspicious: Unknown let data; try { data = await response.json(); } catch (error) { console.error("Failed to parse response as JSON:", error); throw new AppError( "Something went wrong", response.status, response.statusText ); } // for fastapi errors response if (typeof data === "object" && "detail" in data) { throw new AppError(data.detail, response.status, response.statusText); } switch (response.status) { case 401: throw new AuthenticationError( "You are not authenticated. Please login again.", response.status, response.statusText ); case 403: throw new AuthorizationError( "You don't have permission to access this resource.", response.status, response.statusText ); case 404: throw new NotFoundError( "Resource not found", response.status, response.statusText ); // Add more cases as needed default: throw new AppError( "Something went wrong", response.status, response.statusText ); } } // biome-ignore lint/suspicious: Unknown let data; const responseType = mergedOptions.responseType || "json"; try { switch (responseType) { case "json": data = await response.json(); break; case "text": data = await response.text(); break; case "blob": data = await response.blob(); break; case "arrayBuffer": data = await response.arrayBuffer(); break; // Add more cases as needed default: data = await response.text(); } } catch (error) { console.error("Failed to parse response as JSON:", error); throw new AppError( "Failed to parse response", response.status, response.statusText ); } if (responseType === "json") { if (!responseSchema) { return data; } const parsedData = responseSchema.safeParse(data); if (!parsedData.success) { /** The request was successful, but the response data does not match the expected schema. * This is a client side error, and should be fixed by updating the responseSchema to keep things typed. * This error should not be shown to the user , it is for dev only. */ console.error("Invalid API response schema:", parsedData.error); } return data; } return data; } catch (error) { console.error("Request failed:", error); throw error; } } async get( url: string, responseSchema?: z.ZodSchema, options?: Omit ) { return this.request(url, responseSchema, { ...options, method: "GET", }); } async post( url: string, responseSchema?: z.ZodSchema, options?: Omit ) { return this.request(url, responseSchema, { method: "POST", ...options, }); } async put( url: string, responseSchema?: z.ZodSchema, options?: Omit ) { return this.request(url, responseSchema, { method: "PUT", ...options, }); } async delete( url: string, responseSchema?: z.ZodSchema, options?: Omit ) { return this.request(url, responseSchema, { method: "DELETE", ...options, }); } } export const baseApiService = new BaseApiService( typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") || "" : "", process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "" );