add user manager
This commit is contained in:
1 parent
0b07fb2d80
commit
d159fb66a4
10 files changed
+1262
-192
No files matched your search
@@ -0,0 +1,162 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 管理员权限中间件
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
return res.status(403).json({ error: '需要管理员权限' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// 获取系统统计信息
|
||||
router.get('/stats', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
// 获取用户统计
|
||||
const [userStats] = await db.pool.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
SUM(is_admin) as admin_count,
|
||||
SUM(is_active) as active_users,
|
||||
COUNT(*) - SUM(is_active) as inactive_users,
|
||||
COUNT(CASE WHEN last_login IS NOT NULL THEN 1 END) as logged_in_users
|
||||
FROM users
|
||||
`);
|
||||
|
||||
// 获取服务器统计
|
||||
const [serverStats] = await db.pool.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_servers,
|
||||
COUNT(DISTINCT user_id) as users_with_servers,
|
||||
AVG(server_count) as avg_servers_per_user
|
||||
FROM (
|
||||
SELECT user_id, COUNT(*) as server_count
|
||||
FROM servers
|
||||
GROUP BY user_id
|
||||
) user_server_counts
|
||||
`);
|
||||
|
||||
// 获取连接统计
|
||||
const [connectionStats] = await db.pool.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_connections,
|
||||
SUM(duration) as total_duration_seconds,
|
||||
AVG(duration) as avg_duration_seconds,
|
||||
MAX(start_time) as last_connection_time
|
||||
FROM connection_logs
|
||||
`);
|
||||
|
||||
// 获取最近活跃用户
|
||||
const [recentUsers] = await db.pool.execute(`
|
||||
SELECT
|
||||
u.id, u.email, u.last_login,
|
||||
COUNT(s.id) as server_count,
|
||||
COUNT(cl.id) as connection_count
|
||||
FROM users u
|
||||
LEFT JOIN servers s ON u.id = s.user_id
|
||||
LEFT JOIN connection_logs cl ON u.id = cl.user_id
|
||||
WHERE u.last_login IS NOT NULL
|
||||
GROUP BY u.id, u.email, u.last_login
|
||||
ORDER BY u.last_login DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
res.json({
|
||||
user_stats: userStats[0],
|
||||
server_stats: serverStats[0],
|
||||
connection_stats: connectionStats[0],
|
||||
recent_users: recentUsers
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取系统统计错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取系统健康状态
|
||||
router.get('/health', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
// 检查数据库连接
|
||||
const [dbCheck] = await db.pool.execute('SELECT 1 as db_ok');
|
||||
|
||||
// 检查关键表是否存在
|
||||
const [tablesCheck] = await db.pool.execute(`
|
||||
SELECT
|
||||
COUNT(*) as users_table_exists,
|
||||
(SELECT COUNT(*) FROM servers) as servers_count,
|
||||
(SELECT COUNT(*) FROM connection_logs) as logs_count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'users'
|
||||
`);
|
||||
|
||||
// 获取系统信息
|
||||
const systemInfo = {
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
memory_usage: process.memoryUsage(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
|
||||
const healthStatus = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: dbCheck.length > 0 ? 'connected' : 'disconnected',
|
||||
tables: tablesCheck[0],
|
||||
system: systemInfo
|
||||
};
|
||||
|
||||
res.json(healthStatus);
|
||||
} catch (error) {
|
||||
console.error('系统健康检查错误:', error);
|
||||
res.status(500).json({
|
||||
status: 'unhealthy',
|
||||
error: '系统健康检查失败',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 清理过期连接日志
|
||||
router.post('/cleanup', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { days = 30 } = req.body;
|
||||
|
||||
// 清理超过指定天数的连接日志
|
||||
const [result] = await db.pool.execute(
|
||||
'DELETE FROM connection_logs WHERE start_time < DATE_SUB(NOW(), INTERVAL ? DAY)',
|
||||
[days]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `已清理 ${result.affectedRows} 条过期连接日志`,
|
||||
cleaned_rows: result.affectedRows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清理连接日志错误:', error);
|
||||
res.status(500).json({ error: '清理连接日志失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取系统设置(预留接口)
|
||||
router.get('/settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
// 这里可以返回系统配置信息
|
||||
res.json({
|
||||
settings: {
|
||||
max_servers_per_user: 100,
|
||||
max_connections_per_user: 10,
|
||||
session_timeout_minutes: 60,
|
||||
allow_registration: true,
|
||||
require_email_verification: false
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取系统设置错误:', error);
|
||||
res.status(500).json({ error: '获取系统设置失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+32
-5
@@ -82,15 +82,30 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 检查用户是否激活
|
||||
if (user.is_active !== 1) {
|
||||
return res.status(403).json({ error: '账户已被禁用,请联系管理员' });
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: '邮箱或密码错误' });
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
// 更新最后登录时间
|
||||
await db.pool.execute(
|
||||
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[user.id]
|
||||
);
|
||||
|
||||
// 生成JWT令牌(包含管理员信息)
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, email: user.email },
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
isAdmin: user.is_admin === 1
|
||||
},
|
||||
process.env.JWT_SECRET || 'webssh-secret-key',
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
@@ -101,7 +116,8 @@ router.post('/login', async (req, res) => {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
phone: user.phone
|
||||
phone: user.phone,
|
||||
is_admin: user.is_admin === 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -132,7 +148,7 @@ const authenticateToken = (req, res, next) => {
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id, email, phone, created_at FROM users WHERE id = ?',
|
||||
'SELECT id, email, phone, is_admin, is_active, last_login, created_at FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
@@ -140,7 +156,18 @@ router.get('/me', authenticateToken, async (req, res) => {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
res.json({ user: users[0] });
|
||||
const user = users[0];
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
is_admin: user.is_admin === 1,
|
||||
is_active: user.is_active === 1,
|
||||
last_login: user.last_login,
|
||||
created_at: user.created_at
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
|
||||
+114
-180
@@ -1,71 +1,33 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../config/database');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 验证令牌中间件
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
// 所有路由都需要认证
|
||||
router.use(authenticateToken);
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: '访问令牌缺失' });
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'webssh-secret-key', (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({ error: '令牌无效' });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否是管理员
|
||||
const requireAdmin = async (req, res, next) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT role FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
if (users[0].role !== 'admin') {
|
||||
// 管理员权限中间件
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
return res.status(403).json({ error: '需要管理员权限' });
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('检查管理员权限错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取所有用户列表(仅管理员)
|
||||
router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
// 获取所有用户列表(管理员)
|
||||
router.get('/', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(`
|
||||
SELECT
|
||||
id, email, phone, role, status,
|
||||
created_at, updated_at
|
||||
id, email, phone, is_admin, is_active,
|
||||
last_login, created_at, updated_at,
|
||||
(SELECT COUNT(*) FROM servers WHERE user_id = users.id) as server_count
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
// 获取每个用户的服务器数量
|
||||
for (let user of users) {
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[user.id]
|
||||
);
|
||||
user.serverCount = serverCount[0].count;
|
||||
}
|
||||
|
||||
res.json({ users });
|
||||
} catch (error) {
|
||||
console.error('获取用户列表错误:', error);
|
||||
@@ -73,10 +35,61 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新用户(仅管理员)
|
||||
router.post('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
// 获取单个用户详细信息(管理员)
|
||||
router.get('/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { email, phone, password, role = 'user' } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
// 获取用户基本信息
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id, email, phone, is_admin, is_active, last_login, created_at, updated_at FROM users WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 获取用户的服务器列表
|
||||
const [servers] = await db.pool.execute(`
|
||||
SELECT
|
||||
id, name, host, port, username, auth_type,
|
||||
group_name, created_at, updated_at
|
||||
FROM servers
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [id]);
|
||||
|
||||
// 获取用户的连接日志统计
|
||||
const [connectionStats] = await db.pool.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_connections,
|
||||
SUM(duration) as total_duration,
|
||||
MAX(start_time) as last_connection
|
||||
FROM connection_logs
|
||||
WHERE user_id = ?
|
||||
`, [id]);
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
...user,
|
||||
server_count: servers.length,
|
||||
connection_stats: connectionStats[0]
|
||||
},
|
||||
servers
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户详情错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新用户(管理员)
|
||||
router.post('/', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { email, phone, password, is_admin = false } = req.body;
|
||||
|
||||
// 验证输入
|
||||
if (!email || !password) {
|
||||
@@ -87,10 +100,6 @@ router.post('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: '密码长度至少6位' });
|
||||
}
|
||||
|
||||
if (!['admin', 'user'].includes(role)) {
|
||||
return res.status(400).json({ error: '角色参数无效' });
|
||||
}
|
||||
|
||||
// 检查用户是否已存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE email = ?',
|
||||
@@ -107,21 +116,17 @@ router.post('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
|
||||
// 创建用户
|
||||
const [result] = await db.pool.execute(
|
||||
'INSERT INTO users (email, phone, password_hash, role, status) VALUES (?, ?, ?, ?, ?)',
|
||||
[email, phone || null, passwordHash, role, 'active']
|
||||
);
|
||||
|
||||
// 获取新创建的用户详情
|
||||
const [newUser] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[result.insertId]
|
||||
'INSERT INTO users (email, phone, password_hash, is_admin) VALUES (?, ?, ?, ?)',
|
||||
[email, phone || null, passwordHash, is_admin ? 1 : 0]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
message: '用户创建成功',
|
||||
user: {
|
||||
...newUser[0],
|
||||
serverCount: 0
|
||||
id: result.insertId,
|
||||
email,
|
||||
phone,
|
||||
is_admin: is_admin ? 1 : 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -130,108 +135,69 @@ router.post('/', authenticateToken, requireAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 更新用户信息(仅管理员)
|
||||
router.put('/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
// 更新用户信息(管理员)
|
||||
router.put('/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { email, phone, role, status } = req.body;
|
||||
const { id } = req.params;
|
||||
const { email, phone, is_admin, is_active } = req.body;
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[userId]
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 验证角色参数
|
||||
if (role && !['admin', 'user'].includes(role)) {
|
||||
return res.status(400).json({ error: '角色参数无效' });
|
||||
}
|
||||
|
||||
// 验证状态参数
|
||||
if (status && !['active', 'disabled'].includes(status)) {
|
||||
return res.status(400).json({ error: '状态参数无效' });
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
if (email) {
|
||||
const [duplicateUsers] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE email = ? AND id != ?',
|
||||
[email, userId]
|
||||
);
|
||||
|
||||
if (duplicateUsers.length > 0) {
|
||||
return res.status(400).json({ error: '该邮箱已被其他用户使用' });
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
|
||||
if (email) {
|
||||
if (email !== undefined) {
|
||||
updateFields.push('email = ?');
|
||||
updateValues.push(email);
|
||||
}
|
||||
|
||||
if (phone !== undefined) {
|
||||
updateFields.push('phone = ?');
|
||||
updateValues.push(phone || null);
|
||||
updateValues.push(phone);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
updateFields.push('role = ?');
|
||||
updateValues.push(role);
|
||||
if (is_admin !== undefined) {
|
||||
updateFields.push('is_admin = ?');
|
||||
updateValues.push(is_admin ? 1 : 0);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
updateFields.push('status = ?');
|
||||
updateValues.push(status);
|
||||
if (is_active !== undefined) {
|
||||
updateFields.push('is_active = ?');
|
||||
updateValues.push(is_active ? 1 : 0);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: '没有提供更新信息' });
|
||||
return res.status(400).json({ error: '没有需要更新的字段' });
|
||||
}
|
||||
|
||||
updateValues.push(userId);
|
||||
updateValues.push(id);
|
||||
|
||||
// 更新用户信息
|
||||
await db.pool.execute(
|
||||
`UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
|
||||
updateValues
|
||||
);
|
||||
|
||||
// 获取更新后的用户详情
|
||||
const [updatedUser] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
// 获取服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '用户信息更新成功',
|
||||
user: {
|
||||
...updatedUser[0],
|
||||
serverCount: serverCount[0].count
|
||||
}
|
||||
});
|
||||
res.json({ message: '用户信息更新成功' });
|
||||
} catch (error) {
|
||||
console.error('更新用户信息错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 重置用户密码(仅管理员)
|
||||
router.put('/:id/password', authenticateToken, requireAdmin, async (req, res) => {
|
||||
// 重置用户密码(管理员)
|
||||
router.put('/:id/password', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { id } = req.params;
|
||||
const { newPassword } = req.body;
|
||||
|
||||
if (!newPassword) {
|
||||
@@ -239,16 +205,16 @@ router.put('/:id/password', authenticateToken, requireAdmin, async (req, res) =>
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({ error: '新密码长度至少6位' });
|
||||
return res.status(400).json({ error: '密码长度至少6位' });
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[userId]
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
@@ -259,74 +225,42 @@ router.put('/:id/password', authenticateToken, requireAdmin, async (req, res) =>
|
||||
// 更新密码
|
||||
await db.pool.execute(
|
||||
'UPDATE users SET password_hash = ? WHERE id = ?',
|
||||
[newPasswordHash, userId]
|
||||
[newPasswordHash, id]
|
||||
);
|
||||
|
||||
res.json({ message: '密码重置成功' });
|
||||
} catch (error) {
|
||||
console.error('重置用户密码错误:', error);
|
||||
console.error('重置密码错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除用户(仅管理员)
|
||||
router.delete('/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||
// 删除用户(管理员)
|
||||
router.delete('/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查用户是否存在
|
||||
const [existingUsers] = await db.pool.execute(
|
||||
'SELECT id, role FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 不能删除自己
|
||||
if (parseInt(userId) === parseInt(req.user.userId)) {
|
||||
return res.status(400).json({ error: '不能删除自己的账户' });
|
||||
}
|
||||
|
||||
// 获取用户的服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
const count = serverCount[0].count;
|
||||
|
||||
// 删除用户(级联删除相关数据)
|
||||
await db.pool.execute(
|
||||
'DELETE FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `用户删除成功,同时删除了 ${count} 个相关服务器`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除用户错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取当前用户信息(包含角色和状态)
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const [users] = await db.pool.execute(
|
||||
'SELECT id, email, phone, role, status, created_at FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
'SELECT id FROM users WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
res.json({ user: users[0] });
|
||||
// 检查是否为管理员自身(不允许删除自己)
|
||||
if (parseInt(id) === parseInt(req.user.userId)) {
|
||||
return res.status(400).json({ error: '不能删除自己的账户' });
|
||||
}
|
||||
|
||||
// 删除用户(由于外键约束,关联的服务器和分组也会被删除)
|
||||
await db.pool.execute('DELETE FROM users WHERE id = ?', [id]);
|
||||
|
||||
res.json({ message: '用户删除成功' });
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
console.error('删除用户错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
+10
-2
@@ -9,6 +9,8 @@ const path = require('path');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const serverRoutes = require('./routes/servers');
|
||||
const sftpRoutes = require('./routes/sftp');
|
||||
const userRoutes = require('./routes/users');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
|
||||
// Database
|
||||
const db = require('./config/database');
|
||||
@@ -37,14 +39,20 @@ app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/servers', serverRoutes);
|
||||
app.use('/api/sftp', sftpRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
// Socket.io connection handling
|
||||
socketHandler(io);
|
||||
|
||||
// Serve frontend for production
|
||||
app.get('*', (req, res) => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Database connection test
|
||||
db.authenticate()
|
||||
|
||||
@@ -38,8 +38,14 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
// 初始化认证状态
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
const authStore = useAuthStore()
|
||||
authStore.initializeAuth().then(() => {
|
||||
app.mount('#app')
|
||||
})
|
||||
@@ -43,6 +43,12 @@ const routes = [
|
||||
name: 'GroupManagement',
|
||||
component: () => import('@/views/GroupManagement.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/user-management',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/UserManagement.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -60,6 +66,9 @@ router.beforeEach((to, from, next) => {
|
||||
next('/login')
|
||||
} else if (to.meta.requiresGuest && authStore.isAuthenticated) {
|
||||
next('/')
|
||||
} else if (to.meta.requiresAdmin && (!authStore.isAuthenticated || !authStore.user?.is_admin)) {
|
||||
// 检查管理员权限
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '../stores/auth.js'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器 - 添加认证token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器 - 统一错误处理
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
authStore.logout()
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('权限不足,无法访问此资源')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器内部错误,请稍后重试')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(data.error || '请求失败,请稍后重试')
|
||||
}
|
||||
} else if (error.request) {
|
||||
ElMessage.error('网络连接失败,请检查网络设置')
|
||||
} else {
|
||||
ElMessage.error('请求配置错误')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 用户管理API
|
||||
export const userAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: (params = {}) => api.get('/users', { params }),
|
||||
|
||||
// 获取用户详情
|
||||
getUser: (id) => api.get(`/users/${id}`),
|
||||
|
||||
// 创建用户
|
||||
createUser: (userData) => api.post('/users', userData),
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id, userData) => api.put(`/users/${id}`, userData),
|
||||
|
||||
// 重置用户密码
|
||||
resetPassword: (id, password) => api.put(`/users/${id}/password`, { password }),
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id) => api.delete(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 系统管理API
|
||||
export const adminAPI = {
|
||||
// 获取系统统计
|
||||
getStats: () => api.get('/admin/stats'),
|
||||
|
||||
// 系统健康检查
|
||||
getHealth: () => api.get('/admin/health'),
|
||||
|
||||
// 清理连接日志
|
||||
cleanupLogs: (days) => api.post('/admin/cleanup', { days }),
|
||||
|
||||
// 获取系统设置
|
||||
getSettings: () => api.get('/admin/settings'),
|
||||
|
||||
// 更新系统设置
|
||||
updateSettings: (settings) => api.put('/admin/settings', settings)
|
||||
}
|
||||
|
||||
// 服务器管理API
|
||||
export const serverAPI = {
|
||||
// 获取服务器列表
|
||||
getServers: () => api.get('/servers'),
|
||||
|
||||
// 创建服务器
|
||||
createServer: (serverData) => api.post('/servers', serverData),
|
||||
|
||||
// 更新服务器
|
||||
updateServer: (id, serverData) => api.put(`/servers/${id}`, serverData),
|
||||
|
||||
// 删除服务器
|
||||
deleteServer: (id) => api.delete(`/servers/${id}`)
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -7,6 +7,17 @@
|
||||
<h1 class="logo">WebSSH</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- 管理员功能入口 -->
|
||||
<el-button
|
||||
v-if="authStore.user?.is_admin"
|
||||
type="warning"
|
||||
@click="$router.push('/user-management')"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
用户管理
|
||||
</el-button>
|
||||
|
||||
<el-dropdown @command="handleUserCommand">
|
||||
<span class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
@@ -15,7 +26,12 @@
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">
|
||||
<!-- 管理员菜单项 -->
|
||||
<el-dropdown-item v-if="authStore.user?.is_admin" command="userManagement">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
用户管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
注销登录
|
||||
</el-dropdown-item>
|
||||
@@ -152,7 +168,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Folder, Monitor, Connection, Lock, More, Edit, Delete, User, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { Plus, Folder, Monitor, Connection, Lock, More, Edit, Delete, User, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
||||
import { useServersStore } from '@/stores/servers'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ServerForm from '@/components/ServerForm.vue'
|
||||
@@ -259,7 +275,9 @@ const handleDeleteServer = async (serverId, serverName) => {
|
||||
|
||||
// 处理用户操作
|
||||
const handleUserCommand = async (command) => {
|
||||
if (command === 'logout') {
|
||||
if (command === 'userManagement') {
|
||||
router.push('/user-management')
|
||||
} else if (command === 'logout') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要注销登录吗?',
|
||||
|
||||
@@ -0,0 +1,782 @@
|
||||
<template>
|
||||
<div class="user-management-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<h1>用户管理</h1>
|
||||
<div class="header-actions">
|
||||
<el-button @click="goToDashboard">
|
||||
<el-icon><House /></el-icon>
|
||||
返回主页
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showCreateUserDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refreshUsers">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索邮箱或手机号"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select v-model="filterStatus" placeholder="状态筛选" clearable @change="handleFilter">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="已激活" value="active"></el-option>
|
||||
<el-option label="已禁用" value="inactive"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-select v-model="filterRole" placeholder="角色筛选" clearable @change="handleFilter">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="管理员" value="admin"></el-option>
|
||||
<el-option label="普通用户" value="user"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="user-table-container">
|
||||
<el-table
|
||||
:data="filteredUsers"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" />
|
||||
<el-table-column prop="server_count" label="服务器数量" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-primary': row.server_count > 0 }">{{ row.server_count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_login" label="最后登录" width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.last_login">{{ formatDate(row.last_login) }}</span>
|
||||
<span v-else class="text-muted">从未登录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.is_active ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.is_active ? '已激活' : '已禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.is_admin ? 'warning' : ''"
|
||||
size="small"
|
||||
>
|
||||
{{ row.is_admin ? '管理员' : '普通用户' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleViewUser(row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEditUser(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.id !== authStore.user?.id"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDeleteUser(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="totalUsers"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 添加用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="showCreateUserDialog"
|
||||
title="添加用户"
|
||||
width="500px"
|
||||
:before-close="handleCloseCreateDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="createForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="createForm.phone" placeholder="请输入手机号(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="createForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="createForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="is_admin">
|
||||
<el-radio-group v-model="createForm.is_admin">
|
||||
<el-radio :label="false">普通用户</el-radio>
|
||||
<el-radio :label="true">管理员</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="is_active">
|
||||
<el-radio-group v-model="createForm.is_active">
|
||||
<el-radio :label="true">激活</el-radio>
|
||||
<el-radio :label="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleCloseCreateDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleCreateUser" :loading="creating">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="showEditUserDialog"
|
||||
:title="'编辑用户 - ' + (editingUser?.email || '')"
|
||||
width="500px"
|
||||
:before-close="handleCloseEditDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="editForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="editForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="is_admin">
|
||||
<el-radio-group v-model="editForm.is_admin">
|
||||
<el-radio :label="false">普通用户</el-radio>
|
||||
<el-radio :label="true">管理员</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="is_active">
|
||||
<el-radio-group v-model="editForm.is_active">
|
||||
<el-radio :label="true">激活</el-radio>
|
||||
<el-radio :label="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleCloseEditDialog">取消</el-button>
|
||||
<el-button type="success" @click="handleResetPassword" :loading="resetting">
|
||||
重置密码
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleUpdateUser" :loading="updating">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showUserDetailDialog"
|
||||
:title="'用户详情 - ' + (selectedUser?.email || '')"
|
||||
width="600px"
|
||||
>
|
||||
<div v-if="selectedUser" class="user-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="用户ID">{{ selectedUser.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ selectedUser.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ selectedUser.phone || '未设置' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">
|
||||
<el-tag :type="selectedUser.is_admin ? 'warning' : ''">
|
||||
{{ selectedUser.is_admin ? '管理员' : '普通用户' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedUser.is_active ? 'success' : 'danger'">
|
||||
{{ selectedUser.is_active ? '已激活' : '已禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务器数量">{{ selectedUser.server_count || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录">
|
||||
{{ selectedUser.last_login ? formatDate(selectedUser.last_login) : '从未登录' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(selectedUser.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(selectedUser.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 服务器列表 -->
|
||||
<div v-if="userServers.length > 0" class="server-list">
|
||||
<h3>服务器列表</h3>
|
||||
<el-table :data="userServers" size="small">
|
||||
<el-table-column prop="name" label="服务器名称" />
|
||||
<el-table-column prop="host" label="主机地址" />
|
||||
<el-table-column prop="port" label="端口" width="80" />
|
||||
<el-table-column prop="auth_type" label="认证方式" width="100" />
|
||||
<el-table-column prop="group_name" label="分组" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 连接统计 -->
|
||||
<div v-if="connectionStats" class="connection-stats">
|
||||
<h3>连接统计</h3>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="总连接数">{{ connectionStats.total_connections || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总时长">{{ connectionStats.total_duration || 0 }}秒</el-descriptions-item>
|
||||
<el-descriptions-item label="最后连接">
|
||||
{{ connectionStats.last_connection ? formatDate(connectionStats.last_connection) : '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重置密码对话框 -->
|
||||
<el-dialog
|
||||
v-model="showResetPasswordDialog"
|
||||
title="重置密码"
|
||||
width="400px"
|
||||
>
|
||||
<el-form
|
||||
ref="resetFormRef"
|
||||
:model="resetForm"
|
||||
:rules="resetRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input
|
||||
v-model="resetForm.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="resetForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showResetPasswordDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmResetPassword" :loading="resetting">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, House } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const updating = ref(false)
|
||||
const resetting = ref(false)
|
||||
const users = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalUsers = ref(0)
|
||||
|
||||
// 筛选条件
|
||||
const searchKeyword = ref('')
|
||||
const filterStatus = ref('')
|
||||
const filterRole = ref('')
|
||||
|
||||
// 对话框状态
|
||||
const showCreateUserDialog = ref(false)
|
||||
const showEditUserDialog = ref(false)
|
||||
const showUserDetailDialog = ref(false)
|
||||
const showResetPasswordDialog = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const createForm = reactive({
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
is_admin: false,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
email: '',
|
||||
phone: '',
|
||||
is_admin: false,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const resetForm = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 当前操作的用户
|
||||
const selectedUser = ref(null)
|
||||
const editingUser = ref(null)
|
||||
const userServers = ref([])
|
||||
const connectionStats = ref(null)
|
||||
|
||||
// 表单引用
|
||||
const createFormRef = ref()
|
||||
const editFormRef = ref()
|
||||
const resetFormRef = ref()
|
||||
|
||||
// 表单验证规则
|
||||
const createRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== createForm.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const editRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetRules = {
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== resetForm.newPassword) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性:筛选后的用户列表
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = users.value
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filtered = filtered.filter(user =>
|
||||
user.email.toLowerCase().includes(keyword) ||
|
||||
(user.phone && user.phone.includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (filterStatus.value) {
|
||||
filtered = filtered.filter(user =>
|
||||
filterStatus.value === 'active' ? user.is_active : !user.is_active
|
||||
)
|
||||
}
|
||||
|
||||
// 角色筛选
|
||||
if (filterRole.value) {
|
||||
filtered = filtered.filter(user =>
|
||||
filterRole.value === 'admin' ? user.is_admin : !user.is_admin
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 日期格式化
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get('/users')
|
||||
console.log('获取用户列表响应:', response)
|
||||
if (response && response.users) {
|
||||
users.value = response.users
|
||||
totalUsers.value = users.value.length
|
||||
console.log('用户数据已更新:', users.value)
|
||||
} else {
|
||||
console.warn('获取用户列表响应格式不正确:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户详情
|
||||
const fetchUserDetail = async (userId) => {
|
||||
try {
|
||||
const response = await api.get(`/users/${userId}`)
|
||||
if (response.data) {
|
||||
selectedUser.value = response.data.user
|
||||
userServers.value = response.data.servers || []
|
||||
connectionStats.value = response.data.connection_stats || {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户详情失败:', error)
|
||||
ElMessage.error('获取用户详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
const handleCreateUser = async () => {
|
||||
try {
|
||||
await createFormRef.value.validate()
|
||||
creating.value = true
|
||||
|
||||
const response = await api.post('/users', {
|
||||
email: createForm.email,
|
||||
phone: createForm.phone || null,
|
||||
password: createForm.password,
|
||||
is_admin: createForm.is_admin
|
||||
})
|
||||
|
||||
ElMessage.success(response.message || '用户创建成功')
|
||||
handleCloseCreateDialog()
|
||||
fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
if (error.response?.data?.error) {
|
||||
ElMessage.error(error.response.data.error)
|
||||
} else {
|
||||
ElMessage.error('创建用户失败')
|
||||
}
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
const handleUpdateUser = async () => {
|
||||
try {
|
||||
await editFormRef.value.validate()
|
||||
updating.value = true
|
||||
|
||||
const response = await api.put(`/users/${editingUser.value.id}`, {
|
||||
email: editForm.email,
|
||||
phone: editForm.phone || null,
|
||||
is_admin: editForm.is_admin,
|
||||
is_active: editForm.is_active
|
||||
})
|
||||
|
||||
ElMessage.success(response.message || '用户信息更新成功')
|
||||
handleCloseEditDialog()
|
||||
fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
if (error.response?.data?.error) {
|
||||
ElMessage.error(error.response.data.error)
|
||||
} else {
|
||||
ElMessage.error('更新用户失败')
|
||||
}
|
||||
} finally {
|
||||
updating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = () => {
|
||||
showResetPasswordDialog.value = true
|
||||
}
|
||||
|
||||
const handleConfirmResetPassword = async () => {
|
||||
try {
|
||||
await resetFormRef.value.validate()
|
||||
resetting.value = true
|
||||
|
||||
const response = await api.put(`/users/${editingUser.value.id}/password`, {
|
||||
newPassword: resetForm.newPassword
|
||||
})
|
||||
|
||||
ElMessage.success(response.message || '密码重置成功')
|
||||
showResetPasswordDialog.value = false
|
||||
resetForm.newPassword = ''
|
||||
resetForm.confirmPassword = ''
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
if (error.response?.data?.error) {
|
||||
ElMessage.error(error.response.data.error)
|
||||
} else {
|
||||
ElMessage.error('重置密码失败')
|
||||
}
|
||||
} finally {
|
||||
resetting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = async (user) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${user.email}" 吗?此操作将删除该用户的所有数据且不可恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const response = await api.delete(`/users/${user.id}`)
|
||||
if (response.data) {
|
||||
ElMessage.success('用户删除成功')
|
||||
fetchUsers()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除用户失败:', error)
|
||||
if (error.response?.data?.error) {
|
||||
ElMessage.error(error.response.data.error)
|
||||
} else {
|
||||
ElMessage.error('删除用户失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看用户详情
|
||||
const handleViewUser = async (user) => {
|
||||
selectedUser.value = user
|
||||
await fetchUserDetail(user.id)
|
||||
showUserDetailDialog.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEditUser = (user) => {
|
||||
editingUser.value = user
|
||||
Object.assign(editForm, {
|
||||
email: user.email,
|
||||
phone: user.phone || '',
|
||||
is_admin: user.is_admin === 1,
|
||||
is_active: user.is_active === 1
|
||||
})
|
||||
showEditUserDialog.value = true
|
||||
}
|
||||
|
||||
// 对话框关闭处理
|
||||
const handleCloseCreateDialog = () => {
|
||||
showCreateUserDialog.value = false
|
||||
createFormRef.value?.resetFields()
|
||||
Object.assign(createForm, {
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
is_admin: false,
|
||||
is_active: true
|
||||
})
|
||||
}
|
||||
|
||||
const handleCloseEditDialog = () => {
|
||||
showEditUserDialog.value = false
|
||||
editingUser.value = null
|
||||
}
|
||||
|
||||
// 搜索和筛选处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleFilter = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 刷新用户列表
|
||||
const refreshUsers = () => {
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
// 返回主页
|
||||
const goToDashboard = () => {
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-management-container {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.server-list,
|
||||
.connection-stats {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.server-list h3,
|
||||
.connection-stats h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -80,6 +80,9 @@ CREATE TABLE `users` (
|
||||
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
|
||||
`password_hash` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`is_admin` tinyint(1) NULL DEFAULT 0 COMMENT '是否为管理员',
|
||||
`is_active` tinyint(1) NULL DEFAULT 1 COMMENT '是否激活',
|
||||
`last_login` timestamp NULL DEFAULT NULL COMMENT '最后登录时间',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
|
||||
Reference in new issue
Block a user