diff --git a/backend/check_password.js b/backend/check_password.js new file mode 100644 index 0000000..ee4791a --- /dev/null +++ b/backend/check_password.js @@ -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(); diff --git a/backend/fix_admin.js b/backend/fix_admin.js new file mode 100644 index 0000000..09ca3fc --- /dev/null +++ b/backend/fix_admin.js @@ -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(); diff --git a/backend/fix_user_delete.js b/backend/fix_user_delete.js new file mode 100644 index 0000000..ef4cad7 --- /dev/null +++ b/backend/fix_user_delete.js @@ -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(); diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..db9e6ff --- /dev/null +++ b/backend/middleware/auth.js @@ -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 +}; diff --git a/backend/models/LoginHistory.js b/backend/models/LoginHistory.js new file mode 100644 index 0000000..63166fd --- /dev/null +++ b/backend/models/LoginHistory.js @@ -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; diff --git a/backend/models/OperationLog.js b/backend/models/OperationLog.js new file mode 100644 index 0000000..d217adc --- /dev/null +++ b/backend/models/OperationLog.js @@ -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; diff --git a/backend/models/Permission.js b/backend/models/Permission.js new file mode 100644 index 0000000..b7ab3bf --- /dev/null +++ b/backend/models/Permission.js @@ -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; diff --git a/backend/models/Role.js b/backend/models/Role.js new file mode 100644 index 0000000..ad35796 --- /dev/null +++ b/backend/models/Role.js @@ -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; diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..ac28830 --- /dev/null +++ b/backend/models/User.js @@ -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; diff --git a/backend/models/UserRole.js b/backend/models/UserRole.js new file mode 100644 index 0000000..1a15741 --- /dev/null +++ b/backend/models/UserRole.js @@ -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; diff --git a/backend/package-lock.json b/backend/package-lock.json index 5086c94..48fc880 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 229b185..1feddc8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..101b3ce --- /dev/null +++ b/backend/routes/auth.js @@ -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; diff --git a/backend/routes/loginHistory.js b/backend/routes/loginHistory.js new file mode 100644 index 0000000..51434ca --- /dev/null +++ b/backend/routes/loginHistory.js @@ -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; diff --git a/backend/routes/operationLogs.js b/backend/routes/operationLogs.js new file mode 100644 index 0000000..2ceb94c --- /dev/null +++ b/backend/routes/operationLogs.js @@ -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'); diff --git a/backend/routes/roles.js b/backend/routes/roles.js new file mode 100644 index 0000000..675c949 --- /dev/null +++ b/backend/routes/roles.js @@ -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; diff --git a/backend/routes/users.js b/backend/routes/users.js new file mode 100644 index 0000000..5fe5e44 --- /dev/null +++ b/backend/routes/users.js @@ -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; diff --git a/backend/server.js b/backend/server.js index 06565d0..752bf68 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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')); diff --git a/backend/test_inout.ps1 b/backend/test_inout.ps1 deleted file mode 100644 index f46d88e..0000000 --- a/backend/test_inout.ps1 +++ /dev/null @@ -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 diff --git a/backend/uploads/avatars/avatar_user_mjkrahqi31ud7tdo5_1766628699440.jpg b/backend/uploads/avatars/avatar_user_mjkrahqi31ud7tdo5_1766628699440.jpg new file mode 100644 index 0000000..31e2e8a Binary files /dev/null and b/backend/uploads/avatars/avatar_user_mjkrahqi31ud7tdo5_1766628699440.jpg differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f2c00f0..b388a80 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; -import { Layout, Menu, theme, Button } from 'antd'; -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; -import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined } from '@ant-design/icons'; +import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd'; +import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, UserOutlined as UserIcon, HistoryOutlined, AuditOutlined } from '@ant-design/icons'; +import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom'; +import { useAuth } from './context/AuthContext'; import Dashboard from './pages/Dashboard'; import DeviceManagement from './pages/DeviceManagement'; import RackManagement from './pages/RackManagement'; @@ -12,155 +13,386 @@ import ConsumableManagement from './pages/ConsumableManagement'; import ConsumableStatistics from './pages/ConsumableStatistics'; import ConsumableLogs from './pages/ConsumableLogs'; import CategoryManagement from './pages/CategoryManagement'; +import UserManagement from './pages/UserManagement'; +import LoginHistory from './pages/LoginHistory'; +import OperationLogs from './pages/OperationLogs'; +import Login from './pages/Login'; +import { Spin } from 'antd'; -const { Content, Sider } = Layout; +const { Header, Content, Sider } = Layout; -function App() { +const PrivateRoute = ({ children }) => { + const { token, initialized, loading } = useAuth(); + const location = useLocation(); + + if (!initialized) { + return ( +