2025-12-25 10:48:35 +08:00
|
|
|
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');
|
2026-03-06 14:39:52 +08:00
|
|
|
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
|
|
|
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: '用户名和密码不能为空'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 14:39:52 +08:00
|
|
|
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
2026-03-06 14:39:52 +08:00
|
|
|
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 14:39:52 +08:00
|
|
|
if (password.length < PASSWORD_MIN_LENGTH) {
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
2026-03-06 14:39:52 +08:00
|
|
|
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-02-05 15:12:31 +08:00
|
|
|
status: isFirstUser ? 'active' : 'pending'
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (isFirstUser) {
|
|
|
|
|
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
|
2026-02-05 15:12:31 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (!adminRole) {
|
|
|
|
|
adminRole = await Role.create({
|
|
|
|
|
roleId: 'role_admin',
|
|
|
|
|
roleName: '管理员',
|
|
|
|
|
roleCode: 'admin',
|
|
|
|
|
description: '系统管理员,拥有所有权限',
|
|
|
|
|
status: 'active',
|
|
|
|
|
permissions: []
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-05 15:12:31 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
await UserRole.create({
|
|
|
|
|
UserId: user.userId,
|
|
|
|
|
RoleId: adminRole.roleId
|
|
|
|
|
});
|
2026-02-05 15:12:31 +08:00
|
|
|
|
|
|
|
|
const token = generateToken(user);
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: '注册成功,已为您分配管理员权限',
|
|
|
|
|
data: {
|
|
|
|
|
user: {
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
email: user.email,
|
|
|
|
|
realName: user.realName
|
|
|
|
|
},
|
|
|
|
|
token,
|
|
|
|
|
isFirstUser: true
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
} else {
|
|
|
|
|
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
2026-02-05 15:12:31 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (defaultRole) {
|
|
|
|
|
await UserRole.create({
|
|
|
|
|
UserId: user.userId,
|
|
|
|
|
RoleId: defaultRole.roleId
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-05 15:12:31 +08:00
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: '注册成功,请等待管理员审核',
|
|
|
|
|
data: {
|
|
|
|
|
user: {
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
email: user.email,
|
|
|
|
|
realName: user.realName
|
|
|
|
|
},
|
|
|
|
|
isFirstUser: false,
|
|
|
|
|
pendingApproval: true
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
}
|
|
|
|
|
} 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: '账户已禁用'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:12:31 +08:00
|
|
|
if (user.status === 'pending') {
|
|
|
|
|
return res.status(403).json({
|
|
|
|
|
success: false,
|
|
|
|
|
code: 'PENDING_APPROVAL',
|
|
|
|
|
message: '账户待审核,请联系管理员激活'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
|
|
|
|
if (!isPasswordValid) {
|
|
|
|
|
user.loginCount = (user.loginCount || 0) + 1;
|
2026-03-06 14:39:52 +08:00
|
|
|
if (user.loginCount >= MAX_LOGIN_ATTEMPTS) {
|
2025-12-25 10:48:35 +08:00
|
|
|
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: '旧密码和新密码都不能为空'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 14:39:52 +08:00
|
|
|
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
2026-03-06 14:39:52 +08:00
|
|
|
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: '检查失败'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-21 15:21:46 +08:00
|
|
|
router.post('/unlock', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { username, password } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!username || !password) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '用户名和密码不能为空'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const user = await User.findOne({ where: { username } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '用户名或密码错误'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.status !== 'locked') {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '账户未被锁定'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
|
|
|
|
if (!isPasswordValid) {
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '用户名或密码错误'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解锁账户
|
|
|
|
|
user.status = 'active';
|
|
|
|
|
user.loginCount = 0;
|
|
|
|
|
await user.save();
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: '账户解锁成功'
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解锁账户错误:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: '解锁失败',
|
|
|
|
|
error: error.message
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
module.exports = router;
|