diff --git a/README.md b/README.md index a6c9e85..d85f84d 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,8 @@ La aplicación implementa las siguientes medidas de seguridad: - ✅ API Route proxy para todas las llamadas a Dolibarr - ✅ Variables de entorno seguras - ✅ Validación de datos de entrada +- ✅ Validación de token contra Dolibarr en cada request +- ✅ Sin caché de datos en servidor (evita datos antiguos) --- @@ -195,6 +197,10 @@ La aplicación implementa las siguientes medidas de seguridad: - Revisa la URL en `.env.local` - Comprueba que la API esté habilitada en Dolibarr +### Me pide login constantemente / No veo datos +- Si Dolibarr está apagado, el middleware fuerza login y no hay datos +- Asegúrate de tener Docker levantado y Dolibarr accesible en `http://localhost:8200` + ### No se muestran proyectos - Ejecuta `npm run seed` para crear datos de prueba - Verifica las credenciales de la API diff --git a/app/api/dolibarr/[...path]/route.ts b/app/api/dolibarr/[...path]/route.ts index eb1fefb..857c845 100644 --- a/app/api/dolibarr/[...path]/route.ts +++ b/app/api/dolibarr/[...path]/route.ts @@ -36,8 +36,8 @@ export async function GET( headers: { 'Accept': 'application/json', }, - // Cache durante 60 segundos - next: { revalidate: 60 } + // Sin cache en servidor para evitar datos antiguos + cache: 'no-store' }); if (!response.ok) { diff --git a/lib/authService.ts b/lib/authService.ts index 440606a..043802c 100644 --- a/lib/authService.ts +++ b/lib/authService.ts @@ -163,7 +163,8 @@ function saveAuthData(token: string, user?: AuthUser): void { } // También guardar en cookie para que el middleware pueda leerlo - document.cookie = `${AUTH_TOKEN_KEY}=${token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax`; + // Cookie de sesión: se elimina al cerrar el navegador + document.cookie = `${AUTH_TOKEN_KEY}=${token}; path=/; SameSite=Lax`; } /** diff --git a/lib/dolibarrClient.ts b/lib/dolibarrClient.ts index 0edb452..d60a202 100644 --- a/lib/dolibarrClient.ts +++ b/lib/dolibarrClient.ts @@ -28,8 +28,8 @@ async function dolibarrDirectFetch(endpoint: string, options: RequestInit = {}) 'Content-Type': 'application/json', ...options.headers, }, - // Cache durante 60 segundos en servidor - next: { revalidate: 60 } + // Sin cache en servidor para evitar datos antiguos + cache: 'no-store' }); if (!res.ok) { diff --git a/middleware.ts b/middleware.ts index aaf242f..ebc0b89 100644 --- a/middleware.ts +++ b/middleware.ts @@ -7,7 +7,28 @@ const PUBLIC_ROUTES = ["/login"]; // Rutas que siempre deben ser accesibles (assets, API, etc.) const ALWAYS_ALLOWED = ["/_next", "/api", "/favicon.ico", "/avatar.jpg"]; -export function middleware(request: NextRequest) { +async function verifyDolibarrToken(token: string): Promise { + const apiUrl = process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL; + + if (!apiUrl) { + return false; + } + + try { + const response = await fetch(`${apiUrl}/users/info?DOLAPIKEY=${token}`, { + headers: { + Accept: "application/json", + }, + cache: "no-store", + }); + + return response.ok; + } catch { + return false; + } +} + +export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // Permitir siempre assets y APIs @@ -15,21 +36,38 @@ export function middleware(request: NextRequest) { return NextResponse.next(); } - // Obtener token de las cookies (si existe) - const token = request.cookies.get("dolibarr_auth_token")?.value; - // Verificar si es ruta pública const isPublicRoute = PUBLIC_ROUTES.some((route) => pathname === route); - if (!token && !isPublicRoute) { - // No hay token y la ruta es privada -> redirigir a login - const loginUrl = new URL("/login", request.url); - loginUrl.searchParams.set("redirect", pathname); - return NextResponse.redirect(loginUrl); + // Obtener token de las cookies (si existe) + const token = request.cookies.get("dolibarr_auth_token")?.value; + + if (!token) { + if (!isPublicRoute) { + const loginUrl = new URL("/login", request.url); + loginUrl.searchParams.set("redirect", pathname); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); } - if (token && pathname === "/login") { - // Hay token y está en login -> redirigir a home + const isValid = await verifyDolibarrToken(token); + + if (!isValid) { + const loginUrl = new URL("/login", request.url); + loginUrl.searchParams.set("redirect", pathname); + + const response = NextResponse.redirect(loginUrl); + response.cookies.set("dolibarr_auth_token", "", { + path: "/", + expires: new Date(0), + }); + + return response; + } + + if (pathname === "/login") { return NextResponse.redirect(new URL("/", request.url)); } diff --git a/presentaciones/levi/capturas/direct-fetch.png b/presentaciones/levi/capturas/direct-fetch.png index fd52bf3..be9905f 100644 Binary files a/presentaciones/levi/capturas/direct-fetch.png and b/presentaciones/levi/capturas/direct-fetch.png differ diff --git a/presentaciones/levi/codigo_interno.md b/presentaciones/levi/codigo_interno.md index cd1299e..fb186ad 100644 --- a/presentaciones/levi/codigo_interno.md +++ b/presentaciones/levi/codigo_interno.md @@ -116,7 +116,7 @@ **Explicación:** - Si es servidor usa llamada directa. - Si es cliente usa el proxy interno. -- Se aplica caché corta (`revalidate: 60`). +- No se aplica caché en servidor (`cache: 'no-store'`). ---