first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
@@ -0,0 +1,51 @@
import sharp from "sharp";
const MAX_SIZE_BYTES = 512 * 1024; // 512 KB
/**
* Convierte una imagen a WebP y asegura que no supere MAX_SIZE_BYTES.
* Mantiene el aspect ratio y ajusta calidad si la reducción supera el 50%.
* @param inputBuffer Buffer con la imagen original
* @returns Buffer de la imagen en WebP
*/
export const processImageToWebP = async (inputBuffer: Buffer): Promise<Buffer> => {
let quality = 80;
let width: number | null = null;
const metadata = await sharp(inputBuffer).metadata();
const originalWidth = metadata.width || 0;
if (!originalWidth) throw new Error("No se pudo obtener el ancho de la imagen original");
let outputBuffer: Buffer;
while (true) {
const transformer = sharp(inputBuffer).webp({ quality });
if (width) {
transformer.resize({ width, withoutEnlargement: true });
}
outputBuffer = await transformer.toBuffer();
if (outputBuffer.length <= MAX_SIZE_BYTES) break;
// Reducimos width progresivamente
if (!width) {
width = Math.floor(originalWidth * 0.9); // reducimos 10% la primera vez
} else {
width = Math.floor(width * 0.9); // reducciones sucesivas
}
// Calculamos porcentaje de reducción respecto al tamaño original
const reductionPercent = (originalWidth - width) / originalWidth;
// Si se redujo más del 50%, empezamos a bajar la calidad
if (reductionPercent >= 0.5 && quality > 40) {
quality -= 5;
width = null; // volvemos a tamaño original pero con menor calidad
}
// Evitamos bucle infinito
if (width && width < 10) break;
}
return outputBuffer;
};
@@ -0,0 +1,49 @@
import { readFileSync } from "fs";
import { SimpleAuthenticationDetailsProvider, Region } from "oci-common";
import { ObjectStorageClient } from "oci-objectstorage";
const userOcId = process.env.USER_OCID!;
const tenancyOcId = process.env.TENANCY_OCID!;
const privateKeyPath = process.env.PRIVATE_KEY_PATH!;
const fingerprint = process.env.FINGERPRINT!;
const namespace = process.env.NAMESPACE!;
if (!userOcId || !tenancyOcId || !privateKeyPath || !fingerprint || !namespace) {
throw new Error(
"Faltan variables de entorno de Oracle (USER_OCID, TENANCY_OCID, PRIVATE_KEY_PATH, FINGERPRINT, NAMESPACE)"
);
}
const privateKey = readFileSync(privateKeyPath, "utf-8");
const region = Region.fromRegionId("sa-saopaulo-1");
const provider = new SimpleAuthenticationDetailsProvider(
tenancyOcId,
userOcId,
fingerprint,
privateKey,
null,
region
);
const client = new ObjectStorageClient({ authenticationDetailsProvider: provider });
export const subirArchivo = async (bucketName: string, objectName: string, data: Buffer) => {
const request = {
namespaceName: namespace,
bucketName,
objectName,
putObjectBody: data,
};
return client.putObject(request);
};
export const descargarArchivo = async (bucketName: string, objectName: string) => {
const request = {
namespaceName: namespace,
bucketName,
objectName,
};
return client.getObject(request);
};
+40
View File
@@ -0,0 +1,40 @@
import { processImageToWebP } from "./imageCompresionService";
import * as oracleService from "../services/oracleStorageService";
const BUCKET = process.env.BUCKET_NAME!;
if (!BUCKET) throw new Error("Faltan variables de entorno: BUCKET_NAME");
export type ProcessFileResult = {
objectName: string;
originalName: string;
bufferToUpload: Buffer<ArrayBufferLike>;
mimeType: string;
};
export const processFile = async (file: Express.Multer.File) => {
let bufferToUpload = file.buffer;
let mimeType = file.mimetype;
const originalName = file.originalname;
// Si es imagen, procesar con sharp (resize + convertir a jpeg)
if (mimeType.startsWith("image/")) {
bufferToUpload = await processImageToWebP(bufferToUpload);
mimeType = "image/webp";
}
// Generar objectName único para evitar colisiones
const timestamp = Date.now();
// escapamos/normalizamos el originalName ligeramente
const safeOriginal = originalName.replace(/\s+/g, "_");
const objectName = `${timestamp}-${safeOriginal}`;
// Subir a Oracle
await oracleService.subirArchivo(BUCKET, objectName, bufferToUpload);
return {
objectName,
originalName,
bufferToUpload,
mimeType,
};
};
@@ -0,0 +1,14 @@
import type { Request, Response } from "express";
export type ValidateFilesResult = {
files: Express.Multer.File[];
};
export const validateFiles = async (req: Request, res: Response): Promise<ValidateFilesResult> => {
const files = req.files as Express.Multer.File[] | undefined;
if (!files || files.length === 0) throw new Error("No se encontraron archivos");
return {
files,
};
};
@@ -0,0 +1,47 @@
import type { Request, Response } from "express";
import { validateFiles } from "./validateFiles";
import { validateSessionUser } from "./validateUser";
import CompanySchema from "../models/companies";
import EmployeeSchema from "../models/employees";
export type ValidateOrganizationResult = {
files: Express.Multer.File[];
organizationId: string;
sessionUser: string;
};
export const validateOrganization = async (
req: Request,
res: Response
): Promise<ValidateOrganizationResult> => {
const filesResul = await validateFiles(req, res);
const { sessionUser, organizationId } = req.body;
if (!organizationId || !sessionUser) {
throw new Error("Faltan campos obligatorios: userId o profileId");
}
await validateSessionUser(sessionUser);
const company = await CompanySchema.findOne({
_id: organizationId,
});
if (!company) {
throw new Error("No se ha encontrado la organización");
}
const employee = await EmployeeSchema.findOne({
userId: sessionUser,
companyId: organizationId,
});
if (!employee) {
throw new Error("El usuario no pertenece a la organización");
}
return {
files: filesResul.files,
organizationId,
sessionUser,
};
};
+16
View File
@@ -0,0 +1,16 @@
import mongoose from "mongoose";
import UserSchema from "../models/users";
export const validateSessionUser = async (sessionUser: string): Promise<void> => {
const user = await UserSchema.findOne({
_id: new mongoose.Types.ObjectId(sessionUser),
});
console.log(new mongoose.Types.ObjectId(sessionUser));
if (!user) {
throw new Error(
"No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
);
}
};