feat: SMTP, maintenance mode, blog topic bank UI
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)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user