diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts new file mode 100644 index 000000000..0b3639b4f --- /dev/null +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -0,0 +1,171 @@ +import z from "zod"; +import { + AppError, + AuthenticationError, + AuthorizationError, + ValidationError, +} 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 +}; + +export class BaseApiService { + bearerToken: string; + baseUrl: string; + + constructor(bearerToken: string, baseUrl: string) { + this.bearerToken = bearerToken; + this.baseUrl = baseUrl; + } + + setBearerToken(bearerToken: string) { + this.bearerToken = bearerToken; + } + + async request( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ) { + const defaultOptions: RequestOptions = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.bearerToken}`, + }, + method: "GET", + }; + + const mergedOptions: RequestOptions = { + ...defaultOptions, + ...(options ?? {}), + headers: { + ...defaultOptions.headers, + ...(options?.headers ?? {}), + }, + }; + + let requestBody; + + // Serialize body + if (body) { + if (mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === "application/json") { + requestBody = JSON.stringify(body); + } + + if ( + mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === + "application/x-www-form-urlencoded" + ) { + requestBody = new URLSearchParams(body); + } + + mergedOptions.body = requestBody; + } + + if (!this.baseUrl) { + throw new AppError("Base URL is not set."); + } + + if (!this.bearerToken) { + 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) { + if (response.status === 401) { + throw new AuthenticationError( + "You are not authenticated. Please login again." + ); + } + + if (response.status === 403) { + throw new AuthorizationError( + "You don't have permission to access this resource." + ); + } + + throw new AppError(`API Error: ${response.statusText}`); + } + + let data; + + try { + data = await response.json(); + } catch (error) { + throw new AppError(`Failed to parse response as JSON: ${error}`); + } + + if (!responseSchema) { + return data; + } + + const parsedData = responseSchema.safeParse(data); + + if (!parsedData.success) { + throw new ValidationError( + `Invalid response: ${parsedData.error.message}` + ); + } + + return parsedData.data; + } + + async get( + url: string, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ) { + return this.request(url, undefined, responseSchema, { + ...options, + method: "GET", + }); + } + + async post( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "POST", + }); + } + + async put( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "PUT", + }); + } + + async delete( + url: string, + body?: any, + responseSchema?: z.ZodSchema, + options?: RequestOptions + ) { + return this.request(url, body, responseSchema, { + ...options, + method: "DELETE", + }); + } +}