apiClient.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. }
  26. private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
  27. const text = await response.text();
  28. let data: any;
  29. try {
  30. data = text ? JSON.parse(text) : null;
  31. } catch (e) {
  32. data = null;
  33. }
  34. if (!response.ok) {
  35. throw new Error(data?.message || text || 'Request failed');
  36. }
  37. return { data: data as T, status: response.status };
  38. }
  39. // 新しい API 呼び出し方法、{ data, status } を返す
  40. async get<T = any>(url: string): Promise<ApiResponse<T>> {
  41. const response = await fetch(`${this.baseURL}${url}`, {
  42. method: 'GET',
  43. headers: this.getAuthHeaders(),
  44. });
  45. return this.handleResponse<T>(response);
  46. }
  47. async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  48. const response = await fetch(`${this.baseURL}${url}`, {
  49. method: 'POST',
  50. headers: this.getAuthHeaders(),
  51. body: body ? JSON.stringify(body) : undefined,
  52. });
  53. return this.handleResponse<T>(response);
  54. }
  55. async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  56. const response = await fetch(`${this.baseURL}${url}`, {
  57. method: 'PUT',
  58. headers: this.getAuthHeaders(),
  59. body: body ? JSON.stringify(body) : undefined,
  60. });
  61. return this.handleResponse<T>(response);
  62. }
  63. async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
  64. const response = await fetch(`${this.baseURL}${url}`, {
  65. method: 'PATCH',
  66. headers: this.getAuthHeaders(),
  67. body: body ? JSON.stringify(body) : undefined,
  68. });
  69. return this.handleResponse<T>(response);
  70. }
  71. async delete<T = any>(url: string): Promise<ApiResponse<T>> {
  72. const response = await fetch(`${this.baseURL}${url}`, {
  73. method: 'DELETE',
  74. headers: this.getAuthHeaders(),
  75. });
  76. return this.handleResponse<T>(response);
  77. }
  78. // New methods for special formats
  79. async getBlob(url: string): Promise<Blob> {
  80. const response = await fetch(`${this.baseURL}${url}`, {
  81. method: 'GET',
  82. headers: this.getAuthHeaders(),
  83. });
  84. if (!response.ok) {
  85. throw new Error('Request failed');
  86. }
  87. return await response.blob();
  88. }
  89. async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
  90. const headers = this.getAuthHeaders();
  91. // Remove Content-Type to let the browser set it with the correct boundary
  92. delete headers['Content-Type'];
  93. const response = await fetch(`${this.baseURL}${url}`, {
  94. method: 'POST',
  95. headers,
  96. body: formData,
  97. });
  98. return this.handleResponse<T>(response);
  99. }
  100. // Legacy compatibility method — returns raw Response for streaming and other special cases
  101. async request(path: string, options: RequestInit = {}): Promise<Response> {
  102. const apiKey = localStorage.getItem('kb_api_key');
  103. const activeTenantId = localStorage.getItem('kb_active_tenant_id');
  104. const token = localStorage.getItem('authToken');
  105. const headers = new Headers(options.headers);
  106. if (apiKey) headers.set('x-api-key', apiKey);
  107. if (activeTenantId) headers.set('x-tenant-id', activeTenantId);
  108. if (token) headers.set('Authorization', `Bearer ${token}`);
  109. const language = localStorage.getItem('userLanguage') || 'ja';
  110. headers.set('x-user-language', language);
  111. let url = path;
  112. if (!path.startsWith('http')) {
  113. const cleanPath = path.startsWith('/') ? path : `/${path}`;
  114. url = `${this.baseURL}${cleanPath}`;
  115. }
  116. const response = await fetch(url, {
  117. ...options,
  118. headers,
  119. });
  120. if (response.status === 401) {
  121. localStorage.removeItem('kb_api_key');
  122. localStorage.removeItem('authToken');
  123. window.location.href = '/login';
  124. throw new Error('Unauthorized');
  125. }
  126. return response;
  127. }
  128. }
  129. export const apiClient = new ApiClient(API_BASE_URL);