archetecture security fix
This commit is contained in:
+256
@@ -0,0 +1,256 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sendNotification = sendNotification;
|
||||
exports.sendTransactionalEmail = sendTransactionalEmail;
|
||||
const resend_1 = require("resend");
|
||||
const twilio_1 = __importDefault(require("twilio"));
|
||||
const firebase_admin_1 = __importDefault(require("firebase-admin"));
|
||||
const prisma_1 = require("../lib/prisma");
|
||||
const redis_1 = require("../lib/redis");
|
||||
const notificationLocalizationService_1 = require("./notificationLocalizationService");
|
||||
const resendApiKey = process.env.RESEND_API_KEY &&
|
||||
process.env.RESEND_API_KEY !== 're_...' &&
|
||||
process.env.RESEND_API_KEY.startsWith('re_')
|
||||
? process.env.RESEND_API_KEY
|
||||
: null;
|
||||
const resend = resendApiKey ? new resend_1.Resend(resendApiKey) : null;
|
||||
const emailFromAddress = process.env.EMAIL_FROM && process.env.EMAIL_FROM !== 'noreply@example.com'
|
||||
? process.env.EMAIL_FROM
|
||||
: null;
|
||||
const emailFromName = process.env.EMAIL_FROM_NAME && process.env.EMAIL_FROM_NAME !== 'Example App'
|
||||
? process.env.EMAIL_FROM_NAME
|
||||
: null;
|
||||
const smtpHost = process.env.MAIL_HOST;
|
||||
const smtpPort = Number(process.env.MAIL_PORT ?? 0);
|
||||
const smtpUser = process.env.MAIL_USERNAME;
|
||||
const smtpPass = process.env.MAIL_PASSWORD;
|
||||
const smtpSecure = process.env.MAIL_SCHEME === 'smtps' ||
|
||||
process.env.MAIL_ENCRYPTION === 'ssl' ||
|
||||
smtpPort === 465;
|
||||
const smtpFromAddress = process.env.MAIL_FROM_ADDRESS && !process.env.MAIL_FROM_ADDRESS.includes('${')
|
||||
? process.env.MAIL_FROM_ADDRESS
|
||||
: null;
|
||||
const smtpFromName = process.env.MAIL_FROM_NAME && !process.env.MAIL_FROM_NAME.includes('${')
|
||||
? process.env.MAIL_FROM_NAME
|
||||
: null;
|
||||
let smtpTransport = null;
|
||||
if (smtpHost && smtpPort && smtpUser && smtpPass) {
|
||||
try {
|
||||
const nodemailer = require('nodemailer');
|
||||
smtpTransport = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPass,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[Notifications] SMTP transport unavailable:', err?.message ?? String(err));
|
||||
}
|
||||
}
|
||||
const twilioClient = process.env.TWILIO_ACCOUNT_SID &&
|
||||
process.env.TWILIO_AUTH_TOKEN &&
|
||||
process.env.TWILIO_ACCOUNT_SID !== 'AC...' &&
|
||||
process.env.TWILIO_AUTH_TOKEN !== 'your-twilio-auth-token'
|
||||
? (0, twilio_1.default)(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
|
||||
: null;
|
||||
const firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n');
|
||||
const hasFirebaseConfig = !!process.env.FIREBASE_PROJECT_ID &&
|
||||
!!process.env.FIREBASE_CLIENT_EMAIL &&
|
||||
!!firebasePrivateKey &&
|
||||
!firebasePrivateKey.includes('BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY');
|
||||
if (hasFirebaseConfig && !firebase_admin_1.default.apps.length) {
|
||||
try {
|
||||
firebase_admin_1.default.initializeApp({
|
||||
credential: firebase_admin_1.default.credential.cert({
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
privateKey: firebasePrivateKey,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
}),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[Notifications] Firebase init skipped:', err.message);
|
||||
}
|
||||
}
|
||||
function resolveSmtpReplyTo() {
|
||||
if (!process.env.MAIL_REPLY_TO_ADDRESS || process.env.MAIL_REPLY_TO_ADDRESS.includes('${')) {
|
||||
return undefined;
|
||||
}
|
||||
if (process.env.MAIL_REPLY_TO_NAME && !process.env.MAIL_REPLY_TO_NAME.includes('${')) {
|
||||
return `${process.env.MAIL_REPLY_TO_NAME} <${process.env.MAIL_REPLY_TO_ADDRESS}>`;
|
||||
}
|
||||
return process.env.MAIL_REPLY_TO_ADDRESS;
|
||||
}
|
||||
async function sendEmailWithProviders(opts) {
|
||||
const errors = [];
|
||||
if (resend) {
|
||||
try {
|
||||
if (!emailFromAddress || !emailFromName) {
|
||||
throw new Error('Email sender identity is not configured');
|
||||
}
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: `${emailFromName} <${emailFromAddress}>`,
|
||||
to: opts.to,
|
||||
subject: opts.subject,
|
||||
html: opts.html,
|
||||
text: opts.text,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
return {
|
||||
provider: 'resend',
|
||||
providerMessageId: data?.id ?? null,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
errors.push(`Resend: ${err?.message ?? String(err)}`);
|
||||
console.warn('[Notifications] Resend delivery failed, falling back if possible:', err?.message ?? String(err));
|
||||
}
|
||||
}
|
||||
if (smtpTransport) {
|
||||
try {
|
||||
if (!smtpFromAddress) {
|
||||
throw new Error('SMTP sender identity is not configured');
|
||||
}
|
||||
const info = await smtpTransport.sendMail({
|
||||
from: smtpFromName ? `${smtpFromName} <${smtpFromAddress}>` : smtpFromAddress,
|
||||
to: opts.to,
|
||||
subject: opts.subject,
|
||||
html: opts.html,
|
||||
text: opts.text,
|
||||
replyTo: resolveSmtpReplyTo(),
|
||||
});
|
||||
return {
|
||||
provider: 'smtp',
|
||||
providerMessageId: info.messageId ?? null,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
errors.push(`SMTP: ${err?.message ?? String(err)}`);
|
||||
}
|
||||
}
|
||||
if (errors.length) {
|
||||
throw new Error(errors.join(' | '));
|
||||
}
|
||||
throw new Error('No email provider is configured');
|
||||
}
|
||||
async function sendNotification(opts) {
|
||||
const results = [];
|
||||
const resolvedLocale = await (0, notificationLocalizationService_1.resolveNotificationLocale)({
|
||||
companyId: opts.companyId,
|
||||
employeeId: opts.employeeId,
|
||||
renterId: opts.renterId,
|
||||
billingAccountId: opts.billingAccountId,
|
||||
locale: opts.locale,
|
||||
});
|
||||
for (const channel of opts.channels) {
|
||||
try {
|
||||
const rendered = opts.templateKey
|
||||
? await (0, notificationLocalizationService_1.resolveNotificationTemplate)({
|
||||
templateKey: opts.templateKey,
|
||||
channel,
|
||||
locale: resolvedLocale,
|
||||
variables: opts.templateVariables,
|
||||
})
|
||||
: null;
|
||||
const title = rendered?.subject ?? opts.title;
|
||||
const body = rendered?.body ?? opts.body;
|
||||
if (!title || !body) {
|
||||
throw new Error(`Notification content is incomplete for channel ${channel}`);
|
||||
}
|
||||
const notification = await prisma_1.prisma.notification.create({
|
||||
data: {
|
||||
type: opts.type,
|
||||
title,
|
||||
body,
|
||||
data: (opts.data ?? {}),
|
||||
channel,
|
||||
templateKey: rendered?.templateKey ?? opts.templateKey ?? null,
|
||||
locale: rendered?.locale ?? resolvedLocale,
|
||||
status: 'PENDING',
|
||||
companyId: opts.companyId ?? null,
|
||||
employeeId: opts.employeeId ?? null,
|
||||
renterId: opts.renterId ?? null,
|
||||
},
|
||||
});
|
||||
let providerMessageId = null;
|
||||
let success = false;
|
||||
if (channel === 'EMAIL' && opts.email) {
|
||||
const emailResult = await sendEmailWithProviders({
|
||||
to: opts.email,
|
||||
subject: title,
|
||||
html: (0, notificationLocalizationService_1.renderLocalizedEmailHtml)(body, rendered?.locale ?? resolvedLocale),
|
||||
text: body,
|
||||
});
|
||||
providerMessageId = emailResult.providerMessageId;
|
||||
success = true;
|
||||
}
|
||||
if (channel === 'SMS' && opts.phone) {
|
||||
if (!twilioClient)
|
||||
throw new Error('Twilio is not configured');
|
||||
const msg = await twilioClient.messages.create({
|
||||
body,
|
||||
from: process.env.TWILIO_PHONE_NUMBER,
|
||||
to: opts.phone,
|
||||
});
|
||||
providerMessageId = msg.sid;
|
||||
success = true;
|
||||
}
|
||||
if (channel === 'WHATSAPP' && opts.phone) {
|
||||
if (!twilioClient)
|
||||
throw new Error('Twilio is not configured');
|
||||
const msg = await twilioClient.messages.create({
|
||||
body,
|
||||
from: `whatsapp:${process.env.TWILIO_WHATSAPP_NUMBER}`,
|
||||
to: `whatsapp:${opts.phone}`,
|
||||
});
|
||||
providerMessageId = msg.sid;
|
||||
success = true;
|
||||
}
|
||||
if (channel === 'PUSH' && opts.fcmToken) {
|
||||
if (!firebase_admin_1.default.apps.length)
|
||||
throw new Error('Firebase is not configured');
|
||||
const response = await firebase_admin_1.default.messaging().send({
|
||||
token: opts.fcmToken,
|
||||
notification: { title, body },
|
||||
data: Object.fromEntries(Object.entries(opts.data ?? {}).map(([k, v]) => [k, String(v)])),
|
||||
});
|
||||
providerMessageId = response;
|
||||
success = true;
|
||||
}
|
||||
if (channel === 'IN_APP') {
|
||||
// Emit via Socket.io through Redis pub/sub
|
||||
const targetId = opts.employeeId ?? opts.renterId;
|
||||
if (targetId) {
|
||||
await redis_1.redis.publish(`notifications:${targetId}`, JSON.stringify({ ...notification, status: 'DELIVERED' }));
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
await prisma_1.prisma.notification.update({
|
||||
where: { id: notification.id },
|
||||
data: {
|
||||
status: success ? 'SENT' : 'FAILED',
|
||||
sentAt: success ? new Date() : null,
|
||||
providerMessageId,
|
||||
},
|
||||
});
|
||||
results.push({ channel, success });
|
||||
}
|
||||
catch (err) {
|
||||
results.push({ channel, success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async function sendTransactionalEmail(opts) {
|
||||
await sendEmailWithProviders(opts);
|
||||
}
|
||||
//# sourceMappingURL=notificationService.js.map
|
||||
Reference in New Issue
Block a user