- app.js: mount page objects via inline script (CSP blocks eval → data-action buttons dead) - app.js: add tickets list entry to sidebar nav (admin could not find claimed tickets) - sources: GET /sources?manage=1 returns enabled field for admin view - mailer: sendNotifyEmail consumes email-type notification_configs (webhook_url = recipients) - tickets: trigger notify on created/claimed/transferred/updated/status-change - notifications: email type shows recipient field, validates email list - ticket-detail: single-page chat flow (player/staff bubbles) + processing log timeline - css: chat bubble styles
118 lines
4.8 KiB
JavaScript
118 lines
4.8 KiB
JavaScript
/*
|
||
* MC Report System
|
||
* Copyright (C) 2026 Sea Network Technology Studio
|
||
* Author: CangLan <admin@sea-studio.top>
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published
|
||
* by the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
const nodemailer = require('nodemailer');
|
||
const { getPool, query, getRow, getConfig } = require('./db');
|
||
const { logEmail, logSystem } = require('./logger');
|
||
|
||
async function getSmtpConfig() {
|
||
try {
|
||
const rows = await query("SELECT k, v FROM settings WHERE k LIKE 'smtp_%'");
|
||
const cfg = {};
|
||
for (const r of rows) cfg[r.k] = r.v;
|
||
return {
|
||
host: cfg.smtp_host || '',
|
||
port: parseInt(cfg.smtp_port) || 587,
|
||
secure: cfg.smtp_secure === '1',
|
||
user: cfg.smtp_user || '',
|
||
pass: cfg.smtp_pass || '',
|
||
from: cfg.smtp_from || 'noreply@example.com',
|
||
};
|
||
} catch { return { host: '', port: 587, secure: false, user: '', pass: '', from: '' }; }
|
||
}
|
||
|
||
async function getSiteName() {
|
||
const row = await getRow("SELECT v FROM settings WHERE k = 'site_name'");
|
||
return row ? row.v : 'MC举报系统';
|
||
}
|
||
|
||
async function getSiteUrl() {
|
||
const row = await getRow("SELECT v FROM settings WHERE k = 'site_url'");
|
||
return row ? row.v : 'http://localhost:3100';
|
||
}
|
||
|
||
async function getTemplate(code) {
|
||
return getRow('SELECT * FROM email_templates WHERE code = ?', [code]);
|
||
}
|
||
|
||
function renderTemplate(template, vars) {
|
||
let { subject, body } = template;
|
||
for (const [k, v] of Object.entries(vars)) {
|
||
const re = new RegExp(`\\{\\{${k}\\}\\}`, 'g');
|
||
const val = String(v ?? '');
|
||
subject = subject.replace(re, () => val);
|
||
body = body.replace(re, () => val);
|
||
}
|
||
return { subject, body };
|
||
}
|
||
|
||
let _transporter = null;
|
||
let _transporterKey = '';
|
||
|
||
function getTransporter(cfg) {
|
||
const key = `${cfg.host}:${cfg.port}:${cfg.user}`;
|
||
if (_transporter && _transporterKey === key) return _transporter;
|
||
_transporterKey = key;
|
||
_transporter = nodemailer.createTransport({ host: cfg.host, port: cfg.port, secure: cfg.secure, auth: { user: cfg.user, pass: cfg.pass } });
|
||
return _transporter;
|
||
}
|
||
|
||
async function sendEmail(to, templateCode, vars) {
|
||
try {
|
||
const cfg = await getSmtpConfig();
|
||
if (!cfg.host || !cfg.user) { console.log('[Mailer] SMTP未配置'); await logSystem('warn', 'mailer', 'SMTP未配置,邮件未发送', { to, template: templateCode }); return false; }
|
||
const template = await getTemplate(templateCode);
|
||
if (!template) { console.log('[Mailer] 模板不存在:', templateCode); await logEmail(to, templateCode, '', 'failed', '模板不存在: ' + templateCode); return false; }
|
||
const siteName = await getSiteName();
|
||
const allVars = { ...vars, site_name: siteName, site_url: await getSiteUrl() };
|
||
const { subject, body } = renderTemplate(template, allVars);
|
||
const transporter = getTransporter(cfg);
|
||
await transporter.sendMail({ from: `"${siteName}" <${cfg.from}>`, to, subject, html: body });
|
||
await logEmail(to, templateCode, subject, 'sent');
|
||
return true;
|
||
} catch (err) {
|
||
console.error('[Mailer]', err.message);
|
||
await logEmail(to, templateCode, '', 'failed', err.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
module.exports = { sendEmail, sendNotifyEmail, renderTemplate, getTemplate, getSmtpConfig, getSiteName, getSiteUrl };
|
||
|
||
// ---- 事件通知(email 类型配置消费) ----
|
||
// 读取 notification_configs 中 type='email' 且 active=1 的配置, 事件匹配则向收件人发信
|
||
// email 类型配置的 webhook_url 字段复用为收件人邮箱(逗号分隔)
|
||
async function sendNotifyEmail(event, data) {
|
||
try {
|
||
const configs = await query("SELECT * FROM notification_configs WHERE type = 'email' AND active = 1");
|
||
for (const cfg of configs) {
|
||
const events = cfg.events || 'all';
|
||
if (events !== 'all' && !events.split(',').map(s => s.trim()).includes(event)) continue;
|
||
const recipients = String(cfg.webhook_url || '').split(',').map(s => s.trim()).filter(Boolean);
|
||
if (!recipients.length) continue;
|
||
const templateCode = { ticket_created: 'ticket_created', ticket_claimed: 'ticket_claimed', ticket_transferred: 'ticket_transferred', ticket_updated: 'ticket_updated' }[event] || 'ticket_updated';
|
||
for (const to of recipients) {
|
||
sendEmail(to, templateCode, data).catch(() => {});
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('[NotifyEmail]', err.message);
|
||
}
|
||
}
|