first commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
PORT = 4000
|
||||
MONGO_URI = mongodb://horacio:frx8us2w@localhost:20215/turnosxpress
|
||||
TENANCY_OCID = ocid1.tenancy.oc1..aaaaaaaaauyqhe53wuopce3w4f4n6bbrwq4oagkdc5iz5fbgdk2yqqzrkhoa
|
||||
USER_OCID = ocid1.user.oc1..aaaaaaaaogwd4dyl4kgxbqaoorck2yjq7dvb5nyd3bijffwmesq72hqy3qha
|
||||
FINGERPRINT = 24:2e:45:e0:70:1a:7e:cf:c3:db:a2:db:eb:0a:21:ac
|
||||
PRIVATE_KEY_PATH=./keys/oci_api_key_rsa.pem
|
||||
NAMESPACE = grvzezwnyhx9
|
||||
BUCKET_NAME = tx-files
|
||||
RATE_LIMIT_WINDOW_MINUTES = 15
|
||||
RATE_LIMIT_MAX_REQUESTS = 100
|
||||
JWT_SECRET = k34lklJ4HKJ2j3kj@kl23
|
||||
@@ -0,0 +1,11 @@
|
||||
PORT = 4000
|
||||
MONGO_URI = mongodb://txuser:hdR2102Frx8us2WkuN21quAn12@mongodb:27017/turnosxpress
|
||||
TENANCY_OCID = ocid1.tenancy.oc1..aaaaaaaaauyqhe53wuopce3w4f4n6bbrwq4oagkdc5iz5fbgdk2yqqzrkhoa
|
||||
USER_OCID = ocid1.user.oc1..aaaaaaaaogwd4dyl4kgxbqaoorck2yjq7dvb5nyd3bijffwmesq72hqy3qha
|
||||
FINGERPRINT = 24:2e:45:e0:70:1a:7e:cf:c3:db:a2:db:eb:0a:21:ac
|
||||
PRIVATE_KEY_PATH=./keys/oci_api_key_rsa.pem
|
||||
NAMESPACE = grvzezwnyhx9
|
||||
BUCKET_NAME = tx-files
|
||||
RATE_LIMIT_WINDOW_MINUTES = 15
|
||||
RATE_LIMIT_MAX_REQUESTS = 100
|
||||
JWT_SECRET = k34lklJ4HKJ2j3kj@kl23
|
||||
@@ -0,0 +1,11 @@
|
||||
PORT = 4000
|
||||
MONGO_URI = mongodb://horacio:frx8us2w@mongodb:27017/turnosxpress
|
||||
TENANCY_OCID = ocid1.tenancy.oc1..aaaaaaaaauyqhe53wuopce3w4f4n6bbrwq4oagkdc5iz5fbgdk2yqqzrkhoa
|
||||
USER_OCID = ocid1.user.oc1..aaaaaaaaogwd4dyl4kgxbqaoorck2yjq7dvb5nyd3bijffwmesq72hqy3qha
|
||||
FINGERPRINT = 24:2e:45:e0:70:1a:7e:cf:c3:db:a2:db:eb:0a:21:ac
|
||||
PRIVATE_KEY_PATH=./keys/oci_api_key_rsa.pem
|
||||
NAMESPACE = grvzezwnyhx9
|
||||
BUCKET_NAME = tx-files
|
||||
RATE_LIMIT_WINDOW_MINUTES = 15
|
||||
RATE_LIMIT_MAX_REQUESTS = 100
|
||||
JWT_SECRET = k34lklJ4HKJ2j3kj@kl23
|
||||
@@ -0,0 +1,38 @@
|
||||
# --- Etapa 1: Build ---
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias de desarrollo
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copiar el código fuente y archivos de entorno
|
||||
COPY . .
|
||||
COPY .env* .
|
||||
|
||||
# Compilar TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# --- Etapa 2: Runtime mínimo ---
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar solo dependencias de producción
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copiar la carpeta compilada desde la etapa de build
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copiar archivos de entorno a la raíz /app
|
||||
COPY --from=builder /app/.env* ./
|
||||
|
||||
# Copiar la carpeta de claves
|
||||
COPY --from=builder /app/keys ./keys
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
# Comando de inicio que lanza tanto el server como el client
|
||||
CMD [ "sh", "-c", "npm --prefix /app start" ]
|
||||
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHCcx/dF07YGeW
|
||||
3UQpnEFtaJwxg5cU89nR7OZwssajp99l9oL75uw2tNGeEVFMADiW7K4ie36xsFqT
|
||||
irt86arUNGX1qnZlfi8BtnXDmGgcZ8ZL7ClhXJvXAJ1Z8RGYuz8sXibhwL7RqBB/
|
||||
y3W128CPe34U2Ohz3AfqflpKY3JYILSGRMIgPC3BjJL7O03IXxxTZ8BORyeJf2nP
|
||||
7t4BqOqYveRiXMNQEhl553EncS6KZdj9iCwFCdGB69woOW2q/AJ4B4pWX7yZKgVB
|
||||
yGfbTGIjSQ3lcBWG9Ntof8ypyd4Qi8sDLv2bnk6u9Ddh29Fh5MzCpGP/+1gwy/w+
|
||||
EZm2Wx2/AgMBAAECggEACHnSLmKfWRjKYIIiAHRJ1FBo8lzXBrU5reGyOyi0d8yo
|
||||
IUNFoazcRe9zoRETZAf7C5ue8L355QmMWeMbB9SQNJgvTVkJ0K72bn1oIhZakfcY
|
||||
r9ap48kEo+ZqolAMk/NJWz4pLAzqs1U+WqAomOrM65nygmMYRwge2C1dnUmt2V3R
|
||||
Kvi+sStI0iL8PUO9g4LofpzfIjiLSL9KrihqrgB6gZ3nUI87KNUsVDc5waJMF+kx
|
||||
Wlo8ncsd+76w6exOO5nE+N6yUhM6HOZhOIYel0MgNAJE7899YNuQcqpoeEIxUZea
|
||||
lO9ZI7fYgsE1jYZ978YzSCCxYB6dX3W5wZ2zsnU7YQKBgQD73ZQWFC3pcf1qgGzP
|
||||
yo2s0GEw/zsv1CRh2V3CB3/8FY5/iPh0Ztsd3k+324pL2Pg35C9HhtL6v6ESm4EW
|
||||
KAs1CsI4LVWYHcDwXbywOLuuIdY9yDxmvjy0ZXtkqmAP3T9Jl39STavUAJ66sbqw
|
||||
YLqFWGpvZgmHnsLfSx6MStg6nwKBgQDKTjkO0P/xWWhC3AKw4L9Ju56MFvvuDNe6
|
||||
VaZzfnbayDBVMpA57gfVrDyIV2ixDHVLhvCRsI1Mbd7K5l1smjlqGi31Xe6nB/xT
|
||||
cYWJvE5PVg48uqFzGQxsVnU0ffA6H7Tghk8tyGBB99G0ouqVlTP3bYfsuth3uWS+
|
||||
5UU2eCFo4QKBgBzoZ7DJDCP5I17v8o0+8FffauhWwxJQRQS+EBbPCY+DYeGddPbX
|
||||
P1sEQx4VIqBl/Mddd1e8T8bb677Y/k3utqqV24jbGzHkaFquR5PqTxBDbfWVX+li
|
||||
+FC0PR9mtrPbbwIhU0m9BHXGxHElKQpJb98SMNUsXvej7zX1x0HO7VZ9AoGAWSJL
|
||||
225URjc5S7P574qmXUsv2NNLnp/D8NJJ6zr+DPkE6pTDnPJ2OzuO1BeHBfLSPzuu
|
||||
BNK//rut3MHQycfNoiT0uNXxD2DiWLV8u6ne6u+kiDdGaRrRMW6KZ11RmZpcrEwg
|
||||
2LAfURFqMOdS6iL56Xs11ySfuWhSrhUAALa/KaECgYA+i8TlQjxOyjYlyVjnvLN0
|
||||
IE+UvwRk+Iz2FUIP9YRsscmpZT3UFI9EYt08Vj4YHjTGEbFNRGxaGXMKUmDBtfg7
|
||||
HPeo6M9AhZfP++kc4oUFd2SCIQ68U5qz+qaNQz6ivnbQQsbGCKE2yeg9+U+m7YC3
|
||||
HO52419TG1+8xf9hZODkSQ==
|
||||
-----END PRIVATE KEY-----
|
||||
OCI_API_KEY
|
||||
Generated
+4572
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "files",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "ts-node src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"mongoose": "^8.18.2",
|
||||
"multer": "^2.0.2",
|
||||
"oci-sdk": "^2.117.1",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.5.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import archivosRoutes from "./routes/archivos";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json()); // por si mandás JSON en otras rutas
|
||||
|
||||
app.use("/", archivosRoutes);
|
||||
|
||||
// manejo básico de errores
|
||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error(err);
|
||||
if (res.headersSent) return next(err);
|
||||
res.status(err.status ?? 500).json({ error: err.message ?? "Error interno" });
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Request, Response } from "express";
|
||||
import * as oracleService from "../services/oracleStorageService";
|
||||
|
||||
const BUCKET = process.env.BUCKET_NAME!;
|
||||
if (!BUCKET) throw new Error("Faltan variables de entorno: BUCKET_NAME");
|
||||
|
||||
const streamToBuffer = async (
|
||||
stream: ReadableStream<any> | NodeJS.ReadableStream
|
||||
): Promise<Buffer> => {
|
||||
if ("getReader" in stream) {
|
||||
// Es un ReadableStream web
|
||||
const reader = stream.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
||||
} else {
|
||||
// Es un NodeJS ReadableStream
|
||||
const chunks: Buffer[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("data", (chunk) =>
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
);
|
||||
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFile = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { nombre } = req.params;
|
||||
if (!nombre) return res.status(400).json({ error: "Falta parámetro nombre" });
|
||||
|
||||
const obj = await oracleService.descargarArchivo(BUCKET, nombre);
|
||||
// obj.value es un ReadableStream web estándar
|
||||
const body: Buffer = await streamToBuffer(obj.value);
|
||||
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${nombre}"`);
|
||||
res.send(body);
|
||||
} catch (err) {
|
||||
console.error("Error en downloadArchivo:", err);
|
||||
res.status(500).json({ error: "Error al descargar archivo" });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Request, Response } from "express";
|
||||
import DataProfile from "../models/dataprofile";
|
||||
import { processFile } from "../services/processFile";
|
||||
import { validateOrganization } from "../services/validateOrganization";
|
||||
import type { FileUploadParams } from "../models/types";
|
||||
|
||||
export const uploadOrganizationFile = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await validateOrganization(req, res);
|
||||
const procesados: FileUploadParams[] = [];
|
||||
|
||||
for (const file of data.files) {
|
||||
const result = await processFile(file);
|
||||
const { objectName, originalName, bufferToUpload, mimeType } = result;
|
||||
|
||||
await DataProfile.create({
|
||||
objectName,
|
||||
originalName,
|
||||
size: bufferToUpload.length,
|
||||
mimeType,
|
||||
userId: data.sessionUser,
|
||||
organizationId: data.organizationId,
|
||||
});
|
||||
|
||||
procesados.push({ originalName, objectName });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: `${procesados.length} archivos subidos y registrados`,
|
||||
files: procesados,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error en uploadArchivos:", err);
|
||||
return res.status(500).json({ error: "Error al subir archivos" });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import mongoose from "mongoose";
|
||||
import app from "./app";
|
||||
|
||||
const PORT = process.env.PORT ?? 4000;
|
||||
const MONGO_URI = `${process.env.MONGO_URI}`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await mongoose.connect(MONGO_URI);
|
||||
console.log("MongoDB conectada");
|
||||
} catch (err) {
|
||||
console.error("Error conectando MongoDB:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Servidor corriendo en puerto ${PORT}`);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
if (!JWT_SECRET) throw new Error("Falta JWT_SECRET en .env");
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: any; // Puedes tiparlo mejor según tu payload
|
||||
}
|
||||
|
||||
export const verifyToken = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Token no proporcionado" });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(`${token}`, JWT_SECRET);
|
||||
req.user = decoded; // Puedes guardar el payload en req.user
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: "Token inválido" });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
const windowMs = (parseInt(process.env.RATE_LIMIT_WINDOW_MINUTES ?? "15", 10) || 15) * 60 * 1000;
|
||||
const max = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? "100", 10) || 100;
|
||||
|
||||
export const archivosRateLimiter = rateLimit({
|
||||
windowMs,
|
||||
max,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: "Demasiadas solicitudes, por favor intente más tarde.",
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface ICompany extends Document {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const CompanySchema = new Schema<ICompany>({
|
||||
name: { type: String, required: true },
|
||||
});
|
||||
|
||||
export default mongoose.model<ICompany>("Companie", CompanySchema);
|
||||
@@ -0,0 +1,29 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IFile extends Document {
|
||||
objectName: string;
|
||||
originalName: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
employeeId: string;
|
||||
serviceId: string;
|
||||
appointmentId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const DataFileSchema = new Schema<IFile>({
|
||||
objectName: { type: String, required: true },
|
||||
originalName: { type: String, required: true },
|
||||
size: { type: Number, required: true },
|
||||
mimeType: { type: String, required: true },
|
||||
userId: { type: String, required: false },
|
||||
organizationId: { type: String, required: false },
|
||||
employeeId: { type: String, required: false },
|
||||
serviceId: { type: String, required: false },
|
||||
appointmentId: { type: String, required: false },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
});
|
||||
|
||||
export default mongoose.model<IFile>("File", DataFileSchema);
|
||||
@@ -0,0 +1,14 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IEmployee extends Document {
|
||||
name: string;
|
||||
userId: Schema.Types.ObjectId;
|
||||
companyId: Schema.Types.ObjectId;
|
||||
}
|
||||
|
||||
const EmployeeSchema = new Schema<IEmployee>({
|
||||
userId: { type: Schema.Types.ObjectId, required: true, ref: "User" },
|
||||
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
|
||||
});
|
||||
|
||||
export default mongoose.model<IEmployee>("Employee", EmployeeSchema);
|
||||
@@ -0,0 +1,4 @@
|
||||
export type FileUploadParams = {
|
||||
originalName: string;
|
||||
objectName: string;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IUser extends Document {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
const UserSchema = new Schema<IUser>({
|
||||
firstName: { type: String, required: true },
|
||||
lastName: { type: String, required: true },
|
||||
});
|
||||
|
||||
export default mongoose.model<IUser>("User", UserSchema);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { archivosRateLimiter } from "../middlewares/rateLimiter";
|
||||
import { downloadFile } from "../controllers/downloadController";
|
||||
import { uploadOrganizationFile } from "../controllers/uploadOrganizationController";
|
||||
import { verifyToken } from "../middlewares/auth";
|
||||
|
||||
const upload = multer(); // memoria
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
"/upload-by-organization",
|
||||
verifyToken,
|
||||
archivosRateLimiter,
|
||||
upload.array("files"),
|
||||
uploadOrganizationFile
|
||||
);
|
||||
|
||||
// GET /archivos/download/:nombre (nombre = objectName guardado en DB)
|
||||
router.get("/download/:nombre", archivosRateLimiter, downloadFile);
|
||||
|
||||
export default router;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user