Validacion y restriccion de archivos + almacenamiento en carpeta

This commit is contained in:
2025-08-05 15:07:26 -04:00
parent e18c25e2ee
commit 8a54bf7138
10 changed files with 1613 additions and 74 deletions

View File

@@ -0,0 +1,6 @@
DROP VIEW "public"."v_product_store";--> statement-breakpoint
ALTER TABLE "products" ADD COLUMN "gallery" text[] DEFAULT '{}'::text[] NOT NULL;--> statement-breakpoint
CREATE VIEW "public"."v_product_store" AS (
select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.gallery, p.address, p.status, p.user_id, u.fullname, u.email, u.phone
from products p
left join auth.users as u on u.id = p.user_id);

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,13 @@
"when": 1752507413748, "when": 1752507413748,
"tag": "0006_real_tyger_tiger", "tag": "0006_real_tyger_tiger",
"breakpoints": true "breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1754420096323,
"tag": "0007_curved_fantastic_four",
"breakpoints": true
} }
] ]
} }

View File

@@ -16,7 +16,8 @@ export async function seedProducts(db: NodePgDatabase<typeof schema>) {
address:"Calle 1", address:"Calle 1",
status:'PUBLICADO', // PUBLICADO, AGOTADO, BORRADOR status:'PUBLICADO', // PUBLICADO, AGOTADO, BORRADOR
urlImg:'apple.avif', urlImg:'apple.avif',
userId:1 userId:1,
gallery: ["Pruebas"]
} }
]; ];

View File

@@ -1,5 +1,4 @@
import { Controller, Post, UploadedFiles, UseInterceptors, Body } from '@nestjs/common';
import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express'; import { FilesInterceptor } from '@nestjs/platform-express';
import { PicturesService } from './pictures.service'; import { PicturesService } from './pictures.service';
@@ -9,13 +8,14 @@ export class PicturesController {
@Post('upload') @Post('upload')
@UseInterceptors(FilesInterceptor('urlImg')) @UseInterceptors(FilesInterceptor('urlImg'))
async uploadFile(@UploadedFiles() files: Express.Multer.File[]) { async uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
console.log(files); // Aquí puedes acceder a los campos del formulario
// console.log('Archivos:', files);
// console.log('Otros campos del formulario:', body);
const result = await this.picturesService.saveImages(files);
console.log(result);
// const result = await this.picturesService.saveImages(files); return { data: result };
// console.log(result);
return {data: ["result"]}
} }
} }

View File

@@ -14,21 +14,20 @@ export class PicturesService {
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict'); const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
console.log(picturesPath);
let images : string[] = []; let images : string[] = [];
console.log(file);
let count = 0; let count = 0;
// file.forEach(async (file) => { file.forEach(async (file) => {
// // count++ count++
// // const fileName = `${Date.now()}-${count++}-${file.originalname}`; const fileName = `${Date.now()}-${count}-${file.originalname}`;
// // console.log(fileName); images.push(fileName);
// // const filePath = join(picturesPath, fileName); // console.log(fileName);
// // await writeFile(filePath, file.buffer); const filePath = join(picturesPath, fileName);
// // images.push(fileName); await writeFile(filePath, file.buffer);
// }); });
// return [file[0].originalname] // return [file[0].originalname]
return images; return images;

View File

@@ -33,12 +33,12 @@ import { z } from 'zod'; // Asegúrate de importar Zod
// Si tu EditInventory original no contempla FileList, crea un esquema para el formulario. // Si tu EditInventory original no contempla FileList, crea un esquema para el formulario.
// Ejemplo de cómo podrías adaptar tu esquema para el formulario // Ejemplo de cómo podrías adaptar tu esquema para el formulario
const formSchemaWithFiles = editInventory.extend({ // const formSchemaWithFiles = editInventory.extend({
urlImg: z.custom<FileList | undefined | null>().optional(), // Ahora permite FileList para el input file // urlImg: z.custom<FileList | undefined | null>().optional(), // Ahora permite FileList para el input file
}); // });
// Define un tipo para los datos del formulario que incluye el FileList // Define un tipo para los datos del formulario que incluye el FileList
type FormDataWithFiles = z.infer<typeof formSchemaWithFiles>; // type FormDataWithFiles = z.infer<typeof formSchemaWithFiles>;
interface UpdateFormProps { interface UpdateFormProps {
onSuccess?: () => void; onSuccess?: () => void;
@@ -59,7 +59,7 @@ export function UpdateForm({
const [sizeFile, setSizeFile] = useState('0 bytes'); const [sizeFile, setSizeFile] = useState('0 bytes');
const defaultformValues: FormDataWithFiles = { // Usamos el nuevo tipo aquí const defaultformValues: EditInventory = { // Usamos el nuevo tipo aquí
id: defaultValues?.id, id: defaultValues?.id,
title: defaultValues?.title || '', title: defaultValues?.title || '',
description: defaultValues?.description || '', description: defaultValues?.description || '',
@@ -68,21 +68,19 @@ export function UpdateForm({
status: defaultValues?.status || 'BORRADOR', status: defaultValues?.status || 'BORRADOR',
stock: defaultValues?.stock ?? 0, stock: defaultValues?.stock ?? 0,
urlImg: undefined, // Inicializamos como undefined o null para el FileList urlImg: undefined, // Inicializamos como undefined o null para el FileList
userId: defaultValues?.userId
}; };
const form = useForm<FormDataWithFiles>({ // Usamos el nuevo tipo aquí const form = useForm<EditInventory>({ // Usamos el nuevo tipo aquí
resolver: zodResolver(formSchemaWithFiles), // Usamos el esquema extendido resolver: zodResolver(editInventory), // Usamos el esquema extendido
defaultValues: defaultformValues, defaultValues: defaultformValues,
mode: 'onChange', mode: 'onChange',
}); });
const onSubmit = async (data: FormDataWithFiles) => { const onSubmit = async (data: EditInventory) => {
// --- MODIFICACIÓN CLAVE: Crear FormData --- // --- MODIFICACIÓN CLAVE: Crear FormData ---
const formData = new FormData(); const formData = new FormData();
// Añadir otros campos de texto al FormData // Añadir otros campos de texto al FormData
// Asegúrate de que los nombres coincidan con lo que tu backend espera
formData.append('id', data.id ? String(data.id) : ''); // Los IDs a menudo son numéricos, conviértelos a string formData.append('id', data.id ? String(data.id) : ''); // Los IDs a menudo son numéricos, conviértelos a string
formData.append('title', data.title); formData.append('title', data.title);
formData.append('description', data.description); formData.append('description', data.description);
@@ -90,7 +88,6 @@ export function UpdateForm({
formData.append('address', data.address); formData.append('address', data.address);
formData.append('status', data.status); formData.append('status', data.status);
formData.append('stock', String(data.stock)); // Convertir a string formData.append('stock', String(data.stock)); // Convertir a string
formData.append('userId', data.userId ? String(data.userId) : '');
// --- MODIFICACIÓN AQUÍ: Asegurar que cada archivo sea un 'File' --- // --- MODIFICACIÓN AQUÍ: Asegurar que cada archivo sea un 'File' ---
if (data.urlImg) { // Primero, verifica que FileList no sea null/undefined if (data.urlImg) { // Primero, verifica que FileList no sea null/undefined
@@ -229,7 +226,7 @@ export function UpdateForm({
render={({ field: { onChange, onBlur, name, ref } }) => ( render={({ field: { onChange, onBlur, name, ref } }) => (
<FormItem> <FormItem>
<FormLabel>Imagen</FormLabel> <FormLabel>Imagen</FormLabel>
<p>Peso máximo: 2MB / {sizeFile}</p> <p>Peso máximo: 5MB / {sizeFile}</p>
<FormControl> <FormControl>
<Input <Input
type="file" type="file"

View File

@@ -7,31 +7,9 @@ export type EditInventory = z.infer<typeof editInventory>;
export type ProductApiResponseSchema = z.infer<typeof productApiResponseSchema>; export type ProductApiResponseSchema = z.infer<typeof productApiResponseSchema>;
export type allProducts = z.infer<typeof productDetails>; export type allProducts = z.infer<typeof productDetails>;
// --- Esquemas de validación para archivos --- const MAX_FILE_SIZE = 5242880; // 5MB en bytes
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
// Esquema básico para un solo archivo const MAX_FILENAME_LENGTH = 50;
const fileSchema = z.object({
name: z.string().min(1, 'El nombre del archivo no puede estar vacío.'),
size: z.number().int().positive('El tamaño del archivo debe ser positivo.'),
type: z.string().refine(
(type) => type.startsWith('image/'),
'Solo se permiten archivos de imagen.'
),
// Puedes añadir más validaciones personalizadas aquí
// Por ejemplo, limitar el tamaño máximo a 5MB (5 * 1024 * 1024 bytes)
}).refine(
(file) => file.size <= 5 * 1024 * 1024,
'El tamaño del archivo no debe exceder los 5MB.'
);
// Esquema para un array de archivos (cuando el input es multiple)
const filesArraySchema = z.array(fileSchema).max(5, 'Solo se permiten hasta 5 archivos.');
// const formSchema = z.object({
// file: z
// .instanceof(FileList)
// .refine((file) => file?.length == 1, 'File is required.')
// });
export const product = z.object({ export const product = z.object({
id: z.number().optional(), id: z.number().optional(),
@@ -41,7 +19,7 @@ export const product = z.object({
// category: z.string(), // category: z.string(),
stock: z.number(), stock: z.number(),
price: z.string(), price: z.string(),
urlImg: z.any(), urlImg: z.custom<FileList | undefined>().optional(),
// urlImg: z.string(), // urlImg: z.string(),
status: z.string(), status: z.string(),
userId: z.number().optional() userId: z.number().optional()
@@ -60,22 +38,23 @@ export const editInventory = z.object({
stock: z.number(), stock: z.number(),
address: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }), address: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }),
price: z.string(), price: z.string(),
urlImg: z.any(), urlImg: z.custom<FileList | undefined>()
.refine((files) => files && files.length > 0, "Se requiere al menos una imagen")
.refine((files) => files && files.length <= 10, "Máximo 10 imágenes")
.refine((files) =>
files && Array.from(files).every(file => file.size <= MAX_FILE_SIZE),
`El tamaño máximo de cada imagen es de 5MB`
).refine((files) =>
files && Array.from(files).every(file => ACCEPTED_IMAGE_TYPES.includes(file.type)),
"Solo se aceptan archivos .jpg, .jpeg, .png y .webp"
).refine((files) =>
files && Array.from(files).every(file => file.name.length <= MAX_FILENAME_LENGTH),
`El nombre de cada archivo no puede superar los ${MAX_FILENAME_LENGTH} caracteres`
),
status: z.string().min(1, { message: "Debe de seleccionar un valor" }), status: z.string().min(1, { message: "Debe de seleccionar un valor" }),
userId: z.number().optional(), userId: z.number().optional(),
}) })
// export const productDetails = z.object({
// id: z.number().optional(),
// title: z.string().min(5),
// description: z.string().min(10),
// stock: z.number(),
// price: z.string(),
// address: z.string(),
// urlImg: z.string(),
// userId: z.number(),
// })
export const ApiResponseSchema = z.object({ export const ApiResponseSchema = z.object({
message: z.string(), message: z.string(),
data: z.array(product), data: z.array(product),

View File

@@ -54,7 +54,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
[{ 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); // console.log(url,method,data);
const response = await fetchApi({ const response = await fetchApi({
method, method,

View File

@@ -2,7 +2,7 @@
const nextConfig = { const nextConfig = {
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: '2mb', bodySizeLimit: '5mb',
}, },
} }
}; };