diff --git a/README.md b/README.md index 2e3295e..9bb2407 100644 --- a/README.md +++ b/README.md @@ -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. 启动应用 #### 开发模式(推荐) diff --git a/backend/config/database.js b/backend/config/database.js index 5fa3e66..43e4125 100644 --- a/backend/config/database.js +++ b/backend/config/database.js @@ -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 ) `); diff --git a/backend/routes/servers.js b/backend/routes/servers.js index a24cfee..b7a03d4 100644 --- a/backend/routes/servers.js +++ b/backend/routes/servers.js @@ -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 { diff --git a/frontend/src/components/ServerForm.vue b/frontend/src/components/ServerForm.vue index 86b5dc7..917273f 100644 --- a/frontend/src/components/ServerForm.vue +++ b/frontend/src/components/ServerForm.vue @@ -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 + } } }) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index a88d4c0..398bc48 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -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 } } ] diff --git a/frontend/src/stores/servers.js b/frontend/src/stores/servers.js index 5eb99cb..3b850ac 100644 --- a/frontend/src/stores/servers.js +++ b/frontend/src/stores/servers.js @@ -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 } }) \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 24f430f..0bab8b4 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -112,7 +112,7 @@ -
+

分组管理

支持服务器分组,方便管理和快速访问

diff --git a/frontend/src/views/GroupManagement.vue b/frontend/src/views/GroupManagement.vue new file mode 100644 index 0000000..012474b --- /dev/null +++ b/frontend/src/views/GroupManagement.vue @@ -0,0 +1,610 @@ + + + + + \ No newline at end of file diff --git a/install.sql b/install.sql new file mode 100644 index 0000000..fd02019 --- /dev/null +++ b/install.sql @@ -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;