diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index ea7b1cb7d..d95d2e417 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -1,187 +1,231 @@ import type z from "zod"; import { - AppError, - AuthenticationError, - AuthorizationError, - NotFoundError, - ValidationError, + 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; - // Add more options as needed + 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 }; -export class BaseApiService { - bearerToken: string; - baseUrl: string; +class BaseApiService { + bearerToken: string; + baseUrl: string; - noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Add more endpoints as needed + 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; - } + constructor(bearerToken: string, baseUrl: string) { + this.bearerToken = bearerToken; + this.baseUrl = baseUrl; + } - setBearerToken(bearerToken: string) { - this.bearerToken = bearerToken; - } + 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", - }; + 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 ?? {}), - }, - }; + const mergedOptions: RequestOptions = { + ...defaultOptions, + ...(options ?? {}), + headers: { + ...defaultOptions.headers, + ...(options?.headers ?? {}), + }, + }; - if (!this.baseUrl) { - throw new AppError("Base URL is not set."); - } + 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."); - } + 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 fullUrl = new URL(url, this.baseUrl).toString(); - const response = await fetch(fullUrl, mergedOptions); + const response = await fetch(fullUrl, mergedOptions); - if (!response.ok) { - // biome-ignore lint/suspicious: Unknown - let data; + 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); + 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); - } + throw new AppError( + "Something went wrong", + response.status, + response.statusText + ); + } - // for fastapi errors response - if ("detail" in data) { - throw new AppError(data.detail, 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); - } - } + 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; + // biome-ignore lint/suspicious: Unknown + let data; + const responseType = mergedOptions.responseType || "json"; - try { - data = await response.json(); - } catch (error) { - console.error("Failed to parse response as JSON:", error); + 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 + ); + } - throw new AppError("Something went wrong", response.status, response.statusText); - } + if (responseType === "json") { + if (!responseSchema) { + return data; + } + const parsedData = responseSchema.safeParse(data); - if (!responseSchema) { - return 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); + } - const parsedData = responseSchema.safeParse(data); + return 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; + } catch (error) { + console.error("Request failed:", error); + throw error; + } + } - 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 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 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 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, - }); - } + 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 || "" + typeof window !== "undefined" + ? localStorage.getItem("surfsense_bearer_token") || "" + : "", + process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "" );