add group manager
This commit is contained in:
1 parent
a2df934abd
commit
d6976f8ee4
9 files changed
+1177
-34
No files matched your search
@@ -67,14 +67,58 @@
|
||||
### 数据库设计
|
||||
```sql
|
||||
-- 用户表
|
||||
users (id, email, phone, password_hash, created_at, updated_at)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 分组表
|
||||
CREATE TABLE IF NOT EXISTS `groups` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_user_group (user_id, name)
|
||||
);
|
||||
|
||||
-- 服务器表
|
||||
servers (id, user_id, name, host, port, username, password_encrypted,
|
||||
private_key_encrypted, auth_type, group_name, created_at, updated_at)
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
port INT DEFAULT 22,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
password_encrypted TEXT,
|
||||
private_key_encrypted TEXT,
|
||||
auth_type ENUM('password', 'key') DEFAULT 'password',
|
||||
group_id INT,
|
||||
group_name VARCHAR(255) DEFAULT 'Default',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 连接日志表
|
||||
connection_logs (id, user_id, server_id, host, start_time, end_time, duration)
|
||||
CREATE TABLE IF NOT EXISTS connection_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
server_id INT,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
end_time TIMESTAMP NULL,
|
||||
duration INT DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
|
||||
);
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
@@ -131,6 +175,14 @@ cd webssh
|
||||
```
|
||||
|
||||
### 2. 数据库设置
|
||||
|
||||
#### 方式一:使用安装脚本(推荐)
|
||||
```bash
|
||||
# 使用安装脚本自动创建数据库和表结构
|
||||
mysql -u root -p < install.sql
|
||||
```
|
||||
|
||||
#### 方式二:手动创建数据库
|
||||
```sql
|
||||
-- 创建数据库
|
||||
CREATE DATABASE webssh CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
@@ -141,6 +193,17 @@ GRANT ALL PRIVILEGES ON webssh.* TO 'webssh'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
#### 方式三:系统初始化
|
||||
```sql
|
||||
-- 如果需要重新初始化系统,使用初始化脚本
|
||||
mysql -u root -p < init.sql
|
||||
```
|
||||
|
||||
**重要说明**:
|
||||
- 第一次安装推荐使用 `install.sql` 脚本
|
||||
- 如果系统已运行,使用 `init.sql` 会保留现有数据
|
||||
- 安装脚本会自动处理数据迁移和分组关联
|
||||
|
||||
### 3. 安装依赖
|
||||
```bash
|
||||
# 安装后端依赖
|
||||
@@ -187,6 +250,26 @@ ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456
|
||||
- 生产环境请使用强密码生成器创建安全的密钥
|
||||
- 不要将真实的密钥提交到版本控制系统
|
||||
|
||||
### 4.5 数据库初始化验证
|
||||
|
||||
数据库初始化完成后,后端服务会自动创建必要的表结构。如果遇到表结构错误,可以手动执行:
|
||||
|
||||
```bash
|
||||
# 检查数据库表结构
|
||||
mysql -u root -p -D webssh -e "SHOW TABLES;"
|
||||
|
||||
# 验证表结构
|
||||
mysql -u root -p -D webssh -e "DESCRIBE servers; DESCRIBE \`groups\`;"
|
||||
|
||||
# 如果表结构不完整,执行更新脚本
|
||||
mysql -u root -p < update_tables.sql
|
||||
```
|
||||
|
||||
**常见问题解决**:
|
||||
- 如果遇到 `Unknown column 'group_id'` 错误,运行 `update_tables.sql`
|
||||
- 如果遇到 `groups` 表语法错误,确保表名使用反引号
|
||||
- 如果分组数据不显示,运行 `generate_groups_data.sql`
|
||||
|
||||
### 5. 启动应用
|
||||
|
||||
#### 开发模式(推荐)
|
||||
|
||||
@@ -50,6 +50,20 @@ const initializeTables = async () => {
|
||||
)
|
||||
`);
|
||||
|
||||
// Groups table
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS \`groups\` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_user_group (user_id, name)
|
||||
)
|
||||
`);
|
||||
|
||||
// Servers table (with encrypted credentials)
|
||||
await pool.execute(`
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
@@ -62,10 +76,12 @@ const initializeTables = async () => {
|
||||
password_encrypted TEXT,
|
||||
private_key_encrypted TEXT,
|
||||
auth_type ENUM('password', 'key') DEFAULT 'password',
|
||||
group_id INT,
|
||||
group_name VARCHAR(255) DEFAULT 'Default',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES \`groups\`(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
|
||||
|
||||
+226
-12
@@ -29,15 +29,23 @@ router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
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 group_name, name`,
|
||||
s.id, s.name, s.host, s.port, s.username, s.auth_type,
|
||||
s.group_id, s.group_name, g.name as group_name_actual,
|
||||
s.created_at, s.updated_at
|
||||
FROM servers s
|
||||
LEFT JOIN \`groups\` g ON s.group_id = g.id
|
||||
WHERE s.user_id = ?
|
||||
ORDER BY COALESCE(g.name, s.group_name), s.name`,
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
res.json({ servers });
|
||||
// 统一分组名称显示
|
||||
const processedServers = servers.map(server => ({
|
||||
...server,
|
||||
group_name: server.group_name_actual || server.group_name
|
||||
}));
|
||||
|
||||
res.json({ servers: processedServers });
|
||||
} catch (error) {
|
||||
console.error('获取服务器列表错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
@@ -75,11 +83,34 @@ router.post('/', authenticateToken, async (req, res) => {
|
||||
privateKeyEncrypted = encryptionService.encryptPrivateKey(privateKey, req.user.userId);
|
||||
}
|
||||
|
||||
// 处理分组信息
|
||||
let groupId = null;
|
||||
let finalGroupName = groupName || 'Default';
|
||||
|
||||
// 查找对应的分组ID
|
||||
if (groupName) {
|
||||
const [groups] = await db.pool.execute(
|
||||
'SELECT id FROM \`groups\` WHERE user_id = ? AND name = ?',
|
||||
[req.user.userId, groupName]
|
||||
);
|
||||
|
||||
if (groups.length > 0) {
|
||||
groupId = groups[0].id;
|
||||
} else {
|
||||
// 如果分组不存在,创建新分组
|
||||
const [groupResult] = await db.pool.execute(
|
||||
'INSERT INTO \`groups\` (user_id, name, description) VALUES (?, ?, ?)',
|
||||
[req.user.userId, groupName, `${groupName} 分组`]
|
||||
);
|
||||
groupId = groupResult.insertId;
|
||||
}
|
||||
}
|
||||
|
||||
// 插入服务器记录
|
||||
const [result] = await db.pool.execute(
|
||||
`INSERT INTO servers
|
||||
(user_id, name, host, port, username, password_encrypted, private_key_encrypted, auth_type, group_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(user_id, name, host, port, username, password_encrypted, private_key_encrypted, auth_type, group_id, group_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
req.user.userId,
|
||||
name,
|
||||
@@ -89,7 +120,8 @@ router.post('/', authenticateToken, async (req, res) => {
|
||||
passwordEncrypted,
|
||||
privateKeyEncrypted,
|
||||
authType || 'password',
|
||||
groupName || 'Default'
|
||||
groupId,
|
||||
finalGroupName
|
||||
]
|
||||
);
|
||||
|
||||
@@ -152,6 +184,28 @@ router.put('/:id', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
if (groupName) {
|
||||
// 处理分组信息
|
||||
let groupId = null;
|
||||
|
||||
// 查找对应的分组ID
|
||||
const [groups] = await db.pool.execute(
|
||||
'SELECT id FROM `groups` WHERE user_id = ? AND name = ?',
|
||||
[req.user.userId, groupName]
|
||||
);
|
||||
|
||||
if (groups.length > 0) {
|
||||
groupId = groups[0].id;
|
||||
} else {
|
||||
// 如果分组不存在,创建新分组
|
||||
const [groupResult] = await db.pool.execute(
|
||||
'INSERT INTO `groups` (user_id, name, description) VALUES (?, ?, ?)',
|
||||
[req.user.userId, groupName, `${groupName} 分组`]
|
||||
);
|
||||
groupId = groupResult.insertId;
|
||||
}
|
||||
|
||||
updateFields.push('group_id = ?');
|
||||
updateValues.push(groupId);
|
||||
updateFields.push('group_name = ?');
|
||||
updateValues.push(groupName);
|
||||
}
|
||||
@@ -218,18 +272,178 @@ router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
router.get('/groups', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const [groups] = await db.pool.execute(
|
||||
'SELECT DISTINCT group_name FROM servers WHERE user_id = ? ORDER BY group_name',
|
||||
'SELECT id, name, description, created_at, updated_at FROM `groups` WHERE user_id = ? ORDER BY name',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
const groupNames = groups.map(group => group.group_name);
|
||||
res.json({ groups: groupNames });
|
||||
// 获取每个分组下的服务器数量
|
||||
for (let group of groups) {
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE group_id = ? AND user_id = ?',
|
||||
[group.id, req.user.userId]
|
||||
);
|
||||
group.serverCount = serverCount[0].count;
|
||||
}
|
||||
|
||||
res.json({ groups });
|
||||
} catch (error) {
|
||||
console.error('获取服务器分组错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 添加服务器分组
|
||||
router.post('/groups', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '分组名称是必填项' });
|
||||
}
|
||||
|
||||
// 检查分组名称是否已存在
|
||||
const [existingGroups] = await db.pool.execute(
|
||||
'SELECT id FROM `groups` WHERE user_id = ? AND name = ?',
|
||||
[req.user.userId, name]
|
||||
);
|
||||
|
||||
if (existingGroups.length > 0) {
|
||||
return res.status(400).json({ error: '分组名称已存在' });
|
||||
}
|
||||
|
||||
// 插入新的分组记录
|
||||
const [result] = await db.pool.execute(
|
||||
'INSERT INTO `groups` (user_id, name, description) VALUES (?, ?, ?)',
|
||||
[req.user.userId, name, description || `${name} 分组`]
|
||||
);
|
||||
|
||||
// 获取新创建的分组详情
|
||||
const [newGroup] = await db.pool.execute(
|
||||
'SELECT id, name, description, created_at, updated_at FROM `groups` WHERE id = ?',
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
message: '分组添加成功',
|
||||
group: {
|
||||
...newGroup[0],
|
||||
serverCount: 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('添加分组错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新服务器分组
|
||||
router.put('/groups/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const groupId = req.params.id;
|
||||
const { name, description } = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '分组名称是必填项' });
|
||||
}
|
||||
|
||||
// 检查分组是否存在且属于当前用户
|
||||
const [existingGroup] = await db.pool.execute(
|
||||
'SELECT id FROM `groups` WHERE id = ? AND user_id = ?',
|
||||
[groupId, req.user.userId]
|
||||
);
|
||||
|
||||
if (existingGroup.length === 0) {
|
||||
return res.status(404).json({ error: '分组不存在' });
|
||||
}
|
||||
|
||||
// 检查新名称是否已存在(排除当前分组)
|
||||
const [duplicateGroups] = await db.pool.execute(
|
||||
'SELECT id FROM `groups` WHERE user_id = ? AND name = ? AND id != ?',
|
||||
[req.user.userId, name, groupId]
|
||||
);
|
||||
|
||||
if (duplicateGroups.length > 0) {
|
||||
return res.status(400).json({ error: '分组名称已存在' });
|
||||
}
|
||||
|
||||
// 更新分组信息
|
||||
await db.pool.execute(
|
||||
'UPDATE `groups` SET name = ?, description = ? WHERE id = ? AND user_id = ?',
|
||||
[name, description || `${name} 分组`, groupId, req.user.userId]
|
||||
);
|
||||
|
||||
// 获取更新后的分组详情
|
||||
const [updatedGroup] = await db.pool.execute(
|
||||
'SELECT id, name, description, created_at, updated_at FROM `groups` WHERE id = ?',
|
||||
[groupId]
|
||||
);
|
||||
|
||||
// 获取服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE group_id = ? AND user_id = ?',
|
||||
[groupId, req.user.userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '分组更新成功',
|
||||
group: {
|
||||
...updatedGroup[0],
|
||||
serverCount: serverCount[0].count
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新分组错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除服务器分组
|
||||
router.delete('/groups/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const groupId = req.params.id;
|
||||
|
||||
// 检查分组是否存在且属于当前用户
|
||||
const [existingGroup] = await db.pool.execute(
|
||||
'SELECT id FROM `groups` WHERE id = ? AND user_id = ?',
|
||||
[groupId, req.user.userId]
|
||||
);
|
||||
|
||||
if (existingGroup.length === 0) {
|
||||
return res.status(404).json({ error: '分组不存在' });
|
||||
}
|
||||
|
||||
// 获取该分组下的服务器数量
|
||||
const [serverCount] = await db.pool.execute(
|
||||
'SELECT COUNT(*) as count FROM servers WHERE group_id = ? AND user_id = ?',
|
||||
[groupId, req.user.userId]
|
||||
);
|
||||
|
||||
const count = serverCount[0].count;
|
||||
|
||||
// 如果分组中有服务器,不允许删除
|
||||
if (count > 0) {
|
||||
return res.status(400).json({
|
||||
error: `分组中包含 ${count} 个服务器,请先删除或移出所有服务器后再删除分组`
|
||||
});
|
||||
}
|
||||
|
||||
// 删除分组
|
||||
await db.pool.execute(
|
||||
'DELETE FROM `groups` WHERE id = ? AND user_id = ?',
|
||||
[groupId, req.user.userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '分组删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除分组错误:', error);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取服务器详情(包含解密后的认证信息)
|
||||
router.get('/:id/credentials', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -79,6 +79,10 @@ const props = defineProps({
|
||||
serverId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
groupName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,20 +103,12 @@ const form = reactive({
|
||||
authType: 'password',
|
||||
password: '',
|
||||
privateKey: '',
|
||||
groupName: 'Default'
|
||||
groupName: props.groupName || 'Default'
|
||||
})
|
||||
|
||||
// 获取已存在的分组列表
|
||||
// 获取分组列表
|
||||
const existingGroups = computed(() => {
|
||||
const groups = new Set()
|
||||
serversStore.servers.forEach(server => {
|
||||
// 兼容 groupName 和 group_name 字段
|
||||
const groupName = server.groupName || server.group_name
|
||||
if (groupName) {
|
||||
groups.add(groupName)
|
||||
}
|
||||
})
|
||||
return Array.from(groups)
|
||||
return serversStore.groups.map(group => group.name)
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@@ -198,6 +194,11 @@ const loadServerData = async () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 确保服务器列表已加载
|
||||
if (serversStore.servers.length === 0) {
|
||||
await serversStore.fetchServers()
|
||||
}
|
||||
|
||||
const result = await serversStore.getServerCredentials(props.serverId)
|
||||
if (result.success) {
|
||||
const { server, credentials } = result.data
|
||||
@@ -207,7 +208,7 @@ const loadServerData = async () => {
|
||||
form.port = server.port
|
||||
form.username = server.username
|
||||
form.authType = server.authType
|
||||
form.groupName = server.groupName
|
||||
form.groupName = server.group_name || server.groupName || 'Default'
|
||||
|
||||
if (server.authType === 'password') {
|
||||
form.password = credentials.password || ''
|
||||
@@ -224,10 +225,67 @@ const loadServerData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听分组数据变化,确保分组选择器有正确的数据
|
||||
watch(() => serversStore.groups, (newGroups) => {
|
||||
if (newGroups.length > 0 && !props.serverId) {
|
||||
// 添加模式下,确保分组数据正确设置
|
||||
if (props.groupName) {
|
||||
form.groupName = props.groupName
|
||||
} else {
|
||||
form.groupName = newGroups[0].name
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听serverId变化,确保每次打开编辑对话框都重新加载数据
|
||||
watch(() => props.serverId, (newServerId) => {
|
||||
if (newServerId) {
|
||||
// 重置表单数据,确保重新加载
|
||||
Object.keys(form).forEach(key => {
|
||||
if (key === 'port') {
|
||||
form[key] = 22
|
||||
} else if (key === 'authType') {
|
||||
form[key] = 'password'
|
||||
} else if (key === 'groupName') {
|
||||
// 编辑模式下重置为默认值
|
||||
form[key] = 'Default'
|
||||
} else {
|
||||
form[key] = ''
|
||||
}
|
||||
})
|
||||
newGroupName.value = ''
|
||||
|
||||
// 延迟加载数据,确保DOM已更新
|
||||
setTimeout(() => {
|
||||
loadServerData()
|
||||
}, 0)
|
||||
} else {
|
||||
// 添加模式下,确保分组数据正确设置
|
||||
if (props.groupName) {
|
||||
form.groupName = props.groupName
|
||||
} else if (serversStore.groups.length > 0) {
|
||||
form.groupName = serversStore.groups[0].name
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
if (props.serverId) {
|
||||
loadServerData()
|
||||
onMounted(async () => {
|
||||
// 确保分组数据已加载
|
||||
if (serversStore.groups.length === 0) {
|
||||
await serversStore.fetchGroups()
|
||||
}
|
||||
|
||||
// 添加模式下,确保分组数据正确设置
|
||||
if (!props.serverId) {
|
||||
// 如果传入了分组名称,使用传入的分组
|
||||
if (props.groupName) {
|
||||
form.groupName = props.groupName
|
||||
}
|
||||
// 如果没有分组数据,确保至少有一个默认分组
|
||||
else if (serversStore.groups.length > 0) {
|
||||
form.groupName = serversStore.groups[0].name
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ const routes = [
|
||||
name: 'TerminalNew',
|
||||
component: () => import('@/views/TerminalNew.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/group-management',
|
||||
name: 'GroupManagement',
|
||||
component: () => import('@/views/GroupManagement.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -142,6 +142,69 @@ export const useServersStore = defineStore('servers', () => {
|
||||
return servers.value.filter(server => server.group_name === groupName)
|
||||
}
|
||||
|
||||
// 添加服务器分组
|
||||
const addGroup = async (groupData) => {
|
||||
try {
|
||||
const response = await axios.post('/api/servers/groups', groupData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
|
||||
await fetchGroups()
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || '添加分组失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务器分组
|
||||
const updateGroup = async (groupId, groupData) => {
|
||||
try {
|
||||
await axios.put(`/api/servers/groups/${groupId}`, groupData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
|
||||
await fetchGroups()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || '更新分组失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除服务器分组
|
||||
const deleteGroup = async (groupId, deleteServers = false) => {
|
||||
try {
|
||||
await axios.delete(`/api/servers/groups/${groupId}?deleteServers=${deleteServers}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
|
||||
await fetchGroups()
|
||||
await fetchServers()
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || '删除分组失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有分组名称(用于表单选择)
|
||||
const getGroupNames = () => {
|
||||
return groups.value.map(group => group.name)
|
||||
}
|
||||
|
||||
return {
|
||||
servers,
|
||||
groups,
|
||||
@@ -152,6 +215,10 @@ export const useServersStore = defineStore('servers', () => {
|
||||
deleteServer,
|
||||
getServerCredentials,
|
||||
testConnection,
|
||||
getServersByGroup
|
||||
getServersByGroup,
|
||||
addGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
getGroupNames
|
||||
}
|
||||
})
|
||||
@@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="feature-card">
|
||||
<div class="feature-card" @click="$router.push('/group-management')" style="cursor: pointer;">
|
||||
<el-icon size="48" color="#E6A23C"><Folder /></el-icon>
|
||||
<h3>分组管理</h3>
|
||||
<p>支持服务器分组,方便管理和快速访问</p>
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
<template>
|
||||
<div class="group-management">
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<el-button @click="$router.back()" size="small">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<h1>分组管理</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" @click="showAddGroupDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分组
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main class="main-content">
|
||||
<!-- 分组列表 -->
|
||||
<div class="groups-container">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8" v-for="group in groups" :key="group.id">
|
||||
<el-card class="group-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="group-info">
|
||||
<el-icon class="group-icon"><Folder /></el-icon>
|
||||
<span class="group-name">{{ group.name }}</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="editGroup(group)"
|
||||
title="编辑分组"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="deleteGroup(group)"
|
||||
title="删除分组"
|
||||
style="color: #f56c6c;"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 分组服务器列表 -->
|
||||
<div class="servers-list">
|
||||
<div
|
||||
v-for="server in getServersByGroup(group.name)"
|
||||
:key="server.id"
|
||||
class="server-item"
|
||||
>
|
||||
<div class="server-info">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<div class="server-details">
|
||||
<div class="server-name">{{ server.name }}</div>
|
||||
<div class="server-address">{{ server.host }}:{{ server.port }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="editServer(server)"
|
||||
title="编辑服务器"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="deleteServer(server)"
|
||||
title="删除服务器"
|
||||
style="color: #f56c6c;"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加服务器按钮 -->
|
||||
<div class="add-server-btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
text
|
||||
@click="addServerToGroup(group)"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加服务器
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="getServersByGroup(group.name).length === 0" class="empty-servers">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
<span>该分组暂无服务器</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组统计 -->
|
||||
<div class="group-stats">
|
||||
<span class="server-count">
|
||||
服务器数量: {{ getServersByGroup(group.name).length }}
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 空状态提示 -->
|
||||
<div v-if="groups.length === 0" class="empty-state">
|
||||
<el-icon size="64" color="#909399"><FolderOpened /></el-icon>
|
||||
<h3>暂无分组</h3>
|
||||
<p>点击"添加分组"按钮创建您的第一个服务器分组</p>
|
||||
<el-button type="primary" @click="showAddGroupDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分组
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
<!-- 添加分组对话框 -->
|
||||
<el-dialog v-model="showAddGroupDialog" title="添加分组" width="400px">
|
||||
<el-form :model="newGroup" :rules="groupRules" ref="addGroupForm">
|
||||
<el-form-item label="分组名称" prop="name">
|
||||
<el-input v-model="newGroup.name" placeholder="请输入分组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="newGroup.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分组描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddGroupDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAddGroup" :loading="addingGroup">
|
||||
确认添加
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑分组对话框 -->
|
||||
<el-dialog v-model="showEditGroupDialog" :title="'编辑分组 - ' + editingGroup.name" width="400px">
|
||||
<el-form :model="editingGroup" :rules="groupRules" ref="editGroupForm">
|
||||
<el-form-item label="分组名称" prop="name">
|
||||
<el-input v-model="editingGroup.name" placeholder="请输入分组名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="editingGroup.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分组描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditGroupDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleUpdateGroup" :loading="updatingGroup">
|
||||
保存修改
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑服务器对话框 -->
|
||||
<el-dialog v-model="showEditServerDialog" :title="'编辑服务器 - ' + editingServer.name" width="500px">
|
||||
<ServerForm
|
||||
v-if="editingServer.id"
|
||||
:server-id="editingServer.id"
|
||||
@success="handleServerUpdated"
|
||||
@cancel="showEditServerDialog = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加服务器对话框 -->
|
||||
<el-dialog v-model="showAddServerDialog" :title="'添加服务器到 ' + addingServerToGroup.groupName + ' 分组'" width="500px">
|
||||
<ServerForm
|
||||
:group-name="addingServerToGroup.groupName"
|
||||
@success="handleServerAdded"
|
||||
@cancel="showAddServerDialog = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Plus,
|
||||
Folder,
|
||||
Edit,
|
||||
Delete,
|
||||
Monitor,
|
||||
FolderOpened,
|
||||
InfoFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useServersStore } from '@/stores/servers'
|
||||
import ServerForm from '@/components/ServerForm.vue'
|
||||
|
||||
const serversStore = useServersStore()
|
||||
|
||||
// 响应式数据
|
||||
const showAddGroupDialog = ref(false)
|
||||
const showEditGroupDialog = ref(false)
|
||||
const showEditServerDialog = ref(false)
|
||||
const showAddServerDialog = ref(false)
|
||||
const addingGroup = ref(false)
|
||||
const updatingGroup = ref(false)
|
||||
|
||||
const newGroup = ref({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const editingGroup = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const editingServer = ref({
|
||||
id: null,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const addingServerToGroup = ref({
|
||||
groupName: ''
|
||||
})
|
||||
|
||||
const groupRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入分组名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '分组名称长度在 1 到 50 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const groups = computed(() => serversStore.groups)
|
||||
|
||||
// 方法:根据分组名称获取服务器列表
|
||||
const getServersByGroup = (groupName) => {
|
||||
return serversStore.servers.filter(server => server.group_name === groupName)
|
||||
}
|
||||
|
||||
// 方法:添加分组
|
||||
const handleAddGroup = async () => {
|
||||
try {
|
||||
addingGroup.value = true
|
||||
const result = await serversStore.addGroup(newGroup.value)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('分组添加成功')
|
||||
showAddGroupDialog.value = false
|
||||
newGroup.value = { name: '', description: '' }
|
||||
} else {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('添加分组失败')
|
||||
} finally {
|
||||
addingGroup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 方法:编辑分组
|
||||
const editGroup = (group) => {
|
||||
editingGroup.value = { ...group }
|
||||
showEditGroupDialog.value = true
|
||||
}
|
||||
|
||||
const handleUpdateGroup = async () => {
|
||||
try {
|
||||
updatingGroup.value = true
|
||||
const result = await serversStore.updateGroup(editingGroup.value.id, editingGroup.value)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('分组更新成功')
|
||||
showEditGroupDialog.value = false
|
||||
editingGroup.value = { id: null, name: '', description: '' }
|
||||
} else {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('更新分组失败')
|
||||
} finally {
|
||||
updatingGroup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 方法:删除分组
|
||||
const deleteGroup = async (group) => {
|
||||
try {
|
||||
const serversInGroup = getServersByGroup(group.name)
|
||||
|
||||
// 如果分组中有服务器,不允许删除
|
||||
if (serversInGroup.length > 0) {
|
||||
ElMessage.error(`分组 "${group.name}" 中包含 ${serversInGroup.length} 个服务器,请先删除或移出所有服务器后再删除分组`)
|
||||
return
|
||||
}
|
||||
|
||||
// 确认删除空分组
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除分组 "${group.name}" 吗?此操作不可恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const result = await serversStore.deleteGroup(group.id)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('分组删除成功')
|
||||
} else {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败,请重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法:编辑服务器
|
||||
const editServer = (server) => {
|
||||
// 重置表单数据,确保每次编辑都从后台获取最新数据
|
||||
editingServer.value = {
|
||||
id: server.id,
|
||||
name: server.name // 保留名称用于对话框标题
|
||||
}
|
||||
showEditServerDialog.value = true
|
||||
}
|
||||
|
||||
// 方法:删除服务器
|
||||
const deleteServer = async (server) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除服务器 "${server.name}" 吗?此操作不可恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const result = await serversStore.deleteServer(server.id)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('服务器删除成功')
|
||||
} else {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败,请重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法:添加服务器到分组
|
||||
const addServerToGroup = (group) => {
|
||||
addingServerToGroup.value = { groupName: group.name }
|
||||
showAddServerDialog.value = true
|
||||
}
|
||||
|
||||
// 方法:服务器添加成功处理
|
||||
const handleServerAdded = () => {
|
||||
showAddServerDialog.value = false
|
||||
addingServerToGroup.value = { groupName: '' }
|
||||
ElMessage.success('服务器添加成功')
|
||||
}
|
||||
|
||||
// 方法:服务器更新成功处理
|
||||
const handleServerUpdated = () => {
|
||||
showEditServerDialog.value = false
|
||||
editingServer.value = { id: null, name: '' }
|
||||
ElMessage.success('服务器更新成功')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await serversStore.fetchServers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-management {
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.groups-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.group-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
color: #409eff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.servers-list {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.server-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.server-details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.server-address {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.server-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.server-item:hover .server-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-servers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-stats {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.server-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 添加服务器按钮样式 */
|
||||
.add-server-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
margin-top: 8px;
|
||||
border-top: 1px dashed #e0e0e0;
|
||||
}
|
||||
|
||||
.add-server-btn .el-button {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.add-server-btn .el-button:hover {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.el-col {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.server-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Navicat Premium Data Transfer
|
||||
|
||||
Source Server : webssh
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80012
|
||||
Source Host : localhost:3306
|
||||
Source Schema : webssh
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80012
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 11/02/2026 08:53:32
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for connection_logs
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `connection_logs`;
|
||||
CREATE TABLE `connection_logs` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`server_id` int(11) NULL DEFAULT NULL,
|
||||
`host` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`start_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`end_time` timestamp NULL DEFAULT NULL,
|
||||
`duration` int(11) NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id`) USING BTREE,
|
||||
INDEX `server_id`(`server_id`) USING BTREE
|
||||
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for groups
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `groups`;
|
||||
CREATE TABLE `groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`description` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_user_group`(`user_id`, `name`) USING BTREE
|
||||
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for servers
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `servers`;
|
||||
CREATE TABLE `servers` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`host` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`port` int(11) NULL DEFAULT 22,
|
||||
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`password_encrypted` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
|
||||
`private_key_encrypted` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
|
||||
`auth_type` enum('password','key') CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'password',
|
||||
`group_id` int(11) NULL DEFAULT NULL,
|
||||
`group_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'Default',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id`) USING BTREE
|
||||
) ENGINE = MyISAM AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for users
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`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,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `email`(`email`) USING BTREE
|
||||
) ENGINE = MyISAM AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
Reference in new issue
Block a user