commit ce1b546a6c3d8398c8a93af5f86547d9bc99f858 Author: dalaolala Date: Tue Feb 10 13:29:45 2026 +0800 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e3295e --- /dev/null +++ b/README.md @@ -0,0 +1,338 @@ +# WebSSH - 在线SSH终端工具 + +一个基于Web的SSH终端工具,模仿Xshell的功能,提供完整的Web版SSH客户端体验。 + +## ✨ 功能特性 + +### ✅ 已实现功能 + +#### 用户认证系统 (P0) +- 用户注册(邮箱+密码) +- 用户登录(JWT令牌认证) +- 密码加密存储(bcrypt) +- 路由守卫和权限控制 + +#### 服务器资产管理 (P0) +- 服务器增删改查 +- 支持密码和私钥认证 +- AES-256加密存储敏感信息 +- 服务器分组管理 +- 树形结构展示 + +#### SSH终端核心功能 (P0) +- 基于xterm.js的Web终端 +- WebSocket实时通信 +- SSH2库后端连接 +- 终端窗口自适应 +- 命令输入和输出 +- 快捷键支持(Ctrl+C等) + +#### 前端界面 (P0) +- Vue3 + Element Plus现代化界面 +- 响应式设计 +- 服务器管理面板 +- 终端界面 +- 快速连接功能 + +### 🔄 待实现功能 + +- [ ] SFTP文件传输功能 (P1) +- [ ] 多标签页支持 (P1) +- [ ] 分屏显示 (P1) +- [ ] 终端外观定制 (P2) +- [ ] 快捷命令功能 (P2) +- [ ] 连接保持和自动重连 (P1) +- [ ] 操作日志记录 (P2) + +## 🏗️ 技术架构 + +### 前端技术栈 +- **框架**: Vue 3 + Vite +- **UI组件库**: Element Plus +- **终端组件**: xterm.js + xterm-addon-fit +- **状态管理**: Pinia +- **路由**: Vue Router +- **HTTP客户端**: Axios +- **WebSocket**: socket.io-client + +### 后端技术栈 +- **运行时**: Node.js +- **Web框架**: Express.js +- **SSH连接**: ssh2库 +- **实时通信**: Socket.IO +- **数据库**: MySQL +- **认证**: JWT + bcryptjs +- **加密**: AES-256-GCM + +### 数据库设计 +```sql +-- 用户表 +users (id, email, phone, password_hash, created_at, updated_at) + +-- 服务器表 +servers (id, user_id, name, host, port, username, password_encrypted, + private_key_encrypted, auth_type, group_name, created_at, updated_at) + +-- 连接日志表 +connection_logs (id, user_id, server_id, host, start_time, end_time, duration) +``` + +## 📁 项目结构 + +``` +webssh/ +├── backend/ # Node.js后端服务 +│ ├── config/ +│ │ └── database.js # 数据库配置和初始化 +│ ├── routes/ # API路由 +│ │ ├── auth.js # 认证相关路由 +│ │ └── servers.js # 服务器管理路由 +│ ├── socket/ # WebSocket处理 +│ │ └── socketHandler.js # SSH连接处理 +│ ├── utils/ +│ │ └── encryption.js # 加密工具类 +│ ├── .env # 环境变量配置 +│ ├── package.json # 后端依赖配置 +│ └── server.js # 主服务器入口 +├── frontend/ # Vue3前端应用 +│ ├── src/ +│ │ ├── components/ # 可复用组件 +│ │ │ └── ServerForm.vue # 服务器表单组件 +│ │ ├── router/ # 路由配置 +│ │ │ └── index.js # 路由定义和守卫 +│ │ ├── stores/ # 状态管理 +│ │ │ ├── auth.js # 用户认证状态 +│ │ │ ├── servers.js # 服务器管理状态 +│ │ │ └── terminal.js # 终端连接状态 +│ │ ├── views/ # 页面组件 +│ │ │ ├── Dashboard.vue # 主仪表板 +│ │ │ ├── Login.vue # 登录页面 +│ │ │ ├── Register.vue # 注册页面 +│ │ │ └── Terminal.vue # 终端页面 +│ │ ├── App.vue # 根组件 +│ │ └── main.js # 应用入口 +│ ├── index.html # HTML模板 +│ ├── package.json # 前端依赖配置 +│ └── vite.config.js # Vite构建配置 +├── package.json # 根项目配置 +└── README.md # 项目说明文档 +``` + +## 🚀 快速开始 + +### 环境要求 +- Node.js 16+ +- MySQL 5.7+ + +### 1. 克隆项目 +```bash +git clone <项目地址> +cd webssh +``` + +### 2. 数据库设置 +```sql +-- 创建数据库 +CREATE DATABASE webssh CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 创建用户(可选) +CREATE USER 'webssh'@'localhost' IDENTIFIED BY 'webssh'; +GRANT ALL PRIVILEGES ON webssh.* TO 'webssh'@'localhost'; +FLUSH PRIVILEGES; +``` + +### 3. 安装依赖 +```bash +# 安装后端依赖 +cd backend +npm install + +# 安装前端依赖 +cd ../frontend +npm install + +# 返回根目录安装开发依赖 +cd .. +npm install +``` + +### 4. 环境配置 +```bash +# 复制环境配置文件 +cp backend/.env.example backend/.env +``` + +编辑 `backend/.env` 文件: +```env +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=webssh +DB_PASSWORD=webssh +DB_NAME=webssh + +# JWT Secret (生成随机密钥) +JWT_SECRET=your_jwt_secret_key_here + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Encryption Key (必须32字节长) +ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456 +``` + +**重要提示**: +- `ENCRYPTION_KEY` 必须是正好32字节的字符串 +- 生产环境请使用强密码生成器创建安全的密钥 +- 不要将真实的密钥提交到版本控制系统 + +### 5. 启动应用 + +#### 开发模式(推荐) +```bash +# 同时启动前后端(需要concurrently) +npm run dev +``` + +#### 分别启动 +```bash +# 启动后端服务(端口3000) +npm run dev:backend + +# 启动前端服务(端口5173) +npm run dev:frontend +``` + +#### 生产构建 +```bash +# 构建前端 +npm run build + +# 启动生产服务器 +npm start +``` + +### 6. 访问应用 +- 前端地址:http://localhost:5173 +- 后端API:http://localhost:3000 + +## 🔧 使用指南 + +### 首次使用 +1. 访问 http://localhost:5173/register 注册账号 +2. 登录后进入仪表板 +3. 添加你的第一个SSH服务器 +4. 点击服务器名称开始连接 + +### 服务器配置 +- **名称**: 服务器显示名称 +- **主机**: IP地址或域名 +- **端口**: SSH端口(默认22) +- **用户名**: SSH登录用户名 +- **认证方式**: 密码认证或私钥认证 +- **分组**: 服务器分组(可选) + +### 终端操作 +- **连接**: 点击"连接"按钮建立SSH会话 +- **命令输入**: 在底部输入框输入命令 +- **快捷键**: + - `Ctrl+C`: 中断当前命令 + - `Ctrl+D`: 退出会话 + - `Ctrl+L`: 清屏 +- **断开**: 点击"断开"按钮结束连接 + +### 快速连接 +- 不保存服务器信息,直接输入连接参数 +- 适合临时连接或测试使用 + +## 🔒 安全特性 + +### 数据加密 +- **用户密码**: bcrypt加密存储 +- **服务器认证信息**: AES-256-GCM加密 +- **传输加密**: WebSocket over HTTPS(生产环境) + +### 认证授权 +- JWT令牌认证 +- 路由权限控制 +- 用户数据隔离 + +### 输入验证 +- 前后端双重验证 +- SQL注入防护 +- XSS攻击防护 + +## 🐛 故障排除 + +### 常见问题 + +#### 1. 数据库连接失败 +```bash +# 检查MySQL服务状态 +sudo systemctl status mysql + +# 检查数据库用户权限 +mysql -u webssh -p webssh +``` + +#### 2. 加密密钥错误 +```bash +# 确保ENCRYPTION_KEY为32字节 +node -e "console.log(process.env.ENCRYPTION_KEY.length)" +``` + +#### 3. 端口冲突 +```bash +# 检查端口占用 +netstat -tulpn | grep :3000 +netstat -tulpn | grep :5173 +``` + +#### 4. 依赖安装失败 +```bash +# 清除缓存重新安装 +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +``` + +### 日志查看 +```bash +# 后端日志 +cd backend +npm run dev + +# 前端日志 +cd frontend +npm run dev +``` + +## 🤝 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 + +## 🙏 致谢 + +- [xterm.js](https://xtermjs.org/) - 强大的Web终端组件 +- [ssh2](https://github.com/mscdex/ssh2) - Node.js SSH客户端库 +- [Vue.js](https://vuejs.org/) - 渐进式JavaScript框架 +- [Element Plus](https://element-plus.org/) - Vue3组件库 + +## 📞 联系我们 + +如有问题或建议,请通过以下方式联系: +- 提交 Issue +- 发送邮件 + +--- + +**注意**: 本项目仍在开发中,部分功能可能不完善。生产环境使用前请充分测试。 \ No newline at end of file diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..f46be12 --- /dev/null +++ b/backend/.env @@ -0,0 +1,16 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=webssh +DB_PASSWORD=webssh +DB_NAME=webssh + +# JWT Secret +JWT_SECRET=hK9/mP2sR5vX8nL1zQ4wJ7tY3uA6bD0eC+gF/iO= + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Encryption Key (for AES-256) - must be exactly 32 bytes +ENCRYPTION_KEY=abcdefghijklmnopqrstuvwxyz123456 \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..d0fc254 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,16 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=webssh + +# JWT Secret +JWT_SECRET=your_jwt_secret_key_here + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Encryption Key (for AES-256) +ENCRYPTION_KEY=your_32_byte_encryption_key_here \ No newline at end of file diff --git a/backend/check-db.js b/backend/check-db.js new file mode 100644 index 0000000..b722b4f --- /dev/null +++ b/backend/check-db.js @@ -0,0 +1,31 @@ +const mysql = require('mysql2/promise'); +const dotenv = require('dotenv'); +dotenv.config(); + +async function checkDatabase() { + try { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'webssh' + }); + + console.log('=== 检查用户表 ==='); + const [users] = await connection.execute('SELECT id, email FROM users'); + console.log('用户总数:', users.length); + users.forEach(user => console.log('用户:', user)); + + console.log('\n=== 检查服务器表 ==='); + const [servers] = await connection.execute('SELECT id, user_id, name, host FROM servers'); + console.log('服务器总数:', servers.length); + servers.forEach(server => console.log('服务器:', server)); + + await connection.end(); + } catch (error) { + console.error('数据库连接错误:', error.message); + } +} + +checkDatabase(); \ No newline at end of file diff --git a/backend/config/database.js b/backend/config/database.js new file mode 100644 index 0000000..5fa3e66 --- /dev/null +++ b/backend/config/database.js @@ -0,0 +1,97 @@ +const mysql = require('mysql2/promise'); +const dotenv = require('dotenv'); + +dotenv.config(); + +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'webssh' +}; + +// Create connection pool +const pool = mysql.createPool({ + ...dbConfig, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// Test connection +const authenticate = async () => { + try { + const connection = await pool.getConnection(); + console.log('Database connected successfully'); + connection.release(); + + // Initialize database tables + await initializeTables(); + return true; + } catch (error) { + console.error('Database connection failed:', error); + return false; + } +}; + +// Initialize database tables +const initializeTables = async () => { + try { + // Users table + await pool.execute(` + 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 + ) + `); + + // Servers table (with encrypted credentials) + await pool.execute(` + 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_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 + ) + `); + + // Connection logs table + await pool.execute(` + 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 + ) + `); + + console.log('Database tables initialized successfully'); + } catch (error) { + console.error('Error initializing database tables:', error); + } +}; + +module.exports = { + pool, + authenticate, + initializeTables +}; \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..73a92f3 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,33 @@ +const jwt = require('jsonwebtoken'); + +/** + * JWT认证中间件 + */ +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + success: false, + message: '访问令牌不存在' + }); + } + + try { + const secret = process.env.JWT_SECRET || 'webssh-secret-key'; + const decoded = jwt.verify(token, secret); + req.user = decoded; + next(); + } catch (error) { + console.error('JWT验证失败:', error); + return res.status(403).json({ + success: false, + message: '无效的访问令牌' + }); + } +} + +module.exports = { + authenticateToken +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..8f603ae --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1930 @@ +{ + "name": "webssh-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webssh-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.6.5", + "socket.io": "^4.7.5", + "ssh2": "^1.15.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.2.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.2.tgz", + "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmmirror.com/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.16.3", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.16.3.tgz", + "integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmmirror.com/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT", + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..d9043e6 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "webssh-backend", + "version": "1.0.0", + "description": "WebSSH backend server", + "scripts": { + "dev": "nodemon server.js", + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "socket.io": "^4.7.5", + "ssh2": "^1.15.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.6.5", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "multer": "^1.4.5-lts.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..5e8236a --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,196 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); + +const router = express.Router(); + +// 用户注册 +router.post('/register', async (req, res) => { + try { + const { email, phone, password } = req.body; + + // 验证输入 + if (!email || !password) { + return res.status(400).json({ error: '邮箱和密码是必填项' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: '密码长度至少6位' }); + } + + // 检查用户是否已存在 + const [existingUsers] = await db.pool.execute( + 'SELECT id FROM users WHERE email = ?', + [email] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({ error: '该邮箱已被注册' }); + } + + // 加密密码 + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // 创建用户 + const [result] = await db.pool.execute( + 'INSERT INTO users (email, phone, password_hash) VALUES (?, ?, ?)', + [email, phone || null, passwordHash] + ); + + // 生成JWT令牌 + const token = jwt.sign( + { userId: result.insertId, email }, + process.env.JWT_SECRET || 'webssh-secret-key', + { expiresIn: '7d' } + ); + + res.status(201).json({ + message: '注册成功', + token, + user: { + id: result.insertId, + email, + phone + } + }); + } catch (error) { + console.error('注册错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 用户登录 +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: '邮箱和密码是必填项' }); + } + + // 查找用户 + const [users] = await db.pool.execute( + 'SELECT * FROM users WHERE email = ?', + [email] + ); + + if (users.length === 0) { + return res.status(401).json({ error: '邮箱或密码错误' }); + } + + const user = users[0]; + + // 验证密码 + const isValidPassword = await bcrypt.compare(password, user.password_hash); + if (!isValidPassword) { + return res.status(401).json({ error: '邮箱或密码错误' }); + } + + // 生成JWT令牌 + const token = jwt.sign( + { userId: user.id, email: user.email }, + process.env.JWT_SECRET || 'webssh-secret-key', + { expiresIn: '7d' } + ); + + res.json({ + message: '登录成功', + token, + user: { + id: user.id, + email: user.email, + phone: user.phone + } + }); + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 验证令牌中间件 +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: '访问令牌缺失' }); + } + + jwt.verify(token, process.env.JWT_SECRET || 'webssh-secret-key', (err, user) => { + if (err) { + return res.status(403).json({ error: '令牌无效' }); + } + req.user = user; + next(); + }); +}; + +// 获取当前用户信息 +router.get('/me', authenticateToken, async (req, res) => { + try { + const [users] = await db.pool.execute( + 'SELECT id, email, phone, created_at FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({ error: '用户不存在' }); + } + + res.json({ user: users[0] }); + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 修改密码 +router.put('/password', authenticateToken, async (req, res) => { + try { + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({ error: '当前密码和新密码是必填项' }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ error: '新密码长度至少6位' }); + } + + // 获取用户当前密码 + const [users] = await db.pool.execute( + 'SELECT password_hash FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({ error: '用户不存在' }); + } + + // 验证当前密码 + const isValidPassword = await bcrypt.compare(currentPassword, users[0].password_hash); + if (!isValidPassword) { + return res.status(401).json({ error: '当前密码错误' }); + } + + // 加密新密码 + const saltRounds = 12; + const newPasswordHash = await bcrypt.hash(newPassword, saltRounds); + + // 更新密码 + await db.pool.execute( + 'UPDATE users SET password_hash = ? WHERE id = ?', + [newPasswordHash, req.user.userId] + ); + + res.json({ message: '密码修改成功' }); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/servers.js b/backend/routes/servers.js new file mode 100644 index 0000000..a24cfee --- /dev/null +++ b/backend/routes/servers.js @@ -0,0 +1,378 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); +const encryptionService = require('../utils/encryption'); +const { Client } = require('ssh2'); + +const router = express.Router(); + +// 验证令牌中间件 +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: '访问令牌缺失' }); + } + + jwt.verify(token, process.env.JWT_SECRET || 'webssh-secret-key', (err, user) => { + if (err) { + return res.status(403).json({ error: '令牌无效' }); + } + req.user = user; + next(); + }); +}; + +// 获取用户的所有服务器 +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`, + [req.user.userId] + ); + + res.json({ servers }); + } catch (error) { + console.error('获取服务器列表错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 添加新服务器 +router.post('/', authenticateToken, async (req, res) => { + try { + const { name, host, port, username, password, privateKey, authType, groupName } = req.body; + + // 验证必填字段 + if (!name || !host || !username) { + return res.status(400).json({ error: '名称、主机和用户名是必填项' }); + } + + // 验证认证方式 + if (authType === 'password' && !password) { + return res.status(400).json({ error: '密码认证需要提供密码' }); + } + + if (authType === 'key' && !privateKey) { + return res.status(400).json({ error: '密钥认证需要提供私钥' }); + } + + // 加密敏感信息 + let passwordEncrypted = null; + let privateKeyEncrypted = null; + + if (authType === 'password' && password) { + passwordEncrypted = encryptionService.encryptPassword(password, req.user.userId); + } + + if (authType === 'key' && privateKey) { + privateKeyEncrypted = encryptionService.encryptPrivateKey(privateKey, req.user.userId); + } + + // 插入服务器记录 + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + req.user.userId, + name, + host, + port || 22, + username, + passwordEncrypted, + privateKeyEncrypted, + authType || 'password', + groupName || 'Default' + ] + ); + + res.status(201).json({ + message: '服务器添加成功', + server: { + id: result.insertId, + name, + host, + port: port || 22, + username, + authType: authType || 'password', + groupName: groupName || 'Default' + } + }); + } catch (error) { + console.error('添加服务器错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 更新服务器信息 +router.put('/:id', authenticateToken, async (req, res) => { + try { + const serverId = req.params.id; + const { name, host, port, username, password, privateKey, authType, groupName } = req.body; + + // 检查服务器是否存在且属于当前用户 + const [servers] = await db.pool.execute( + 'SELECT id FROM servers WHERE id = ? AND user_id = ?', + [serverId, req.user.userId] + ); + + if (servers.length === 0) { + return res.status(404).json({ error: '服务器不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (name) { + updateFields.push('name = ?'); + updateValues.push(name); + } + + if (host) { + updateFields.push('host = ?'); + updateValues.push(host); + } + + if (port) { + updateFields.push('port = ?'); + updateValues.push(port); + } + + if (username) { + updateFields.push('username = ?'); + updateValues.push(username); + } + + if (groupName) { + updateFields.push('group_name = ?'); + updateValues.push(groupName); + } + + // 处理认证信息更新 + if (authType) { + updateFields.push('auth_type = ?'); + updateValues.push(authType); + + if (authType === 'password' && password) { + const passwordEncrypted = encryptionService.encryptPassword(password, req.user.userId); + updateFields.push('password_encrypted = ?'); + updateValues.push(passwordEncrypted); + updateFields.push('private_key_encrypted = NULL'); + } else if (authType === 'key' && privateKey) { + const privateKeyEncrypted = encryptionService.encryptPrivateKey(privateKey, req.user.userId); + updateFields.push('private_key_encrypted = ?'); + updateValues.push(privateKeyEncrypted); + updateFields.push('password_encrypted = NULL'); + } + } + + if (updateFields.length === 0) { + return res.status(400).json({ error: '没有提供更新信息' }); + } + + updateValues.push(serverId, req.user.userId); + + await db.pool.execute( + `UPDATE servers SET ${updateFields.join(', ')} + WHERE id = ? AND user_id = ?`, + updateValues + ); + + res.json({ message: '服务器更新成功' }); + } catch (error) { + console.error('更新服务器错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 删除服务器 +router.delete('/:id', authenticateToken, async (req, res) => { + try { + const serverId = req.params.id; + + const [result] = await db.pool.execute( + 'DELETE FROM servers WHERE id = ? AND user_id = ?', + [serverId, req.user.userId] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: '服务器不存在' }); + } + + res.json({ message: '服务器删除成功' }); + } catch (error) { + console.error('删除服务器错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取服务器分组 +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', + [req.user.userId] + ); + + const groupNames = groups.map(group => group.group_name); + res.json({ groups: groupNames }); + } catch (error) { + console.error('获取服务器分组错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取服务器详情(包含解密后的认证信息) +router.get('/:id/credentials', authenticateToken, async (req, res) => { + try { + const serverId = req.params.id; + + const [servers] = await db.pool.execute( + `SELECT id, name, host, port, username, password_encrypted, + private_key_encrypted, auth_type, group_name + FROM servers WHERE id = ? AND user_id = ?`, + [serverId, req.user.userId] + ); + + if (servers.length === 0) { + return res.status(404).json({ error: '服务器不存在' }); + } + + const server = servers[0]; + let credentials = {}; + + // 解密认证信息 + if (server.auth_type === 'password' && server.password_encrypted) { + credentials.password = encryptionService.decryptPassword( + server.password_encrypted, + req.user.userId + ); + } else if (server.auth_type === 'key' && server.private_key_encrypted) { + credentials.privateKey = encryptionService.decryptPrivateKey( + server.private_key_encrypted, + req.user.userId + ); + } + + res.json({ + server: { + id: server.id, + name: server.name, + host: server.host, + port: server.port, + username: server.username, + authType: server.auth_type, + groupName: server.group_name + }, + credentials + }); + } catch (error) { + console.error('获取服务器认证信息错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 测试服务器连接(使用当前表单数据) +router.post('/test-connection', authenticateToken, async (req, res) => { + try { + const { + host, + port = 22, + username, + password, + privateKey, + authType = 'password' + } = req.body; + + // 验证必填字段 + if (!host || !username) { + return res.status(400).json({ + success: false, + error: '主机地址和用户名是必填项' + }); + } + + // 验证认证信息 + if (authType === 'password' && !password) { + return res.status(400).json({ + success: false, + error: '密码认证需要提供密码' + }); + } + + if (authType === 'key' && !privateKey) { + return res.status(400).json({ + success: false, + error: '密钥认证需要提供私钥' + }); + } + + // 创建SSH连接配置 + const sshConfig = { + host: host, + port: parseInt(port) || 22, + username: username, + readyTimeout: 10000, // 10秒超时 + }; + + // 添加认证信息 + if (authType === 'password' && password) { + sshConfig.password = password; + } else if (authType === 'key' && privateKey) { + sshConfig.privateKey = privateKey; + } + + // 测试连接 + const conn = new Client(); + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + conn.end(); + res.status(408).json({ + success: false, + error: '连接超时' + }); + resolve(); + }, 10000); + + conn.on('ready', () => { + clearTimeout(timeout); + conn.end(); + res.json({ + success: true, + message: '连接测试成功' + }); + resolve(); + }); + + conn.on('error', (err) => { + clearTimeout(timeout); + console.error('SSH连接测试错误:', err); + res.status(500).json({ + success: false, + error: `连接失败: ${err.message || '未知错误'}` + }); + resolve(); + }); + + conn.connect(sshConfig); + }); + + } catch (error) { + console.error('测试连接错误:', error); + res.status(500).json({ + success: false, + error: '服务器内部错误' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/sftp-upload.js b/backend/routes/sftp-upload.js new file mode 100644 index 0000000..c33ec33 --- /dev/null +++ b/backend/routes/sftp-upload.js @@ -0,0 +1,181 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const SftpHandler = require('../utils/sftpHandler'); +const { authenticateToken } = require('../middleware/auth'); +const { getServerById } = require('../utils/database'); +const { decrypt } = require('../utils/encryption'); + +const router = express.Router(); + +// 配置multer用于文件上传 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const tempDir = path.join(__dirname, '../temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + cb(null, tempDir); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + '-' + file.originalname); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 100 * 1024 * 1024 // 100MB限制 + } +}); + +// 文件上传 +router.post('/upload', authenticateToken, upload.single('file'), async (req, res) => { + try { + const { serverId, path: remotePath } = req.body; + const userId = req.user.id; + + if (!req.file) { + return res.status(400).json({ + success: false, + message: '没有选择文件' + }); + } + + if (!serverId || !remotePath) { + // 清理临时文件 + fs.unlinkSync(req.file.path); + return res.status(400).json({ + success: false, + message: '服务器ID和路径不能为空' + }); + } + + // 获取服务器配置 + const server = await getServerById(serverId); + if (!server || server.user_id !== userId) { + fs.unlinkSync(req.file.path); + return res.status(403).json({ + success: false, + message: '服务器不存在或无权访问' + }); + } + + // 解密敏感信息 + const serverConfig = { + host: server.host, + port: server.port, + username: server.username, + password: server.password ? decrypt(server.password, userId) : null, + privateKey: server.private_key ? decrypt(server.private_key, userId) : null + }; + + // 连接SFTP并上传文件 + const sftpHandler = new SftpHandler(serverConfig); + await sftpHandler.connect(); + await sftpHandler.uploadFile(req.file.path, remotePath); + await sftpHandler.disconnect(); + + // 清理临时文件 + fs.unlinkSync(req.file.path); + + res.json({ + success: true, + message: '文件上传成功', + filename: req.file.originalname, + size: req.file.size + }); + + } catch (error) { + console.error('文件上传错误:', error); + + // 清理临时文件 + if (req.file && fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path); + } + + res.status(500).json({ + success: false, + message: '文件上传失败: ' + error.message + }); + } +}); + +// 文件下载 +router.get('/download', authenticateToken, async (req, res) => { + try { + const { serverId, path: remotePath } = req.query; + const userId = req.user.id; + + if (!serverId || !remotePath) { + return res.status(400).json({ + success: false, + message: '服务器ID和路径不能为空' + }); + } + + // 获取服务器配置 + const server = await getServerById(serverId); + if (!server || server.user_id !== userId) { + return res.status(403).json({ + success: false, + message: '服务器不存在或无权访问' + }); + } + + // 解密敏感信息 + const serverConfig = { + host: server.host, + port: server.port, + username: server.username, + password: server.password ? decrypt(server.password, userId) : null, + privateKey: server.private_key ? decrypt(server.private_key, userId) : null + }; + + // 创建临时目录 + const tempDir = path.join(__dirname, '../temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const tempFilePath = path.join(tempDir, `download_${Date.now()}_${path.basename(remotePath)}`); + + // 连接SFTP并下载文件 + const sftpHandler = new SftpHandler(serverConfig); + await sftpHandler.connect(); + await sftpHandler.downloadFile(remotePath, tempFilePath); + await sftpHandler.disconnect(); + + // 设置响应头 + const filename = path.basename(remotePath); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`); + res.setHeader('Content-Type', 'application/octet-stream'); + + // 发送文件 + const fileStream = fs.createReadStream(tempFilePath); + fileStream.pipe(res); + + // 文件传输完成后清理临时文件 + fileStream.on('end', () => { + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + }); + + fileStream.on('error', () => { + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + }); + + } catch (error) { + console.error('文件下载错误:', error); + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/sftp.js b/backend/routes/sftp.js new file mode 100644 index 0000000..b8280b3 --- /dev/null +++ b/backend/routes/sftp.js @@ -0,0 +1,431 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const router = express.Router(); +const SftpHandler = require('../utils/sftpHandler'); +const { authenticateToken } = require('../middleware/auth'); +const { getServerById } = require('../utils/database'); +const encryptionService = require('../utils/encryption'); + +// 配置multer用于文件上传 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const tempDir = path.join(__dirname, '../temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + cb(null, tempDir); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + '-' + file.originalname); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 100 * 1024 * 1024 // 100MB限制 + } +}); + +// 存储活跃的SFTP连接 +const sftpConnections = new Map(); + +// 获取SFTP连接 +async function getSftpConnection(userId, serverId) { + const key = `${userId}-${serverId}`; + + if (sftpConnections.has(key)) { + const connection = sftpConnections.get(key); + if (connection.isConnected) { + return connection; + } + sftpConnections.delete(key); + } + + // 获取服务器配置 + const server = await getServerById(serverId); + if (!server || server.user_id !== userId) { + throw new Error('服务器不存在或无权访问'); + } + + // 解密敏感信息 + const serverConfig = { + host: server.host, + port: server.port, + username: server.username, + }; + + if (server.password_encrypted) { + serverConfig.password = encryptionService.decryptPassword( + server.password_encrypted, + userId + ); + } + if (server.private_key_encrypted) { + serverConfig.privateKey = encryptionService.decryptPrivateKey( + server.private_key_encrypted, + userId + ); + }; + + const sftpHandler = new SftpHandler(serverConfig); + await sftpHandler.connect(); + + sftpConnections.set(key, sftpHandler); + + // 设置定时器自动断开连接(10分钟无活动) + setTimeout(() => { + if (sftpConnections.has(key)) { + sftpConnections.get(key).disconnect(); + sftpConnections.delete(key); + } + }, 10 * 60 * 1000); + + return sftpHandler; +} + +// 连接SFTP +router.post('/connect', authenticateToken, async (req, res) => { + try { + const { serverId } = req.body; + const userId = req.user.userId; + + console.log('SFTP连接请求 - 用户ID:', userId, '服务器ID:', serverId); + + const sftpHandler = await getSftpConnection(userId, serverId); + + // 获取根目录列表 + const files = await sftpHandler.listDirectory('.'); + + res.json({ + success: true, + message: 'SFTP连接成功', + currentPath: '/', + files: files + }); + } catch (error) { + console.error('SFTP连接错误:', error); + res.status(500).json({ + success: false, + message: 'SFTP连接失败: ' + error.message + }); + } +}); + +// 断开SFTP连接 +router.post('/disconnect', authenticateToken, async (req, res) => { + try { + const { serverId } = req.body; + const userId = req.user.userId; + const key = `${userId}-${serverId}`; + + if (sftpConnections.has(key)) { + await sftpConnections.get(key).disconnect(); + sftpConnections.delete(key); + } + + res.json({ + success: true, + message: 'SFTP连接已断开' + }); + } catch (error) { + console.error('断开SFTP连接错误:', error); + res.status(500).json({ + success: false, + message: '断开连接失败: ' + error.message + }); + } +}); + +// 列出目录内容 +router.get('/list', authenticateToken, async (req, res) => { + try { + const { serverId, path = '.' } = req.query; + const userId = req.user.userId; + + const sftpHandler = await getSftpConnection(userId, serverId); + const files = await sftpHandler.listDirectory(path); + + res.json({ + success: true, + currentPath: path, + files: files + }); + } catch (error) { + console.error('列出目录错误:', error); + res.status(500).json({ + success: false, + message: '列出目录失败: ' + error.message + }); + } +}); + +// 读取文件内容 +router.get('/file', authenticateToken, async (req, res) => { + try { + const { serverId, path } = req.query; + const userId = req.user.userId; + + if (!path) { + return res.status(400).json({ + success: false, + message: '文件路径不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + const content = await sftpHandler.getFileContent(path); + + res.json({ + success: true, + content: content, + path: path + }); + } catch (error) { + console.error('读取文件错误:', error); + res.status(500).json({ + success: false, + message: '读取文件失败: ' + error.message + }); + } +}); + +// 保存文件内容 +router.post('/file', authenticateToken, async (req, res) => { + try { + const { serverId, path, content } = req.body; + const userId = req.user.userId; + + if (!path || content === undefined) { + return res.status(400).json({ + success: false, + message: '文件路径和内容不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + + // 创建临时文件保存内容,然后上传 + const fs = require('fs'); + const os = require('os'); + const pathModule = require('path'); + + const tempPath = pathModule.join(os.tmpdir(), `sftp_edit_${Date.now()}.tmp`); + fs.writeFileSync(tempPath, content, 'utf8'); + + await sftpHandler.uploadFile(tempPath, path); + fs.unlinkSync(tempPath); + + res.json({ + success: true, + message: '文件保存成功' + }); + } catch (error) { + console.error('保存文件错误:', error); + res.status(500).json({ + success: false, + message: '保存文件失败: ' + error.message + }); + } +}); + +// 创建目录 +router.post('/mkdir', authenticateToken, async (req, res) => { + try { + const { serverId, path } = req.body; + const userId = req.user.userId; + + if (!path) { + return res.status(400).json({ + success: false, + message: '目录路径不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + await sftpHandler.createDirectory(path); + + res.json({ + success: true, + message: '目录创建成功' + }); + } catch (error) { + console.error('创建目录错误:', error); + res.status(500).json({ + success: false, + message: '创建目录失败: ' + error.message + }); + } +}); + +// 删除文件或目录 +router.delete('/delete', authenticateToken, async (req, res) => { + try { + const { serverId, path, type } = req.body; + const userId = req.user.userId; + + if (!path) { + return res.status(400).json({ + success: false, + message: '路径不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + + if (type === 'directory') { + await sftpHandler.deleteDirectory(path); + } else { + await sftpHandler.deleteFile(path); + } + + res.json({ + success: true, + message: '删除成功' + }); + } catch (error) { + console.error('删除错误:', error); + res.status(500).json({ + success: false, + message: '删除失败: ' + error.message + }); + } +}); + +// 重命名文件或目录 +router.post('/rename', authenticateToken, async (req, res) => { + try { + const { serverId, oldPath, newPath } = req.body; + const userId = req.user.userId; + + if (!oldPath || !newPath) { + return res.status(400).json({ + success: false, + message: '原路径和新路径不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + await sftpHandler.renameFile(oldPath, newPath); + + res.json({ + success: true, + message: '重命名成功' + }); + } catch (error) { + console.error('重命名错误:', error); + res.status(500).json({ + success: false, + message: '重命名失败: ' + error.message + }); + } +}); + +// 文件上传 +router.post('/upload', authenticateToken, upload.single('file'), async (req, res) => { + try { + const { serverId, path: remotePath } = req.body; + const userId = req.user.userId; + + if (!req.file) { + return res.status(400).json({ + success: false, + message: '没有选择文件' + }); + } + + if (!serverId || !remotePath) { + // 清理临时文件 + fs.unlinkSync(req.file.path); + return res.status(400).json({ + success: false, + message: '服务器ID和路径不能为空' + }); + } + + const sftpHandler = await getSftpConnection(userId, serverId); + await sftpHandler.uploadFile(req.file.path, remotePath); + + // 清理临时文件 + fs.unlinkSync(req.file.path); + + res.json({ + success: true, + message: '文件上传成功', + filename: req.file.originalname, + size: req.file.size + }); + + } catch (error) { + console.error('文件上传错误:', error); + + // 清理临时文件 + if (req.file && fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path); + } + + res.status(500).json({ + success: false, + message: '文件上传失败: ' + error.message + }); + } +}); + +// 文件下载 +router.get('/download', authenticateToken, async (req, res) => { + try { + const { serverId, path: remotePath } = req.query; + const userId = req.user.userId; + + if (!serverId || !remotePath) { + return res.status(400).json({ + success: false, + message: '服务器ID和路径不能为空' + }); + } + + // 创建临时目录 + const tempDir = path.join(__dirname, '../temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + const tempFilePath = path.join(tempDir, `download_${Date.now()}_${path.basename(remotePath)}`); + + const sftpHandler = await getSftpConnection(userId, serverId); + await sftpHandler.downloadFile(remotePath, tempFilePath); + + // 设置响应头 + const filename = path.basename(remotePath); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`); + res.setHeader('Content-Type', 'application/octet-stream'); + + // 发送文件 + const fileStream = fs.createReadStream(tempFilePath); + fileStream.pipe(res); + + // 文件传输完成后清理临时文件 + fileStream.on('end', () => { + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + }); + + fileStream.on('error', () => { + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + }); + + } catch (error) { + console.error('文件下载错误:', error); + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..5344eec --- /dev/null +++ b/backend/server.js @@ -0,0 +1,61 @@ +const express = require('express'); +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const path = require('path'); + +// Routes +const authRoutes = require('./routes/auth'); +const serverRoutes = require('./routes/servers'); +const sftpRoutes = require('./routes/sftp'); + +// Database +const db = require('./config/database'); + +// Socket handlers +const socketHandler = require('./socket/socketHandler'); + +// Load environment variables +dotenv.config(); + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: "http://localhost:5173", // Vite dev server + methods: ["GET", "POST"] + } +}); + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.static(path.join(__dirname, '../frontend/dist'))); + +// API Routes +app.use('/api/auth', authRoutes); +app.use('/api/servers', serverRoutes); +app.use('/api/sftp', sftpRoutes); + +// Socket.io connection handling +socketHandler(io); + +// Serve frontend for production +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); +}); + +// Database connection test +db.authenticate() + .then(() => { + console.log('Database connection established successfully.'); + }) + .catch(err => { + console.error('Unable to connect to the database:', err); + }); + +const PORT = process.env.PORT || 3000; +httpServer.listen(PORT, () => { + console.log(`WebSSH server running on port ${PORT}`); +}); \ No newline at end of file diff --git a/backend/socket/socketHandler.js b/backend/socket/socketHandler.js new file mode 100644 index 0000000..ca8bf34 --- /dev/null +++ b/backend/socket/socketHandler.js @@ -0,0 +1,262 @@ +const { Client } = require('ssh2'); +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); +const encryptionService = require('../utils/encryption'); + +// 存储活跃的SSH连接 +const activeConnections = new Map(); + +const socketHandler = (io) => { + io.on('connection', (socket) => { + console.log('用户连接:', socket.id); + + // 验证用户身份 + socket.on('authenticate', async (token) => { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'webssh-secret-key'); + socket.userId = decoded.userId; + socket.emit('authenticated', { success: true }); + console.log('用户认证成功:', decoded.userId); + } catch (error) { + socket.emit('authenticated', { success: false, error: '认证失败' }); + socket.disconnect(); + } + }); + + // 建立SSH连接 + socket.on('connect-ssh', async (serverId) => { + if (!socket.userId) { + socket.emit('ssh-error', { error: '未认证' }); + return; + } + + try { + // 获取服务器信息和认证信息 + const [servers] = await db.pool.execute( + `SELECT id, name, host, port, username, password_encrypted, + private_key_encrypted, auth_type + FROM servers WHERE id = ? AND user_id = ?`, + [serverId, socket.userId] + ); + + if (servers.length === 0) { + socket.emit('ssh-error', { error: '服务器不存在' }); + return; + } + + const server = servers[0]; + + // 解密认证信息 + let credentials = {}; + if (server.auth_type === 'password' && server.password_encrypted) { + credentials.password = encryptionService.decryptPassword( + server.password_encrypted, + socket.userId + ); + } else if (server.auth_type === 'key' && server.private_key_encrypted) { + credentials.privateKey = encryptionService.decryptPrivateKey( + server.private_key_encrypted, + socket.userId + ); + } + + // 创建SSH连接 + const conn = new Client(); + + conn.on('ready', () => { + console.log('SSH连接已建立:', server.host); + socket.emit('ssh-connected'); + + // 存储连接 + activeConnections.set(socket.id, { + conn, + serverId: server.id, + userId: socket.userId + }); + + // 创建shell会话 + conn.shell({}, (err, stream) => { + if (err) { + socket.emit('ssh-error', { error: err.message }); + return; + } + + // 转发终端输出到前端 + stream.on('data', (data) => { + socket.emit('ssh-data', data.toString()); + }); + + // 处理来自前端的输入 + socket.on('ssh-input', (input) => { + stream.write(input); + }); + + // 处理窗口大小调整 + socket.on('resize', (size) => { + stream.setWindow(size.rows, size.cols, size.height, size.width); + }); + + // 处理连接关闭 + stream.on('close', () => { + socket.emit('ssh-closed'); + activeConnections.delete(socket.id); + }); + + socket.sshStream = stream; + }); + }); + + conn.on('error', (err) => { + console.error('SSH连接错误:', err.message); + socket.emit('ssh-error', { error: err.message }); + }); + + conn.on('close', () => { + console.log('SSH连接已关闭:', server.host); + socket.emit('ssh-closed'); + activeConnections.delete(socket.id); + }); + + // 连接配置 + const connConfig = { + host: server.host, + port: server.port || 22, + username: server.username, + readyTimeout: 20000, + keepaliveInterval: 30000 + }; + + // 添加认证信息 + if (server.auth_type === 'password') { + connConfig.password = credentials.password; + } else if (server.auth_type === 'key') { + connConfig.privateKey = credentials.privateKey; + } + + // 开始连接 + conn.connect(connConfig); + + } catch (error) { + console.error('SSH连接准备错误:', error); + socket.emit('ssh-error', { error: '连接失败' }); + } + }); + + // 快速连接(不保存服务器信息) + socket.on('quick-connect', (connectionInfo) => { + if (!socket.userId) { + socket.emit('ssh-error', { error: '未认证' }); + return; + } + + const { host, port, username, password, privateKey } = connectionInfo; + + const conn = new Client(); + + conn.on('ready', () => { + console.log('快速SSH连接已建立:', host); + socket.emit('ssh-connected'); + + // 存储连接 + activeConnections.set(socket.id, { + conn, + quickConnect: true, + userId: socket.userId + }); + + // 创建shell会话 + conn.shell({}, (err, stream) => { + if (err) { + socket.emit('ssh-error', { error: err.message }); + return; + } + + stream.on('data', (data) => { + socket.emit('ssh-data', data.toString()); + }); + + socket.on('ssh-input', (input) => { + stream.write(input); + }); + + socket.on('resize', (size) => { + stream.setWindow(size.rows, size.cols, size.height, size.width); + }); + + stream.on('close', () => { + socket.emit('ssh-closed'); + activeConnections.delete(socket.id); + }); + + socket.sshStream = stream; + }); + }); + + conn.on('error', (err) => { + console.error('快速SSH连接错误:', err.message); + socket.emit('ssh-error', { error: err.message }); + }); + + conn.on('close', () => { + console.log('快速SSH连接已关闭:', host); + socket.emit('ssh-closed'); + activeConnections.delete(socket.id); + }); + + // 连接配置 + const connConfig = { + host, + port: port || 22, + username, + readyTimeout: 20000, + keepaliveInterval: 30000 + }; + + if (password) { + connConfig.password = password; + } else if (privateKey) { + connConfig.privateKey = privateKey; + } + + conn.connect(connConfig); + }); + + // 发送命令到终端 + socket.on('send-command', (command) => { + if (socket.sshStream) { + socket.sshStream.write(command + '\n'); + } + }); + + // 断开SSH连接 + socket.on('disconnect-ssh', () => { + const connection = activeConnections.get(socket.id); + if (connection) { + connection.conn.end(); + activeConnections.delete(socket.id); + } + }); + + // 处理连接断开 + socket.on('disconnect', () => { + console.log('用户断开连接:', socket.id); + const connection = activeConnections.get(socket.id); + if (connection) { + connection.conn.end(); + activeConnections.delete(socket.id); + } + }); + + // 心跳检测 + socket.on('ping', () => { + socket.emit('pong'); + }); + }); + + // 全局错误处理 + io.on('error', (error) => { + console.error('Socket.IO错误:', error); + }); +}; + +module.exports = socketHandler; \ No newline at end of file diff --git a/backend/utils/database.js b/backend/utils/database.js new file mode 100644 index 0000000..c1a375b --- /dev/null +++ b/backend/utils/database.js @@ -0,0 +1,107 @@ +const { pool } = require('../config/database'); + +// 通用查询函数 +async function query(sql, params = []) { + try { + const [rows] = await pool.execute(sql, params); + return rows; + } catch (error) { + console.error('数据库查询错误:', error); + throw error; + } +} + +// 根据ID获取服务器 +async function getServerById(id) { + const sql = 'SELECT * FROM servers WHERE id = ?'; + const result = await query(sql, [id]); + return result.length > 0 ? result[0] : null; +} + +// 获取用户的所有服务器 +async function getUserServers(userId) { + const sql = 'SELECT * FROM servers WHERE user_id = ? ORDER BY name'; + return await query(sql, [userId]); +} + +// 创建服务器 +async function createServer(serverData) { + const sql = ` + INSERT INTO servers (user_id, name, host, port, username, password_encrypted, private_key_encrypted, group_name, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) + `; + const result = await query(sql, [ + serverData.user_id, + serverData.name, + serverData.host, + serverData.port, + serverData.username, + serverData.password, + serverData.private_key, + serverData.group_name || 'default' + ]); + + return { id: result.insertId, ...serverData }; +} + +// 更新服务器 +async function updateServer(id, serverData) { + const sql = ` + UPDATE servers + SET name = ?, host = ?, port = ?, username = ?, password_encrypted = ?, private_key_encrypted = ?, group_name = ?, updated_at = NOW() + WHERE id = ? + `; + + await query(sql, [ + serverData.name, + serverData.host, + serverData.port, + serverData.username, + serverData.password, + serverData.private_key, + serverData.group_name, + id + ]); + + return { id, ...serverData }; +} + +// 删除服务器 +async function deleteServer(id) { + const sql = 'DELETE FROM servers WHERE id = ?'; + await query(sql, [id]); + return true; +} + +// 获取用户信息 +async function getUserByEmail(email) { + const sql = 'SELECT * FROM users WHERE email = ?'; + const result = await query(sql, [email]); + return result.length > 0 ? result[0] : null; +} + +async function getUserById(id) { + const sql = 'SELECT id, email, created_at FROM users WHERE id = ?'; + const result = await query(sql, [id]); + return result.length > 0 ? result[0] : null; +} + +// 创建用户 +async function createUser(userData) { + const sql = 'INSERT INTO users (email, password, created_at) VALUES (?, ?, NOW())'; + const result = await query(sql, [userData.email, userData.password]); + return { id: result.insertId, email: userData.email }; +} + +module.exports = { + pool, + query, + getServerById, + getUserServers, + createServer, + updateServer, + deleteServer, + getUserByEmail, + getUserById, + createUser +}; \ No newline at end of file diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js new file mode 100644 index 0000000..48b99e1 --- /dev/null +++ b/backend/utils/encryption.js @@ -0,0 +1,107 @@ +const crypto = require('crypto'); +const dotenv = require('dotenv'); + +dotenv.config(); + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const AUTH_TAG_LENGTH = 16; + +class EncryptionService { + constructor() { + this.encryptionKey = process.env.ENCRYPTION_KEY || 'default-encryption-key-32bytes-long'; + if (!this.encryptionKey || this.encryptionKey.length !== 32) { + throw new Error('ENCRYPTION_KEY must be exactly 32 bytes long'); + } + } + + /** + * 生成用户特定的加密密钥(基于用户ID) + */ + generateUserKey(userId) { + const userKey = crypto.createHash('sha256') + .update(this.encryptionKey + userId.toString()) + .digest('hex') + .substring(0, 32); + return userKey; + } + + /** + * 加密数据 + */ + encrypt(data, userId) { + if (!data) return null; + + const userKey = this.generateUserKey(userId); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, userKey, iv); + + let encrypted = cipher.update(data, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + // 返回 IV + AuthTag + EncryptedData + return iv.toString('hex') + authTag.toString('hex') + encrypted; + } + + /** + * 解密数据 + */ + decrypt(encryptedData, userId) { + if (!encryptedData) return null; + + try { + const userKey = this.generateUserKey(userId); + + // 提取 IV、AuthTag 和加密数据 + const iv = Buffer.from(encryptedData.substring(0, IV_LENGTH * 2), 'hex'); + const authTag = Buffer.from( + encryptedData.substring(IV_LENGTH * 2, IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2), + 'hex' + ); + const encrypted = encryptedData.substring(IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2); + + const decipher = crypto.createDecipheriv(ALGORITHM, userKey, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch (error) { + console.error('Decryption failed:', error); + return null; + } + } + + /** + * 加密服务器密码 + */ + encryptPassword(password, userId) { + return this.encrypt(password, userId); + } + + /** + * 解密服务器密码 + */ + decryptPassword(encryptedPassword, userId) { + return this.decrypt(encryptedPassword, userId); + } + + /** + * 加密私钥 + */ + encryptPrivateKey(privateKey, userId) { + return this.encrypt(privateKey, userId); + } + + /** + * 解密私钥 + */ + decryptPrivateKey(encryptedPrivateKey, userId) { + return this.decrypt(encryptedPrivateKey, userId); + } +} + +module.exports = new EncryptionService(); \ No newline at end of file diff --git a/backend/utils/sftpHandler.js b/backend/utils/sftpHandler.js new file mode 100644 index 0000000..048ea63 --- /dev/null +++ b/backend/utils/sftpHandler.js @@ -0,0 +1,215 @@ +const { Client } = require('ssh2'); +const path = require('path'); +const fs = require('fs'); + +class SftpHandler { + constructor(serverConfig) { + this.serverConfig = serverConfig; + this.sshClient = new Client(); + this.sftp = null; + this.isConnected = false; + } + + async connect() { + return new Promise((resolve, reject) => { + this.sshClient.on('ready', () => { + this.sshClient.sftp((err, sftp) => { + if (err) { + reject(err); + return; + } + this.sftp = sftp; + this.isConnected = true; + resolve(sftp); + }); + }).on('error', (err) => { + reject(err); + }).connect({ + host: this.serverConfig.host, + port: this.serverConfig.port || 22, + username: this.serverConfig.username, + password: this.serverConfig.password, + privateKey: this.serverConfig.privateKey, + readyTimeout: 20000 + }); + }); + } + + async disconnect() { + if (this.sshClient) { + this.sshClient.end(); + } + this.isConnected = false; + this.sftp = null; + } + + async listDirectory(directory = '.') { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.readdir(directory, (err, list) => { + if (err) { + reject(err); + return; + } + + const files = list.map(item => { + const isDirectory = item.longname.startsWith('d'); + const isSymlink = item.longname.startsWith('l'); + + return { + name: item.filename, + path: path.posix.join(directory === '.' ? '' : directory, item.filename), + type: isDirectory ? 'directory' : (isSymlink ? 'symlink' : 'file'), + size: item.attrs.size, + modified: item.attrs.mtime, + permissions: item.attrs.mode.toString(8).slice(-3), + owner: item.attrs.uid, + group: item.attrs.gid + }; + }); + + // 排序:目录在前,文件在后 + files.sort((a, b) => { + if (a.type === 'directory' && b.type !== 'directory') return -1; + if (a.type !== 'directory' && b.type === 'directory') return 1; + return a.name.localeCompare(b.name); + }); + + resolve(files); + }); + }); + } + + async getFileContent(filePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.readFile(filePath, 'utf8', (err, data) => { + if (err) { + reject(err); + return; + } + resolve(data); + }); + }); + } + + async uploadFile(localPath, remotePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.fastPut(localPath, remotePath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async downloadFile(remotePath, localPath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.fastGet(remotePath, localPath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async createDirectory(remotePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.mkdir(remotePath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async deleteFile(remotePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.unlink(remotePath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async deleteDirectory(remotePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.rmdir(remotePath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async renameFile(oldPath, newPath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.rename(oldPath, newPath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async getFileStats(remotePath) { + if (!this.isConnected) { + throw new Error('SFTP not connected'); + } + + return new Promise((resolve, reject) => { + this.sftp.stat(remotePath, (err, stats) => { + if (err) { + reject(err); + return; + } + resolve(stats); + }); + }); + } +} + +module.exports = SftpHandler; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3d510ad --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + WebSSH - 在线SSH终端 + + +
+ + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9fbfef2 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1481 @@ +{ + "name": "webssh-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webssh-frontend", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", + "element-plus": "^2.3.14", + "pinia": "^2.1.7", + "socket.io-client": "^4.7.5", + "vue": "^3.3.4", + "vue-router": "^4.2.5", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "xterm-addon-web-links": "^0.9.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.0", + "vite": "^4.5.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.2", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.2.tgz", + "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT", + "peer": true + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/xterm-addon-web-links": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/xterm-addon-web-links/-/xterm-addon-web-links-0.9.0.tgz", + "integrity": "sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-web-links instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..fadd6a2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "webssh-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.5", + "pinia": "^2.1.7", + "element-plus": "^2.3.14", + "@element-plus/icons-vue": "^2.3.1", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "xterm-addon-web-links": "^0.9.0", + "socket.io-client": "^4.7.5", + "axios": "^1.6.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.0", + "vite": "^4.5.0" + } +} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..104f4e5 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,59 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ServerForm.vue b/frontend/src/components/ServerForm.vue new file mode 100644 index 0000000..86b5dc7 --- /dev/null +++ b/frontend/src/components/ServerForm.vue @@ -0,0 +1,343 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/SftpFileManager.vue b/frontend/src/components/SftpFileManager.vue new file mode 100644 index 0000000..bae7185 --- /dev/null +++ b/frontend/src/components/SftpFileManager.vue @@ -0,0 +1,518 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..04e836a --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,45 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import axios from 'axios' + +import App from './App.vue' +import router from './router' + +// 配置axios拦截器自动添加认证头 +axios.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}, (error) => { + return Promise.reject(error) +}) + +// 配置axios响应拦截器处理认证错误 +axios.interceptors.response.use((response) => { + return response +}, (error) => { + if (error.response?.status === 401) { + // 清除本地存储的token并重定向到登录页 + localStorage.removeItem('token') + window.location.href = '/login' + } + return Promise.reject(error) +}) + +const app = createApp(App) + +// 注册Element Plus图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) + +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..bb02bb1 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,56 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const routes = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + meta: { requiresGuest: true } + }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/Register.vue'), + meta: { requiresGuest: true } + }, + { + path: '/', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { requiresAuth: true } + }, + { + path: '/terminal/:serverId?', + name: 'Terminal', + component: () => import('@/views/Terminal.vue'), + meta: { requiresAuth: true } + }, + { + path: '/quick-connect', + name: 'QuickConnect', + component: () => import('@/views/QuickConnect.vue'), + meta: { requiresAuth: true } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + + // 检查是否需要认证 + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + next('/login') + } else if (to.meta.requiresGuest && authStore.isAuthenticated) { + next('/') + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js new file mode 100644 index 0000000..c6f7244 --- /dev/null +++ b/frontend/src/stores/auth.js @@ -0,0 +1,125 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import axios from 'axios' + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const token = ref(localStorage.getItem('token')) + const isAuthenticated = ref(!!localStorage.getItem('token')) + + // 登录 + const login = async (email, password) => { + try { + const response = await axios.post('/api/auth/login', { + email, + password + }) + + const { token: newToken, user: userData } = response.data + + // 保存到本地存储 + localStorage.setItem('token', newToken) + token.value = newToken + user.value = userData + isAuthenticated.value = true + + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '登录失败' + } + } + } + + // 注册 + const register = async (email, phone, password) => { + try { + const response = await axios.post('/api/auth/register', { + email, + phone, + password + }) + + const { token: newToken, user: userData } = response.data + + // 保存到本地存储 + localStorage.setItem('token', newToken) + token.value = newToken + user.value = userData + isAuthenticated.value = true + + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '注册失败' + } + } + } + + // 注销 + const logout = () => { + localStorage.removeItem('token') + token.value = null + user.value = null + isAuthenticated.value = false + } + + // 获取当前用户信息 + const fetchCurrentUser = async () => { + try { + const response = await axios.get('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token.value}` + } + }) + + user.value = response.data.user + return { success: true } + } catch (error) { + logout() + return { success: false } + } + } + + // 修改密码 + const changePassword = async (currentPassword, newPassword) => { + try { + await axios.put('/api/auth/password', { + currentPassword, + newPassword + }, { + headers: { + 'Authorization': `Bearer ${token.value}` + } + }) + + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '修改密码失败' + } + } + } + + // 初始化认证状态 + const initializeAuth = async () => { + if (token.value) { + await fetchCurrentUser() + } + } + + return { + user, + token, + isAuthenticated, + login, + register, + logout, + fetchCurrentUser, + changePassword, + initializeAuth + } +}) \ No newline at end of file diff --git a/frontend/src/stores/servers.js b/frontend/src/stores/servers.js new file mode 100644 index 0000000..5eb99cb --- /dev/null +++ b/frontend/src/stores/servers.js @@ -0,0 +1,157 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import axios from 'axios' +import { useAuthStore } from './auth' + +export const useServersStore = defineStore('servers', () => { + const servers = ref([]) + const groups = ref([]) + const loading = ref(false) + + const authStore = useAuthStore() + + // 获取所有服务器 + const fetchServers = async () => { + loading.value = true + try { + const response = await axios.get('/api/servers', { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + servers.value = response.data.servers + await fetchGroups() + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '获取服务器列表失败' + } + } finally { + loading.value = false + } + } + + // 获取服务器分组 + const fetchGroups = async () => { + try { + const response = await axios.get('/api/servers/groups', { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + groups.value = response.data.groups + } catch (error) { + console.error('获取分组失败:', error) + } + } + + // 添加服务器 + const addServer = async (serverData) => { + try { + const response = await axios.post('/api/servers', serverData, { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + + await fetchServers() + return { success: true, data: response.data } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '添加服务器失败' + } + } + } + + // 更新服务器 + const updateServer = async (serverId, serverData) => { + try { + await axios.put(`/api/servers/${serverId}`, serverData, { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + + await fetchServers() + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '更新服务器失败' + } + } + } + + // 删除服务器 + const deleteServer = async (serverId) => { + try { + await axios.delete(`/api/servers/${serverId}`, { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + + await fetchServers() + return { success: true } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '删除服务器失败' + } + } + } + + // 获取服务器认证信息 + const getServerCredentials = async (serverId) => { + try { + const response = await axios.get(`/api/servers/${serverId}/credentials`, { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + return { success: true, data: response.data } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '获取认证信息失败' + } + } + } + + // 测试服务器连接(使用表单数据) + const testConnection = async (connectionData) => { + try { + const response = await axios.post('/api/servers/test-connection', connectionData, { + headers: { + 'Authorization': `Bearer ${authStore.token}` + } + }) + return { success: true, data: response.data } + } catch (error) { + return { + success: false, + error: error.response?.data?.error || '连接测试失败' + } + } + } + + // 按分组获取服务器 + const getServersByGroup = (groupName) => { + return servers.value.filter(server => server.group_name === groupName) + } + + return { + servers, + groups, + loading, + fetchServers, + addServer, + updateServer, + deleteServer, + getServerCredentials, + testConnection, + getServersByGroup + } +}) \ No newline at end of file diff --git a/frontend/src/stores/sftp.js b/frontend/src/stores/sftp.js new file mode 100644 index 0000000..84754c9 --- /dev/null +++ b/frontend/src/stores/sftp.js @@ -0,0 +1,315 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import axios from 'axios'; + +export const useSftpStore = defineStore('sftp', () => { + const isConnected = ref(false); + const currentServer = ref(null); + const currentPath = ref('/'); + const files = ref([]); + const loading = ref(false); + const error = ref(null); + const selectedFiles = ref(new Set()); + + // 连接SFTP + const connectSftp = async (server) => { + try { + loading.value = true; + error.value = null; + + const response = await axios.post('/api/sftp/connect', { + serverId: server.id + }); + + if (response.data.success) { + isConnected.value = true; + currentServer.value = server; + currentPath.value = response.data.currentPath; + files.value = response.data.files; + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + return false; + } finally { + loading.value = false; + } + }; + + // 断开SFTP连接 + const disconnectSftp = async () => { + try { + if (currentServer.value) { + await axios.post('/api/sftp/disconnect', { + serverId: currentServer.value.id + }); + } + } catch (err) { + console.error('断开SFTP连接错误:', err); + } finally { + isConnected.value = false; + currentServer.value = null; + currentPath.value = '/'; + files.value = []; + selectedFiles.value.clear(); + } + }; + + // 列出目录内容 + const listDirectory = async (path = '.') => { + try { + loading.value = true; + error.value = null; + + const response = await axios.get('/api/sftp/list', { + params: { + serverId: currentServer.value.id, + path: path + } + }); + + if (response.data.success) { + currentPath.value = response.data.currentPath; + files.value = response.data.files; + selectedFiles.value.clear(); + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + } finally { + loading.value = false; + } + }; + + // 进入目录 + const enterDirectory = async (directoryName) => { + const newPath = currentPath.value === '/' ? + `/${directoryName}` : + `${currentPath.value}/${directoryName}`; + await listDirectory(newPath); + }; + + // 返回上级目录 + const goToParentDirectory = async () => { + if (currentPath.value === '/' || currentPath.value === '.') { + return; + } + + const parentPath = currentPath.value.split('/').slice(0, -1).join('/') || '/'; + await listDirectory(parentPath); + }; + + // 读取文件内容 + const readFile = async (filePath) => { + try { + const response = await axios.get('/api/sftp/file', { + params: { + serverId: currentServer.value.id, + path: filePath + } + }); + + if (response.data.success) { + return response.data.content; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 保存文件内容 + const saveFile = async (filePath, content) => { + try { + const response = await axios.post('/api/sftp/file', { + serverId: currentServer.value.id, + path: filePath, + content: content + }); + + if (response.data.success) { + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 创建目录 + const createDirectory = async (dirName) => { + try { + const newPath = currentPath.value === '/' ? + `/${dirName}` : + `${currentPath.value}/${dirName}`; + + const response = await axios.post('/api/sftp/mkdir', { + serverId: currentServer.value.id, + path: newPath + }); + + if (response.data.success) { + await listDirectory(currentPath.value); + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 删除文件或目录 + const deleteItem = async (itemPath, itemType) => { + try { + const response = await axios.delete('/api/sftp/delete', { + data: { + serverId: currentServer.value.id, + path: itemPath, + type: itemType + } + }); + + if (response.data.success) { + await listDirectory(currentPath.value); + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 重命名文件或目录 + const renameItem = async (oldPath, newName) => { + try { + const parentPath = oldPath.split('/').slice(0, -1).join('/') || '/'; + const newPath = parentPath === '/' ? + `/${newName}` : + `${parentPath}/${newName}`; + + const response = await axios.post('/api/sftp/rename', { + serverId: currentServer.value.id, + oldPath: oldPath, + newPath: newPath + }); + + if (response.data.success) { + await listDirectory(currentPath.value); + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 上传文件 + const uploadFile = async (file, targetPath = null) => { + try { + const uploadPath = targetPath || + (currentPath.value === '/' ? + `/${file.name}` : + `${currentPath.value}/${file.name}`); + + // 创建FormData对象 + const formData = new FormData(); + formData.append('file', file); + formData.append('serverId', currentServer.value.id); + formData.append('path', uploadPath); + + const response = await axios.post('/api/sftp/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + if (response.data.success) { + await listDirectory(currentPath.value); + return true; + } else { + throw new Error(response.data.message); + } + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 下载文件 + const downloadFile = async (filePath, fileName) => { + try { + const response = await axios.get(`/api/sftp/download?serverId=${currentServer.value.id}&path=${encodeURIComponent(filePath)}`, { + responseType: 'blob' + }); + + // 创建下载链接 + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + return true; + } catch (err) { + error.value = err.response?.data?.message || err.message; + throw err; + } + }; + + // 切换文件选择状态 + const toggleFileSelection = (filePath) => { + if (selectedFiles.value.has(filePath)) { + selectedFiles.value.delete(filePath); + } else { + selectedFiles.value.add(filePath); + } + }; + + // 清除所有选择 + const clearSelection = () => { + selectedFiles.value.clear(); + }; + + return { + // 状态 + isConnected, + currentServer, + currentPath, + files, + loading, + error, + selectedFiles, + + // 操作 + connectSftp, + disconnectSftp, + listDirectory, + enterDirectory, + goToParentDirectory, + readFile, + saveFile, + createDirectory, + deleteItem, + renameItem, + uploadFile, + downloadFile, + toggleFileSelection, + clearSelection + }; +}); \ No newline at end of file diff --git a/frontend/src/stores/terminal.js b/frontend/src/stores/terminal.js new file mode 100644 index 0000000..51ba9b2 --- /dev/null +++ b/frontend/src/stores/terminal.js @@ -0,0 +1,200 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { io } from 'socket.io-client' + +export const useTerminalStore = defineStore('terminal', () => { + const socket = ref(null) + const isConnected = ref(false) + const isConnecting = ref(false) + const currentConnection = ref(null) + const terminalOutput = ref('') + const connectionError = ref(null) + + // 连接到Socket.IO服务器 + const connectSocket = (token) => { + return new Promise((resolve, reject) => { + socket.value = io({ + auth: { + token + } + }) + + socket.value.on('connect', () => { + console.log('Socket连接成功') + // 连接成功后立即发送认证 + socket.value.emit('authenticate', token) + }) + + socket.value.on('authenticated', (data) => { + if (data.success) { + console.log('Socket认证成功') + // 认证成功后设置Socket事件监听器 + setupSocketListeners() + resolve() // 认证成功,解析Promise + } else { + console.error('Socket认证失败:', data.error) + reject(new Error('Socket认证失败')) + } + }) + + socket.value.on('connect_error', (error) => { + console.error('Socket连接错误:', error) + reject(error) + }) + + // 设置连接超时 + setTimeout(() => { + if (!socket.value.connected) { + reject(new Error('Socket连接超时')) + } + }, 10000) + }) + } + + // 监听Socket事件(在Socket连接建立后设置) + const setupSocketListeners = () => { + if (!socket.value || socket.value._listenersSetup) return + + // 标记已设置监听器,避免重复设置 + socket.value._listenersSetup = true + + socket.value.on('ssh-connected', () => { + isConnected.value = true + isConnecting.value = false + connectionError.value = null + console.log('SSH连接成功') + }) + + socket.value.on('ssh-data', (data) => { + // 确保数据是字符串格式 + const output = typeof data === 'string' ? data : String(data) + terminalOutput.value += output + + // 限制输出长度,防止内存溢出 + if (terminalOutput.value.length > 50000) { + terminalOutput.value = terminalOutput.value.slice(-25000) + } + }) + + socket.value.on('ssh-error', (data) => { + isConnecting.value = false + isConnected.value = false + connectionError.value = data.error + console.error('SSH连接错误:', data.error) + }) + + socket.value.on('ssh-closed', () => { + isConnected.value = false + isConnecting.value = false + currentConnection.value = null + console.log('SSH连接已关闭') + }) + + socket.value.on('disconnect', () => { + isConnected.value = false + isConnecting.value = false + console.log('Socket连接断开') + // 清除监听器标记 + socket.value._listenersSetup = false + }) + } + + // 连接到SSH服务器 + const connectToServer = async (serverId) => { + if (!socket.value) { + throw new Error('Socket未连接') + } + + isConnecting.value = true + connectionError.value = null + terminalOutput.value = '' + currentConnection.value = { serverId, type: 'saved' } + + socket.value.emit('connect-ssh', serverId) + } + + // 快速连接 + const quickConnect = (connectionInfo) => { + if (!socket.value) { + throw new Error('Socket未连接') + } + + isConnecting.value = true + connectionError.value = null + terminalOutput.value = '' + currentConnection.value = { ...connectionInfo, type: 'quick' } + + socket.value.emit('quick-connect', connectionInfo) + } + + // 发送命令到终端 + const sendCommand = (command) => { + if (socket.value && isConnected.value) { + socket.value.emit('send-command', command) + } + } + + // 发送输入到终端 + const sendInput = (input) => { + if (socket.value && isConnected.value) { + socket.value.emit('ssh-input', input) + } + } + + // 调整终端大小 + const resizeTerminal = (size) => { + if (socket.value && isConnected.value) { + socket.value.emit('resize', size) + } + } + + // 断开连接 + const disconnect = () => { + if (socket.value) { + socket.value.emit('disconnect-ssh') + } + + isConnected.value = false + isConnecting.value = false + currentConnection.value = null + terminalOutput.value = '' + connectionError.value = null + } + + // 清除终端输出 + const clearOutput = () => { + terminalOutput.value = '' + } + + // 断开Socket连接 + const disconnectSocket = () => { + if (socket.value) { + socket.value.disconnect() + socket.value = null + } + + isConnected.value = false + isConnecting.value = false + currentConnection.value = null + terminalOutput.value = '' + connectionError.value = null + } + + return { + socket, + isConnected, + isConnecting, + currentConnection, + terminalOutput, + connectionError, + connectSocket, + connectToServer, + quickConnect, + sendCommand, + sendInput, + resizeTerminal, + disconnect, + clearOutput, + disconnectSocket + } +}) \ No newline at end of file diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..6e90c79 --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,474 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..faa9402 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,146 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/QuickConnect.vue b/frontend/src/views/QuickConnect.vue new file mode 100644 index 0000000..436e986 --- /dev/null +++ b/frontend/src/views/QuickConnect.vue @@ -0,0 +1,401 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..313b400 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,182 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Terminal.vue b/frontend/src/views/Terminal.vue new file mode 100644 index 0000000..de88542 --- /dev/null +++ b/frontend/src/views/Terminal.vue @@ -0,0 +1,1124 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..7a7f353 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + }, + '/socket.io': { + target: 'http://localhost:3000', + changeOrigin: true, + ws: true + } + } + } +}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1cee7f0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,372 @@ +{ + "name": "webssh-xshell", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webssh-xshell", + "version": "1.0.0", + "devDependencies": { + "concurrently": "^8.2.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbf412e --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "webssh-xshell", + "version": "1.0.0", + "description": "Web-based Xshell clone with user authentication", + "scripts": { + "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", + "dev:backend": "cd backend && npm run dev", + "dev:frontend": "cd frontend && npm run dev", + "build": "npm run build:frontend", + "build:frontend": "cd frontend && npm run build", + "start": "cd backend && npm start" + }, + "devDependencies": { + "concurrently": "^8.2.2" + } +} \ No newline at end of file diff --git a/ysxq.txt b/ysxq.txt new file mode 100644 index 0000000..7ed69d8 --- /dev/null +++ b/ysxq.txt @@ -0,0 +1,152 @@ +现在想要开发一个web版本的xshell工具,功能模仿xshell,但是要集成注册和登录 + + +一、 用户与权限系统 (User Center) +这是最基础的门槛,确保数据隔离。 + +P0 - 注册与登录 + +用户注册(邮箱/手机号 + 密码)。 + +用户登录(JWT 或 Session 鉴权)。 + +密码找回/重置。 + +二、 资产管理 (Asset Management) +模仿 Xshell 左侧的“会话管理器”。 + +P0 - 服务器(会话)增删改查 + +添加服务器:主机名 (Name)、IP、端口 (Port)、用户名 (User)。 + +认证方式选择:密码认证、私钥 (PEM/PPK) 认证。 + +核心安全需求:所有密码和私钥在数据库中必须加密存储(使用 AES-256,密钥与用户 ID 绑定),绝不能明文存储。 + +P1 - 分组管理 (Tree View) + +支持文件夹层级结构(类似 Xshell 左侧树状图)。 + +支持拖拽排序、归类。 + +P1 - 快速连接 + +不保存会话,直接输入 ssh user@ip 进行一次性连接。 + +三、 核心终端功能 (Terminal Core) +这是模仿 Xshell 体验的重中之重,前端通常使用 xterm.js,后端使用 SSH2 库。 + +P0 - SSH 连接与交互 + +基于 WebSocket 实现全双工通信。 + +支持标准的 Shell 操作(输入命令、回显、Ctrl+C 中断等)。 + +支持 Vim/Top 等全屏应用的正确渲染。 + +P0 - 终端自适应 (Resize) + +浏览器窗口大小改变时,自动同步发送 window-change 信号给后端,防止 Vim 错位。 + +P1 - 复制粘贴 + +选中文本自动复制(模仿 Xshell 行为)。 + +右键菜单粘贴,或 Shift+Insert 粘贴。 + +P1 - 多标签页 (Tabs) + +支持同时打开多个 SSH 会话,顶部 Tab 切换。 + +支持分屏显示(Split View,左右或上下分屏)。 + +P2 - 外观定制 + +字号调整、字体设置(如 Fira Code)。 + +配色方案切换(黑客绿、Solarized、Monokai 等)。 + +四、 文件传输 (SFTP) +Xshell 通常配合 Xftp 使用,Web 版需要集成轻量级文件管理。 + +P1 - 侧边栏文件树 + +在终端侧边展示当前目录的文件列表。 + +支持双击进入目录、返回上级。 + +P1 - 上传/下载 + +上传:支持拖拽本地文件到浏览器窗口,自动上传到当前目录。 + +下载:右键文件 -> 下载。 + +P2 - 在线编辑 + +双击文本文件,弹窗进行代码编辑,保存后自动上传覆盖(通过 SFTP 协议)。 + +五、 高级辅助功能 (Advanced Features) +这是让你的工具“像 Xshell”的关键。 + +P1 - 保持连接 (Keep-Alive) + +发送心跳包,防止防火墙或 Nginx 切断 WebSocket 连接。 + +断线自动重连机制。 + +P2 - 快捷命令 (Snippets) + +侧边栏或底部栏预设常用命令(如 tail -f logs, docker ps)。 + +点击按钮直接发送命令到终端。 + +P2 - 脚本录制与回放 + +记录操作日志(Asciinema 格式),用于审计。 + +推荐技术架构 +为了实现上述需求,建议的技术选型如下: + +前端 (Frontend) + +框架: Vue3。 + +终端组件:xterm.js (业界标准,VS Code 也在用)。 + +UI 库:Ant Design 或 Element Plus (用于布局和表单)。 + +后端 (Backend) + +语言: Node.js (生态好,SSH 库丰富)。 + +SSH 库: + +Go: golang.org/x/crypto/ssh + +Node.js: ssh2 + +WebSocket: + +Node.js: socket.io 或 ws + +Web 框架: NestJS/Express (Node)。 + +数据库 + +MySQL:存储用户信息、资产信息。 + +Redis:存储临时的 WebSocket Session 状态。 + + +============================================ + +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +方法2:使用在线工具生成 + +访问 https://www.random.org/strings/ +生成32个字符的随机字符串(字母和数字组合) +方法3:手动创建32字节字符串 + +使用任何32个字符的组合,例如: +k3yF0rW3bSSH2024Pr0j3ctS3cur1ty! +MyS3cur3K3yF0rW3bSSHPr0j3ct2024 \ No newline at end of file