c40ef90ad1
8. SMTP: emailService.js (nodemailer), templates (welcome/payment/low_credits) /api/admin/email/test — тест отправки app_settings category=smtp (HOST/PORT/USER/PASS/FROM/ENABLED) 9. Maintenance mode: middleware в index.js, MAINTENANCE_MODE в engine settings При true → 503 для всех запросов кроме /uploads и /api/settings 10. Blog topic bank: DB: blog_topics(category,topic,is_used,source,priority) 40 тем мигрированы из хардкода (source=hardcoded) autogen.js: getNextTopic берёт из DB, fallback на TOPIC_BANK admin API: GET/POST /blog-topics, DELETE /:id, POST /generate (AI +10)
112 lines
4.2 KiB
JavaScript
112 lines
4.2 KiB
JavaScript
/**
|
|
* emailService.js — отправка email уведомлений через SMTP.
|
|
* Использует nodemailer. Настройки из app_settings (category=smtp).
|
|
*/
|
|
const nodemailer = require('nodemailer');
|
|
const settings = require('./settings');
|
|
|
|
let _transporter = null;
|
|
let _configHash = null;
|
|
|
|
async function getTransporter() {
|
|
const [host, port, user, pass, from, enabled] = await Promise.all([
|
|
settings.get('SMTP_HOST', ''),
|
|
settings.get('SMTP_PORT', '587'),
|
|
settings.get('SMTP_USER', ''),
|
|
settings.get('SMTP_PASS', ''),
|
|
settings.get('SMTP_FROM', 'ZeroPost <noreply@zeropost.ru>'),
|
|
settings.get('SMTP_ENABLED', 'false'),
|
|
]);
|
|
|
|
if (enabled !== 'true') return null;
|
|
if (!host || !user) return null;
|
|
|
|
const hash = `${host}:${port}:${user}:${pass}`;
|
|
if (_transporter && hash === _configHash) return _transporter;
|
|
|
|
_transporter = nodemailer.createTransport({
|
|
host, port: parseInt(port),
|
|
secure: parseInt(port) === 465,
|
|
auth: { user, pass },
|
|
tls: { rejectUnauthorized: false },
|
|
});
|
|
_configHash = hash;
|
|
return _transporter;
|
|
}
|
|
|
|
/**
|
|
* Отправить email.
|
|
* @param {string} to — адрес получателя
|
|
* @param {string} subject — тема
|
|
* @param {string} html — HTML тело
|
|
* @param {string} [text] — plain text fallback
|
|
*/
|
|
async function send({ to, subject, html, text }) {
|
|
const transporter = await getTransporter();
|
|
if (!transporter) {
|
|
console.log(`[Email] SMTP disabled or not configured, skip: ${subject} → ${to}`);
|
|
return { skipped: true };
|
|
}
|
|
const from = await settings.get('SMTP_FROM', 'ZeroPost <noreply@zeropost.ru>');
|
|
try {
|
|
const info = await transporter.sendMail({ from, to, subject, html, text });
|
|
console.log(`[Email] sent: ${subject} → ${to} (${info.messageId})`);
|
|
return { ok: true, messageId: info.messageId };
|
|
} catch (err) {
|
|
console.error(`[Email] send error: ${err.message}`);
|
|
return { error: err.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Шаблоны уведомлений
|
|
*/
|
|
const templates = {
|
|
welcome({ email, credits }) {
|
|
return {
|
|
subject: 'Добро пожаловать в ZeroPost!',
|
|
html: `
|
|
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
|
|
<h2>Привет! 👋</h2>
|
|
<p>Рады видеть тебя в ZeroPost.</p>
|
|
<p>На твой счёт зачислено <b>${credits} кредитов</b> для начала работы.</p>
|
|
<p><a href="https://app.zeropost.ru" style="color:#6366f1">Открыть приложение →</a></p>
|
|
<hr style="margin:24px 0;border:none;border-top:1px solid #eee">
|
|
<p style="color:#999;font-size:12px">ZeroPost · Автоматизация контента</p>
|
|
</div>
|
|
`,
|
|
};
|
|
},
|
|
payment_success({ amount, plan, email }) {
|
|
return {
|
|
subject: `✅ Оплата ${amount}₽ прошла успешно`,
|
|
html: `
|
|
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
|
|
<h2>Оплата подтверждена</h2>
|
|
<p>Тариф <b>${plan}</b> активирован.</p>
|
|
<p>Сумма: <b>${amount}₽</b></p>
|
|
<p><a href="https://app.zeropost.ru/billing" style="color:#6366f1">История платежей →</a></p>
|
|
<hr style="margin:24px 0;border:none;border-top:1px solid #eee">
|
|
<p style="color:#999;font-size:12px">ZeroPost · Автоматизация контента</p>
|
|
</div>
|
|
`,
|
|
};
|
|
},
|
|
low_credits({ credits, email }) {
|
|
return {
|
|
subject: '⚠️ Кредиты заканчиваются',
|
|
html: `
|
|
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
|
|
<h2>Осталось ${credits} кредитов</h2>
|
|
<p>Пополни баланс чтобы продолжить генерацию контента.</p>
|
|
<p><a href="https://app.zeropost.ru/plans" style="color:#6366f1">Выбрать тариф →</a></p>
|
|
<hr style="margin:24px 0;border:none;border-top:1px solid #eee">
|
|
<p style="color:#999;font-size:12px">ZeroPost · Автоматизация контента</p>
|
|
</div>
|
|
`,
|
|
};
|
|
},
|
|
};
|
|
|
|
module.exports = { send, templates };
|