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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
Reference in New Issue
Block a user