const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const db = require('../db');
const { authMiddleware } = require('../middleware/auth');
const router = express.Router();
// Verification codes in memory: userId -> { code, expires }
const verifyCodes = new Map();
setInterval(() => {
const now = Date.now();
for (const [k, v] of verifyCodes) if (v.expires < now) verifyCodes.delete(k);
}, 60000);
function sendCodeEmail(email, code, username) {
return new Promise((resolve, reject) => {
const host = db.getSetting('smtp_host');
if (!host) return reject(new Error('SMTP 未配置'));
const nodemailer = require('nodemailer');
const transporter = 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') },
tls: { rejectUnauthorized: false },
});
const siteName = db.getSetting('site_name') || 'RainWeb';
const color = db.getSetting('primary_color') || '#6750a4';
transporter.sendMail({
from: `"${db.getSetting('smtp_from_name')}" <${db.getSetting('smtp_from_email')}>`,
to: email,
subject: '验证码 - ' + siteName,
html: `
您好 ${username},
您的验证码为:
${code}
有效期 10 分钟。
`,
}).then(() => resolve()).catch(reject);
});
}
router.get('/', authMiddleware, (req, res) => {
const user = db.get('SELECT id, username, email, email_verified, role, avatar, qq, nickname, website, bio, title, title_color, created_at FROM users WHERE id = ?', [req.user.id]);
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
});
// Email is read-only after registration
router.put('/', authMiddleware, (req, res) => {
const { avatar, qq, bio, nickname, website } = req.body;
if (avatar !== undefined) {
// 头像仅接受站内上传路径(sha256 hash + 图片扩展名),防止外链、路径穿越与引号绕过
if (typeof avatar !== 'string' || !/^\/uploads\/avatars\/[0-9a-f]{64}\.(png|jpe?g|gif|webp)$/i.test(avatar))
return res.status(400).json({ error: '头像必须是站内路径' });
db.run('UPDATE users SET avatar = ? WHERE id = ?', [avatar, req.user.id]);
}
if (qq !== undefined) {
// QQ 号:纯数字 5-12 位,或空字符串清空
const q = String(qq).trim();
if (q !== '' && !/^\d{5,12}$/.test(q))
return res.status(400).json({ error: 'QQ 号需为 5-12 位数字' });
db.run('UPDATE users SET qq = ? WHERE id = ?', [q, req.user.id]);
}
if (bio !== undefined) {
// 个性签名:非字符串忽略,字符串 trim 后限长 200 字符
if (typeof bio === 'string') {
const b = bio.trim();
if (b.length > 200) return res.status(400).json({ error: '个性签名不能超过 200 字符' });
db.run('UPDATE users SET bio = ? WHERE id = ?', [b, req.user.id]);
}
}
if (nickname !== undefined) {
// 昵称:非字符串忽略,字符串 trim 后限长 20 字符
if (typeof nickname === 'string') {
const n = nickname.trim();
if (n.length > 20) return res.status(400).json({ error: '昵称最多 20 字' });
db.run('UPDATE users SET nickname = ? WHERE id = ?', [n, req.user.id]);
}
}
if (website !== undefined) {
// 个人博客:非字符串忽略,字符串 trim ≤200,必须 http(s) 开头;空串=清空
if (typeof website === 'string') {
const w = website.trim();
if (w !== '') {
if (w.length > 200) return res.status(400).json({ error: '个人博客不能超过 200 字' });
if (!/^https?:\/\//i.test(w)) return res.status(400).json({ error: '个人博客需以 http(s):// 开头' });
}
db.run('UPDATE users SET website = ? WHERE id = ?', [w, req.user.id]);
}
}
res.json({ message: '已更新' });
});
// Send verification code for password change
router.post('/send-pw-code', authMiddleware, async (req, res) => {
const user = db.get('SELECT id, username, email FROM users WHERE id = ?', [req.user.id]);
if (!user || !user.email) return res.status(400).json({ error: '未设置邮箱' });
const code = crypto.randomInt(10000000, 100000000).toString();
verifyCodes.set(req.user.id, { code, expires: Date.now() + 600000 });
try {
await sendCodeEmail(user.email, code, user.username);
res.json({ message: '验证码已发送至 ' + user.email });
} catch (e) {
res.status(500).json({ error: '发送失败: ' + e.message });
}
});
// Change password with verification code
router.put('/password', authMiddleware, (req, res) => {
const { code, oldPassword, newPassword } = req.body;
if (!code || !oldPassword || !newPassword) return res.status(400).json({ error: '参数不完整' });
if (newPassword.length < 6) return res.status(400).json({ error: '密码至少6位' });
const entry = verifyCodes.get(req.user.id);
if (!entry || entry.code !== code) return res.status(400).json({ error: '验证码错误或已过期' });
verifyCodes.delete(req.user.id);
const user = db.get('SELECT password FROM users WHERE id = ?', [req.user.id]);
if (!bcrypt.compareSync(oldPassword, user.password)) return res.status(401).json({ error: '原密码错误' });
db.run('UPDATE users SET password = ? WHERE id = ?', [bcrypt.hashSync(newPassword, 10), req.user.id]);
res.json({ message: '密码已修改' });
});
// Resend email verification (8-digit code) for existing user
router.post('/resend-verify', authMiddleware, async (req, res) => {
const user = db.get('SELECT id, username, email FROM users WHERE id = ?', [req.user.id]);
if (!user || !user.email) return res.status(400).json({ error: '未设置邮箱' });
if (user.email_verified) return res.status(400).json({ error: '邮箱已验证' });
const code = crypto.randomInt(10000000, 100000000).toString();
db.run('DELETE FROM pending_users WHERE email = ?', [user.email]);
db.run('INSERT INTO pending_users (username, password, email, token) VALUES (?, ?, ?, ?)',
[user.username, '', user.email, code]);
try {
await sendCodeEmail(user.email, code, user.username);
res.json({ message: '验证码已发送' });
} catch (e) { res.status(500).json({ error: '发送失败: ' + e.message }); }
});
module.exports = router;