feat: Implement Phase 1 & 2 - Full monorepo architecture
## Backend API (apps/api) - Express.js server with TypeScript - JWT authentication with access/refresh tokens - Multi-tenant middleware (schema per tenant) - Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts - SAT integration: CFDI 4.0 XML parser, FIEL authentication - Metrics engine: 50+ financial metrics (Core, Startup, Enterprise) - Rate limiting, CORS, Helmet security ## Frontend Web (apps/web) - Next.js 14 with App Router - Authentication pages: login, register, forgot-password - Dashboard layout with Sidebar and Header - Dashboard pages: overview, cash-flow, revenue, expenses, metrics - Zustand stores for auth and UI state - Theme support with flash prevention ## Database Package (packages/database) - PostgreSQL migrations with multi-tenant architecture - Public schema: plans, tenants, users, sessions, subscriptions - Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts - Tenant management functions - Seed data for plans and super admin ## Shared Package (packages/shared) - TypeScript types: auth, tenant, financial, metrics, reports - Zod validation schemas for all entities - Utility functions for formatting ## UI Package (packages/ui) - Chart components: LineChart, BarChart, AreaChart, PieChart - Data components: DataTable, MetricCard, KPICard, AlertBadge - PeriodSelector and Skeleton components ## Infrastructure - Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog - Makefile with 25+ development commands - Development scripts: dev-setup.sh, dev-down.sh - Complete .env.example template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
212
apps/web/src/lib/api.ts
Normal file
212
apps/web/src/lib/api.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Cliente API para comunicación con el backend
|
||||
*/
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
interface ApiResponse<T = unknown> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface RequestOptions extends Omit<RequestInit, 'body'> {
|
||||
body?: unknown;
|
||||
params?: Record<string, string | number | boolean | undefined>;
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
status: number;
|
||||
data?: unknown;
|
||||
|
||||
constructor(message: string, status: number, data?: unknown) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el token de autenticación del storage
|
||||
*/
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('horux-auth-storage');
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return parsed?.state?.token || null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye la URL con query params
|
||||
*/
|
||||
function buildUrl(endpoint: string, params?: Record<string, string | number | boolean | undefined>): string {
|
||||
const url = new URL(endpoint.startsWith('http') ? endpoint : `${API_BASE_URL}${endpoint}`);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cliente fetch base con manejo de errores y auth
|
||||
*/
|
||||
async function fetchApi<T>(endpoint: string, options: RequestOptions = {}): Promise<ApiResponse<T>> {
|
||||
const { body, params, headers: customHeaders, ...restOptions } = options;
|
||||
|
||||
const token = getAuthToken();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...customHeaders,
|
||||
};
|
||||
|
||||
if (token) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const config: RequestInit = {
|
||||
...restOptions,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (body) {
|
||||
config.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = buildUrl(endpoint, params);
|
||||
const response = await fetch(url, config);
|
||||
|
||||
let data: T | undefined;
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = (data as { message?: string })?.message ||
|
||||
(data as { error?: string })?.error ||
|
||||
`Error ${response.status}`;
|
||||
throw new ApiError(errorMessage, response.status, data);
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
error instanceof Error ? error.message : 'Error de conexión',
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API Client con métodos HTTP
|
||||
*/
|
||||
export const api = {
|
||||
get<T>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||
return fetchApi<T>(endpoint, { ...options, method: 'GET' });
|
||||
},
|
||||
|
||||
post<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||
return fetchApi<T>(endpoint, { ...options, method: 'POST', body });
|
||||
},
|
||||
|
||||
put<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||
return fetchApi<T>(endpoint, { ...options, method: 'PUT', body });
|
||||
},
|
||||
|
||||
patch<T>(endpoint: string, body?: unknown, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||
return fetchApi<T>(endpoint, { ...options, method: 'PATCH', body });
|
||||
},
|
||||
|
||||
delete<T>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||
return fetchApi<T>(endpoint, { ...options, method: 'DELETE' });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints tipados para la API
|
||||
*/
|
||||
export const endpoints = {
|
||||
// Auth
|
||||
auth: {
|
||||
login: '/auth/login',
|
||||
register: '/auth/register',
|
||||
logout: '/auth/logout',
|
||||
me: '/auth/me',
|
||||
refresh: '/auth/refresh',
|
||||
},
|
||||
|
||||
// Users
|
||||
users: {
|
||||
base: '/users',
|
||||
byId: (id: string) => `/users/${id}`,
|
||||
profile: '/users/profile',
|
||||
},
|
||||
|
||||
// Strategies
|
||||
strategies: {
|
||||
base: '/strategies',
|
||||
byId: (id: string) => `/strategies/${id}`,
|
||||
activate: (id: string) => `/strategies/${id}/activate`,
|
||||
deactivate: (id: string) => `/strategies/${id}/deactivate`,
|
||||
backtest: (id: string) => `/strategies/${id}/backtest`,
|
||||
},
|
||||
|
||||
// Trades
|
||||
trades: {
|
||||
base: '/trades',
|
||||
byId: (id: string) => `/trades/${id}`,
|
||||
active: '/trades/active',
|
||||
history: '/trades/history',
|
||||
},
|
||||
|
||||
// Portfolio
|
||||
portfolio: {
|
||||
base: '/portfolio',
|
||||
balance: '/portfolio/balance',
|
||||
positions: '/portfolio/positions',
|
||||
performance: '/portfolio/performance',
|
||||
},
|
||||
|
||||
// Market Data
|
||||
market: {
|
||||
prices: '/market/prices',
|
||||
ticker: (symbol: string) => `/market/ticker/${symbol}`,
|
||||
candles: (symbol: string) => `/market/candles/${symbol}`,
|
||||
},
|
||||
|
||||
// Analytics
|
||||
analytics: {
|
||||
dashboard: '/analytics/dashboard',
|
||||
performance: '/analytics/performance',
|
||||
risk: '/analytics/risk',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export { ApiError };
|
||||
export type { ApiResponse, RequestOptions };
|
||||
Reference in New Issue
Block a user