feat: Fase 1 — Auth JWT+MFA, portales CRUD, UI completa
API: - AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify - JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator - PackagesModule: CRUD paquetes + historial de estados - PreAlertsModule: pre-alertas por usuario - UsersModule: gestión de usuarios + roles + activación - B2BModule: solicitudes de carga pesada/cotización - ValidationPipe global + CORS configurado Web (Next.js 15): - globals.css completo (design system + utility classes) - Layout raíz con WhatsApp flotante - /login + /registro funcionales con JWT y redirección por rol - /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil - /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría - /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho - /tracking: tracking real con progreso visual + historial - /calculadora: calculadora interactiva real (API SENAE §15) - /como-funciona, /tarifas, /quienes-somos, /casillero - /carga-pesada + /carga-pesada/cotizacion (formulario B2B) - lib/api.ts: cliente HTTP con auto-refresh de token Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA, OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001/api";
|
||||
|
||||
// ─── Auth token storage ─────────────────────────────────────────────────────
|
||||
export const getToken = (): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("mw_access");
|
||||
};
|
||||
export const setToken = (t: string) => localStorage.setItem("mw_access", t);
|
||||
export const getRefresh = (): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("mw_refresh");
|
||||
};
|
||||
export const setRefresh = (t: string) => localStorage.setItem("mw_refresh", t);
|
||||
export const clearAuth = () => {
|
||||
localStorage.removeItem("mw_access");
|
||||
localStorage.removeItem("mw_refresh");
|
||||
localStorage.removeItem("mw_user");
|
||||
};
|
||||
export const getUser = (): any | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
try { return JSON.parse(localStorage.getItem("mw_user") ?? "null"); } catch { return null; }
|
||||
};
|
||||
export const setUser = (u: any) => localStorage.setItem("mw_user", JSON.stringify(u));
|
||||
|
||||
// ─── Fetch wrapper ──────────────────────────────────────────────────────────
|
||||
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(opts.headers as Record<string, string>),
|
||||
};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...opts, headers });
|
||||
|
||||
// Token expirado — intentar refresh
|
||||
if (res.status === 401 && getRefresh()) {
|
||||
const refreshed = await tryRefresh();
|
||||
if (refreshed) {
|
||||
headers["Authorization"] = `Bearer ${getToken()}`;
|
||||
const retry = await fetch(`${API_BASE}${path}`, { ...opts, headers });
|
||||
if (!retry.ok) throw await extractError(retry);
|
||||
return retry.json();
|
||||
} else {
|
||||
clearAuth();
|
||||
window.location.href = "/login";
|
||||
throw new Error("Session expired");
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) throw await extractError(res);
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function extractError(res: Response): Promise<Error> {
|
||||
try {
|
||||
const body = await res.json();
|
||||
return new Error(body.message ?? "Error del servidor");
|
||||
} catch {
|
||||
return new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRefresh(): Promise<boolean> {
|
||||
const token = getRefresh();
|
||||
if (!token) return false;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken: token }),
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
setToken(data.accessToken);
|
||||
setRefresh(data.refreshToken);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
// ─── Auth ───────────────────────────────────────────────────────────────────
|
||||
export const api = {
|
||||
auth: {
|
||||
register: (body: any) => request<any>("/auth/register", { method: "POST", body: JSON.stringify(body) }),
|
||||
login: (body: any) => request<any>("/auth/login", { method: "POST", body: JSON.stringify(body) }),
|
||||
me: () => request<any>("/auth/me"),
|
||||
logout: (refreshToken: string) => request<any>("/auth/logout", { method: "POST", body: JSON.stringify({ refreshToken }) }),
|
||||
setupMfa: () => request<any>("/auth/mfa/setup", { method: "POST" }),
|
||||
verifyMfa:(totpCode: string) => request<any>("/auth/mfa/verify", { method: "POST", body: JSON.stringify({ totpCode }) }),
|
||||
},
|
||||
packages: {
|
||||
list: (params?: Record<string,string>) => request<any[]>("/packages" + (params ? "?" + new URLSearchParams(params) : "")),
|
||||
get: (id: string) => request<any>(`/packages/${id}`),
|
||||
create: (body: any) => request<any>("/packages", { method: "POST", body: JSON.stringify(body) }),
|
||||
updateStatus: (id: string, body: any) => request<any>(`/packages/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
},
|
||||
preAlerts: {
|
||||
list: () => request<any[]>("/pre-alerts"),
|
||||
create: (body: any) => request<any>("/pre-alerts", { method: "POST", body: JSON.stringify(body) }),
|
||||
delete: (id: string) => request<any>(`/pre-alerts/${id}`, { method: "DELETE" }),
|
||||
updateStatus: (id: string, body: any) => request<any>(`/pre-alerts/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
},
|
||||
users: {
|
||||
list: (search?: string) => request<any[]>("/users" + (search ? `?search=${search}` : "")),
|
||||
get: (id: string) => request<any>(`/users/${id}`),
|
||||
updateRole: (id: string, role: string) => request<any>(`/users/${id}/role`, { method: "PATCH", body: JSON.stringify({ role }) }),
|
||||
setActive: (id: string, isActive: boolean) => request<any>(`/users/${id}/active`, { method: "PATCH", body: JSON.stringify({ isActive }) }),
|
||||
},
|
||||
tracking: {
|
||||
search: (id: string) => request<any>(`/tracking/${id}`),
|
||||
},
|
||||
calculator: {
|
||||
calculate: (params: Record<string,string>) => request<any>("/calculator?" + new URLSearchParams(params)),
|
||||
},
|
||||
b2b: {
|
||||
create: (body: any) => request<any>("/b2b", { method: "POST", body: JSON.stringify(body) }),
|
||||
list: () => request<any[]>("/b2b"),
|
||||
updateStatus: (id: string, body: any) => request<any>(`/b2b/${id}/status`, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user