From caee73e16b5de15c49482e70569caa42a5e3d3d5 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 3 Sep 2025 09:37:05 +0000 Subject: [PATCH] Update file upload handling to use Busboy for improved efficiency Replace Multer with Busboy for multipart/form-data parsing in the webhook endpoint, enhancing file handling and dependency management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cfbfe47c-0c9a-4ebf-8a41-2937407eccff Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/cfbfe47c-0c9a-4ebf-8a41-2937407eccff/0kKwZtc --- .replit | 2 +- ...tic-backup-1756892066702_1756892066703.txt | 199 ++++++++++++++++++ package-lock.json | 122 ++--------- package.json | 4 +- server/routes.ts | 78 +++++-- 5 files changed, 284 insertions(+), 121 deletions(-) create mode 100644 attached_assets/Pasted-Actions-Using-DatabaseStorage-production-PostgreSQL-Automatic-backup-1756892066702_1756892066703.txt diff --git a/.replit b/.replit index 22e8e1f..2e1a3ae 100644 --- a/.replit +++ b/.replit @@ -37,4 +37,4 @@ args = "npm run dev" waitForPort = 5000 [agent] -integrations = ["javascript_log_in_with_replit==1.0.0", "javascript_object_storage==1.0.0", "javascript_database==1.0.0"] +integrations = ["javascript_object_storage==1.0.0", "javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"] diff --git a/attached_assets/Pasted-Actions-Using-DatabaseStorage-production-PostgreSQL-Automatic-backup-1756892066702_1756892066703.txt b/attached_assets/Pasted-Actions-Using-DatabaseStorage-production-PostgreSQL-Automatic-backup-1756892066702_1756892066703.txt new file mode 100644 index 0000000..9ef3c0e --- /dev/null +++ b/attached_assets/Pasted-Actions-Using-DatabaseStorage-production-PostgreSQL-Automatic-backup-1756892066702_1756892066703.txt @@ -0,0 +1,199 @@ +Actions + + + + +} + +✅ Using DatabaseStorage (production PostgreSQL) + +⏰ Automatic backup scheduled for daily 2:00 AM+ (native timer) + +file:///app/dist/index.js:11 + + throw Error('Dynamic require of "' + x2 + '" is not supported'); + + + +^ + +Error: Dynamic require of "path" is not supported + + at file:///app/dist/index.js:11:9 + + at node_modules/mime-types/index.js (file:///app/dist/index.js:13971:19) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/type-is/index.js (file:///app/dist/index.js:14061:16) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/lib/make-middleware.js (file:///app/dist/index.js:18269:15) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/index.js (file:///app/dist/index.js:22048:26) + + at __require2 (file:///app/dist/index.js:17:50) + + at file:///app/dist/index.js:29500:29 + +Node.js v20.19.4 + +Applying DLC migration... + +NOTICE: column "stock_epuise" of relation "dlc_products" already exists, skipping + +NOTICE: column "stock_epuise_by" of relation "dlc_products" already exists, skipping + +NOTICE: column "stock_epuise_at" of relation "dlc_products" already exists, skipping + +NOTICE: relation "idx_dlc_products_stock_epuise" already exists, skipping + +ALTER TABLE + + CREATE INDEX + +🔗 Database initialization: { + + NODE_ENV: 'production', + + isProduction: true, + + hasDbUrl: true, + + dbHost: 'logiflow-db' + +} + +🐳 PRODUCTION: Using standard PostgreSQL + +✅ PostgreSQL connection test successful + +🔗 Database initialization: { + + NODE_ENV: 'production', + + isProduction: true, + + hasDbUrl: true, + + dbHost: 'logiflow-db:5432' + +} + +✅ Using DatabaseStorage (production PostgreSQL) + +⏰ Automatic backup scheduled for daily 2:00 AM+ (native timer) + +file:///app/dist/index.js:11 + + throw Error('Dynamic require of "' + x2 + '" is not supported'); + + + +^ + +Error: Dynamic require of "path" is not supported + + at file:///app/dist/index.js:11:9 + + at node_modules/mime-types/index.js (file:///app/dist/index.js:13971:19) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/type-is/index.js (file:///app/dist/index.js:14061:16) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/lib/make-middleware.js (file:///app/dist/index.js:18269:15) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/index.js (file:///app/dist/index.js:22048:26) + + at __require2 (file:///app/dist/index.js:17:50) + + at file:///app/dist/index.js:29500:29 + +Node.js v20.19.4 + +Applying DLC migration... + +NOTICE: column "stock_epuise" of relation "dlc_products" already exists, skipping + +NOTICE: column "stock_epuise_by" of relation "dlc_products" already exists, skipping + +NOTICE: column "stock_epuise_at" of relation "dlc_products" already exists, skipping + +NOTICE: relation "idx_dlc_products_stock_epuise" already exists, skipping + +ALTER TABLE + + CREATE INDEX + +🔗 Database initialization: { + + NODE_ENV: 'production', + + isProduction: true, + + hasDbUrl: true, + + dbHost: 'logiflow-db' + +} + +🐳 PRODUCTION: Using standard PostgreSQL + +✅ PostgreSQL connection test successful + +🔗 Database initialization: { + + NODE_ENV: 'production', + + isProduction: true, + + hasDbUrl: true, + + dbHost: 'logiflow-db:5432' + +} + +✅ Using DatabaseStorage (production PostgreSQL) + +⏰ Automatic backup scheduled for daily 2:00 AM+ (native timer) + +file:///app/dist/index.js:11 + + throw Error('Dynamic require of "' + x2 + '" is not supported'); + + + +^ + +Error: Dynamic require of "path" is not supported + + at file:///app/dist/index.js:11:9 + + at node_modules/mime-types/index.js (file:///app/dist/index.js:13971:19) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/type-is/index.js (file:///app/dist/index.js:14061:16) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/lib/make-middleware.js (file:///app/dist/index.js:18269:15) + + at __require2 (file:///app/dist/index.js:17:50) + + at node_modules/multer/index.js (file:///app/dist/index.js:22048:26) + + at __require2 (file:///app/dist/index.js:17:50) + + at file:///app/dist/index.js:29500:29 + +Node.js v20.19.4 + diff --git a/package-lock.json b/package-lock.json index 4e6650d..43c4e45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,12 +41,13 @@ "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", "@types/bcrypt": "^6.0.0", + "@types/busboy": "^1.5.4", "@types/form-data": "^2.2.1", "@types/memoizee": "^0.4.12", - "@types/multer": "^2.0.0", "@types/node-fetch": "^2.6.13", "@types/pg": "^8.15.4", "bcrypt": "^6.0.0", + "busboy": "^1.6.0", "caniuse-lite": "^1.0.30001727", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -68,7 +69,6 @@ "lucide-react": "^0.453.0", "memoizee": "^0.4.17", "memorystore": "^1.6.7", - "multer": "^2.0.2", "nanoid": "^5.1.5", "next-themes": "^0.4.6", "node-cron": "^4.2.1", @@ -3618,16 +3618,27 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3719,6 +3730,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -3731,6 +3743,7 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -3762,6 +3775,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, "license": "MIT" }, "node_modules/@types/memoizee": { @@ -3774,17 +3788,9 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, - "node_modules/@types/multer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", - "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/node": { "version": "20.16.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", @@ -3859,12 +3865,14 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -3892,6 +3900,7 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -3902,6 +3911,7 @@ "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -3996,12 +4006,6 @@ "node": ">= 8" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4211,6 +4215,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/bufferutil": { @@ -4427,21 +4432,6 @@ "node": ">= 6" } }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node_modules/connect-pg-simple": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz", @@ -6957,15 +6947,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -7038,36 +7019,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/multer/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -8063,20 +8014,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8521,15 +8458,6 @@ "node": ">=10.0.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -8897,12 +8825,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", diff --git a/package.json b/package.json index b08c0d4..df234ac 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,13 @@ "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", "@types/bcrypt": "^6.0.0", + "@types/busboy": "^1.5.4", "@types/form-data": "^2.2.1", "@types/memoizee": "^0.4.12", - "@types/multer": "^2.0.0", "@types/node-fetch": "^2.6.13", "@types/pg": "^8.15.4", "bcrypt": "^6.0.0", + "busboy": "^1.6.0", "caniuse-lite": "^1.0.30001727", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -70,7 +71,6 @@ "lucide-react": "^0.453.0", "memoizee": "^0.4.17", "memorystore": "^1.6.7", - "multer": "^2.0.2", "nanoid": "^5.1.5", "next-themes": "^0.4.6", "node-cron": "^4.2.1", diff --git a/server/routes.ts b/server/routes.ts index 5124227..54d7587 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -49,11 +49,9 @@ import { eq, desc, or, isNull } from "drizzle-orm"; import { invoiceVerificationService } from "./invoiceVerification"; import { backupService } from "./backupService"; import { weatherService } from "./weatherService.js"; -import multer from "multer"; +import Busboy from 'busboy'; import fetch from "node-fetch"; -const upload = multer({ storage: multer.memoryStorage() }); - export async function registerRoutes(app: Express): Promise { // Detect environment const environment = process.env.NODE_ENV || 'development'; @@ -72,16 +70,66 @@ export async function registerRoutes(app: Express): Promise { await setupAuth(app); // Route BAP pour envoi webhook n8n - app.post('/api/bap/send-webhook', isAuthenticated, upload.single('pdf'), async (req: any, res) => { + app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => { try { - const { recipient } = req.body; - const file = req.file; + // Vérifier que l'utilisateur est admin + const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); + if (!user || user.role !== 'admin') { + return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); + } - if (!file) { + // Parser le multipart/form-data avec busboy + const busboy = Busboy({ headers: req.headers }); + let fileBuffer: Buffer | null = null; + let fileName = ''; + let recipient = ''; + let mimeType = ''; + + const parsePromise = new Promise((resolve, reject) => { + busboy.on('file', (fieldname, file, info) => { + const { filename, mimeType: fileMimeType } = info; + + if (fieldname === 'pdf') { + fileName = filename; + mimeType = fileMimeType; + + const chunks: Buffer[] = []; + file.on('data', (chunk) => { + chunks.push(chunk); + }); + + file.on('end', () => { + fileBuffer = Buffer.concat(chunks); + }); + } else { + file.resume(); // Ignorer les autres fichiers + } + }); + + busboy.on('field', (fieldname, val) => { + if (fieldname === 'recipient') { + recipient = val; + } + }); + + busboy.on('finish', () => { + resolve(); + }); + + busboy.on('error', (err) => { + reject(err); + }); + }); + + req.pipe(busboy); + await parsePromise; + + // Valider les données + if (!fileBuffer) { return res.status(400).json({ error: 'Aucun fichier PDF fourni' }); } - if (file.mimetype !== 'application/pdf') { + if (mimeType !== 'application/pdf') { return res.status(400).json({ error: 'Le fichier doit être un PDF' }); } @@ -89,24 +137,18 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ error: 'Destinataire invalide' }); } - // Vérifier que l'utilisateur est admin - const user = await storage.getUser(req.user.claims ? req.user.claims.sub : req.user.id); - if (!user || user.role !== 'admin') { - return res.status(403).json({ error: 'Accès refusé - Admin uniquement' }); - } - console.log('📤 BAP: Envoi webhook n8n', { recipient, - fileName: file.originalname, - fileSize: file.size, + fileName, + fileSize: fileBuffer?.length || 0, userId: user.id }); // Créer FormData pour l'envoi vers n8n const FormData = (await import('form-data')).default; const formData = new FormData(); - formData.append('pdf', file.buffer, { - filename: file.originalname, + formData.append('pdf', fileBuffer, { + filename: fileName, contentType: 'application/pdf' }); formData.append('recipient', recipient);