const express = require('express'); const nodemailer = require('nodemailer'); const crypto = require('crypto'); const bcrypt = require('bcryptjs'); const rateLimit = require('express-rate-limit'); const db = require('../db'); const { authMiddleware, adminOnly } = require('../middleware/auth'); const router = express.Router(); // M2:验证码重发/校验限流——15 分钟窗口内每 IP 最多 10 次(防 8 位验证码爆破 + 邮箱轰炸) const verifyLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false, message: { error: '操作过于频繁,请 15 分钟后再试' }, }); function getTransporter() { const host = db.getSetting('smtp_host'); if (!host) return null; return nodemailer.createTransport({ host, port: parseInt(db.getSetting('smtp_port')) || 587, secure: parseInt(db.getSetting('smtp_port')) === 465, auth: { user: db.getSetting('smtp_user'), pass: db.getSetting('smtp_pass') }, // L4:保留 rejectUnauthorized: false(不改行为)——内网 SMTP 多为自签证书,改 true 会大面积失败。 // 风险:SMTP 凭据在 TLS 握手时可能被中间人嗅探。生产环境建议改 true 并将自签证书加入系统 CA。 tls: { rejectUnauthorized: false }, }); } function emailTemplate(title, body) { const siteName = db.getSetting('site_name') || 'RainWeb'; const color = db.getSetting('primary_color') || '#6750a4'; return `

${siteName}

${body}
`; } router.post('/test', authMiddleware, adminOnly, async (req, res) => { const transporter = getTransporter(); if (!transporter) return res.status(400).json({ error: 'SMTP 未配置' }); const to = req.body.email || req.user.email; if (!to) return res.status(400).json({ error: '没有收件人,请在个人中心设置邮箱或在请求中指定 email 参数' }); try { await transporter.sendMail({ from: `"${db.getSetting('smtp_from_name')}" <${db.getSetting('smtp_from_email')}>`, to, subject: 'RainWeb 邮件配置测试', html: emailTemplate('测试邮件', '

如果您收到此邮件,说明 SMTP 配置正确。

'), }); res.json({ message: '测试邮件已发送至 ' + to }); } catch (e) { console.error('邮件发送失败:', e.message); res.status(500).json({ error: '邮件发送失败' }); } }); // Re-send verification code router.post('/send-verify', verifyLimiter, async (req, res) => { const { email, username } = req.body; if (!email || !username) return res.status(400).json({ error: '参数不完整' }); // 先取出旧 pending 记录(其中存有注册时加密的密码),避免删除后无法重建; // M2:只接受 15 分钟内的有效记录(过期视为已失效并拒绝,防旧码无限重发) const pending = db.get( `SELECT * FROM pending_users WHERE email = ? AND created_at > datetime('now','-15 minutes')`, [email]); if (!pending) return res.status(400).json({ error: '未找到待验证的注册信息,请重新注册' }); // 先确认 SMTP 已配置,避免删除旧记录后无法发送新验证码 const transporter = getTransporter(); if (!transporter) return res.status(400).json({ error: 'SMTP 未配置,请联系管理员' }); // 生成新的验证码,删除旧记录后重新插入新的 pending 记录 const code = crypto.randomInt(10000000, 100000000).toString(); db.run('DELETE FROM pending_users WHERE email = ?', [email]); db.run('INSERT INTO pending_users (username, password, email, token) VALUES (?, ?, ?, ?)', [pending.username, pending.password, email, code]); try { const siteName = db.getSetting('site_name') || 'RainWeb'; const color = db.getSetting('primary_color') || '#6750a4'; await transporter.sendMail({ from: `"${db.getSetting('smtp_from_name')}" <${db.getSetting('smtp_from_email')}>`, to: email, subject: '验证邮箱 - ' + siteName, html: `

${siteName}

您好 ${username},

您的邮箱验证码为:

${code}

验证码有效期为 10 分钟。

`, }); res.json({ message: '验证码已发送' }); } catch (e) { console.error('邮件发送失败:', e.message); res.status(500).json({ error: '邮件发送失败' }); } }); // Complete registration with verification code router.post('/complete-register', verifyLimiter, async (req, res) => { const { code, username, password } = req.body; if (!code || !username || !password) return res.status(400).json({ error: '参数不完整' }); // M2:pending TTL——仅接受创建 15 分钟内的验证码,超时删除并拒绝(防 8 位码无限期可试 + 表无限增长) const pending = db.get( `SELECT * FROM pending_users WHERE token = ? AND username = ? AND created_at > datetime('now','-15 minutes')`, [code, username]); if (!pending) { // 顺手清理所有过期残留 db.run(`DELETE FROM pending_users WHERE created_at < datetime('now','-15 minutes')`); return res.status(400).json({ error: '验证码错误或已过期' }); } if (!bcrypt.compareSync(password, pending.password)) return res.status(400).json({ error: '密码不匹配' }); const existing = db.get('SELECT id FROM users WHERE username = ?', [username]); if (existing) return res.status(400).json({ error: '用户名已存在' }); db.run('INSERT INTO users (username, password, email, email_verified) VALUES (?, ?, ?, 1)', [username, pending.password, pending.email]); db.run('DELETE FROM pending_users WHERE token = ?', [code]); res.json({ message: '注册成功,请登录' }); }); module.exports = router; module.exports.getTransporter = getTransporter; module.exports.emailTemplate = emailTemplate;