204 lines
7.3 KiB
JavaScript
204 lines
7.3 KiB
JavaScript
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('/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 } = req.body;
|
||
|
||
if (!username || !password || !email || !game_name || !game_uid) {
|
||
return res.status(400).json({ error: '所有字段均为必填' });
|
||
}
|
||
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]);
|
||
if (existingUser) {
|
||
return res.status(400).json({ error: '用户名已存在' });
|
||
}
|
||
const existingEmail = await getRow('SELECT id FROM users WHERE email = ?', [email]);
|
||
if (existingEmail) {
|
||
return res.status(400).json({ error: '邮箱已被注册' });
|
||
}
|
||
|
||
const hashed = await bcrypt.hash(password, 10);
|
||
const verifyToken = uuid();
|
||
const verifyExpires = new Date(Date.now() + 24 * 3600 * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||
|
||
await query(
|
||
'INSERT INTO users(username, password, email, game_name, game_uid, active, email_verified, verify_token, verify_expires) VALUES (?,?,?,?,?,?,?,?,?)',
|
||
[username, hashed, email, game_name, game_uid, 0, 0, verifyToken, verifyExpires]
|
||
);
|
||
|
||
const siteUrl = await getSiteUrl();
|
||
const sent = await sendEmail(email, 'verify_email', {
|
||
username, game_name, game_uid,
|
||
verify_link: `${siteUrl}#/verify?token=${verifyToken}`,
|
||
});
|
||
|
||
if (!sent) {
|
||
await query('UPDATE users SET email_verified = 1, active = 1, verify_token = NULL, verify_expires = NULL WHERE username = ?', [username]);
|
||
return res.status(201).json({ message: '注册成功!SMTP未配置,账号已自动激活,请直接登录' });
|
||
}
|
||
|
||
res.status(201).json({ message: '注册成功,请查收验证邮件' });
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
router.get('/verify-email', async (req, res) => {
|
||
try {
|
||
const { token } = req.query;
|
||
if (!token) return res.status(400).json({ error: '无效的验证链接' });
|
||
|
||
const user = await getRow('SELECT * FROM users WHERE verify_token = ? AND verify_expires > NOW()', [token]);
|
||
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) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
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,
|
||
},
|
||
});
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
router.get('/me', authenticate, async (req, res) => {
|
||
try {
|
||
const user = await getRow(
|
||
'SELECT id, username, email, game_name, game_uid, role, email_verified, created_at FROM users WHERE id = ?',
|
||
[req.user.id]
|
||
);
|
||
if (!user) return res.status(404).json({ error: '用户不存在' });
|
||
res.json(user);
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
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: '邮箱格式不正确' });
|
||
}
|
||
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) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
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) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|