186 lines
5.3 KiB
TypeScript
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); |