Merge branch 'main' of https://git.profit-planet.partners/DK404/CentralBackend
This commit is contained in:
commit
edd32feca3
@ -35,6 +35,9 @@ class ContractUploadController {
|
|||||||
try {
|
try {
|
||||||
const signatureMeta = normalizeSignature(signatureImage);
|
const signatureMeta = normalizeSignature(signatureImage);
|
||||||
const repo = new UserDocumentRepository(unitOfWork);
|
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) {
|
if (signatureMeta) {
|
||||||
await repo.insertDocument({
|
await repo.insertDocument({
|
||||||
userId,
|
userId,
|
||||||
@ -85,6 +88,8 @@ class ContractUploadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup standalone signature record after contracts are saved
|
// 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) {
|
if (signatureMeta) {
|
||||||
await repo.deleteSignatureDocumentsForUser(userId);
|
await repo.deleteSignatureDocumentsForUser(userId);
|
||||||
logger.info('[ContractUploadController] signature cleanup completed for user', { userId });
|
logger.info('[ContractUploadController] signature cleanup completed for user', { userId });
|
||||||
@ -111,6 +116,8 @@ class ContractUploadController {
|
|||||||
try {
|
try {
|
||||||
const signatureMeta = normalizeSignature(signatureImage);
|
const signatureMeta = normalizeSignature(signatureImage);
|
||||||
const repo = new UserDocumentRepository(unitOfWork);
|
const repo = new UserDocumentRepository(unitOfWork);
|
||||||
|
|
||||||
|
// NOTE: TEMP storage only; the PDF embeds the signature from `signatureMeta.base64`.
|
||||||
if (signatureMeta) {
|
if (signatureMeta) {
|
||||||
await repo.insertDocument({
|
await repo.insertDocument({
|
||||||
userId,
|
userId,
|
||||||
@ -160,11 +167,15 @@ class ContractUploadController {
|
|||||||
throw new Error('No active documents are available at this moment.');
|
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) {
|
if (signatureMeta) {
|
||||||
await repo.deleteSignatureDocumentsForUser(userId);
|
await repo.deleteSignatureDocumentsForUser(userId);
|
||||||
logger.info('[ContractUploadController] signature cleanup completed for company user', { userId });
|
logger.info('[ContractUploadController] signature cleanup completed for company user', { userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await unitOfWork.commit();
|
||||||
|
|
||||||
logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`);
|
logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`);
|
||||||
res.json({ success: true, uploads, skipped, downloadUrls: uploads.map(u => u.url || null) });
|
res.json({ success: true, uploads, skipped, downloadUrls: uploads.map(u => u.url || null) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -40,7 +40,12 @@ class ReferralRegistrationController {
|
|||||||
|
|
||||||
static async registerPersonalReferral(req, res) {
|
static async registerPersonalReferral(req, res) {
|
||||||
const { refToken, lang, ...registrationData } = req.body;
|
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();
|
const unitOfWork = new UnitOfWork();
|
||||||
await unitOfWork.start();
|
await unitOfWork.start();
|
||||||
try {
|
try {
|
||||||
@ -52,14 +57,24 @@ class ReferralRegistrationController {
|
|||||||
res.json({ success: true, userId: user.id, email: user.email });
|
res.json({ success: true, userId: user.id, email: user.email });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await unitOfWork.rollback(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 });
|
res.status(400).json({ success: false, message: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async registerCompanyReferral(req, res) {
|
static async registerCompanyReferral(req, res) {
|
||||||
const { refToken, lang, ...registrationData } = req.body;
|
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 {
|
const {
|
||||||
companyEmail,
|
companyEmail,
|
||||||
password,
|
password,
|
||||||
@ -68,6 +83,7 @@ class ReferralRegistrationController {
|
|||||||
contactPersonName,
|
contactPersonName,
|
||||||
contactPersonPhone
|
contactPersonPhone
|
||||||
} = registrationData;
|
} = registrationData;
|
||||||
|
|
||||||
const unitOfWork = new UnitOfWork();
|
const unitOfWork = new UnitOfWork();
|
||||||
await unitOfWork.start();
|
await unitOfWork.start();
|
||||||
try {
|
try {
|
||||||
@ -85,7 +101,11 @@ class ReferralRegistrationController {
|
|||||||
res.json({ success: true, userId: user.id, email: user.email });
|
res.json({ success: true, userId: user.id, email: user.email });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await unitOfWork.rollback(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 });
|
res.status(400).json({ success: false, message: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,16 @@ class UserDocumentRepository {
|
|||||||
this.unitOfWork = unitOfWork;
|
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({
|
async insertDocument({
|
||||||
userId,
|
userId,
|
||||||
documentType,
|
documentType,
|
||||||
@ -16,7 +26,7 @@ class UserDocumentRepository {
|
|||||||
mimeType
|
mimeType
|
||||||
}) {
|
}) {
|
||||||
logger.info('UserDocumentRepository.insertDocument:start', { userId, documentType, originalFilename });
|
logger.info('UserDocumentRepository.insertDocument:start', { userId, documentType, originalFilename });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO user_documents (
|
INSERT INTO user_documents (
|
||||||
user_id, document_type, contract_type, object_storage_id, signatureBase64,
|
user_id, document_type, contract_type, object_storage_id, signatureBase64,
|
||||||
@ -44,7 +54,7 @@ class UserDocumentRepository {
|
|||||||
|
|
||||||
async insertIdMetadata({ userDocumentId, idType, idNumber, expiryDate }) {
|
async insertIdMetadata({ userDocumentId, idType, idNumber, expiryDate }) {
|
||||||
logger.info('UserDocumentRepository.insertIdMetadata:start', { userDocumentId, idType, idNumber });
|
logger.info('UserDocumentRepository.insertIdMetadata:start', { userDocumentId, idType, idNumber });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
await conn.query(
|
await conn.query(
|
||||||
`INSERT INTO user_id_documents (user_document_id, id_type, id_number, expiry_date)
|
`INSERT INTO user_id_documents (user_document_id, id_type, id_number, expiry_date)
|
||||||
@ -70,7 +80,7 @@ class UserDocumentRepository {
|
|||||||
originalFilenameBack
|
originalFilenameBack
|
||||||
}) {
|
}) {
|
||||||
logger.info('UserDocumentRepository.insertIdDocument:start', { userId, documentType, idType, idNumber });
|
logger.info('UserDocumentRepository.insertIdDocument:start', { userId, documentType, idType, idNumber });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
await conn.query(
|
await conn.query(
|
||||||
`INSERT INTO user_id_documents (
|
`INSERT INTO user_id_documents (
|
||||||
@ -98,7 +108,7 @@ class UserDocumentRepository {
|
|||||||
|
|
||||||
async getDocumentsForUser(userId) {
|
async getDocumentsForUser(userId) {
|
||||||
logger.info('UserDocumentRepository.getDocumentsForUser:start', { userId });
|
logger.info('UserDocumentRepository.getDocumentsForUser:start', { userId });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
const [rows] = await conn.query(
|
const [rows] = await conn.query(
|
||||||
`SELECT * FROM user_documents WHERE user_id = ?`,
|
`SELECT * FROM user_documents WHERE user_id = ?`,
|
||||||
@ -114,7 +124,7 @@ class UserDocumentRepository {
|
|||||||
|
|
||||||
async getIdDocumentsForUser(userId) {
|
async getIdDocumentsForUser(userId) {
|
||||||
logger.info('UserDocumentRepository.getIdDocumentsForUser:start', { userId });
|
logger.info('UserDocumentRepository.getIdDocumentsForUser:start', { userId });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
const [rows] = await conn.query(
|
const [rows] = await conn.query(
|
||||||
`SELECT * FROM user_id_documents WHERE user_id = ?`,
|
`SELECT * FROM user_id_documents WHERE user_id = ?`,
|
||||||
@ -130,7 +140,7 @@ class UserDocumentRepository {
|
|||||||
|
|
||||||
async getAllObjectStorageIdsForUser(userId) {
|
async getAllObjectStorageIdsForUser(userId) {
|
||||||
logger.info('UserDocumentRepository.getAllObjectStorageIdsForUser:start', { userId });
|
logger.info('UserDocumentRepository.getAllObjectStorageIdsForUser:start', { userId });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
// Get object_storage_id from user_documents
|
// Get object_storage_id from user_documents
|
||||||
const [docRows] = await conn.query(
|
const [docRows] = await conn.query(
|
||||||
@ -152,7 +162,7 @@ class UserDocumentRepository {
|
|||||||
|
|
||||||
async deleteSignatureDocumentsForUser(userId) {
|
async deleteSignatureDocumentsForUser(userId) {
|
||||||
logger.info('UserDocumentRepository.deleteSignatureDocumentsForUser:start', { userId });
|
logger.info('UserDocumentRepository.deleteSignatureDocumentsForUser:start', { userId });
|
||||||
const conn = this.unitOfWork.connection;
|
const conn = this._getConn();
|
||||||
try {
|
try {
|
||||||
const [result] = await conn.query(
|
const [result] = await conn.query(
|
||||||
`DELETE FROM user_documents WHERE user_id = ? AND document_type = 'signature'`,
|
`DELETE FROM user_documents WHERE user_id = ? AND document_type = 'signature'`,
|
||||||
|
|||||||
@ -157,5 +157,7 @@ router.get('/news/active', NewsController.listActive);
|
|||||||
router.get('/invoices/mine', authMiddleware, InvoiceController.listMine);
|
router.get('/invoices/mine', authMiddleware, InvoiceController.listMine);
|
||||||
router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList);
|
router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList);
|
||||||
|
|
||||||
|
// NOTE: Contract signing uses UnitOfWork; any DB cleanup must happen before commit() closes the connection.
|
||||||
|
|
||||||
// export
|
// export
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -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);
|
router.post('/document-templates/:id/generate-pdf-with-signature', authMiddleware, DocumentTemplateController.generatePdfWithSignature);
|
||||||
|
|
||||||
// Document uploads (moved from routes/documents.js)
|
// 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/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/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);
|
router.post('/upload/contract/personal', authMiddleware, upload.single('contract'), ContractUploadController.uploadPersonalContract);
|
||||||
|
|||||||
@ -6,18 +6,27 @@ const { logger } = require('../../middleware/logger');
|
|||||||
class MailService {
|
class MailService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.brevo = new brevo.TransactionalEmailsApi();
|
this.brevo = new brevo.TransactionalEmailsApi();
|
||||||
|
|
||||||
|
const rawApiKey = process.env.BREVO_API_KEY;
|
||||||
|
const apiKey = (rawApiKey || '').trim(); // helps catch whitespace/newline issues
|
||||||
|
|
||||||
this.brevo.setApiKey(
|
this.brevo.setApiKey(
|
||||||
brevo.TransactionalEmailsApiApiKeys.apiKey,
|
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 = {
|
this.sender = {
|
||||||
email: process.env.BREVO_SENDER_EMAIL,
|
email: process.env.BREVO_SENDER_EMAIL,
|
||||||
name: process.env.BREVO_SENDER_NAME,
|
name: process.env.BREVO_SENDER_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.loginUrl = process.env.LOGIN_URL;
|
this.loginUrl = process.env.LOGIN_URL;
|
||||||
// Use the single correct templates directory
|
|
||||||
this.templatesDir = path.join(__dirname, '..', '..', 'mailTemplates');
|
this.templatesDir = path.join(__dirname, '..', '..', 'mailTemplates');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +58,18 @@ class MailService {
|
|||||||
return template;
|
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 }) {
|
async sendRegistrationEmail({ email, firstName, lastName, userType, companyName, lang }) {
|
||||||
logger.info('MailService.sendRegistrationEmail:start', { email, userType, lang });
|
logger.info('MailService.sendRegistrationEmail:start', { email, userType, lang });
|
||||||
let subject, text;
|
let subject, text;
|
||||||
@ -90,13 +111,21 @@ class MailService {
|
|||||||
logger.info('MailService.sendRegistrationEmail:email_sent', { email, userType, lang: chosenLang });
|
logger.info('MailService.sendRegistrationEmail:email_sent', { email, userType, lang: chosenLang });
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendVerificationCodeEmail({ email, code, expiresAt, lang }) {
|
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 chosenLang = lang || 'en';
|
||||||
const subject = chosenLang === 'de'
|
const subject = chosenLang === 'de'
|
||||||
? 'Ihr ProfitPlanet E-Mail-Verifizierungscode'
|
? 'Ihr ProfitPlanet E-Mail-Verifizierungscode'
|
||||||
@ -114,10 +143,17 @@ class MailService {
|
|||||||
payload.textContent = text;
|
payload.textContent = text;
|
||||||
|
|
||||||
const data = await this.brevo.sendTransacEmail(payload);
|
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;
|
return data;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,7 +182,15 @@ class MailService {
|
|||||||
logger.info('MailService.sendLoginNotificationEmail:email_sent', { email, ip, lang: chosenLang });
|
logger.info('MailService.sendLoginNotificationEmail:email_sent', { email, ip, lang: chosenLang });
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,7 +219,15 @@ class MailService {
|
|||||||
logger.info('MailService.sendPasswordResetEmail:email_sent', { email, token, lang: chosenLang });
|
logger.info('MailService.sendPasswordResetEmail:email_sent', { email, token, lang: chosenLang });
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user