mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
♻️ refactor: migrate todo, contacts, storage to shared-api-client
- Update todo, contacts, storage web apps to use @manacore/shared-api-client - Maintain backward compatibility with existing legacy wrappers - Todo: apiClient wrapper for setAccessToken/getAccessToken pattern - Contacts: fetchWithAuth/fetchWithAuthFormData wrappers - Storage: toLegacyResponse wrapper for ApiResponse format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fa78769e82
commit
5322709fca
7 changed files with 191 additions and 185 deletions
|
|
@ -30,6 +30,7 @@
|
|||
"vite": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,71 +1,80 @@
|
|||
/**
|
||||
* Centralized API client with authentication
|
||||
* API Client for Contacts backend
|
||||
* Uses @manacore/shared-api-client for consistent error handling
|
||||
*/
|
||||
|
||||
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { API_BASE } from './config';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
const API_URL = PUBLIC_BACKEND_URL || 'http://localhost:3015';
|
||||
|
||||
/**
|
||||
* Make an authenticated API request
|
||||
* @param url API endpoint (will be prefixed with API_BASE)
|
||||
* @param options Fetch options
|
||||
* @returns Parsed JSON response
|
||||
* Contacts API client instance
|
||||
* - Auto token handling via authStore.getValidToken()
|
||||
* - Consistent ApiResult<T> response format
|
||||
*/
|
||||
export const api = createApiClient({
|
||||
baseUrl: API_URL,
|
||||
apiPrefix: '/api/v1',
|
||||
getAuthToken: () => authStore.getValidToken(),
|
||||
timeout: 30000,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
/**
|
||||
* Legacy fetchWithAuth wrapper for backward compatibility
|
||||
* Converts ApiResult to throw-based pattern
|
||||
*/
|
||||
export async function fetchWithAuth<T = unknown>(
|
||||
url: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = await authStore.getAccessToken();
|
||||
const method = options.method || 'GET';
|
||||
const body = options.body ? JSON.parse(options.body as string) : undefined;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
||||
let result: ApiResult<T>;
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
result = await api.post<T>(url, body);
|
||||
break;
|
||||
case 'PUT':
|
||||
result = await api.put<T>(url, body);
|
||||
break;
|
||||
case 'PATCH':
|
||||
result = await api.patch<T>(url, body);
|
||||
break;
|
||||
case 'DELETE':
|
||||
result = await api.delete<T>(url);
|
||||
break;
|
||||
default:
|
||||
result = await api.get<T>(url);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${url}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Request failed');
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return result.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated API request without JSON content type
|
||||
* Used for file uploads (FormData)
|
||||
* Legacy fetchWithAuthFormData for file uploads
|
||||
* Uses the shared API client's upload method
|
||||
*/
|
||||
export async function fetchWithAuthFormData<T = unknown>(
|
||||
url: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = await authStore.getAccessToken();
|
||||
const formData = options.body as FormData;
|
||||
const result = await api.upload<T>(url, formData);
|
||||
|
||||
const headers: HeadersInit = {
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${url}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return result.data as T;
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { ApiResult };
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"vite": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,48 +1,68 @@
|
|||
/**
|
||||
* API Client for Storage Backend
|
||||
* Uses @manacore/shared-api-client for consistent error handling
|
||||
*/
|
||||
|
||||
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3016/api/v1';
|
||||
const API_URL = 'http://localhost:3016';
|
||||
|
||||
/**
|
||||
* Storage API client instance
|
||||
* - Auto token handling via authStore.getValidToken()
|
||||
* - Consistent ApiResult<T> response format
|
||||
*/
|
||||
const api = createApiClient({
|
||||
baseUrl: API_URL,
|
||||
apiPrefix: '/api/v1',
|
||||
getAuthToken: () => authStore.getAccessToken(),
|
||||
timeout: 30000,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
// Legacy type alias for backward compatibility
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function getHeaders(): Promise<HeadersInit> {
|
||||
const token = await authStore.getAccessToken();
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
/**
|
||||
* Convert ApiResult to legacy ApiResponse format
|
||||
*/
|
||||
function toLegacyResponse<T>(result: ApiResult<T>): ApiResponse<T> {
|
||||
if (result.error) {
|
||||
return { error: result.error.message };
|
||||
}
|
||||
return headers;
|
||||
return { data: result.data ?? undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy request wrapper for backward compatibility
|
||||
*/
|
||||
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
const method = options.method || 'GET';
|
||||
const body = options.body ? JSON.parse(options.body as string) : undefined;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return { error: errorData.message || `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
let result: ApiResult<T>;
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
result = await api.post<T>(endpoint, body);
|
||||
break;
|
||||
case 'PUT':
|
||||
result = await api.put<T>(endpoint, body);
|
||||
break;
|
||||
case 'PATCH':
|
||||
result = await api.patch<T>(endpoint, body);
|
||||
break;
|
||||
case 'DELETE':
|
||||
result = await api.delete<T>(endpoint);
|
||||
break;
|
||||
default:
|
||||
result = await api.get<T>(endpoint);
|
||||
}
|
||||
|
||||
return toLegacyResponse(result);
|
||||
}
|
||||
|
||||
// File Types
|
||||
|
|
@ -112,38 +132,20 @@ export const filesApi = {
|
|||
get: (id: string) => request<StorageFile>(`/files/${id}`),
|
||||
|
||||
upload: async (file: File, folderId?: string): Promise<ApiResponse<StorageFile>> => {
|
||||
const token = await authStore.getAccessToken();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (folderId) {
|
||||
formData.append('parentFolderId', folderId);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/files/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return { error: errorData.message || `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
const result = await api.upload<StorageFile>('/files/upload', formData);
|
||||
return toLegacyResponse(result);
|
||||
},
|
||||
|
||||
download: async (id: string): Promise<Blob | null> => {
|
||||
const token = await authStore.getAccessToken();
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/files/${id}/download`, {
|
||||
const response = await fetch(`${API_URL}/api/v1/files/${id}/download`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"vite": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,106 +1,89 @@
|
|||
import { browser } from '$app/environment';
|
||||
/**
|
||||
* API Client for Todo backend
|
||||
* Uses @manacore/shared-api-client for consistent error handling
|
||||
*/
|
||||
|
||||
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
interface ApiOptions {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
const API_URL = PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
|
||||
// Token storage for manual token management (legacy pattern)
|
||||
let currentToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Todo API client instance
|
||||
* - Supports manual token setting via setAccessToken()
|
||||
* - Consistent ApiResult<T> response format
|
||||
* - Runtime URL injection for Docker
|
||||
*/
|
||||
export const api = createApiClient({
|
||||
baseUrl: API_URL,
|
||||
apiPrefix: '',
|
||||
getAuthToken: async () => currentToken,
|
||||
timeout: 30000,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
/**
|
||||
* Legacy token management functions
|
||||
* Used by auth store to set token after login
|
||||
*/
|
||||
export function setAccessToken(token: string | null) {
|
||||
currentToken = token;
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
export function getAccessToken(): string | null {
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backend URL, preferring runtime-injected value in browser
|
||||
* This allows Docker to inject PUBLIC_BACKEND_URL_CLIENT at runtime
|
||||
* instead of using the build-time PUBLIC_BACKEND_URL
|
||||
* Wrapper for legacy code that expects throws instead of ApiResult
|
||||
* Converts ApiResult to throw-based pattern for backward compatibility
|
||||
*/
|
||||
function getBackendUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const runtimeUrl = (window as Window & { __PUBLIC_BACKEND_URL__?: string })
|
||||
.__PUBLIC_BACKEND_URL__;
|
||||
if (runtimeUrl) {
|
||||
return runtimeUrl;
|
||||
}
|
||||
export async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options: { method?: string; body?: unknown } = {}
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body } = options;
|
||||
|
||||
let result: ApiResult<T>;
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
result = await api.post<T>(endpoint, body);
|
||||
break;
|
||||
case 'PUT':
|
||||
result = await api.put<T>(endpoint, body);
|
||||
break;
|
||||
case 'PATCH':
|
||||
result = await api.patch<T>(endpoint, body);
|
||||
break;
|
||||
case 'DELETE':
|
||||
result = await api.delete<T>(endpoint);
|
||||
break;
|
||||
default:
|
||||
result = await api.get<T>(endpoint);
|
||||
}
|
||||
return PUBLIC_BACKEND_URL || 'http://localhost:3018';
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
return result.data as T;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private accessToken: string | null = null;
|
||||
/**
|
||||
* Legacy apiClient wrapper for backward compatibility
|
||||
*/
|
||||
export const apiClient = {
|
||||
setAccessToken,
|
||||
getAccessToken,
|
||||
get: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
||||
post: <T>(endpoint: string, body?: unknown) => fetchApi<T>(endpoint, { method: 'POST', body }),
|
||||
put: <T>(endpoint: string, body?: unknown) => fetchApi<T>(endpoint, { method: 'PUT', body }),
|
||||
patch: <T>(endpoint: string, body?: unknown) => fetchApi<T>(endpoint, { method: 'PATCH', body }),
|
||||
delete: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
// Use getter to evaluate URL at request time (browser may hydrate after construction)
|
||||
private get baseUrl(): string {
|
||||
return getBackendUrl();
|
||||
}
|
||||
|
||||
setAccessToken(token: string | null) {
|
||||
this.accessToken = token;
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
async fetch<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
|
||||
const { method = 'GET', body, headers = {} } = options;
|
||||
|
||||
const requestHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
|
||||
if (this.accessToken) {
|
||||
requestHeaders['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'An error occurred';
|
||||
try {
|
||||
const errorData = (await response.json()) as ApiError;
|
||||
errorMessage = errorData.message || errorMessage;
|
||||
} catch {
|
||||
errorMessage = response.statusText || errorMessage;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
get<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'GET', headers });
|
||||
}
|
||||
|
||||
post<T>(endpoint: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'POST', body, headers });
|
||||
}
|
||||
|
||||
put<T>(endpoint: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'PUT', body, headers });
|
||||
}
|
||||
|
||||
patch<T>(endpoint: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'PATCH', body, headers });
|
||||
}
|
||||
|
||||
delete<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'DELETE', headers });
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
// Re-export types for convenience
|
||||
export type { ApiResult };
|
||||
|
|
|
|||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -1085,6 +1085,9 @@ importers:
|
|||
|
||||
apps/contacts/apps/web:
|
||||
dependencies:
|
||||
'@manacore/shared-api-client':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-api-client
|
||||
'@manacore/shared-auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-auth
|
||||
|
|
@ -3866,6 +3869,9 @@ importers:
|
|||
|
||||
apps/storage/apps/web:
|
||||
dependencies:
|
||||
'@manacore/shared-api-client':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-api-client
|
||||
'@manacore/shared-auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-auth
|
||||
|
|
@ -4094,6 +4100,9 @@ importers:
|
|||
|
||||
apps/todo/apps/web:
|
||||
dependencies:
|
||||
'@manacore/shared-api-client':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-api-client
|
||||
'@manacore/shared-auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-auth
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue