Files

186 lines
5.3 KiB
TypeScript

import { API_BASE_URL } from '../utils/constants';
interface ApiResponse<T = any> {
data: T;
status: number;
}
class ApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
private getAuthHeaders(): Record<string, string> {
const apiKey = localStorage.getItem('kb_api_key');
const activeTenantId = localStorage.getItem('kb_active_tenant_id');
const token = localStorage.getItem('authToken') || localStorage.getItem('token');
const language = localStorage.getItem('userLanguage') || 'zh';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-user-language': language,
};
if (apiKey) {
if (apiKey.startsWith('kb_')) {
headers['x-api-key'] = apiKey;
} else {
headers['Authorization'] = `Bearer ${apiKey}`;
}
} else if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (activeTenantId && activeTenantId !== 'undefined' && activeTenantId !== 'null' && activeTenantId !== 'default') {
headers['x-tenant-id'] = activeTenantId;
}
return headers;
}
private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
const text = await response.text();
let data: any;
try {
data = text ? JSON.parse(text) : null;
} catch (e) {
data = null;
}
if (!response.ok) {
throw new Error(data?.message || text || 'Request failed');
}
return { data: data as T, status: response.status };
}
// 新しい API 呼び出し方法、{ data, status } を返す
async get<T = any>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'GET',
headers: this.getAuthHeaders(),
});
return this.handleResponse<T>(response);
}
async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers: this.getAuthHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
return this.handleResponse<T>(response);
}
async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'PUT',
headers: this.getAuthHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
return this.handleResponse<T>(response);
}
async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'PATCH',
headers: this.getAuthHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
return this.handleResponse<T>(response);
}
async delete<T = any>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'DELETE',
headers: this.getAuthHeaders(),
});
return this.handleResponse<T>(response);
}
// New methods for special formats
async getBlob(url: string): Promise<Blob> {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'GET',
headers: this.getAuthHeaders(),
});
if (!response.ok) {
throw new Error('Request failed');
}
return await response.blob();
}
async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
const headers = this.getAuthHeaders();
// Remove Content-Type to let the browser set it with the correct boundary
delete headers['Content-Type'];
const response = await fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers,
body: formData,
});
return this.handleResponse<T>(response);
}
// Legacy compatibility method — returns raw Response for streaming and other special cases
async request(path: string, options: RequestInit = {}): Promise<Response> {
const authHeaders = this.getAuthHeaders();
const headers = new Headers(options.headers);
// Merge auth headers into request headers
Object.entries(authHeaders).forEach(([key, value]) => {
// Don't override if already set
if (headers.has(key)) return;
// Don't set Content-Type if body is FormData (let browser set it with boundary)
if (key === 'Content-Type' && options.body instanceof FormData) return;
headers.set(key, value);
});
let url = path;
if (!path.startsWith('http')) {
const cleanPath = path.startsWith('/') ? path : `/${path}`;
url = `${this.baseURL}${cleanPath}`;
}
const response = await fetch(url, {
...options,
headers,
});
if (response.status === 401) {
localStorage.removeItem('kb_api_key');
localStorage.removeItem('authToken');
window.location.href = '/login';
throw new Error('Unauthorized');
}
return response;
}
private handleUnauthorized() {
console.warn('[ApiClient] 401 Unauthorized detected. Cleaning up and redirecting to login...');
localStorage.removeItem('kb_api_key');
localStorage.removeItem('authToken');
localStorage.removeItem('token');
localStorage.removeItem('kb_active_tenant_id');
// Only redirect if we are not already on the login page
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
}
}
export const apiClient = new ApiClient(API_BASE_URL);