239 lines
5.5 KiB
TypeScript
239 lines
5.5 KiB
TypeScript
/**
|
|
* API Client for communicating with Backend via Envoy Gateway
|
|
*/
|
|
|
|
const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_URL ||
|
|
process.env.ENVOY_GATEWAY_URL ||
|
|
"http://85.208.253.135:8000";
|
|
|
|
const AUTH_STORAGE_KEYS = {
|
|
accessToken: "auth_token",
|
|
refreshToken: "auth_refresh_token",
|
|
} as const;
|
|
|
|
export interface ApiError {
|
|
message: string;
|
|
code?: number;
|
|
details?: any;
|
|
}
|
|
|
|
export class ApiClient {
|
|
private baseURL: string;
|
|
private defaultHeaders: Record<string, string>;
|
|
|
|
constructor(baseURL: string = API_BASE_URL) {
|
|
this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash
|
|
this.defaultHeaders = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get authorization token from localStorage
|
|
*/
|
|
private getAuthToken(): string | null {
|
|
if (typeof window === "undefined") return null;
|
|
return localStorage.getItem(AUTH_STORAGE_KEYS.accessToken);
|
|
}
|
|
|
|
/**
|
|
* Set authorization token in localStorage
|
|
*/
|
|
setAuthToken(token: string): void {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.accessToken, token);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set refresh token in localStorage
|
|
*/
|
|
setRefreshToken(token: string): void {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.refreshToken, token);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set access and refresh tokens in localStorage
|
|
*/
|
|
setAuthTokens(tokens: { access: string; refresh?: string }): void {
|
|
this.setAuthToken(tokens.access);
|
|
|
|
if (tokens.refresh) {
|
|
this.setRefreshToken(tokens.refresh);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear authorization token
|
|
*/
|
|
clearAuthToken(): void {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.removeItem(AUTH_STORAGE_KEYS.accessToken);
|
|
localStorage.removeItem(AUTH_STORAGE_KEYS.refreshToken);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get headers with authentication token
|
|
*/
|
|
private getHeaders(
|
|
customHeaders?: Record<string, string>,
|
|
): Record<string, string> {
|
|
const headers = { ...this.defaultHeaders, ...customHeaders };
|
|
const token = this.getAuthToken();
|
|
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Handle API response
|
|
*/
|
|
private async handleResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
let errorData: any;
|
|
try {
|
|
errorData = await response.json();
|
|
} catch {
|
|
errorData = { message: response.statusText };
|
|
}
|
|
|
|
const error: ApiError = {
|
|
message: errorData.msg || errorData.message || "An error occurred",
|
|
code: errorData.code || response.status,
|
|
details: errorData,
|
|
};
|
|
|
|
// If unauthorized, clear token
|
|
if (response.status === 401) {
|
|
this.clearAuthToken();
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
// Handle empty responses
|
|
const contentType = response.headers.get("content-type");
|
|
if (!contentType || !contentType.includes("application/json")) {
|
|
return {} as T;
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* GET request
|
|
*/
|
|
async get<T>(
|
|
endpoint: string,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: this.getHeaders(customHeaders),
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
/**
|
|
* POST request
|
|
*/
|
|
async post<T>(
|
|
endpoint: string,
|
|
data?: any,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: this.getHeaders(customHeaders),
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
/**
|
|
* POST request with FormData (e.g. file upload). Does not set Content-Type so browser sets multipart/form-data.
|
|
*/
|
|
async postFormData<T>(
|
|
endpoint: string,
|
|
formData: FormData,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const headers = { ...this.getHeaders(customHeaders) };
|
|
delete headers["Content-Type"];
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
/**
|
|
* PUT request
|
|
*/
|
|
async put<T>(
|
|
endpoint: string,
|
|
data?: any,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: "PUT",
|
|
headers: this.getHeaders(customHeaders),
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
/**
|
|
* PATCH request
|
|
*/
|
|
async patch<T>(
|
|
endpoint: string,
|
|
data?: any,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: "PATCH",
|
|
headers: this.getHeaders(customHeaders),
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
|
|
/**
|
|
* DELETE request
|
|
*/
|
|
async delete<T>(
|
|
endpoint: string,
|
|
customHeaders?: Record<string, string>,
|
|
): Promise<T> {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: "DELETE",
|
|
headers: this.getHeaders(customHeaders),
|
|
});
|
|
|
|
return this.handleResponse<T>(response);
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const apiClient = new ApiClient();
|