apiClient.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { API_BASE_URL } from '../utils/constants';
  2. interface ApiResponse<T = any> {
  3. data: T;
  4. status: number;
  5. }
  6. class ApiClient {
  7. private baseURL: string;
  8. constructor(baseURL: string) {
  9. this.baseURL = baseURL;
  10. }
  11. private getAuthHeaders(): Record<string, string> {
  12. // V2 API key auth (primary)
  13. const apiKey = localStorage.getItem('kb_api_key');
  14. const activeTenantId = localStorage.getItem('kb_active_tenant_id');
  15. // Legacy JWT token (fallback, kept for compatibility during transition)
  16. const token = localStorage.getItem('authToken') || localStorage.getItem('token');
  17. const language = localStorage.getItem('userLanguage') || 'ja';
  18. return {
  19. 'Content-Type': 'application/json',
  20. 'x-user-language': language,
  21. ...(apiKey && { 'x-api-key': apiKey }),
  22. ...(activeTenantId && { 'x-tenant-id': activeTenantId }),
  23. ...(token && { Authorization: `Bearer ${token}` }),
  24. };
  25. if (apiKey) {
  26. if (apiKey.startsWith('kb_')) {
  27. headers['x-api-key'] = apiKey;
  28. } else {
  29. headers['Authorization'] = `Bearer ${apiKey}`;
  30. }
  31. } else if (token) {
  32. headers['Authorization'] = `Bearer ${token}`;
  33. }
  34. // Final fail-safe: Ensure no header is 'undefined' string
  35. Object.keys(headers).forEach(key => {
  36. if (headers[key]?.toLowerCase().includes('undefined')) {
  37. delete headers[key];
  38. }
  39. });
  40. if (activeTenantId && isValid(activeTenantId)) {
  41. headers['x-tenant-id'] = activeTenantId;
  42. }
  43. // DEBUG: Only log first few chars
  44. console.log('[ApiClient] Auth Headers:', {
  45. hasApiKey: !!headers['x-api-key'],
  46. hasAuth: !!headers['Authorization'],
  47. authPreview: headers['Authorization']?.substring(0, 20),
  48. tenantId: headers['x-tenant-id']
  49. });
  50. return headers;
  51. }
  52. private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
  53. const text = await response.text();
  54. let data: any;
  55. try {
  56. data = text ? JSON.parse(text) : null;
  57. } catch (e) {
  58. data = null;
  59. }
  60. if (!response.ok) {
  61. throw new Error(data?.message || text || 'Request failed');
  62. }
  63. return { data: data as T, status: response.status };
  64. }
  65. // 新しい API 呼び出し方法、{ data, status } を返す
  66. async get<T = any>(url: string): Promise<ApiResponse<T>> {
  67. const response = await fetch(`${this.baseURL}${url}`, {
  68. method: 'GET',
  69. headers: this.getAuthHeaders(),
  70. });
  71. return this.handleResponse<T>(response);
  72. }
  73. async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  74. const response = await fetch(`${this.baseURL}${url}`, {
  75. method: 'POST',
  76. headers: this.getAuthHeaders(),
  77. body: body ? JSON.stringify(body) : undefined,
  78. });
  79. return this.handleResponse<T>(response);
  80. }
  81. async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  82. const response = await fetch(`${this.baseURL}${url}`, {
  83. method: 'PUT',
  84. headers: this.getAuthHeaders(),
  85. body: body ? JSON.stringify(body) : undefined,
  86. });
  87. return this.handleResponse<T>(response);
  88. }
  89. async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  90. const response = await fetch(`${this.baseURL}${url}`, {
  91. method: 'PATCH',
  92. headers: this.getAuthHeaders(),
  93. body: body ? JSON.stringify(body) : undefined,
  94. });
  95. return this.handleResponse<T>(response);
  96. }
  97. async delete<T = any>(url: string): Promise<ApiResponse<T>> {
  98. const response = await fetch(`${this.baseURL}${url}`, {
  99. method: 'DELETE',
  100. headers: this.getAuthHeaders(),
  101. });
  102. return this.handleResponse<T>(response);
  103. }
  104. // New methods for special formats
  105. async getBlob(url: string): Promise<Blob> {
  106. const response = await fetch(`${this.baseURL}${url}`, {
  107. method: 'GET',
  108. headers: this.getAuthHeaders(),
  109. });
  110. if (!response.ok) {
  111. throw new Error('Request failed');
  112. }
  113. return await response.blob();
  114. }
  115. async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
  116. const headers = this.getAuthHeaders();
  117. // Remove Content-Type to let the browser set it with the correct boundary
  118. delete headers['Content-Type'];
  119. const response = await fetch(`${this.baseURL}${url}`, {
  120. method: 'POST',
  121. headers,
  122. body: formData,
  123. });
  124. return this.handleResponse<T>(response);
  125. }
  126. // Legacy compatibility method — returns raw Response for streaming and other special cases
  127. async request(path: string, options: RequestInit = {}): Promise<Response> {
  128. const apiKey = localStorage.getItem('kb_api_key');
  129. const activeTenantId = localStorage.getItem('kb_active_tenant_id');
  130. const token = localStorage.getItem('authToken');
  131. const headers = new Headers(options.headers);
  132. if (apiKey) headers.set('x-api-key', apiKey);
  133. if (activeTenantId) headers.set('x-tenant-id', activeTenantId);
  134. if (token) headers.set('Authorization', `Bearer ${token}`);
  135. const language = localStorage.getItem('userLanguage') || 'ja';
  136. headers.set('x-user-language', language);
  137. let url = path;
  138. if (!path.startsWith('http')) {
  139. const cleanPath = path.startsWith('/') ? path : `/${path}`;
  140. url = `${this.baseURL}${cleanPath}`;
  141. }
  142. const response = await fetch(url, {
  143. ...options,
  144. headers,
  145. });
  146. if (response.status === 401) {
  147. localStorage.removeItem('kb_api_key');
  148. localStorage.removeItem('authToken');
  149. window.location.href = '/login';
  150. throw new Error('Unauthorized');
  151. }
  152. return response;
  153. }
  154. private handleUnauthorized() {
  155. console.warn('[ApiClient] 401 Unauthorized detected. Cleaning up and redirecting to login...');
  156. localStorage.removeItem('kb_api_key');
  157. localStorage.removeItem('authToken');
  158. localStorage.removeItem('token');
  159. localStorage.removeItem('kb_active_tenant_id');
  160. // Only redirect if we are not already on the login page
  161. if (window.location.pathname !== '/login') {
  162. window.location.href = '/login';
  163. }
  164. }
  165. }
  166. export const apiClient = new ApiClient(API_BASE_URL);