From 7b06ea93c579a6c160e6f12332730f2f6536efc6 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Sun, 18 Jan 2026 22:45:52 +0100 Subject: [PATCH 1/2] fix: api key brevo --- .../ReferralRegistrationController.js | 28 ++++++-- services/email/MailService.js | 68 ++++++++++++++++--- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/controller/referral/ReferralRegistrationController.js b/controller/referral/ReferralRegistrationController.js index a2c3ba6..d3de7dd 100644 --- a/controller/referral/ReferralRegistrationController.js +++ b/controller/referral/ReferralRegistrationController.js @@ -40,7 +40,12 @@ class ReferralRegistrationController { static async registerPersonalReferral(req, res) { const { refToken, lang, ...registrationData } = req.body; - logger.info('ReferralRegistrationController:registerPersonalReferral:start', { refToken, lang, registrationData }); + logger.info('ReferralRegistrationController:registerPersonalReferral:start', { + refToken, + lang, + email: registrationData.email + }); + const unitOfWork = new UnitOfWork(); await unitOfWork.start(); try { @@ -52,14 +57,24 @@ class ReferralRegistrationController { res.json({ success: true, userId: user.id, email: user.email }); } catch (error) { await unitOfWork.rollback(error); - logger.error('ReferralRegistrationController:registerPersonalReferral:error', { refToken, error }); + logger.error('ReferralRegistrationController:registerPersonalReferral:error', { + refToken, + message: error?.message, + name: error?.name + }); res.status(400).json({ success: false, message: error.message }); } } static async registerCompanyReferral(req, res) { const { refToken, lang, ...registrationData } = req.body; - logger.info('ReferralRegistrationController:registerCompanyReferral:start', { refToken, lang, registrationData }); + logger.info('ReferralRegistrationController:registerCompanyReferral:start', { + refToken, + lang, + companyEmail: registrationData.companyEmail, + companyName: registrationData.companyName + }); + const { companyEmail, password, @@ -68,6 +83,7 @@ class ReferralRegistrationController { contactPersonName, contactPersonPhone } = registrationData; + const unitOfWork = new UnitOfWork(); await unitOfWork.start(); try { @@ -85,7 +101,11 @@ class ReferralRegistrationController { res.json({ success: true, userId: user.id, email: user.email }); } catch (error) { await unitOfWork.rollback(error); - logger.error('ReferralRegistrationController:registerCompanyReferral:error', { refToken, error }); + logger.error('ReferralRegistrationController:registerCompanyReferral:error', { + refToken, + message: error?.message, + name: error?.name + }); res.status(400).json({ success: false, message: error.message }); } } diff --git a/services/email/MailService.js b/services/email/MailService.js index 5dfeb65..4a9c681 100644 --- a/services/email/MailService.js +++ b/services/email/MailService.js @@ -6,18 +6,27 @@ const { logger } = require('../../middleware/logger'); class MailService { constructor() { this.brevo = new brevo.TransactionalEmailsApi(); + + const rawApiKey = process.env.BREVO_API_KEY; + const apiKey = (rawApiKey || '').trim(); // helps catch whitespace/newline issues + this.brevo.setApiKey( brevo.TransactionalEmailsApiApiKeys.apiKey, - process.env.BREVO_API_KEY + apiKey ); + logger.info('MailService:brevo_api_key_loaded', { + present: Boolean(apiKey), + length: apiKey.length, + last4: apiKey.length >= 4 ? apiKey.slice(-4) : null + }); + this.sender = { email: process.env.BREVO_SENDER_EMAIL, name: process.env.BREVO_SENDER_NAME, }; this.loginUrl = process.env.LOGIN_URL; - // Use the single correct templates directory this.templatesDir = path.join(__dirname, '..', '..', 'mailTemplates'); } @@ -49,6 +58,18 @@ class MailService { return template; } + _extractBrevoErrorDetails(error) { + const status = error?.response?.status; + const data = error?.response?.data; + let dataSafe; + try { + dataSafe = typeof data === 'string' ? data.slice(0, 2000) : JSON.parse(JSON.stringify(data)); + } catch (_) { + dataSafe = '[unserializable_response_data]'; + } + return { status, data: dataSafe }; + } + async sendRegistrationEmail({ email, firstName, lastName, userType, companyName, lang }) { logger.info('MailService.sendRegistrationEmail:start', { email, userType, lang }); let subject, text; @@ -90,13 +111,21 @@ class MailService { logger.info('MailService.sendRegistrationEmail:email_sent', { email, userType, lang: chosenLang }); return data; } catch (error) { - logger.error('MailService.sendRegistrationEmail:error', { email, error: error.message }); + const brevo = this._extractBrevoErrorDetails(error); + logger.error('MailService.sendRegistrationEmail:error', { + email, + userType, + lang: chosenLang, + message: error?.message, + brevoStatus: brevo.status, + brevoData: brevo.data + }); throw error; } } async sendVerificationCodeEmail({ email, code, expiresAt, lang }) { - logger.info('MailService.sendVerificationCodeEmail:start', { email, code, expiresAt, lang }); + logger.info('MailService.sendVerificationCodeEmail:start', { email, expiresAt, lang }); // don't log code const chosenLang = lang || 'en'; const subject = chosenLang === 'de' ? 'Ihr ProfitPlanet E-Mail-Verifizierungscode' @@ -114,10 +143,17 @@ class MailService { payload.textContent = text; const data = await this.brevo.sendTransacEmail(payload); - logger.info('MailService.sendVerificationCodeEmail:email_sent', { email, code, lang: chosenLang }); + logger.info('MailService.sendVerificationCodeEmail:email_sent', { email, lang: chosenLang }); // don't log code return data; } catch (error) { - logger.error('MailService.sendVerificationCodeEmail:error', { email, error: error.message }); + const brevo = this._extractBrevoErrorDetails(error); + logger.error('MailService.sendVerificationCodeEmail:error', { + email, + lang: chosenLang, + message: error?.message, + brevoStatus: brevo.status, + brevoData: brevo.data + }); throw error; } } @@ -146,7 +182,15 @@ class MailService { logger.info('MailService.sendLoginNotificationEmail:email_sent', { email, ip, lang: chosenLang }); return data; } catch (error) { - logger.error('MailService.sendLoginNotificationEmail:error', { email, error: error.message }); + const brevo = this._extractBrevoErrorDetails(error); + logger.error('MailService.sendLoginNotificationEmail:error', { + email, + ip, + lang: chosenLang, + message: error?.message, + brevoStatus: brevo.status, + brevoData: brevo.data + }); throw error; } } @@ -175,7 +219,15 @@ class MailService { logger.info('MailService.sendPasswordResetEmail:email_sent', { email, token, lang: chosenLang }); return data; } catch (error) { - logger.error('MailService.sendPasswordResetEmail:error', { email, error: error.message }); + const brevo = this._extractBrevoErrorDetails(error); + logger.error('MailService.sendPasswordResetEmail:error', { + email, + token, + lang: chosenLang, + message: error?.message, + brevoStatus: brevo.status, + brevoData: brevo.data + }); throw error; } } From 6e28cc3146d711426690083943d3d573f78854e8 Mon Sep 17 00:00:00 2001 From: DeathKaioken Date: Sun, 18 Jan 2026 23:08:36 +0100 Subject: [PATCH 2/2] fix: contract gen --- .../documents/ContractUploadController.js | 13 +++++++++- .../documents/UserDocumentRepository.js | 24 +++++++++++++------ routes/getRoutes.js | 2 ++ routes/postRoutes.js | 1 + 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/controller/documents/ContractUploadController.js b/controller/documents/ContractUploadController.js index 917b0d2..e6533c4 100644 --- a/controller/documents/ContractUploadController.js +++ b/controller/documents/ContractUploadController.js @@ -35,6 +35,9 @@ class ContractUploadController { try { const signatureMeta = normalizeSignature(signatureImage); const repo = new UserDocumentRepository(unitOfWork); + + // NOTE: this DB insert is only a TEMP storage of the raw signature image. + // The contract PDF embeds the signature from `signatureMeta.base64` during PDF generation. if (signatureMeta) { await repo.insertDocument({ userId, @@ -85,6 +88,8 @@ class ContractUploadController { } // Cleanup standalone signature record after contracts are saved + // NOTE: deleting the signature row here DOES NOT remove it from the generated contract PDF. + // The PDF already contains the signature pixels and is uploaded to object storage. if (signatureMeta) { await repo.deleteSignatureDocumentsForUser(userId); logger.info('[ContractUploadController] signature cleanup completed for user', { userId }); @@ -111,6 +116,8 @@ class ContractUploadController { try { const signatureMeta = normalizeSignature(signatureImage); const repo = new UserDocumentRepository(unitOfWork); + + // NOTE: TEMP storage only; the PDF embeds the signature from `signatureMeta.base64`. if (signatureMeta) { await repo.insertDocument({ userId, @@ -160,11 +167,15 @@ class ContractUploadController { throw new Error('No active documents are available at this moment.'); } - await unitOfWork.commit(); + // IMPORTANT: cleanup must happen BEFORE commit() because commit closes/nulls the connection. + // NOTE: cleanup only removes the temporary signature row, not the signature embedded in PDFs. if (signatureMeta) { await repo.deleteSignatureDocumentsForUser(userId); logger.info('[ContractUploadController] signature cleanup completed for company user', { userId }); } + + await unitOfWork.commit(); + logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`); res.json({ success: true, uploads, skipped, downloadUrls: uploads.map(u => u.url || null) }); } catch (error) { diff --git a/repositories/documents/UserDocumentRepository.js b/repositories/documents/UserDocumentRepository.js index b589494..2cdb8a4 100644 --- a/repositories/documents/UserDocumentRepository.js +++ b/repositories/documents/UserDocumentRepository.js @@ -5,6 +5,16 @@ class UserDocumentRepository { this.unitOfWork = unitOfWork; } + _getConn() { + const conn = this.unitOfWork && this.unitOfWork.connection; + if (!conn) { + const err = new Error('UnitOfWork connection is not available (was commit()/rollback() already called?)'); + err.code = 'UOW_CONNECTION_MISSING'; + throw err; + } + return conn; + } + async insertDocument({ userId, documentType, @@ -16,7 +26,7 @@ class UserDocumentRepository { mimeType }) { logger.info('UserDocumentRepository.insertDocument:start', { userId, documentType, originalFilename }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); const query = ` INSERT INTO user_documents ( user_id, document_type, contract_type, object_storage_id, signatureBase64, @@ -44,7 +54,7 @@ class UserDocumentRepository { async insertIdMetadata({ userDocumentId, idType, idNumber, expiryDate }) { logger.info('UserDocumentRepository.insertIdMetadata:start', { userDocumentId, idType, idNumber }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { await conn.query( `INSERT INTO user_id_documents (user_document_id, id_type, id_number, expiry_date) @@ -70,7 +80,7 @@ class UserDocumentRepository { originalFilenameBack }) { logger.info('UserDocumentRepository.insertIdDocument:start', { userId, documentType, idType, idNumber }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { await conn.query( `INSERT INTO user_id_documents ( @@ -98,7 +108,7 @@ class UserDocumentRepository { async getDocumentsForUser(userId) { logger.info('UserDocumentRepository.getDocumentsForUser:start', { userId }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { const [rows] = await conn.query( `SELECT * FROM user_documents WHERE user_id = ?`, @@ -114,7 +124,7 @@ class UserDocumentRepository { async getIdDocumentsForUser(userId) { logger.info('UserDocumentRepository.getIdDocumentsForUser:start', { userId }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { const [rows] = await conn.query( `SELECT * FROM user_id_documents WHERE user_id = ?`, @@ -130,7 +140,7 @@ class UserDocumentRepository { async getAllObjectStorageIdsForUser(userId) { logger.info('UserDocumentRepository.getAllObjectStorageIdsForUser:start', { userId }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { // Get object_storage_id from user_documents const [docRows] = await conn.query( @@ -152,7 +162,7 @@ class UserDocumentRepository { async deleteSignatureDocumentsForUser(userId) { logger.info('UserDocumentRepository.deleteSignatureDocumentsForUser:start', { userId }); - const conn = this.unitOfWork.connection; + const conn = this._getConn(); try { const [result] = await conn.query( `DELETE FROM user_documents WHERE user_id = ? AND document_type = 'signature'`, diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 7ac9f8f..de114de 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -157,5 +157,7 @@ router.get('/news/active', NewsController.listActive); router.get('/invoices/mine', authMiddleware, InvoiceController.listMine); router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList); +// NOTE: Contract signing uses UnitOfWork; any DB cleanup must happen before commit() closes the connection. + // export module.exports = router; \ No newline at end of file diff --git a/routes/postRoutes.js b/routes/postRoutes.js index c3ff547..5dbaeb1 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -68,6 +68,7 @@ router.post('/document-templates/:id/revise', authMiddleware, upload.single('fil router.post('/document-templates/:id/generate-pdf-with-signature', authMiddleware, DocumentTemplateController.generatePdfWithSignature); // Document uploads (moved from routes/documents.js) +// NOTE: contract upload flow stores a temporary signature row and must delete it BEFORE UnitOfWork.commit(). router.post('/upload/personal-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), PersonalDocumentController.uploadPersonalId); router.post('/upload/company-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), CompanyDocumentController.uploadCompanyId); router.post('/upload/contract/personal', authMiddleware, upload.single('contract'), ContractUploadController.uploadPersonalContract);