Update security measures and improve caching strategy in API calls

This commit is contained in:
Levi Planelles 2026-05-18 09:50:31 +02:00
parent 8ae76d86c7
commit 33d4f1df70
7 changed files with 62 additions and 17 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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`;
}
/**

View File

@ -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) {

View File

@ -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<boolean> {
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));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View File

@ -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'`).
---