Files
MC_Report/backend/routes/auth.js

245 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]);
const existingEmail = await getRow('SELECT id FROM users WHERE email = ?', [email]);
if (existingUser || 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) {
console.error('[auth]', err);
res.status(500).json({ error: '服务器内部错误' });
}
});
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) {
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,
},
});
} 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, created_at FROM users WHERE id = ?',
[req.user.id]
);
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
} 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, 'verify_email', {
username: user.username, game_name: user.game_name, game_uid: user.game_uid,
verify_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;