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: `

${siteName}

您好 ${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;