apiClient.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. const rawApiKey = localStorage.getItem('kb_api_key');
  13. const rawToken = localStorage.getItem('authToken') || localStorage.getItem('token');
  14. const activeTenantId = localStorage.getItem('kb_active_tenant_id');
  15. const language = localStorage.getItem('userLanguage') || 'ja';
  16. // Helper to filter out invalid values
  17. const isValid = (val: string | null) => {
  18. if (!val) return false;
  19. const v = val.trim().toLowerCase();
  20. return v !== '' && v !== 'undefined' && v !== 'null' && v !== '[object object]';
  21. };
  22. const apiKey = isValid(rawApiKey) ? rawApiKey : null;
  23. const token = isValid(rawToken) ? rawToken : null;
  24. const headers: Record<string, string> = {
  25. 'Content-Type': 'application/json',
  26. 'x-user-language': language,
  27. };
  28. if (apiKey) {
  29. if (apiKey.startsWith('kb_')) {
  30. headers['x-api-key'] = apiKey;
  31. } else {
  32. headers['Authorization'] = `Bearer ${apiKey}`;
  33. }
  34. } else if (token) {
  35. headers['Authorization'] = `Bearer ${token}`;
  36. }
  37. // Final fail-safe: Ensure no header is 'undefined' string
  38. Object.keys(headers).forEach(key => {
  39. if (headers[key]?.toLowerCase().includes('undefined')) {
  40. delete headers[key];
  41. }
  42. });
  43. if (activeTenantId && isValid(activeTenantId)) {
  44. headers['x-tenant-id'] = activeTenantId;
  45. }
  46. // DEBUG: Only log first few chars
  47. console.log('[ApiClient] Auth Headers:', {
  48. hasApiKey: !!headers['x-api-key'],
  49. hasAuth: !!headers['Authorization'],
  50. authPreview: headers['Authorization']?.substring(0, 20),
  51. tenantId: headers['x-tenant-id']
  52. });
  53. return headers;
  54. }
  55. // New API call method, returns { data, status }
  56. async get<T = any>(url: string): Promise<ApiResponse<T>> {
  57. const response = await fetch(`${this.baseURL}${url}`, {
  58. method: 'GET',
  59. headers: this.getAuthHeaders(),
  60. });
  61. const data = await response.json();
  62. if (response.status === 401) {
  63. this.handleUnauthorized();
  64. throw new Error(data.message || 'Unauthorized');
  65. }
  66. if (!response.ok) {
  67. throw new Error(data.message || 'Request failed');
  68. }
  69. return { data, status: response.status };
  70. }
  71. async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  72. const response = await fetch(`${this.baseURL}${url}`, {
  73. method: 'POST',
  74. headers: this.getAuthHeaders(),
  75. body: body ? JSON.stringify(body) : undefined,
  76. });
  77. const data = await response.json();
  78. if (response.status === 401) {
  79. this.handleUnauthorized();
  80. throw new Error(data.message || 'Unauthorized');
  81. }
  82. if (!response.ok) {
  83. throw new Error(data.message || 'Request failed');
  84. }
  85. return { data, status: response.status };
  86. }
  87. async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  88. const response = await fetch(`${this.baseURL}${url}`, {
  89. method: 'PUT',
  90. headers: this.getAuthHeaders(),
  91. body: body ? JSON.stringify(body) : undefined,
  92. });
  93. const data = await response.json();
  94. if (response.status === 401) {
  95. this.handleUnauthorized();
  96. throw new Error(data.message || 'Unauthorized');
  97. }
  98. if (!response.ok) {
  99. throw new Error(data.message || 'Request failed');
  100. }
  101. return { data, status: response.status };
  102. }
  103. async delete<T = any>(url: string): Promise<ApiResponse<T>> {
  104. const response = await fetch(`${this.baseURL}${url}`, {
  105. method: 'DELETE',
  106. headers: this.getAuthHeaders(),
  107. });
  108. const data = await response.json();
  109. if (response.status === 401) {
  110. this.handleUnauthorized();
  111. throw new Error(data.message || 'Unauthorized');
  112. }
  113. if (!response.ok) {
  114. throw new Error(data.message || 'Request failed');
  115. }
  116. return { data, status: response.status };
  117. }
  118. // Legacy compatibility method — returns raw Response for streaming and other special cases
  119. async request(path: string, options: RequestInit = {}): Promise<Response> {
  120. const authHeaders = this.getAuthHeaders();
  121. const headers = new Headers(options.headers);
  122. Object.entries(authHeaders).forEach(([k, v]) => headers.set(k, v));
  123. const language = localStorage.getItem('userLanguage') || 'ja';
  124. headers.set('x-user-language', language);
  125. let url = path;
  126. if (!path.startsWith('http')) {
  127. const cleanPath = path.startsWith('/') ? path : `/${path}`;
  128. url = `${this.baseURL}${cleanPath}`;
  129. }
  130. const response = await fetch(url, {
  131. ...options,
  132. headers,
  133. });
  134. if (response.status === 401) {
  135. this.handleUnauthorized();
  136. throw new Error('Unauthorized');
  137. }
  138. return response;
  139. }
  140. private handleUnauthorized() {
  141. console.warn('[ApiClient] 401 Unauthorized detected. Cleaning up and redirecting to login...');
  142. localStorage.removeItem('kb_api_key');
  143. localStorage.removeItem('authToken');
  144. localStorage.removeItem('token');
  145. localStorage.removeItem('kb_active_tenant_id');
  146. // Only redirect if we are not already on the login page
  147. if (window.location.pathname !== '/login') {
  148. window.location.href = '/login';
  149. }
  150. }
  151. }
  152. export const apiClient = new ApiClient(API_BASE_URL);