## 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>
213 lines
5.1 KiB
TypeScript
213 lines
5.1 KiB
TypeScript
/**
|
|
* 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 };
|