feat: 实现用户认证与权限管理系统
- 添加用户、角色、权限等数据模型 - 实现JWT认证中间件和密码加密 - 添加用户注册、登录、个人信息管理接口 - 实现前端认证上下文和受保护路由 - 添加登录历史记录和操作日志功能 - 提供管理员初始化脚本和修复工具 - 实现完整的登录页面和用户管理界面
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
const { sequelize } = require('./db');
|
||||
const User = require('./models/User');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('数据库连接成功!\n');
|
||||
|
||||
const users = await User.findAll({
|
||||
attributes: ['userId', 'username', 'password', 'email', 'status', 'createdAt']
|
||||
});
|
||||
|
||||
console.log('用户列表:');
|
||||
console.log('============');
|
||||
|
||||
for (const user of users) {
|
||||
console.log('用户名:', user.username);
|
||||
console.log('密码(加密后):', user.password);
|
||||
console.log('状态:', user.status);
|
||||
console.log('创建时间:', user.createdAt);
|
||||
console.log('------------');
|
||||
}
|
||||
|
||||
console.log('\n' + users.length + ' 个用户');
|
||||
|
||||
await sequelize.close();
|
||||
} catch (error) {
|
||||
console.error('错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,125 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { sequelize } = require('./db');
|
||||
const User = require('./models/User');
|
||||
const Role = require('./models/Role');
|
||||
const UserRole = require('./models/UserRole');
|
||||
|
||||
async function fixAdminUser() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('数据库连接成功!\n');
|
||||
|
||||
const adminUser = await User.findOne({
|
||||
where: { username: 'admin' }
|
||||
});
|
||||
|
||||
if (!adminUser) {
|
||||
console.log('未找到 admin 用户');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('当前 admin 用户信息:');
|
||||
console.log('用户名:', adminUser.username);
|
||||
console.log('邮箱:', adminUser.email);
|
||||
console.log('状态:', adminUser.status);
|
||||
console.log('');
|
||||
|
||||
const userRoles = await UserRole.findAll({
|
||||
where: { UserId: adminUser.userId },
|
||||
include: [{ model: Role }]
|
||||
});
|
||||
|
||||
console.log('当前用户角色:');
|
||||
if (userRoles.length === 0) {
|
||||
console.log(' - 无角色分配');
|
||||
} else {
|
||||
userRoles.forEach(ur => {
|
||||
console.log(` - ${ur.Role.roleName} (${ur.Role.roleCode})`);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
|
||||
let adminRole = await Role.findOne({
|
||||
where: { roleCode: 'admin' }
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
console.log('正在创建管理员角色...');
|
||||
adminRole = await Role.create({
|
||||
roleId: 'role_admin',
|
||||
roleName: '管理员',
|
||||
roleCode: 'admin',
|
||||
description: '系统管理员,拥有所有权限',
|
||||
status: 'active',
|
||||
permissions: []
|
||||
});
|
||||
console.log('管理员角色创建成功!');
|
||||
} else {
|
||||
console.log('管理员角色已存在');
|
||||
}
|
||||
|
||||
const existingUserRole = await UserRole.findOne({
|
||||
where: {
|
||||
UserId: adminUser.userId,
|
||||
RoleId: adminRole.roleId
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingUserRole) {
|
||||
console.log('正在为 admin 用户分配管理员角色...');
|
||||
await UserRole.create({
|
||||
UserId: adminUser.userId,
|
||||
RoleId: adminRole.roleId
|
||||
});
|
||||
console.log('角色分配成功!');
|
||||
} else {
|
||||
console.log('管理员角色已分配给该用户');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
const newPassword = 'admin123';
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
||||
|
||||
await adminUser.update({
|
||||
password: hashedPassword
|
||||
});
|
||||
|
||||
console.log('密码已重置为: admin123');
|
||||
console.log('');
|
||||
|
||||
const updatedUser = await User.findOne({
|
||||
where: { username: 'admin' },
|
||||
include: [{ model: Role }]
|
||||
});
|
||||
|
||||
console.log('修复后的 admin 用户信息:');
|
||||
console.log('用户名:', updatedUser.username);
|
||||
console.log('邮箱:', updatedUser.email);
|
||||
console.log('状态:', updatedUser.status);
|
||||
console.log('角色:',
|
||||
updatedUser.Roles && updatedUser.Roles.length > 0
|
||||
? updatedUser.Roles.map(r => r.roleName).join(', ')
|
||||
: '无角色'
|
||||
);
|
||||
console.log('');
|
||||
|
||||
const testResult = bcrypt.compareSync(newPassword, updatedUser.password);
|
||||
console.log('密码验证测试:');
|
||||
console.log(` 输入密码: "${newPassword}"`);
|
||||
console.log(` 验证结果: ${testResult ? '✓ 成功' : '✗ 失败'}`);
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('修复完成!现在可以使用以下信息登录:');
|
||||
console.log(' 用户名: admin');
|
||||
console.log(' 密码: admin123');
|
||||
console.log('========================================');
|
||||
|
||||
} catch (error) {
|
||||
console.error('修复过程中发生错误:', error.message);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
fixAdminUser();
|
||||
@@ -0,0 +1,133 @@
|
||||
const { sequelize } = require('./db');
|
||||
const Role = require('./models/Role');
|
||||
const User = require('./models/User');
|
||||
const UserRole = require('./models/UserRole');
|
||||
|
||||
async function fixForeignKeys() {
|
||||
try {
|
||||
console.log('开始修复外键约束...');
|
||||
|
||||
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await sequelize.query('DROP TABLE IF EXISTS user_roles');
|
||||
await sequelize.query('DROP TABLE IF EXISTS users');
|
||||
await sequelize.query('DROP TABLE IF EXISTS roles');
|
||||
|
||||
console.log('已删除现有表,正在重新创建...');
|
||||
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
roleId TEXT PRIMARY KEY,
|
||||
roleName TEXT NOT NULL,
|
||||
roleCode TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
permissions TEXT DEFAULT '[]',
|
||||
sort INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'active',
|
||||
createdAt DATETIME,
|
||||
updatedAt DATETIME
|
||||
)
|
||||
`);
|
||||
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
userId TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
realName TEXT,
|
||||
avatar TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
lastLoginTime DATETIME,
|
||||
lastLoginIp TEXT,
|
||||
loginCount INTEGER DEFAULT 0,
|
||||
remark TEXT,
|
||||
createdAt DATETIME,
|
||||
updatedAt DATETIME
|
||||
)
|
||||
`);
|
||||
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
UserId TEXT NOT NULL,
|
||||
RoleId TEXT NOT NULL,
|
||||
createdAt DATETIME,
|
||||
updatedAt DATETIME,
|
||||
FOREIGN KEY (UserId) REFERENCES users(userId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (RoleId) REFERENCES roles(roleId) ON DELETE CASCADE,
|
||||
UNIQUE(UserId, RoleId)
|
||||
)
|
||||
`);
|
||||
|
||||
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||
|
||||
console.log('外键约束修复完成!');
|
||||
console.log('正在重新初始化角色数据...');
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
await Role.bulkCreate([
|
||||
{
|
||||
roleId: 'role_admin',
|
||||
roleName: '管理员',
|
||||
roleCode: 'admin',
|
||||
description: '系统管理员,拥有所有权限',
|
||||
permissions: '[]',
|
||||
sort: 1,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
roleId: 'role_operator',
|
||||
roleName: '操作员',
|
||||
roleCode: 'operator',
|
||||
description: '设备操作员,可进行设备操作',
|
||||
permissions: '[]',
|
||||
sort: 2,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
roleId: 'role_viewer',
|
||||
roleName: '访客',
|
||||
roleCode: 'viewer',
|
||||
description: '只读权限,只能查看数据',
|
||||
permissions: '[]',
|
||||
sort: 3,
|
||||
status: 'active'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('角色数据初始化完成!');
|
||||
|
||||
const hashedPassword = await bcrypt.hash('admin123', SALT_ROUNDS);
|
||||
|
||||
const adminUser = await User.create({
|
||||
userId: 'user_admin',
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
email: 'admin@example.com',
|
||||
realName: '系统管理员',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
await UserRole.create({
|
||||
UserId: adminUser.userId,
|
||||
RoleId: 'role_admin'
|
||||
});
|
||||
|
||||
console.log('管理员账户创建完成!');
|
||||
console.log('用户名: admin');
|
||||
console.log('密码: admin123');
|
||||
|
||||
console.log('\n修复完成!请重启后端服务。');
|
||||
|
||||
} catch (error) {
|
||||
console.error('修复外键约束失败:', error);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
fixForeignKeys();
|
||||
@@ -0,0 +1,151 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
const LoginHistory = require('../models/LoginHistory');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'idc-management-secret-key-2024';
|
||||
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
||||
|
||||
const getBrowserInfo = (userAgent) => {
|
||||
let device = 'Desktop';
|
||||
let browser = 'Unknown';
|
||||
let os = 'Unknown';
|
||||
|
||||
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
|
||||
device = 'Mobile';
|
||||
if (/iPad/i.test(userAgent)) device = 'Tablet';
|
||||
}
|
||||
|
||||
if (/Firefox/i.test(userAgent)) {
|
||||
browser = 'Firefox';
|
||||
} else if (/Chrome/i.test(userAgent)) {
|
||||
browser = 'Chrome';
|
||||
} else if (/Safari/i.test(userAgent)) {
|
||||
browser = 'Safari';
|
||||
} else if (/Edge/i.test(userAgent)) {
|
||||
browser = 'Edge';
|
||||
} else if (/MSIE|Trident/i.test(userAgent)) {
|
||||
browser = 'IE';
|
||||
}
|
||||
|
||||
if (/Windows/i.test(userAgent)) {
|
||||
os = 'Windows';
|
||||
} else if (/Mac OS/i.test(userAgent)) {
|
||||
os = 'macOS';
|
||||
} else if (/Linux/i.test(userAgent)) {
|
||||
os = 'Linux';
|
||||
} else if (/Android/i.test(userAgent)) {
|
||||
os = 'Android';
|
||||
} else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) {
|
||||
os = 'iOS';
|
||||
}
|
||||
|
||||
return { device, browser, os };
|
||||
};
|
||||
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
roleId: user.roleId
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: TOKEN_EXPIRY }
|
||||
);
|
||||
};
|
||||
|
||||
const verifyToken = (token) => {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const authMiddleware = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '未提供认证令牌'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (!decoded) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '令牌无效或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status === 'locked') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被锁定'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status === 'inactive') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已禁用'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = decoded;
|
||||
req.userModel = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('认证中间件错误:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '认证失败'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (decoded) {
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
if (user && user.status === 'active') {
|
||||
req.user = decoded;
|
||||
req.userModel = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
authMiddleware,
|
||||
optionalAuth,
|
||||
JWT_SECRET,
|
||||
TOKEN_EXPIRY
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const LoginHistory = sequelize.define('LoginHistory', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户名'
|
||||
},
|
||||
realName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '真实姓名'
|
||||
},
|
||||
loginTime: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '登录时间'
|
||||
},
|
||||
loginIp: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '登录IP'
|
||||
},
|
||||
userAgent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '浏览器UA'
|
||||
},
|
||||
loginType: {
|
||||
type: DataTypes.ENUM('success', 'failed'),
|
||||
defaultValue: 'success',
|
||||
comment: '登录结果'
|
||||
},
|
||||
failReason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '失败原因'
|
||||
}
|
||||
}, {
|
||||
tableName: 'login_histories',
|
||||
timestamps: true,
|
||||
createdAt: 'loginTime',
|
||||
updatedAt: false
|
||||
});
|
||||
|
||||
module.exports = LoginHistory;
|
||||
@@ -0,0 +1,82 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const OperationLog = sequelize.define('OperationLog', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作人ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作人用户名'
|
||||
},
|
||||
realName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作人真实姓名'
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作模块'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '操作描述'
|
||||
},
|
||||
targetId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '目标对象ID'
|
||||
},
|
||||
targetName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '目标对象名称'
|
||||
},
|
||||
oldValue: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '旧值'
|
||||
},
|
||||
newValue: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '新值'
|
||||
},
|
||||
ip: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作IP'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('success', 'failed'),
|
||||
defaultValue: 'success',
|
||||
comment: '操作状态'
|
||||
},
|
||||
errorMessage: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息'
|
||||
}
|
||||
}, {
|
||||
tableName: 'operation_logs',
|
||||
timestamps: true,
|
||||
createdAt: 'operateTime',
|
||||
updatedAt: false
|
||||
});
|
||||
|
||||
module.exports = OperationLog;
|
||||
@@ -0,0 +1,48 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const Permission = sequelize.define('Permission', {
|
||||
permissionId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
permissionName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
permissionCode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
parentId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('menu', 'button'),
|
||||
defaultValue: 'button'
|
||||
},
|
||||
path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
sort: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
tableName: 'permissions',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Permission;
|
||||
@@ -0,0 +1,40 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const Role = sequelize.define('Role', {
|
||||
roleId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
roleName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
roleCode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
permissions: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: []
|
||||
},
|
||||
sort: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
}
|
||||
}, {
|
||||
tableName: 'roles',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = Role;
|
||||
@@ -0,0 +1,63 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const User = sequelize.define('User', {
|
||||
userId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
realName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'locked'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
lastLoginTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
lastLoginIp: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
loginCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = User;
|
||||
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
const User = require('./User');
|
||||
const Role = require('./Role');
|
||||
|
||||
const UserRole = sequelize.define('UserRole', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_roles',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
UserRole.belongsTo(User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
||||
UserRole.belongsTo(Role, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
||||
User.hasMany(UserRole, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
||||
Role.hasMany(UserRole, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
||||
|
||||
module.exports = UserRole;
|
||||
Generated
+117
@@ -8,6 +8,7 @@
|
||||
"name": "idc-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.2.0",
|
||||
"csv-writer": "^1.6.0",
|
||||
@@ -16,6 +17,7 @@
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mysql2": "^3.16.0",
|
||||
"sequelize": "^6.32.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
@@ -281,6 +283,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -398,6 +409,12 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -761,6 +778,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -1522,12 +1548,103 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.2.0",
|
||||
"csv-writer": "^1.6.0",
|
||||
@@ -15,6 +16,7 @@
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mysql2": "^3.16.0",
|
||||
"sequelize": "^6.32.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
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 router = express.Router();
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
const generateId = () => {
|
||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
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 < 3 || username.length > 20) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名长度必须在3-20个字符之间'
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '密码长度不能少于6个字符'
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
phone,
|
||||
realName: realName || username,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
} else {
|
||||
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
||||
|
||||
if (defaultRole) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: defaultRole.roleId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: isFirstUser ? '注册成功,已为您分配管理员权限' : '注册成功',
|
||||
data: {
|
||||
user: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName
|
||||
},
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('注册错误:', error);
|
||||
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') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被锁定,请联系管理员'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status === 'inactive') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已禁用'
|
||||
});
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
user.loginCount = (user.loginCount || 0) + 1;
|
||||
if (user.loginCount >= 5) {
|
||||
user.status = 'locked';
|
||||
}
|
||||
await user.save();
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
|
||||
user.lastLoginTime = new Date();
|
||||
user.lastLoginIp = req.ip || req.connection.remoteAddress;
|
||||
user.loginCount = 0;
|
||||
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
|
||||
},
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
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 roles = await Role.findAll({
|
||||
include: [{
|
||||
model: User,
|
||||
where: { userId: req.user.userId },
|
||||
attributes: []
|
||||
}]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user,
|
||||
roles: roles.map(r => ({
|
||||
roleId: r.roleId,
|
||||
roleName: r.roleName,
|
||||
roleCode: r.roleCode
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取profile错误:', error);
|
||||
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) {
|
||||
console.error('更新profile错误:', error);
|
||||
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 < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '新密码长度不能少于6个字符'
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('修改密码错误:', error);
|
||||
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) {
|
||||
console.error('检查管理员错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '检查失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,120 @@
|
||||
const express = require('express');
|
||||
const LoginHistory = require('../models/LoginHistory');
|
||||
const User = require('../models/User');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, userId, loginType, startDate, endDate } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
const where = {};
|
||||
|
||||
if (userId) where.userId = userId;
|
||||
if (loginType) where.loginType = loginType;
|
||||
if (startDate || endDate) {
|
||||
where.loginTime = {};
|
||||
if (startDate) where.loginTime[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.loginTime[Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
const { count, rows } = await LoginHistory.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['loginTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
histories: rows.map(h => ({
|
||||
id: h.id,
|
||||
userId: h.userId,
|
||||
username: h.username,
|
||||
realName: h.realName,
|
||||
loginTime: h.loginTime,
|
||||
loginIp: h.loginIp,
|
||||
userAgent: h.userAgent,
|
||||
loginType: h.loginType,
|
||||
failReason: h.failReason
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取登录历史错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取登录历史失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/user/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const { count, rows } = await LoginHistory.findAndCountAll({
|
||||
where: { userId: req.params.userId },
|
||||
limit,
|
||||
offset,
|
||||
order: [['loginTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
histories: rows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户登录历史错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取登录历史失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
await LoginHistory.destroy({ where: { id: req.params.id } });
|
||||
res.json({ success: true, message: '删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除登录历史错误:', error);
|
||||
res.status(500).json({ success: false, message: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const where = { loginType: 'success' };
|
||||
|
||||
if (days) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
where.loginTime = { [Op.lt]: cutoffDate };
|
||||
}
|
||||
|
||||
await LoginHistory.destroy({ where });
|
||||
res.json({ success: true, message: '清理成功' });
|
||||
} catch (error) {
|
||||
console.error('清理登录历史错误:', error);
|
||||
res.status(500).json({ success: false, message: '清理失败' });
|
||||
}
|
||||
});
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,162 @@
|
||||
const express = require('express');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const ACTION_TYPES = {
|
||||
USER_CREATE: '创建用户',
|
||||
USER_UPDATE: '修改用户',
|
||||
USER_DELETE: '删除用户',
|
||||
USER_LOGIN: '用户登录',
|
||||
USER_LOGOUT: '用户登出',
|
||||
ROLE_CREATE: '创建角色',
|
||||
ROLE_UPDATE: '修改角色',
|
||||
ROLE_DELETE: '删除角色',
|
||||
ROLE_ASSIGN: '分配角色',
|
||||
DEVICE_CREATE: '创建设备',
|
||||
DEVICE_UPDATE: '修改设备',
|
||||
DEVICE_DELETE: '删除设备',
|
||||
CONSUMABLE_IN: '耗材入库',
|
||||
CONSUMABLE_OUT: '耗材出库',
|
||||
CONSUMABLE_RECORD: '耗材记录',
|
||||
SYSTEM_CONFIG: '系统配置'
|
||||
};
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, userId, action, module, startDate, endDate } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
const where = {};
|
||||
|
||||
if (userId) where.userId = userId;
|
||||
if (action) where.action = action;
|
||||
if (module) where.module = module;
|
||||
if (startDate || endDate) {
|
||||
where.operateTime = {};
|
||||
if (startDate) where.operateTime[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.operateTime[Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
const { count, rows } = await OperationLog.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['operateTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logs: rows.map(l => ({
|
||||
id: l.id,
|
||||
userId: l.userId,
|
||||
username: l.username,
|
||||
realName: l.realName,
|
||||
action: l.action,
|
||||
module: l.module,
|
||||
description: l.description,
|
||||
targetId: l.targetId,
|
||||
targetName: l.targetName,
|
||||
oldValue: l.oldValue,
|
||||
newValue: l.newValue,
|
||||
ip: l.ip,
|
||||
status: l.status,
|
||||
errorMessage: l.errorMessage,
|
||||
operateTime: l.operateTime
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/actions', authMiddleware, (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: Object.entries(ACTION_TYPES).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
label: value
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/modules', authMiddleware, (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [
|
||||
{ key: 'user', value: 'user', label: '用户管理' },
|
||||
{ key: 'role', value: 'role', label: '角色管理' },
|
||||
{ key: 'device', value: 'device', label: '设备管理' },
|
||||
{ key: 'consumable', value: 'consumable', label: '耗材管理' },
|
||||
{ key: 'system', value: 'system', label: '系统设置' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
await OperationLog.destroy({ where: { id: req.params.id } });
|
||||
res.json({ success: true, message: '删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除操作日志错误:', error);
|
||||
res.status(500).json({ success: false, message: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const where = {};
|
||||
|
||||
if (days) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
where.operateTime = { [Op.lt]: cutoffDate };
|
||||
}
|
||||
|
||||
await OperationLog.destroy({ where });
|
||||
res.json({ success: true, message: '清理成功' });
|
||||
} catch (error) {
|
||||
console.error('清理操作日志错误:', error);
|
||||
res.status(500).json({ success: false, message: '清理失败' });
|
||||
}
|
||||
});
|
||||
|
||||
const logOperation = async (req, action, module, description, targetId, targetName, oldValue, newValue, status = 'success', errorMessage = null) => {
|
||||
try {
|
||||
await OperationLog.create({
|
||||
userId: req.user?.userId,
|
||||
username: req.user?.username,
|
||||
realName: req.userModel?.realName,
|
||||
action,
|
||||
module,
|
||||
description,
|
||||
targetId,
|
||||
targetName,
|
||||
oldValue: oldValue ? JSON.stringify(oldValue) : null,
|
||||
newValue: newValue ? JSON.stringify(newValue) : null,
|
||||
ip: req.ip,
|
||||
status,
|
||||
errorMessage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录操作日志错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
module.exports.ACTION_TYPES = ACTION_TYPES;
|
||||
module.exports.logOperation = logOperation;
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
@@ -0,0 +1,275 @@
|
||||
const express = require('express');
|
||||
const Role = require('../models/Role');
|
||||
const Permission = require('../models/Permission');
|
||||
const UserRole = require('../models/UserRole');
|
||||
const User = require('../models/User');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const generateId = () => {
|
||||
return 'role_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, roleName, status } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const where = {};
|
||||
if (roleName) {
|
||||
where.roleName = { [Op.like]: `%${roleName}%` };
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const { count, rows: roles } = await Role.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
roles
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取角色列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/all', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
where: { status: 'active' },
|
||||
order: [['sort', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: roles
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:roleId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const role = await Role.findByPk(req.params.roleId);
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const permissions = await Permission.findAll({
|
||||
where: { status: 'active' },
|
||||
order: [['sort', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
role,
|
||||
permissions,
|
||||
rolePermissions: role.permissions || []
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取角色详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { roleName, roleCode, description, permissions, status, sort } = req.body;
|
||||
|
||||
if (!roleName || !roleCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '角色名称和角色编码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const existingRole = await Role.findOne({ where: { roleCode } });
|
||||
if (existingRole) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '角色编码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const role = await Role.create({
|
||||
roleId: generateId(),
|
||||
roleName,
|
||||
roleCode,
|
||||
description,
|
||||
permissions: permissions || [],
|
||||
status: status || 'active',
|
||||
sort: sort || 0
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '创建成功',
|
||||
data: role
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建角色失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { roleName, description, permissions, status, sort } = req.body;
|
||||
const role = await Role.findByPk(req.params.roleId);
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (roleName !== undefined) role.roleName = roleName;
|
||||
if (description !== undefined) role.description = description;
|
||||
if (permissions !== undefined) role.permissions = permissions;
|
||||
if (status !== undefined) role.status = status;
|
||||
if (sort !== undefined) role.sort = sort;
|
||||
|
||||
await role.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
data: role
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新角色失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const role = await Role.findByPk(req.params.roleId);
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (role.roleCode === 'admin') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能删除管理员角色'
|
||||
});
|
||||
}
|
||||
|
||||
const userCount = await UserRole.count({ where: { RoleId: role.roleId } });
|
||||
if (userCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该角色下有用户,不能删除'
|
||||
});
|
||||
}
|
||||
|
||||
await role.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除角色失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/init-roles', async (req, res) => {
|
||||
try {
|
||||
const defaultRoles = [
|
||||
{
|
||||
roleId: 'role_admin',
|
||||
roleName: '管理员',
|
||||
roleCode: 'admin',
|
||||
description: '系统管理员,拥有所有权限',
|
||||
permissions: ['*'],
|
||||
status: 'active',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
roleId: 'role_operator',
|
||||
roleName: '运维人员',
|
||||
roleCode: 'operator',
|
||||
description: '负责日常运维操作',
|
||||
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
|
||||
status: 'active',
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
roleId: 'role_viewer',
|
||||
roleName: '只读用户',
|
||||
roleCode: 'viewer',
|
||||
description: '仅能查看数据',
|
||||
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
||||
status: 'active',
|
||||
sort: 3
|
||||
}
|
||||
];
|
||||
|
||||
for (const roleData of defaultRoles) {
|
||||
await Role.upsert(roleData);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '初始化角色成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初始化角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '初始化角色失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,450 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const User = require('../models/User');
|
||||
const Role = require('../models/Role');
|
||||
const UserRole = require('../models/UserRole');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
const generateId = () => {
|
||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
const getWhereClause = (query) => {
|
||||
const where = {};
|
||||
|
||||
if (query.username) {
|
||||
where.username = { [Op.like]: `%${query.username}%` };
|
||||
}
|
||||
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
}
|
||||
|
||||
if (query.realName) {
|
||||
where.realName = { [Op.like]: `%${query.realName}%` };
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, username, status, realName } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const where = getWhereClause({ username, status, realName });
|
||||
|
||||
const { count, rows: users } = await User.findAndCountAll({
|
||||
where,
|
||||
attributes: { exclude: ['password'] },
|
||||
limit,
|
||||
offset,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
for (const user of users) {
|
||||
const userRoles = await UserRole.findAll({
|
||||
include: [{
|
||||
model: Role,
|
||||
where: { status: 'active' }
|
||||
}],
|
||||
where: { UserId: user.userId }
|
||||
});
|
||||
|
||||
user.dataValues.roles = userRoles.map(ur => ({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
}));
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
users
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/all', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
where: { status: 'active' },
|
||||
attributes: ['userId', 'username', 'realName', 'email'],
|
||||
order: [['realName', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const userRoles = await UserRole.findAll({
|
||||
include: [{
|
||||
model: Role,
|
||||
where: { status: 'active' }
|
||||
}],
|
||||
where: { UserId: user.userId }
|
||||
});
|
||||
|
||||
user.dataValues.roles = userRoles.map(ur => ({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { username, password, email, phone, realName, roleIds, status, remark } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
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 user = await User.create({
|
||||
userId: generateId(),
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email,
|
||||
phone,
|
||||
realName: realName || username,
|
||||
status: status || 'active',
|
||||
remark
|
||||
});
|
||||
|
||||
if (roleIds && roleIds.length > 0) {
|
||||
for (const roleId of roleIds) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: roleId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '创建成功',
|
||||
data: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { username, email, phone, realName, roleIds, status, remark, newPassword } = req.body;
|
||||
const user = await User.findByPk(req.params.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (username !== undefined && username !== user.username) {
|
||||
const existingUser = await User.findOne({
|
||||
where: { username, userId: { [Op.ne]: user.userId } }
|
||||
});
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
});
|
||||
}
|
||||
user.username = username;
|
||||
}
|
||||
|
||||
if (email !== undefined) user.email = email;
|
||||
if (phone !== undefined) user.phone = phone;
|
||||
if (realName !== undefined) user.realName = realName;
|
||||
if (status !== undefined) user.status = status;
|
||||
if (remark !== undefined) user.remark = remark;
|
||||
|
||||
if (newPassword && newPassword.length >= 6) {
|
||||
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
if (roleIds !== undefined) {
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
|
||||
for (const roleId of roleIds) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: roleId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await User.findByPk(req.params.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
data: updatedUser
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:userId/password', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { newPassword } = req.body;
|
||||
const user = await User.findByPk(req.params.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '密码长度不能少于6个字符'
|
||||
});
|
||||
}
|
||||
|
||||
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('重置密码错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '重置密码失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.userId === req.user.userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能删除当前登录用户'
|
||||
});
|
||||
}
|
||||
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
await user.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.files || !req.files.avatar) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的头像文件'
|
||||
});
|
||||
}
|
||||
|
||||
const avatarFile = req.files.avatar;
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (!allowedTypes.includes(avatarFile.mimetype)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
|
||||
});
|
||||
}
|
||||
|
||||
if (avatarFile.size > 5 * 1024 * 1024) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '图片大小不能超过 5MB'
|
||||
});
|
||||
}
|
||||
|
||||
const uploadDir = path.join(__dirname, '..', 'uploads', 'avatars');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const ext = path.extname(avatarFile.name) || '.jpg';
|
||||
const filename = `avatar_${user.userId}_${Date.now()}${ext}`;
|
||||
const filepath = path.join(uploadDir, filename);
|
||||
|
||||
await avatarFile.mv(filepath);
|
||||
|
||||
if (user.avatar && user.avatar.startsWith('/uploads/avatars/')) {
|
||||
const oldFile = path.join(__dirname, '..', user.avatar);
|
||||
if (fs.existsSync(oldFile)) {
|
||||
fs.unlinkSync(oldFile);
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/uploads/avatars/${filename}`;
|
||||
user.avatar = avatarUrl;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '头像上传成功',
|
||||
data: { avatar: avatarUrl }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传头像错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '上传头像失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.avatar && user.avatar.startsWith('/uploads/avatars/')) {
|
||||
const oldFile = path.join(__dirname, '..', user.avatar);
|
||||
if (fs.existsSync(oldFile)) {
|
||||
fs.unlinkSync(oldFile);
|
||||
}
|
||||
}
|
||||
|
||||
user.avatar = null;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '头像删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除头像错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除头像失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -42,6 +42,11 @@ const backgroundRoutes = require('./routes/background');
|
||||
const consumableRoutes = require('./routes/consumables');
|
||||
const consumableRecordRoutes = require('./routes/consumableRecords');
|
||||
const consumableCategoryRoutes = require('./routes/consumableCategories');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const usersRoutes = require('./routes/users');
|
||||
const rolesRoutes = require('./routes/roles');
|
||||
const loginHistoryRoutes = require('./routes/loginHistory');
|
||||
const operationLogsRoutes = require('./routes/operationLogs');
|
||||
|
||||
// 使用路由
|
||||
app.use('/api/devices', deviceRoutes);
|
||||
@@ -52,6 +57,11 @@ app.use('/api/background', backgroundRoutes);
|
||||
app.use('/api/consumables', consumableRoutes);
|
||||
app.use('/api/consumable-records', consumableRecordRoutes);
|
||||
app.use('/api/consumable-categories', consumableCategoryRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use('/api/roles', rolesRoutes);
|
||||
app.use('/api/login-history', loginHistoryRoutes);
|
||||
app.use('/api/operation-logs', operationLogsRoutes);
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
$postData = @{
|
||||
consumableId = "CON1766493042245"
|
||||
type = "in"
|
||||
quantity = 5
|
||||
operator = "测试管理员"
|
||||
reason = "测试入库"
|
||||
notes = "测试日志记录功能"
|
||||
} | ConvertTo-Json
|
||||
|
||||
$response = Invoke-RestMethod -Uri "http://localhost:8000/api/consumables/quick-inout" -Method Post -Body $postData -ContentType "application/json"
|
||||
|
||||
Write-Host "入库API响应:"
|
||||
$response | ConvertTo-Json -Depth 5
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Reference in New Issue
Block a user