Files
yunrui_asset/backend/routes/auth.js
T
zhang1106 2826a00192 refactor(frontend): 重构状态管理使用Zustand替代Context API
feat(auth): 添加账户锁定功能及自动解锁机制
feat(user): 在用户模型中添加lockedUntil字段
feat(api): 实现账户锁定逻辑和剩余尝试次数提示

perf(3d): 优化3D场景状态管理性能
perf(floorplan): 优化平面图状态管理性能

chore(deps): 添加zustand依赖
chore(config): 更新安全配置锁定时间为3分钟

docs: 更新部分组件注释
style: 调整登录页面样式
2026-05-08 11:17:29 +08:00

461 lines
12 KiB
JavaScript

const logger = require('../utils/logger').module('AuthRoute');
const express = require('express');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const Role = require('../models/Role');
const UserRole = require('../models/UserRole');
const { generateToken, authMiddleware } = require('../middleware/auth');
const {
SALT_ROUNDS,
MAX_LOGIN_ATTEMPTS,
LOCK_TIME,
PASSWORD_MIN_LENGTH,
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
} = require('../config');
const { generateId } = require('../utils/idGenerator');
const router = express.Router();
router.post('/register', async (req, res) => {
try {
const { username, password, email, phone, realName } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空',
});
}
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
return res.status(400).json({
success: false,
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`,
});
}
if (password.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在',
});
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const userCount = await User.count();
const isFirstUser = userCount === 0;
const user = await User.create({
userId: generateId({ prefix: 'USR' }),
username,
password: hashedPassword,
email,
phone,
realName: realName || username,
status: isFirstUser ? 'active' : 'pending',
});
if (isFirstUser) {
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
if (!adminRole) {
adminRole = await Role.create({
roleId: 'role_admin',
roleName: '管理员',
roleCode: 'admin',
description: '系统管理员,拥有所有权限',
status: 'active',
permissions: [],
});
}
await UserRole.create({
UserId: user.userId,
RoleId: adminRole.roleId,
});
const token = generateToken(user);
res.status(201).json({
success: true,
message: '注册成功,已为您分配管理员权限',
data: {
user: {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName,
},
token,
isFirstUser: true,
},
});
} else {
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
if (defaultRole) {
await UserRole.create({
UserId: user.userId,
RoleId: defaultRole.roleId,
});
}
res.status(201).json({
success: true,
message: '注册成功,请等待管理员审核',
data: {
user: {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName,
},
isFirstUser: false,
pendingApproval: true,
},
});
}
} catch (error) {
logger.error('注册错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '注册失败',
error: error.message,
});
}
});
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空',
});
}
const user = await User.findOne({ where: { username } });
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
if (user.status === 'locked') {
const now = new Date();
if (user.lockedUntil && user.lockedUntil > now) {
const remainingMinutes = Math.ceil((user.lockedUntil - now) / 60000);
return res.status(403).json({
success: false,
message: `账户已被锁定,请在 ${remainingMinutes} 分钟后重试`,
});
}
user.status = 'active';
user.loginCount = 0;
user.lockedUntil = null;
await user.save();
}
if (user.status === 'inactive') {
return res.status(403).json({
success: false,
message: '账户已禁用',
});
}
if (user.status === 'pending') {
return res.status(403).json({
success: false,
code: 'PENDING_APPROVAL',
message: '账户待审核,请联系管理员激活',
});
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
user.loginCount = (user.loginCount || 0) + 1;
if (user.loginCount >= MAX_LOGIN_ATTEMPTS) {
user.status = 'locked';
user.lockedUntil = new Date(Date.now() + LOCK_TIME);
}
await user.save();
const remainingAttempts = MAX_LOGIN_ATTEMPTS - user.loginCount;
let message = '用户名或密码错误';
if (remainingAttempts > 0) {
message += `,剩余 ${remainingAttempts} 次尝试机会`;
} else {
message = `账户已被锁定,请在 3 分钟后重试`;
}
return res.status(401).json({
success: false,
message,
});
}
const token = generateToken(user);
// 查询用户角色(通过 UserRole 中间表)
const userRoles = await UserRole.findAll({
where: { UserId: user.userId },
include: [{ model: Role }],
});
const roles = userRoles.map(ur => ur.Role || ur);
user.lastLoginTime = new Date();
user.lastLoginIp = req.ip || req.connection.remoteAddress;
user.loginCount = 0;
user.lockedUntil = null;
await user.save();
res.json({
success: true,
message: '登录成功',
data: {
user: {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName,
avatar: user.avatar,
roles: roles.map(r => ({
roleId: r.roleId,
roleName: r.roleName,
roleCode: r.roleCode,
})),
},
token,
},
});
} catch (error) {
logger.error('登录错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '登录失败',
error: error.message,
});
}
});
router.get('/profile', authMiddleware, async (req, res) => {
try {
const user = await User.findByPk(req.user.userId, {
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
const userRoles = await UserRole.findAll({
where: { UserId: user.userId },
include: [{ model: Role }],
});
res.json({
success: true,
data: {
user,
roles: userRoles.map(ur => {
const role = ur.Role || ur;
return {
roleId: role.roleId,
roleName: role.roleName,
roleCode: role.roleCode,
};
}),
},
});
} catch (error) {
logger.error('获取profile错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '获取用户信息失败',
});
}
});
router.put('/profile', authMiddleware, async (req, res) => {
try {
const { email, phone, realName, avatar } = req.body;
const user = await User.findByPk(req.user.userId);
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
if (email !== undefined) {
user.email = email;
}
if (phone !== undefined) {
user.phone = phone;
}
if (realName !== undefined) {
user.realName = realName;
}
if (avatar !== undefined) {
user.avatar = avatar;
}
await user.save();
res.json({
success: true,
message: '更新成功',
data: {
userId: user.userId,
username: user.username,
email: user.email,
phone: user.phone,
realName: user.realName,
avatar: user.avatar,
},
});
} catch (error) {
logger.error('更新profile错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '更新失败',
});
}
});
router.put('/password', authMiddleware, async (req, res) => {
try {
const { oldPassword, newPassword } = req.body;
if (!oldPassword || !newPassword) {
return res.status(400).json({
success: false,
message: '旧密码和新密码都不能为空',
});
}
if (newPassword.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
const user = await User.findByPk(req.user.userId);
const isPasswordValid = await bcrypt.compare(oldPassword, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '旧密码错误',
});
}
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
await user.save();
res.json({
success: true,
message: '密码修改成功',
});
} catch (error) {
logger.error('修改密码错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '密码修改失败',
});
}
});
router.post('/check-admin', async (req, res) => {
try {
const userCount = await User.count();
res.json({
success: true,
data: {
hasAdmin: userCount > 0,
userCount,
},
});
} catch (error) {
logger.error('检查管理员错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '检查失败',
});
}
});
router.post('/unlock', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空',
});
}
const user = await User.findOne({ where: { username } });
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
if (user.status !== 'locked') {
return res.status(400).json({
success: false,
message: '账户未被锁定',
});
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
// 解锁账户
user.status = 'active';
user.loginCount = 0;
await user.save();
res.json({
success: true,
message: '账户解锁成功',
});
} catch (error) {
logger.error('解锁账户错误', { error: error.message, stack: error.stack });
res.status(500).json({
success: false,
message: '解锁失败',
error: error.message,
});
}
});
module.exports = router;