209 lines
11 KiB
TypeScript
209 lines
11 KiB
TypeScript
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"),
|
|
updateProfile: (body: { firstName?: string; lastName?: string; phone?: string }) =>
|
|
request<any>("/auth/me", { method: "PATCH", body: JSON.stringify(body) }),
|
|
changePassword: (body: { oldPassword: string; newPassword: string }) =>
|
|
request<any>("/auth/password", { method: "PATCH", body: JSON.stringify(body) }),
|
|
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 }) }),
|
|
disableMfa: (totpCode: string) => request<any>("/auth/mfa/disable", { 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) }),
|
|
verify: (id: string, body: any) => request<any>(`/packages/${id}/verify`, { method: "PATCH", body: JSON.stringify(body) }),
|
|
pendingDeclaration: () => request<any[]>("/packages/pending-declaration"),
|
|
uploadPhotos: (id: string, formData: FormData) => {
|
|
const token = getToken();
|
|
return fetch(`${API_BASE}/packages/${id}/photos`, {
|
|
method: "POST",
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
body: formData,
|
|
}).then(r => { if (!r.ok) throw new Error("Upload failed"); return r.json(); });
|
|
},
|
|
senaeDeclare: (id: string, body: any) => request<any>(`/packages/${id}/senae/declare`, { method: "POST", body: JSON.stringify(body) }),
|
|
register: (body: any) => request<any>("/packages/register", { method: "POST", 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) }),
|
|
uploadInvoice: (id: string, file: File) => {
|
|
const token = getToken();
|
|
const fd = new FormData();
|
|
fd.append("invoice", file);
|
|
return fetch(`${API_BASE}/pre-alerts/${id}/invoice`, {
|
|
method: "POST",
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
body: fd,
|
|
}).then(r => { if (!r.ok) throw new Error("Upload failed"); return r.json(); });
|
|
},
|
|
},
|
|
users: {
|
|
list: (search?: string) => request<any[]>("/users" + (search ? `?search=${search}` : "")),
|
|
get: (id: string) => request<any>(`/users/${id}`),
|
|
create: (body: { email: string; password: string; firstName: string; lastName: string; phone?: string; role: string }) =>
|
|
request<any>("/users", { method: "POST", body: JSON.stringify(body) }),
|
|
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) }),
|
|
},
|
|
tariffs: {
|
|
get: () => request<any>("/tariffs"),
|
|
update: (body: any) => request<any>("/tariffs", { method: "PUT", body: JSON.stringify(body) }),
|
|
},
|
|
auditLogs: {
|
|
list: (params?: Record<string,string>) => request<any>("/audit-logs" + (params ? "?" + new URLSearchParams(params) : "")),
|
|
},
|
|
products: {
|
|
scan: (url: string) => request<any>("/products/scan", { method: "POST", body: JSON.stringify({ url }) }),
|
|
},
|
|
warehouses: {
|
|
list: () => request<any[]>("/warehouses"),
|
|
getDefault: () => request<any>("/warehouses/default"),
|
|
get: (id: string) => request<any>(`/warehouses/${id}`),
|
|
create: (body: any) => request<any>("/warehouses", { method: "POST", body: JSON.stringify(body) }),
|
|
update: (id: string, body: any) => request<any>(`/warehouses/${id}`, { method: "PATCH", body: JSON.stringify(body) }),
|
|
setDefault: (id: string) => request<any>(`/warehouses/${id}/set-default`, { method: "PATCH" }),
|
|
remove: (id: string) => request<any>(`/warehouses/${id}`, { method: "DELETE" }),
|
|
},
|
|
integrations: {
|
|
list: () => request<any[]>("/integrations"),
|
|
status: () => request<any>("/integrations/status"),
|
|
batchUpsert: (items: Array<{ key: string; value: string | null; isActive: boolean }>) =>
|
|
request<any>("/integrations/batch", { method: "PUT", body: JSON.stringify({ items }) }),
|
|
},
|
|
notificationTemplates: {
|
|
list: () => request<any[]>("/notification-templates"),
|
|
seed: () => request<any>("/notification-templates/seed", { method: "POST" }),
|
|
update: (id: string, body: { body: string; subject?: string; isActive?: boolean }) =>
|
|
request<any>(`/notification-templates/${id}`, { method: "PUT", body: JSON.stringify(body) }),
|
|
},
|
|
notifications: {
|
|
list: (limit = 20) => request<any[]>(`/notifications?limit=${limit}`),
|
|
},
|
|
payments: {
|
|
list: (status?: string) => request<any[]>(`/payments${status ? `?status=${status}` : ""}`),
|
|
packageDetail: (packageId: string) => request<any>(`/payments/package/${packageId}`),
|
|
byTracking: (trackingId: string) => request<any>(`/payments/track/${trackingId}`),
|
|
createIntent: (packageId: string, provider = "stripe") =>
|
|
request<any>("/payments/intent", { method: "POST", body: JSON.stringify({ packageId, provider }) }),
|
|
confirm: (paymentId: string) =>
|
|
request<any>(`/payments/${paymentId}/confirm`, { method: "POST" }),
|
|
confirmSession: (sessionId: string) =>
|
|
request<any>("/payments/confirm-session", { method: "POST", body: JSON.stringify({ sessionId }) }),
|
|
},
|
|
consolidations: {
|
|
list: () => request<any[]>("/consolidations"),
|
|
get: (id: string) => request<any>(`/consolidations/${id}`),
|
|
create: (notes?: string) => request<any>("/consolidations", { method: "POST", body: JSON.stringify({ notes }) }),
|
|
addPackage: (id: string, packageId: string) =>
|
|
request<any>(`/consolidations/${id}/packages`, { method: "POST", body: JSON.stringify({ packageId }) }),
|
|
removePackage: (id: string, packageId: string) =>
|
|
request<any>(`/consolidations/${id}/packages/${packageId}`, { method: "DELETE" }),
|
|
close: (id: string, courierTracking?: string) =>
|
|
request<any>(`/consolidations/${id}/close`, { method: "POST", body: JSON.stringify({ courierTracking }) }),
|
|
dispatch: (id: string, courierTracking: string) =>
|
|
request<any>(`/consolidations/${id}/dispatch`, { method: "POST", body: JSON.stringify({ courierTracking }) }),
|
|
},
|
|
};
|