add user manager

This commit is contained in:
dalaolala committed 2026-02-12 11:09:43 +08:00
1 parent 0b07fb2d80
commit d159fb66a4
10 files changed
+1265 -195

No files matched your search

+162
View File
@@ -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
View File
@@ -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: '服务器内部错误' });
+116 -182
View File
@@ -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: '访问令牌缺失' });
// 管理员权限中间件
const requireAdmin = (req, res, next) => {
if (!req.user || !req.user.isAdmin) {
return res.status(403).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();
});
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') {
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: '服务器内部错误' });
}
});
+11 -3
View File
@@ -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) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
});
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()
+8 -2
View File
@@ -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')
})
+9
View File
@@ -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()
}
+121
View File
@@ -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
+21 -3
View File
@@ -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(
'确定要注销登录吗?',
+782
View File
@@ -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>
+3
View File
@@ -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,