cambios en el crear producto y en el refresh token

This commit is contained in:
2025-08-14 15:38:10 -04:00
parent 854b31f594
commit 6a28e141a9
19 changed files with 278 additions and 400 deletions

View File

@@ -43,7 +43,14 @@ export class JwtRefreshGuard implements CanActivate {
} }
private extractTokenFromHeader(request: Request): string | undefined { private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? []; const token = request.body.token
return type === 'Bearer' ? token : undefined; console.log(token);
if (token) {
return token;
}
// console.log(request.headers.authorization);
// const [type, token] = request.headers.authorization?.split(' ') ?? [];
// return type === 'Bearer' ? token : undefined;
} }
} }

View File

@@ -11,6 +11,7 @@ import {
HttpCode, HttpCode,
Patch, Patch,
Post, Post,
Req,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@@ -51,13 +52,18 @@ export class AuthController {
// } // }
@UseGuards(JwtRefreshGuard) @UseGuards(JwtRefreshGuard)
@Patch('refresh-token') @Public()
@HttpCode(200)
@Patch('refresh')
//@RequirePermissions('auth:refresh-token') //@RequirePermissions('auth:refresh-token')
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) { async refreshToken(@Req() req: Request,@Body() refreshTokenDto: RefreshTokenDto) {
console.log("Refrescando token..."); console.log("Pepe");
console.log(refreshTokenDto); console.log(req['user']);
//console.log(refreshTokenDto);
return await this.authService.refreshToken(refreshTokenDto); return null
// return await this.authService.refreshToken(refreshTokenDto);
} }
// @Public() // @Public()

View File

@@ -8,7 +8,7 @@ export class RefreshTokenDto {
}) })
refresh_token: string; refresh_token: string;
@ApiProperty() // @ApiProperty()
@IsNumber() // @IsNumber()
user_id: number; // user_id: number;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

View File

@@ -34,18 +34,29 @@ export default async function SurveyResponsePage({
const product = data.data const product = data.data
const lorem = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore placeat est corporis minus exercitationem impedit ab architecto dolorum nihil nam facilis suscipit porro, iure et quidem illo mollitia officia amet?" // const lorem = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore placeat est corporis minus exercitationem impedit ab architecto dolorum nihil nam facilis suscipit porro, iure et quidem illo mollitia officia amet?"
// console.log(data.data); // console.log(data.data);
return ( return (
// <PageContainer> // <PageContainer>
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'> <main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
<div className='w-full flex justify-between flex-col'>
<img <img
className="border-2 object-cover w-full f-full min-h-[400px] md:h-[85vh] aspect-square rounded-2xl" className="border-2 object-contain w-full f-full min-h-[400px] md:h-[70vh] aspect-square rounded-2xl"
src={`http://localhost:3000/${product.urlImg}`} src={`http://localhost:3000/uploads/inventory/${product.userId}/${product.urlImg}`}
alt="" alt=""
/> />
<section className=''>
<img
className="border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
src={`http://localhost:3000/uploads/inventory/${product.userId}/${product.urlImg}`}
alt=""
/>
</section>
</div>
<Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0"> <Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0">
<CardHeader className='py-2 px-2 md:px-4 lg:px-6'> <CardHeader className='py-2 px-2 md:px-4 lg:px-6'>
<CardTitle className="font-bold text-2xl"> <CardTitle className="font-bold text-2xl">

View File

@@ -2,9 +2,6 @@
import { safeFetchApi } from '@/lib'; import { safeFetchApi } from '@/lib';
import { loginResponseSchema, UserFormValue } from '../schemas/login'; import { loginResponseSchema, UserFormValue } from '../schemas/login';
type LoginActionSuccess = { type LoginActionSuccess = {
message: string; message: string;
user: { user: {

View File

@@ -1,21 +1,32 @@
'use server'; 'use server';
import { safeFetchApi } from '@/lib'; import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
import { import {
RefreshTokenResponseSchema, RefreshTokenResponseSchema,
RefreshTokenValue, RefreshTokenValue,
} from '../schemas/refreshToken'; } from '../schemas/refreshToken';
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => { export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
// return null // Descomentar esto evita que se tenga que borrar cache al navegador try {
const [error, data] = await safeFetchApi( const body = {
RefreshTokenResponseSchema, token: refreshToken.token,
'/auth/refreshToken', }
'POST',
refreshToken, // Usa la nueva instancia `refreshApi`
); const response = await refreshApi.patch('/auth/refresh', body);
if (error) {
console.error('Error:', error); const parsed = RefreshTokenResponseSchema.safeParse(response.data);
} else {
return data; if (!parsed.success) {
console.error('Error de validación en la respuesta de refresh token:', {
errors: parsed.error.errors,
receivedData: response.data,
});
return null;
}
return parsed.data;
} catch (error: any) { // Captura el error para acceso a error.response
console.error('Error al renovar el token:', error.response?.data || error.message);
return null;
} }
}; };

View File

@@ -117,18 +117,19 @@ export const getProductById = async (id: number) => {
return data; return data;
}; };
export const createProductAction = async (payload: InventoryTable) => { export const createProductAction = async (payload: FormData) => {
const session = await auth() const session = await auth()
const userId = session?.user?.id const userId = session?.user?.id
const { id, ...payloadWithoutId } = payload;
payloadWithoutId.userId = userId if (userId) {
payload.append('userId', String(userId));
}
const [error, data] = await safeFetchApi( const [error, data] = await safeFetchApi(
productMutate, productMutate,
'/products', '/products',
'POST', 'POST',
payloadWithoutId, payload,
); );
if (error) { if (error) {
@@ -136,7 +137,7 @@ export const createProductAction = async (payload: InventoryTable) => {
throw new Error('Error al crear el producto'); throw new Error('Error al crear el producto');
} }
return payloadWithoutId; return data;
}; };
export const updateUserAction2 = async (payload: InventoryTable) => { export const updateUserAction2 = async (payload: InventoryTable) => {

View File

@@ -16,56 +16,83 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@repo/shadcn/select'; } from '@repo/shadcn/select';
import { Textarea } from '@repo/shadcn/textarea';
import { Input } from '@repo/shadcn/input'; import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation";
import { EditInventory, editInventory } from '@/feactures/inventory/schemas/inventory'; import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory';
import { Textarea } from '@repo/shadcn/textarea';
import { STATUS } from '@/constants/status' import { STATUS } from '@/constants/status'
import { useState, useEffect } from 'react';
import { sizeFormate } from "@/feactures/inventory/utils/sizeFormate"
interface CreateFormProps { interface CreateFormProps {
onSuccess?: () => void; onSuccess?: () => void;
onCancel?: () => void; onCancel?: () => void;
defaultValues?: Partial<EditInventory>;
} }
export function CreateForm({ export function CreateForm({
onSuccess, onSuccess,
onCancel onCancel,
}: CreateFormProps) { }: CreateFormProps) {
const { const {
mutate: saveAccountingAccounts, mutate: saveProduct,
isPending: isSaving, isPending: isSaving,
isError,
} = useCreateUser(); } = useCreateUser();
const defaultformValues = { const [sizeFile, setSizeFile] = useState('0 bytes');
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
useEffect(() => {
return () => {
previewUrls.forEach(url => URL.revokeObjectURL(url));
};
}, [previewUrls]);
const defaultformValues: EditInventory = {
title: '', title: '',
description: '', description: '',
address: '',
price: '', price: '',
address: '',
status: 'BORRADOR',
stock: 0, stock: 0,
urlImg: '', urlImg: undefined,
status: '' };
}
const form = useForm<EditInventory>({ const form = useForm<EditInventory>({
resolver: zodResolver(editInventory), resolver: zodResolver(editInventory),
defaultValues: defaultformValues, defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation mode: 'onChange',
}); });
const onSubmit = async (data: EditInventory) => { const onSubmit = async (data: EditInventory) => {
const formData = new FormData();
saveAccountingAccounts(data, { formData.append('title', data.title);
formData.append('description', data.description);
formData.append('price', String(data.price));
formData.append('address', data.address);
formData.append('status', data.status);
formData.append('stock', String(data.stock));
if (data.urlImg) {
for (let i = 0; i < data.urlImg.length; i++) {
const file = data.urlImg[i];
if (file) {
formData.append('urlImg', file);
}
}
}
saveProduct(formData as any, {
onSuccess: () => { onSuccess: () => {
form.reset(); form.reset();
onSuccess?.(); onSuccess?.();
}, },
onError: (e) => { onError: (error) => {
console.error("Error al guardar el producto:", error);
form.setError('root', { form.setError('root', {
type: 'manual', type: 'manual',
message: e.message, message: error.message || 'Error al guardar el producto',
}); });
}, },
}); });
@@ -101,9 +128,7 @@ export function CreateForm({
<FormItem> <FormItem>
<FormLabel>Precio</FormLabel> <FormLabel>Precio</FormLabel>
<FormControl> <FormControl>
<Input {...field} <Input {...field} />
// value={field.value?.toString() ?? ''}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -158,7 +183,7 @@ export function CreateForm({
render={({ field }) => ( render={({ field }) => (
<FormItem className="w-full"> <FormItem className="w-full">
<FormLabel>Estatus</FormLabel> <FormLabel>Estatus</FormLabel>
<Select onValueChange={(value) => field.onChange(value)}> <Select value={field.value} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" /> <SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger> </SelectTrigger>
@@ -175,20 +200,55 @@ export function CreateForm({
)} )}
/> />
<div className="col-span-2">
<FormField <FormField
control={form.control} control={form.control}
name="urlImg" name="urlImg"
render={({ field }) => ( render={({ field: { onChange, onBlur, name, ref } }) => (
<FormItem> <FormItem>
<FormLabel>Imagen</FormLabel> <FormLabel>Imagen</FormLabel>
<p>Peso máximo: 5MB / {sizeFile} <span className='text-xs text-destructive'>(Máximo 10 archivos)</span></p>
<FormControl> <FormControl>
<Input {...field}/> <Input
type="file"
multiple
onBlur={onBlur}
name={name}
ref={ref}
onChange={(e) => {
if (e.target.files) {
const files = Array.from(e.target.files);
let size = 0;
const newPreviewUrls: string[] = [];
files.forEach(element => {
size += element.size;
newPreviewUrls.push(URL.createObjectURL(element));
});
const tamañoFormateado = sizeFormate(size);
setSizeFile(tamañoFormateado);
setPreviewUrls(newPreviewUrls);
onChange(e.target.files);
} else {
setPreviewUrls([]);
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
{previewUrls.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{previewUrls.map((url, index) => (
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
))}
</div>
)}
</FormItem> </FormItem>
)} )}
/> />
</div> </div>
</div>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button variant="outline" type="button" onClick={onCancel}> <Button variant="outline" type="button" onClick={onCancel}>

View File

@@ -1,5 +1,4 @@
'use client'; 'use client';
import { DataTable } from '@repo/shadcn/table/data-table'; import { DataTable } from '@repo/shadcn/table/data-table';
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton'; import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
import { columns } from './product-tables/columns'; import { columns } from './product-tables/columns';
@@ -27,7 +26,7 @@ export default function UsersAdminList({
const {data, isLoading} = useProductQuery(filters) const {data, isLoading} = useProductQuery(filters)
console.log(data?.data); // console.log(data?.data);
if (isLoading) { if (isLoading) {
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />; return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;

View File

@@ -1,231 +0,0 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@repo/shadcn/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@repo/shadcn/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form';
import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation";
import { editInventory, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
import { Textarea } from '@repo/shadcn/components/ui/textarea';
import {STATUS} from '@/constants/status'
import { useState } from 'react';
import {sizeFormate} from "@/feactures/inventory/utils/sizeFormate"
interface UpdateFormProps {
onSuccess?: () => void;
onCancel?: () => void;
defaultValues?: Partial<EditInventory>;
}
export function UpdateForm({
onSuccess,
onCancel,
defaultValues,
}: UpdateFormProps) {
const {
mutate: saveAccountingAccounts,
isPending: isSaving,
isError, // isError no se usa en el template, podrías usarlo para mostrar un mensaje global
} = useUpdateUser();
const [sizeFile, setSizeFile] = useState('0 bytes');
const defaultformValues: EditInventory = {
id: defaultValues?.id,
title: defaultValues?.title || '',
description: defaultValues?.description || '',
price: defaultValues?.price || '',
address: defaultValues?.address || '',
status: defaultValues?.status || 'BORRADOR',
stock: defaultValues?.stock ?? 0,
urlImg: [],
userId: defaultValues?.userId
};
const form = useForm<EditInventory>({
resolver: zodResolver(editInventory),
defaultValues: defaultformValues,
mode: 'onChange', // Enable real-time validation
});
const onSubmit = async (data: EditInventory) => {
// console.log(data);
saveAccountingAccounts(data, {
onSuccess: () => {
form.reset();
onSuccess?.();
},
onError: (error) => { // Captura el error para mostrar un mensaje más específico si es posible
console.error("Error al guardar el producto:", error);
form.setError('root', {
type: 'manual',
message: error.message || 'Error al guardar el producto', // Mejor mensaje de error
});
},
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{form.formState.errors.root && (
<div className="text-destructive text-sm">
{form.formState.errors.root.message}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Nombre/Título</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem >
<FormLabel>Precio</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Dirección</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Descripción</FormLabel>
<FormControl>
<Textarea {...field} className="resize-none"/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="stock"
render={({ field }) => (
<FormItem>
<FormLabel>Cantidad/Stock</FormLabel>
<FormControl>
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))}/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select value={field.value} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
<SelectContent>
{Object.entries(STATUS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="col-span-2">
<FormField
control={form.control}
name="urlImg"
render={({ field: { onChange, onBlur, name, ref } }) => (
<FormItem>
<FormLabel>Imagen</FormLabel>
<p>Peso máximo: 2MB / {sizeFile}</p>
<FormControl>
<Input
type="file"
multiple
onBlur={onBlur}
name={name}
ref={ref}
onChange={(e) => {
if (e.target.files) {
const files = Array.from(e.target.files);
// Calcular el tamaño total de los archivos
let size = 0;
files.forEach(element => size += element.size)
const tamañoFormateado = sizeFormate(size)
setSizeFile(tamañoFormateado);
}
onChange(e.target.files);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="flex justify-end gap-4">
<Button variant="outline" type="button" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -15,6 +15,7 @@ import { ImageIcon } from 'lucide-react';
export function ProductList() { export function ProductList() {
const router = useRouter(); const router = useRouter();
const { data: produts } = useAllProductQuery(); const { data: produts } = useAllProductQuery();
console.log(produts);
const handle = (id: number) => { const handle = (id: number) => {
router.push(`/dashboard/productos/${id}`); router.push(`/dashboard/productos/${id}`);
@@ -43,7 +44,7 @@ export function ProductList() {
<CardContent className="p-0 flex-grow"> <CardContent className="p-0 flex-grow">
<img <img
className="object-cover w-full h-full aspect-square border" className="object-cover w-full h-full aspect-square border"
src={`http://localhost:3000/${data.urlImg}`} src={`http://localhost:3000/uploads/inventory/${data.userId}/${data.urlImg}`}
alt="" alt=""
/> />
</CardContent> </CardContent>

View File

@@ -6,7 +6,7 @@ import { updateUserAction, createProductAction, updateUserAction2 } from "../act
export function useCreateUser() { export function useCreateUser() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (data: EditInventory) => createProductAction(data), mutationFn: (data: any) => createProductAction(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product'] }),
}) })
return mutation return mutation

View File

@@ -102,11 +102,12 @@ const authConfig: NextAuthConfig = {
callbacks: { callbacks: {
async jwt({ async jwt({
token, token,
user user,
}: { }: {
token: any; token: any;
user: User; user: User;
}) { }) {
try {
// Si es un nuevo login, asignamos los datos // Si es un nuevo login, asignamos los datos
if (user) { if (user) {
token.id = user.id; token.id = user.id;
@@ -118,30 +119,44 @@ const authConfig: NextAuthConfig = {
token.access_expire_in = user.access_expire_in; token.access_expire_in = user.access_expire_in;
token.refresh_token = user.refresh_token; token.refresh_token = user.refresh_token;
token.refresh_expire_in = user.refresh_expire_in; token.refresh_expire_in = user.refresh_expire_in;
return token; // IMPORTANTE: retornamos el token aquí para evitar que entre en el siguiente 'if'
} }
// Renovar access_token si ha expirado // Verificar si el access_token ha expirado
if (Date.now() / 1000 > (token.access_expire_in as number)) { const now = Date.now() / 1000;
if (Date.now() / 1000 > (token.refresh_expire_in as number)) { if (now < (token.access_expire_in as number)) {
return token; // Si el token no ha expirado, lo retornamos sin cambios
}
// Si el access_token ha expirado, verificar el refresh_token
if (now > (token.refresh_expire_in as number)) {
console.log("Refresh token ha expirado. Forzando logout.");
return null; // Forzar logout return null; // Forzar logout
} }
try { // Si el access_token ha expirado pero el refresh_token es válido, renovar
console.log("Renovando token de acceso...");
const res = await resfreshTokenAction({ const res = await resfreshTokenAction({
token: token.refresh_token as string, token: token.refresh_token as string,
}); });
if (!res) throw new Error('Failed to refresh token');
token.access_token = res.tokens.access_token;
token.access_expire_in = res.tokens.access_expire_in;
token.refresh_token = res.tokens.refresh_token;
token.refresh_expire_in = res.tokens.refresh_expire_in;
} catch (error) {
console.log("error: ",error);
return null;
}
}
return token; console.log("Pepe");
if (!res) throw new Error('Fallo al renovar el token');
// Actualizar el token con la nueva información
return {
...token,
access_token: res.tokens.access_token,
access_expire_in: res.tokens.access_expire_in,
refresh_token: res.tokens.refresh_token,
refresh_expire_in: res.tokens.refresh_expire_in,
};
} catch (error) {
console.error("Error al renovar el token: ", error);
return null; // Fallo al renovar, forzar logout
}
}, },
async session({ session, token }: { session: Session; token: DefaultJWT }) { async session({ session, token }: { session: Session; token: DefaultJWT }) {
session.access_token = token.access_token as string; session.access_token = token.access_token as string;

View File

@@ -1,19 +1,18 @@
'use server'; 'use server';
import { env } from '@/lib/env'; // Importamos la configuración de entorno validada import { env } from '@/lib/env';
import axios from 'axios'; import axios from 'axios';
import { z } from 'zod'; import { z } from 'zod';
// Crear instancia de Axios con la URL base validada // Crear instancia de Axios con la URL base validada
const fetchApi = axios.create({ const fetchApi = axios.create({
baseURL: env.API_URL, // Aquí usamos env.API_URL en vez de process.env.BACKEND_URL baseURL: env.API_URL,
// No establecer Content-Type aquí. Axios lo manejará automáticamente con FormData.
}); });
// Interceptor para incluir el token automáticamente en las peticiones // Interceptor para incluir el token automáticamente en las peticiones
// ESTE INTERCEPTOR ESTÁ BIEN PARA EL RESTO DE LAS PETICIONES AUTENTICADAS
fetchApi.interceptors.request.use(async (config: any) => { fetchApi.interceptors.request.use(async (config: any) => {
try { try {
// Importación dinámica para evitar la referencia circular const { auth } = await import('@/lib/auth'); // Importación dinámica
const { auth } = await import('@/lib/auth');
const session = await auth(); const session = await auth();
const token = session?.access_token; const token = session?.access_token;
@@ -22,44 +21,35 @@ fetchApi.interceptors.request.use(async (config: any) => {
} }
// **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente. // **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente.
// Esto es necesario porque 'multipart/form-data' requiere un 'boundary' que Axios añade.
if (config.data instanceof FormData) { if (config.data instanceof FormData) {
delete config.headers['Content-Type']; delete config.headers['Content-Type'];
} else { } else {
// Para otros tipos de datos, asegura que el Content-Type sea 'application/json'
config.headers['Content-Type'] = 'application/json'; config.headers['Content-Type'] = 'application/json';
} }
} catch (error) {
console.error('Error getting auth token:', error);
}
return config; return config;
} catch (error) {
console.error('Error al obtener el token de autenticación para el interceptor:', error);
// IMPORTANTE: Si ocurre un error aquí, es mejor rechazar la promesa
// para que la solicitud no se envíe sin autorización.
return Promise.reject(error);
}
}); });
/** // safeFetchApi sigue siendo útil para el resto de las llamadas que requieren autenticación
* 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 method - Método HTTP (GET, POST, PUT, PATCH, DELETE)
* @param data - Datos a enviar (puede ser un objeto JSON o FormData para archivos)
* @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>>( export const safeFetchApi = async <T extends z.ZodSchema<any>>(
schema: T, schema: T,
url: string, url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET', method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
data?: any, // Renombrado a 'data' para mayor claridad y consistencia con Axios data?: any,
): Promise< ): Promise<
[{ type: string; message: string; details?: any } | null, z.infer<T> | null] [{ type: string; message: string; details?: any } | null, z.infer<T> | null]
> => { > => {
try { try {
console.log(url,method,data);
const response = await fetchApi({ const response = await fetchApi({
method, method,
url, url,
data, // Axios usa 'data' para el body de POST/PUT/PATCH data,
}); });
const parsed = schema.safeParse(response.data); const parsed = schema.safeParse(response.data);

View File

@@ -0,0 +1,11 @@
import axios from 'axios';
import { env } from '@/lib/env'; // Asegúrate de que env está correctamente configurado
// Crea una instancia de Axios SÓLO para la API de refresh token
// Sin el interceptor que obtiene la sesión para evitar la dependencia circular
export const refreshApi = axios.create({
baseURL: env.API_URL,
headers: {
'Content-Type': 'application/json', // El refresh token se envía en el body JSON
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB