Cambios en refresh token para no dar error. No actualiza access token de la cookie/session. Elimina access token de la cookie para forzar cerrar la session en caso de error

This commit is contained in:
2026-03-23 10:20:48 -04:00
parent 0666877811
commit f88ab2a971
7 changed files with 85 additions and 127 deletions

View File

@@ -8,7 +8,7 @@ import { AuthService } from './auth.service';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) {} constructor(private readonly authService: AuthService) { }
@Public() @Public()
@HttpCode(200) @HttpCode(200)
@@ -28,6 +28,8 @@ export class AuthController {
return await this.authService.signIn(signInUserDto); return await this.authService.signIn(signInUserDto);
} }
@Public()
@HttpCode(200)
@Post('sign-out') @Post('sign-out')
//@RequirePermissions('auth:sign-out') //@RequirePermissions('auth:sign-out')
async signOut(@Body() signOutUserDto: SignOutUserDto) { async signOut(@Body() signOutUserDto: SignOutUserDto) {
@@ -47,6 +49,10 @@ export class AuthController {
@Patch('refresh') @Patch('refresh')
//@RequirePermissions('auth:refresh-token') //@RequirePermissions('auth:refresh-token')
async refreshToken(@Body() refreshTokenDto: any) { async refreshToken(@Body() refreshTokenDto: any) {
// console.log('REFRESCANDO');
// console.log(refreshTokenDto);
// console.log('-----------');
return await this.authService.refreshToken(refreshTokenDto); return await this.authService.refreshToken(refreshTokenDto);
} }

View File

@@ -40,7 +40,7 @@ export class AuthService {
private readonly config: ConfigService<Env>, private readonly config: ConfigService<Env>,
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>, @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private readonly mailService: MailService, private readonly mailService: MailService,
) {} ) { }
//Decode Tokens //Decode Tokens
// Método para decodificar el token y obtener los datos completos // Método para decodificar el token y obtener los datos completos

View File

@@ -1,27 +1,47 @@
'use server'; 'use server';
import { safeFetchApi } from '@/lib'; // import { safeFetchApi } from '@/lib';
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { logoutResponseSchema } from '../schemas/logout'; import { logoutResponseSchema } from '../schemas/logout';
export const logoutAction = async (user_id: string) => { export const logoutAction = async (user_id: string) => {
const payload = { user_id }; try {
const response = await refreshApi.post('/auth/sign-out', { user_id });
const [error, data] = await safeFetchApi( const parsed = logoutResponseSchema.safeParse(response.data);
logoutResponseSchema,
'/auth/sign-out',
'POST',
payload,
);
if (error) { if (!parsed.success) {
console.error('Error:', error); console.error('Error de validación en la respuesta de refresh token:', {
// Devuelve un objeto con la propiedad 'type' para que el callback de NextAuth lo reconozca como un error errors: parsed.error.errors,
return { receivedData: response.data,
type: 'API_ERROR', });
message: error.message, return null;
};
} }
return parsed.data;
} catch (error: any) { // Captura el error para acceso a error.response
console.error('Error al cerrar sesion:', error.response?.data || error.message);
return null;
}
// const payload = { user_id };
// const [error, data] = await safeFetchApi(
// logoutResponseSchema,
// '/auth/sign-out',
// 'POST',
// payload,
// );
// if (error) {
// console.error('Error:', error);
// // Devuelve un objeto con la propiedad 'type' para que el callback de NextAuth lo reconozca como un error
// return {
// type: 'API_ERROR',
// message: error.message,
// };
// }
(await cookies()).delete('refresh_token'); (await cookies()).delete('refresh_token');
}; };

View File

@@ -1,4 +1,3 @@
// auth/actions/refresh-token-action.ts
'use server'; 'use server';
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
import { import {

View File

@@ -4,12 +4,19 @@ import { tokensSchema } from './login';
// Esquema para el refresh token // Esquema para el refresh token
export const refreshTokenSchema = z.object({ export const refreshTokenSchema = z.object({
token: z.string(), refreshToken: z.string(),
}); });
export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>; export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>;
// Esquema final para la respuesta del backend // Esquema final para la respuesta del backend
export const RefreshTokenResponseSchema = z.object({ // export const RefreshTokenResponseSchema = z.object({
tokens: tokensSchema, // // tokens: tokensSchema,
}); // access_token: z.string(),
// access_expire_in: z.number(),
// refresh_token: z.string(),
// refresh_expire_in: z.number()
// });
export const RefreshTokenResponseSchema = tokensSchema

View File

@@ -6,7 +6,12 @@ import { cache } from 'react';
export const getValidAccessToken = cache(async () => { export const getValidAccessToken = cache(async () => {
const session = await auth(); const session = await auth();
if (!session?.access_token) return null; if (!session?.access_token) {
// console.log('No hay Access Token');
return null
}
// console.log('Si hay Access Token');
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
// Restamos 10s para tener margen de seguridad // Restamos 10s para tener margen de seguridad
@@ -14,24 +19,35 @@ export const getValidAccessToken = cache(async () => {
// A. Si es válido, lo retornamos directo // A. Si es válido, lo retornamos directo
if (isValid) return session.access_token; if (isValid) return session.access_token;
// console.log('Access Token Expiró');
// B. Si expiró, buscamos la cookie // B. Si expiró, buscamos la cookie
const cookieStore = cookies(); const cookieStore = cookies();
const refreshToken = (await cookieStore).get('refresh_token')?.value; const refreshTokenCookie = await cookieStore
const refreshToken = refreshTokenCookie.get('refresh_token')?.value;
if (!refreshToken) {
// console.log('No hay Refresh Token');
return null
} // No hay refresh token, fin del juego
// console.log('Si hay Refresh Token');
if (!refreshToken) return null; // No hay refresh token, fin del juego
// C. Intentamos refrescar // C. Intentamos refrescar
const newTokens = await resfreshTokenAction({ token: refreshToken }); const newTokens = await resfreshTokenAction({ refreshToken });
if (!newTokens) { if (!newTokens) {
// console.log('No hay token nuevo');
// Si falla el refresh (token revocado o expirado), borramos cookie // Si falla el refresh (token revocado o expirado), borramos cookie
(await cookieStore).delete('refresh_token'); (await cookieStore).delete('refresh_token');
(await cookieStore).delete('authjs.session-token');
return null; return null;
} }
// console.log('Si hay token nuevo');
// D. Guardamos el nuevo refresh token en cookie y retornamos el access token // D. Guardamos el nuevo refresh token en cookie y retornamos el access token
(await cookieStore).set('refresh_token', newTokens.tokens.refresh_token, { (await cookieStore).set('refresh_token', newTokens.refresh_token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', sameSite: 'lax',
@@ -39,5 +55,13 @@ export const getValidAccessToken = cache(async () => {
maxAge: 7 * 24 * 60 * 60, maxAge: 7 * 24 * 60 * 60,
}); });
return newTokens.tokens.access_token; // (await cookieStore).set('authjs.session-token', newTokens.access_token, {
// httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
// sameSite: 'lax',
// path: '/',
// maxAge: 7 * 24 * 60 * 60,
// });
return newTokens.access_token;
}); });

View File

@@ -1,98 +0,0 @@
'use server';
import { env } from '@/lib/env'; // Importamos la configuración de entorno validada
import axios, { InternalAxiosRequestConfig } from 'axios';
import { z } from 'zod';
// Crear instancia de Axios con la URL base validada
const fetchApi = axios.create({
baseURL: env.API_URL, // Aquí usamos env.API_URL en vez de process.env.BACKEND_URL
headers: {
'Content-Type': 'application/json',
},
});
// Interceptor para incluir el token automáticamente en las peticiones
fetchApi.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
try {
const { getValidAccessToken } = await import('@/lib/auth-token');
const token = await getValidAccessToken();
if (token) {
config.headers.set('Authorization', `Bearer ${token}`);
}
} catch (err) {
console.error('Error getting auth token:', err);
}
return config;
},
);
/**
* Función para hacer peticiones con validación de respuesta
* @param schema - Esquema de Zod para validar la respuesta
* @param url - Endpoint a consultar
* @param config - Configuración opcional de Axios
* @returns [error, data] -> Devuelve el error como objeto estructurado si hay fallo, o los datos validados
*/
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
schema: T,
url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
body?: any,
): Promise<
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
> => {
try {
const response = await fetchApi({
method,
url,
data: body,
});
const parsed = schema.safeParse(response.data);
if (!parsed.success) {
console.error('Validation Error Details:', {
errors: parsed.error.errors,
receivedData: response.data,
expectedSchema: schema,
data: response.data.data,
});
// console.error(parsed.error.errors)
return [
{
type: 'VALIDATION_ERROR',
message: 'Validation error',
details: parsed.error.errors,
},
null,
];
}
return [null, parsed.data];
} catch (error: any) {
const errorDetails = {
status: error.response?.status,
statusText: error.response?.statusText,
message: error.message,
url: error.config?.url,
method: error.config?.method,
requestData: error.config?.data,
responseData: error.response?.data,
headers: error.config?.headers,
};
// console.log(error)
return [
{
type: 'API_ERROR',
message: error.response?.data?.message || 'Unknown API error',
details: errorDetails,
},
null,
];
}
};
export { fetchApi };