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: 调整登录页面样式
This commit is contained in:
@@ -8,7 +8,7 @@ module.exports = {
|
||||
|
||||
MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5,
|
||||
|
||||
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 30) * 60 * 1000,
|
||||
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 3) * 60 * 1000,
|
||||
|
||||
TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h',
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ const User = sequelize.define(
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
lockedUntil: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '账户锁定过期时间,NULL表示未锁定或已解锁',
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
|
||||
+24
-5
@@ -8,6 +8,7 @@ const { generateToken, authMiddleware } = require('../middleware/auth');
|
||||
const {
|
||||
SALT_ROUNDS,
|
||||
MAX_LOGIN_ATTEMPTS,
|
||||
LOCK_TIME,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_MAX_LENGTH,
|
||||
@@ -154,10 +155,18 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
|
||||
if (user.status === 'locked') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被锁定,请联系管理员',
|
||||
});
|
||||
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') {
|
||||
@@ -180,12 +189,21 @@ router.post('/login', async (req, res) => {
|
||||
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: '用户名或密码错误',
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,6 +219,7 @@ router.post('/login', async (req, res) => {
|
||||
user.lastLoginTime = new Date();
|
||||
user.lastLoginIp = req.ip || req.connection.remoteAddress;
|
||||
user.loginCount = 0;
|
||||
user.lockedUntil = null;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -133,6 +133,11 @@ const migrations = [
|
||||
description: '为 operation_logs 表添加 requestId 字段和复合索引,支持请求追踪',
|
||||
migrate: migrateOperationLogRequestId,
|
||||
},
|
||||
{
|
||||
name: '用户账户锁定时间',
|
||||
description: '为 users 表添加 lockedUntil 字段,支持账户自动解锁',
|
||||
migrate: migrateUserLockedUntil,
|
||||
},
|
||||
];
|
||||
|
||||
async function runMigrations() {
|
||||
@@ -951,6 +956,18 @@ async function migrateOperationLogRequestId() {
|
||||
console.log(' 操作日志requestId字段和索引迁移完成');
|
||||
}
|
||||
|
||||
async function migrateUserLockedUntil() {
|
||||
const tableName = 'users';
|
||||
|
||||
if (!(await tableExists(tableName))) {
|
||||
console.log(` ${tableName} 表不存在,跳过`);
|
||||
return;
|
||||
}
|
||||
|
||||
await addColumnIfNotExists(tableName, 'lockedUntil', 'DATETIME');
|
||||
console.log(' users 表 lockedUntil 字段迁移完成');
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
runMigrations().catch(error => {
|
||||
console.error('迁移执行失败:', error);
|
||||
|
||||
Reference in New Issue
Block a user