Validacion y restriccion de archivos + almacenamiento en carpeta
This commit is contained in:
@@ -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);
|
||||||
1550
apps/api/src/database/migrations/meta/0007_snapshot.json
Normal file
1550
apps/api/src/database/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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"]}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '2mb',
|
bodySizeLimit: '5mb',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user