feat(security): implement permission checks in tenant CRUD routes and enhance API error handling
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { getToken } from './auth';
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
// Normalize API_BASE_URL to remove trailing slash
|
||||
const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
|
||||
@@ -172,6 +173,23 @@ async function fetchApi<T = any>(
|
||||
|
||||
// Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
|
||||
// Si es 403 (Forbidden), mostrar toast de permisos insuficientes
|
||||
if (response.status === 403) {
|
||||
if (browser) {
|
||||
toast.error('No tienes permisos para realizar esta acción', {
|
||||
duration: 4000,
|
||||
description: 'Contacta a tu administrador si crees que esto es un error'
|
||||
});
|
||||
}
|
||||
// Retornar el error 403 sin intentar refresh
|
||||
const data = await response.json();
|
||||
return {
|
||||
error: data.detail || 'No tienes permisos para realizar esta acción',
|
||||
status: 403
|
||||
};
|
||||
}
|
||||
|
||||
// Si es 401, intentar refrescar el token
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
// Campo de comentario estatus
|
||||
comments_status: invoice.comments_status || '',
|
||||
// Campos que van en diferentes recursos pero se editan aquí
|
||||
transport_mode: invoice.logistics?.[0]?.transport_mode || null,
|
||||
transport_mode: invoice.logistics?.transport_mode || null,
|
||||
is_mixed: invoice.compliance_mx?.is_mixed || null,
|
||||
print_stamp: invoice.financials?.seal_value_2500 || false,
|
||||
rule_3121_parties_ii: false,
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function refreshAccessToken(
|
||||
* @param cookies - Objeto de cookies de SvelteKit
|
||||
* @param fetch - Función fetch de SvelteKit
|
||||
* @param redirectUrl - URL a la que redirigir si falla la autenticación (opcional)
|
||||
* @param timeout - Timeout en milisegundos (default: 10000ms)
|
||||
* @param timeout - Timeout en milisegundos (default: 30000ms)
|
||||
*/
|
||||
export async function authenticatedFetch(
|
||||
endpoint: string,
|
||||
@@ -141,7 +141,7 @@ export async function authenticatedFetch(
|
||||
cookies: Cookies,
|
||||
fetch: typeof globalThis.fetch,
|
||||
redirectUrl?: string,
|
||||
timeout: number = 10000
|
||||
timeout: number = 30000
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const baseUrl = getServerApiUrl();
|
||||
@@ -180,6 +180,12 @@ export async function authenticatedFetch(
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Si es 403, no intentar refrescar - es un problema de permisos
|
||||
if (response.status === 403) {
|
||||
console.warn('🚫 [API] Acceso denegado (403):', endpoint);
|
||||
return response; // Retornar directamente para que el llamador maneje el error
|
||||
}
|
||||
|
||||
// Si es 401, intentar refrescar el token
|
||||
if (response.status === 401) {
|
||||
const newToken = await refreshAccessToken(cookies, fetch);
|
||||
@@ -346,3 +352,32 @@ export async function getActiveCompanyId(
|
||||
|
||||
return companyId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para manejar respuestas de API y convertir errores 403 en formato adecuado
|
||||
* para mostrar toasts en el cliente
|
||||
*/
|
||||
export async function handleApiResponse<T = any>(
|
||||
response: Response
|
||||
): Promise<{ data?: T; error?: { detail: string; status: number; isForbidden?: boolean } }> {
|
||||
if (response.ok) {
|
||||
// Para respuestas sin contenido (204)
|
||||
if (response.status === 204) {
|
||||
return { data: null as T };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
}
|
||||
|
||||
// Manejar errores
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Error desconocido' }));
|
||||
|
||||
const error = {
|
||||
detail: errorData.detail || errorData.message || 'Error en la petición',
|
||||
status: response.status,
|
||||
isForbidden: response.status === 403
|
||||
};
|
||||
|
||||
return { error };
|
||||
}
|
||||
|
||||
84
frontend/src/lib/utils/error-handler.ts
Normal file
84
frontend/src/lib/utils/error-handler.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Utilidades para manejar errores de API en el cliente
|
||||
*/
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export interface ApiError {
|
||||
detail: string;
|
||||
status: number;
|
||||
isForbidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja errores de API mostrando el toast apropiado
|
||||
* @param error - El error a manejar (puede ser un objeto ApiError o un string)
|
||||
* @returns true si se manejó un error, false si no había error
|
||||
*/
|
||||
export function handleApiError(error?: ApiError | string | null): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
// Si es un string, convertirlo a objeto
|
||||
if (typeof error === 'string') {
|
||||
// Detectar si es un error 403
|
||||
if (error.includes('403') || error.toLowerCase().includes('forbidden')) {
|
||||
toast.error(error, {
|
||||
duration: 5000,
|
||||
description: 'No tienes permisos para realizar esta acción'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otros errores en formato string
|
||||
toast.error(error, {
|
||||
duration: 4000
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Es un objeto ApiError
|
||||
if (error.isForbidden || error.status === 403) {
|
||||
// Mostrar el mensaje específico del backend si está disponible
|
||||
const message = error.detail || 'No tienes permisos para realizar esta acción';
|
||||
toast.error(message, {
|
||||
duration: 5000,
|
||||
description: error.detail ? 'Contacta a tu administrador si crees que esto es un error' : undefined
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error.status === 401) {
|
||||
toast.error('Sesión expirada', {
|
||||
duration: 3000,
|
||||
description: 'Por favor, inicia sesión nuevamente'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otros errores
|
||||
toast.error(error.detail || 'Error en la operación', {
|
||||
duration: 4000
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook para usar en componentes Svelte con $effect
|
||||
* Muestra automáticamente un toast cuando hay un error
|
||||
*
|
||||
* Ejemplo de uso en +page.svelte:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { handleApiError } from '$lib/utils/error-handler';
|
||||
* let { data } = $props();
|
||||
*
|
||||
* $effect(() => {
|
||||
* handleApiError(data.error);
|
||||
* });
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useErrorHandler(error?: ApiError | null) {
|
||||
if (error) {
|
||||
handleApiError(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user