const express = require('express'); const bcrypt = require('bcryptjs'); const { v4: uuid } = require('uuid'); const { query, getRow } = require('../db'); const { generateToken, authenticate } = require('../middleware/auth'); const { sendEmail } = require('../mailer'); const { verify: verifyCaptcha } = require('./captcha'); const { validateLengths } = require('../middleware/security'); const router = express.Router(); async function getSiteUrl() { const row = await getRow("SELECT v FROM settings WHERE k = 'site_url'"); return row?.v || 'http://localhost:3100'; } router.post('/send-verify-code', async (req, res) => { try { const { email } = req.body; if (!email) return res.status(400).json({ error: '请输入邮箱' }); const code = String(Math.floor(100000 + Math.random() * 900000)); const codeId = uuid(); await query("DELETE FROM captchas WHERE created_at < NOW() - INTERVAL 5 MINUTE"); await query('INSERT INTO captchas(id, question, answer) VALUES (?,?,?)', [codeId, email, code]); const sent = await sendEmail(email, 'email_code', { username: email.split('@')[0], game_name: '', game_uid: '', code, }); if (!sent) { await query('DELETE FROM captchas WHERE id = ?', [codeId]); return res.status(200).json({ code_id: codeId, code, dev: true }); } res.json({ code_id: codeId, message: '验证码已发送' }); } catch (err) { console.error(err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/register', validateLengths({ username: 20, password: 100, email: 100, game_name: 50, game_uid: 50, }), async (req, res) => { try { const { username, password, email, game_name, game_uid, captcha_id, captcha_answer, source, code_id, code } = req.body; if (!username || !password || !email || !game_name) { return res.status(400).json({ error: '所有字段均为必填' }); } if (source === 'netease' && !game_uid) return res.status(400).json({ error: '网易端必须填写UID' }); if (!code_id || !code) return res.status(400).json({ error: '请先完成邮箱验证' }); const codeRow = await getRow("SELECT * FROM captchas WHERE id = ? AND created_at > NOW() - INTERVAL 10 MINUTE", [code_id]); if (!codeRow || codeRow.answer !== code) return res.status(400).json({ error: '验证码无效或已过期' }); await query('DELETE FROM captchas WHERE id = ?', [code_id]); if (username.length < 2 || username.length > 20) { return res.status(400).json({ error: '用户名需2-20个字符' }); } if (password.length < 6) { return res.status(400).json({ error: '密码至少6位' }); } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return res.status(400).json({ error: '邮箱格式不正确' }); } if (captcha_id && captcha_answer) { const captchaOk = await verifyCaptcha(captcha_id, captcha_answer); if (!captchaOk) { return res.status(400).json({ error: '人机验证失败' }); } } const existingUser = await getRow('SELECT id FROM users WHERE username = ?', [username]); const existingEmail = await getRow('SELECT id FROM users WHERE email = ?', [email]); if (existingUser || existingEmail) { return res.status(400).json({ error: '用户名或邮箱已被使用' }); } if (game_name && (source || 'netease') === 'netease') { const dup = await getRow('SELECT id FROM users WHERE game_name = ? AND source = ?', [game_name, source || 'netease']); if (dup) return res.status(400).json({ error: '该游戏名在此来源下已注册' }); } const hashed = await bcrypt.hash(password, 10); const r = await query( 'INSERT INTO users(username, password, email, game_name, game_uid, active, email_verified, source) VALUES (?,?,?,?,?,?,?,?)', [username, hashed, email, game_name, game_uid||'', 1, 1, source || 'netease'] ); try { await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)', [r.insertId, source || 'netease', game_name, game_uid || '']); } catch {} res.status(201).json({ message: '注册成功,请登录' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/verify-email', async (req, res) => { try { const { code } = req.body; if (!code) return res.status(400).json({ error: '请输入验证码' }); const user = await getRow('SELECT * FROM users WHERE verify_token = ? AND verify_expires > NOW()', [code]); if (!user) return res.status(400).json({ error: '验证码无效或已过期' }); await query('UPDATE users SET email_verified = 1, active = 1, verify_token = NULL, verify_expires = NULL WHERE id = ?', [user.id]); await query('INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)', [user.id, user.username, 'verify_email', 'user', user.id, '邮箱验证通过']); res.json({ message: '邮箱验证成功,现在可以登录了' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/login', async (req, res) => { try { const { username, password } = req.body; if (!username || !password) return res.status(400).json({ error: '请输入用户名和密码' }); const user = await getRow('SELECT * FROM users WHERE username = ?', [username]); if (!user) return res.status(401).json({ error: '用户名或密码错误' }); const valid = await bcrypt.compare(password, user.password); if (!valid) return res.status(401).json({ error: '用户名或密码错误' }); if (!user.active) return res.status(403).json({ error: '账号未激活,请先验证邮箱' }); const token = generateToken(user); await query( 'INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)', [user.id, user.username, 'login', 'user', user.id, '登录系统'] ); res.json({ token, user: { id: user.id, username: user.username, email: user.email, game_name: user.game_name, game_uid: user.game_uid, role: user.role, source: user.source, }, }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.get('/me', authenticate, async (req, res) => { try { const user = await getRow( 'SELECT id, username, email, game_name, game_uid, role, email_verified, source, created_at FROM users WHERE id = ?', [req.user.id] ); if (!user) return res.status(404).json({ error: '用户不存在' }); user.identities = await query('SELECT id, source, game_name, game_uid, created_at FROM user_identities WHERE user_id = ? ORDER BY id', [req.user.id]); res.json(user); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); // ---- 多来源身份管理(一个账号可同时绑定网易端 + 皮肤站) ---- router.get('/identities', authenticate, async (req, res) => { try { const rows = await query('SELECT id, source, game_name, game_uid, created_at FROM user_identities WHERE user_id = ? ORDER BY id', [req.user.id]); res.json(rows); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/identities', authenticate, async (req, res) => { try { const { source, game_name, game_uid } = req.body; if (!['netease','skin'].includes(source)) return res.status(400).json({ error: '无效的来源' }); if (!game_name) return res.status(400).json({ error: '游戏名不能为空' }); if (source === 'netease' && !game_uid) return res.status(400).json({ error: '网易端必须填写UID' }); const dup = await getRow('SELECT id FROM user_identities WHERE user_id = ? AND source = ?', [req.user.id, source]); if (dup) return res.status(400).json({ error: '该来源已绑定,可先删除再重新绑定' }); const dupName = await getRow('SELECT id FROM user_identities WHERE source = ? AND game_name = ?', [source, game_name]); if (dupName) return res.status(400).json({ error: '该游戏名已绑定其他账号' }); const r = await query('INSERT INTO user_identities(user_id, source, game_name, game_uid) VALUES (?,?,?,?)', [req.user.id, source, game_name, game_uid || '']); res.status(201).json({ id: r.insertId, message: '绑定成功' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.delete('/identities/:id', authenticate, async (req, res) => { try { const id = parseInt(req.params.id); const ident = await getRow('SELECT * FROM user_identities WHERE id = ? AND user_id = ?', [id, req.user.id]); if (!ident) return res.status(404).json({ error: '身份不存在' }); const count = await getRow('SELECT COUNT(*) as c FROM user_identities WHERE user_id = ?', [req.user.id]); if (count.c <= 1) return res.status(400).json({ error: '至少保留一个身份' }); await query('DELETE FROM user_identities WHERE id = ?', [id]); res.json({ message: '已解绑' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.put('/profile', authenticate, async (req, res) => { try { const fields = {}; const values = []; if (req.body.game_name) { fields.game_name = req.body.game_name; } if (req.body.game_uid) { fields.game_uid = req.body.game_uid; } if (req.body.email) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(req.body.email)) { return res.status(400).json({ error: '邮箱格式不正确' }); } const dup = await getRow('SELECT id FROM users WHERE email = ? AND id != ?', [req.body.email, req.user.id]); if (dup) return res.status(400).json({ error: '邮箱已被其他账号使用' }); fields.email = req.body.email; } if (Object.keys(fields).length === 0) { return res.status(400).json({ error: '无更新内容' }); } const sets = Object.keys(fields).map(k => `${k} = ?`).join(', '); await query(`UPDATE users SET ${sets} WHERE id = ?`, [...Object.values(fields), req.user.id]); const updated = await getRow( 'SELECT id, username, email, game_name, game_uid, role FROM users WHERE id = ?', [req.user.id] ); const token = generateToken(updated); res.json({ message: '更新成功', token, user: updated }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.put('/password', authenticate, async (req, res) => { try { const { oldPassword, newPassword } = req.body; if (!oldPassword || !newPassword) { return res.status(400).json({ error: '请提供新旧密码' }); } if (newPassword.length < 6) { return res.status(400).json({ error: '新密码至少6位' }); } const user = await getRow('SELECT * FROM users WHERE id = ?', [req.user.id]); const valid = await bcrypt.compare(oldPassword, user.password); if (!valid) return res.status(400).json({ error: '旧密码错误' }); const hashed = await bcrypt.hash(newPassword, 10); await query('UPDATE users SET password = ? WHERE id = ?', [hashed, req.user.id]); res.json({ message: '密码修改成功' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/forgot-password', async (req, res) => { try { const { email } = req.body; if (!email) return res.status(400).json({ error: '请输入邮箱' }); const user = await getRow('SELECT * FROM users WHERE email = ?', [email]); if (!user) return res.json({ message: '如果邮箱已注册,重置链接已发送' }); const resetToken = uuid(); await query("UPDATE users SET reset_token = ?, reset_expires = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE id = ?", [resetToken, user.id]); const site = await getRow("SELECT v FROM settings WHERE k='site_url'"); await sendEmail(email, 'reset_password', { username: user.username, game_name: user.game_name, game_uid: user.game_uid, reset_link: `${site?.v||'http://localhost:3100'}#/reset?token=${resetToken}`, }); res.json({ message: '如果邮箱已注册,重置链接已发送' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); router.post('/reset-password', async (req, res) => { try { const { token, password } = req.body; if (!token || !password) return res.status(400).json({ error: '参数不完整' }); if (password.length < 6) return res.status(400).json({ error: '密码至少6位' }); const user = await getRow("SELECT * FROM users WHERE reset_token = ? AND reset_expires > NOW()", [token]); if (!user) return res.status(400).json({ error: '重置链接无效或已过期' }); const hashed = await bcrypt.hash(password, 10); await query("UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?", [hashed, user.id]); res.json({ message: '密码重置成功,请登录' }); } catch (err) { console.error('[auth]', err); res.status(500).json({ error: '服务器内部错误' }); } }); module.exports = router;