cambios en el crear producto y en el refresh token
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 |
@@ -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">
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
11
apps/web/lib/refreshApi.ts
Normal file
11
apps/web/lib/refreshApi.ts
Normal 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 |
Reference in New Issue
Block a user