diff --git a/Dockerfile b/Dockerfile index 28071b5..c88dcab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ RUN npm install # 暴露应用程序的端口 EXPOSE 3000 # 运行应用程序 -CMD ["node", "server.js"] \ No newline at end of file +CMD ["npm", "start"] \ No newline at end of file diff --git a/Issue/issue.md b/Issue/issue.md index 9794e51..d9ac23a 100644 --- a/Issue/issue.md +++ b/Issue/issue.md @@ -150,7 +150,7 @@ proxy: ``` -#### 13、解决国内服务器上hubcmdui无法使用http代理请求 +#### 13、[项目已实现]解决国内服务器上hubcmdui无法使用http代理请求 简单的讲,需要解决两个问题: 1. dns污染,请自行搭建smartdns服务 2. 修改axios.get相关代码 diff --git a/hubcmdui/.env.example b/hubcmdui/.env.example new file mode 100644 index 0000000..ece3cb6 --- /dev/null +++ b/hubcmdui/.env.example @@ -0,0 +1,21 @@ +# HubCmdUI 环境变量配置示例 +# 复制此文件为 .env 并修改相应的值 + +# HTTP 代理配置 (用于所有出站请求) +# HTTP_PROXY=http://proxy.example.com:8080 +# HTTPS_PROXY=https://proxy.example.com:8080 +# NO_PROXY=localhost,127.0.0.1,.local + +# 如果代理需要用户名和密码认证 +# HTTP_PROXY=http://username:password@proxy.example.com:8080 +# HTTPS_PROXY=https://username:password@proxy.example.com:8080 + + +# 服务器配置 +PORT=3000 + +# 会话密钥 +SESSION_SECRET=OhTq3faqSKoxbV%NJV + +# 服务器端口 +PORT=3000 diff --git a/hubcmdui/README.md b/hubcmdui/README.md index 4d29144..f0f0096 100644 --- a/hubcmdui/README.md +++ b/hubcmdui/README.md @@ -26,22 +26,27 @@ --- -## 📝 源码构建运行 -#### 1. 克隆项目 -```bash -git clone git@github.com:dqzboy/Docker-Proxy.git -``` +## 📝 源码运行 -#### 2. 安装依赖 ```bash +# 克隆项目并启动 +git clone git@github.com:dqzboy/Docker-Proxy.git cd Docker-Proxy/hubcmdui npm install +npm start ``` -#### 3. 启动服务 -```bash -node server.js -``` +系统会自动检测并完成: +- ✅ 依赖包安装(如果需要) +- ✅ SQLite数据库初始化(如果需要) +- ✅ 启动服务 + + +### 访问系统 + +- **主页**: http://localhost:3000 +- **管理面板**: http://localhost:3000/admin +- **默认账户**: root / admin@123 ## 📦 Docker 方式运行 @@ -52,7 +57,7 @@ docker pull dqzboy/hubcmd-ui:latest #### 2. 运行 hubcmd-ui 容器 ```bash -docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data/config:/app/data -v ./data/docs:/app/documentation -v ./data/user/users.json:/app/users.json -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui +docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data:/app/data -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui ``` - `-v` 参数解释:左边是宿主机上的 Docker socket 文件路径,右边是容器内的映射路径 @@ -71,6 +76,56 @@ docker logs -f [容器ID或名称] --- +## 🌐 代理配置 + +支持通过环境变量配置 HTTP 代理,用于所有出站网络请求。 + +### 环境变量配置 + +```bash +# HTTP 代理配置 +export HTTP_PROXY=http://proxy.example.com:8080 +export HTTPS_PROXY=https://proxy.example.com:8080 +export NO_PROXY=localhost,127.0.0.1,.local + +# 启动应用 +npm start +``` + +### Docker 部署代理配置 + +```bash +docker run -d \ + -e HTTP_PROXY=http://proxy.example.com:8080 \ + -e HTTPS_PROXY=https://proxy.example.com:8080 \ + -e NO_PROXY=localhost,127.0.0.1,.local \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v ./data:/app/data \ + -p 30080:3000 \ + dqzboy/hubcmd-ui +``` + +### Docker Compose 代理配置 + +```yaml +version: '3.8' +services: + hubcmdui: + image: dqzboy/hubcmd-ui + environment: + - HTTP_PROXY=http://proxy.example.com:8080 + - HTTPS_PROXY=https://proxy.example.com:8080 + - NO_PROXY=localhost,127.0.0.1,.local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # SQLite数据库文件 + - ./data:/app/data + ports: + - "30080:3000" +``` + +--- + ## UI界面 - 默认容器监听`3000`端口,映射宿主机端口`30080` @@ -141,6 +196,41 @@ docker logs -f [容器ID或名称] --- +## 🚀 系统特性 + +### 数据存储优化 +- **SQLite数据库**: 所有数据统一存储在SQLite数据库中 +- **Session管理**: 使用数据库存储用户会话,自动清理过期会话 +- **配置管理**: 系统配置、用户数据、文档内容统一存储 +- **零文件依赖**: 不再依赖JSON文件存储,简化部署和维护 + +### 功能特性 +- 🔐 **用户认证**: 基于数据库的用户管理系统 +- ⚙️ **配置管理**: 灵活的系统配置和菜单管理 +- 📚 **文档系统**: 内置Markdown文档管理 +- 🔍 **镜像搜索**: Docker Hub镜像搜索和代理 +- 📊 **系统监控**: 实时系统状态监控 +- 🎨 **响应式界面**: 现代化的Web管理界面 + +## 📁 项目结构 + +``` +hubcmdui/ +├── database/ # SQLite数据库相关 +│ └── database.js # 数据库管理模块 +├── services/ # 业务服务层 +│ ├── configServiceDB.js # 配置服务 +│ ├── userServiceDB.js # 用户服务 +│ └── documentationServiceDB.js # 文档服务 +├── routes/ # API路由 +├── web/ # 前端静态文件 +├── middleware/ # 中间件 +└── data/ # 数据目录(SQLite文件) + └── app.db # SQLite数据库文件 +``` + +--- + ## 🫶 赞助 如果你觉得这个项目对你有帮助,请给我点个Star。并且情况允许的话,可以给我一点点支持,总之非常感谢支持😊 diff --git a/hubcmdui/app.js b/hubcmdui/app.js index 16a68c5..17a7dff 100644 --- a/hubcmdui/app.js +++ b/hubcmdui/app.js @@ -20,8 +20,8 @@ const { requireLogin, sessionActivity, sanitizeRequestBody, securityHeaders } = console.log(`服务器启动,时间戳: ${global.serverStartTime}`); logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`); -// 添加 session 文件存储模块 - 先导入session-file-store并创建对象 -const FileStore = require('session-file-store')(session); +// 使用 SQLite 存储 session - 替代文件存储 +const SQLiteStore = require('connect-sqlite3')(session); // 确保目录结构存在 ensureDirectoriesExist().catch(err => { @@ -43,7 +43,7 @@ app.use(sessionActivity); app.use(sanitizeRequestBody); app.use(securityHeaders); -// 会话配置 +// 会话配置 - 使用SQLite存储 app.use(session({ secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key', resave: false, @@ -52,9 +52,10 @@ app.use(session({ secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 // 24小时 }, - store: new FileStore({ - path: path.join(__dirname, 'data', 'sessions'), - ttl: 86400 + store: new SQLiteStore({ + db: 'app.db', + dir: path.join(__dirname, 'data'), + table: 'sessions' }) })); @@ -118,23 +119,67 @@ server.listen(PORT, async () => { try { // 确保目录存在 await ensureDirectoriesExist(); + + // 启动Session清理任务 + await startSessionCleanupTask(); + logger.success('系统初始化完成'); } catch (error) { logger.error('系统初始化失败:', error); } }); -// 注册进程事件处理 -process.on('SIGINT', () => { +// 启动定期清理过期会话的任务 +async function startSessionCleanupTask() { + const database = require('./database/database'); + + // 立即清理一次 + try { + await database.cleanExpiredSessions(); + } catch (error) { + logger.error('清理过期会话失败:', error); + } + + // 每小时清理一次过期会话 + setInterval(async () => { + try { + await database.cleanExpiredSessions(); + } catch (error) { + logger.error('定期清理过期会话失败:', error); + } + }, 60 * 60 * 1000); // 1小时 +} + +// 监听进程退出事件,确保数据库连接正确关闭 +process.on('SIGINT', async () => { + logger.info('收到SIGINT信号,正在关闭服务器...'); + const database = require('./database/database'); + await database.close(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + logger.info('收到SIGTERM信号,正在关闭服务器...'); + const database = require('./database/database'); + await database.close(); + process.exit(0); +}); + +// 注册进程事件处理 - 优雅关闭 +process.on('SIGINT', async () => { logger.info('接收到中断信号,正在关闭服务...'); + const database = require('./database/database'); + await database.close(); server.close(() => { logger.info('服务器已关闭'); process.exit(0); }); }); -process.on('SIGTERM', () => { +process.on('SIGTERM', async () => { logger.info('接收到终止信号,正在关闭服务...'); + const database = require('./database/database'); + await database.close(); server.close(() => { logger.info('服务器已关闭'); process.exit(0); @@ -169,14 +214,14 @@ function registerRoutes(app) { app.use('/api', authRouter); logger.info('认证路由已注册'); - // 配置路由 - 函数式注册 + // 配置路由 - 使用 Express Router const configRouter = require('./routes/config'); - if (typeof configRouter === 'function') { - logger.info('配置路由是一个函数,正在注册...'); - configRouter(app); + if (configRouter && typeof configRouter === 'object') { + logger.info('配置路由是一个 Router 对象,正在注册...'); + app.use('/api/config', configRouter); logger.info('配置路由已注册'); } else { - logger.error('配置路由不是一个函数,无法注册', typeof configRouter); + logger.error('配置路由不是一个有效的 Router 对象,无法注册', typeof configRouter); } logger.success('✓ 所有路由已注册'); diff --git a/hubcmdui/cleanup.js b/hubcmdui/cleanup.js index b6853d2..7f48435 100644 --- a/hubcmdui/cleanup.js +++ b/hubcmdui/cleanup.js @@ -25,11 +25,33 @@ process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); // 优雅退出函数 -function gracefulShutdown() { +async function gracefulShutdown() { logger.info('接收到退出信号,正在关闭...'); // 这里可以添加清理代码,如关闭数据库连接等 try { + // 关闭HTTP代理服务 + try { + const httpProxyService = require('./services/httpProxyService'); + if (httpProxyService && httpProxyService.isRunning) { + logger.info('正在关闭HTTP代理服务...'); + await httpProxyService.stop(); + } + } catch (err) { + logger.debug('HTTP代理服务未运行,跳过清理'); + } + + // 关闭数据库连接 + try { + const database = require('./database/database'); + if (database) { + logger.info('正在关闭数据库连接...'); + await database.close(); + } + } catch (err) { + logger.debug('数据库未连接,跳过清理'); + } + // 关闭任何可能的资源 try { const docker = require('./services/dockerService').getDockerConnection(); diff --git a/hubcmdui/compatibility-layer.js b/hubcmdui/compatibility-layer.js index 3c10aa7..d6eccf5 100644 --- a/hubcmdui/compatibility-layer.js +++ b/hubcmdui/compatibility-layer.js @@ -202,7 +202,7 @@ module.exports = function(app) { // 文档接口 app.get('/api/documentation', async (req, res) => { try { - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const documents = await docService.getPublishedDocuments(); res.json(documents); } catch (error) { @@ -324,7 +324,7 @@ module.exports = function(app) { // 获取单个文档接口 app.get('/api/documentation/:id', async (req, res) => { try { - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const document = await docService.getDocument(req.params.id); // 如果文档不是发布状态,只有已登录用户才能访问 @@ -345,7 +345,7 @@ module.exports = function(app) { // 文档列表接口 app.get('/api/documentation-list', requireLogin, async (req, res) => { try { - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const documents = await docService.getDocumentationList(); res.json(documents); } catch (error) { @@ -586,28 +586,21 @@ module.exports = function(app) { return res.status(401).json({ error: '验证码错误' }); } - const userService = require('./services/userService'); - const users = await userService.getUsers(); - const user = users.users.find(u => u.username === username); + const userServiceDB = require('./services/userServiceDB'); + const user = await userServiceDB.validateUser(username, password); if (!user) { logger.warn(`User ${username} not found`); return res.status(401).json({ error: '用户名或密码错误' }); } - const bcrypt = require('bcrypt'); - if (bcrypt.compareSync(password, user.password)) { - req.session.user = { username: user.username }; - - // 更新用户登录信息 - await userService.updateUserLoginInfo(username); - - logger.info(`User ${username} logged in successfully`); - res.json({ success: true }); - } else { - logger.warn(`Login failed for user: ${username}`); - res.status(401).json({ error: '用户名或密码错误' }); - } + req.session.user = { username: user.username }; + + // 更新用户登录信息 + await userServiceDB.updateUserLoginInfo(username); + + logger.info(`User ${username} logged in successfully`); + res.json({ success: true }); } catch (error) { logger.error('登录失败:', error); res.status(500).json({ error: '登录处理失败', details: error.message }); @@ -781,7 +774,7 @@ module.exports = function(app) { app.get('/api/documents', requireLogin, async (req, res) => { try { logger.info('兼容层处理获取文档列表请求'); - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const documents = await docService.getDocumentationList(); res.json(documents); } catch (err) { @@ -794,7 +787,7 @@ module.exports = function(app) { app.get('/api/documents/:id', async (req, res) => { try { logger.info(`兼容层处理获取文档请求: ${req.params.id}`); - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const document = await docService.getDocument(req.params.id); // 如果文档不是发布状态,只有已登录用户才能访问 @@ -817,7 +810,7 @@ module.exports = function(app) { try { logger.info(`兼容层处理更新文档请求: ${req.params.id}`); const { title, content, published } = req.body; - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); // 检查必需参数 if (!title) { @@ -839,7 +832,7 @@ module.exports = function(app) { try { logger.info('兼容层处理创建文档请求'); const { title, content, published } = req.body; - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); // 检查必需参数 if (!title) { @@ -861,7 +854,7 @@ module.exports = function(app) { app.delete('/api/documents/:id', requireLogin, async (req, res) => { try { logger.info(`兼容层处理删除文档请求: ${req.params.id}`); - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); await docService.deleteDocument(req.params.id); res.json({ success: true, message: '文档已删除' }); @@ -875,7 +868,7 @@ module.exports = function(app) { app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => { try { logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`); - const docService = require('./services/documentationService'); + const docService = require('./services/documentationServiceDB'); const result = await docService.toggleDocumentPublish(req.params.id); res.json({ @@ -924,8 +917,8 @@ module.exports = function(app) { // 用户信息接口 app.get('/api/user-info', requireLogin, async (req, res) => { try { - const userService = require('./services/userService'); - const userStats = await userService.getUserStats(req.session.user.username); + const userServiceDB = require('./services/userServiceDB'); + const userStats = await userServiceDB.getUserStats(req.session.user.username); res.json(userStats); } catch (error) { @@ -944,8 +937,8 @@ module.exports = function(app) { } try { - const userService = require('./services/userService'); - await userService.changePassword(username, currentPassword, newPassword); + const userServiceDB = require('./services/userServiceDB'); + await userServiceDB.changePassword(username, currentPassword, newPassword); res.json({ success: true, message: '密码修改成功' }); } catch (error) { logger.error(`用户 ${username} 修改密码失败:`, error); diff --git a/hubcmdui/config/menu.json b/hubcmdui/config/menu.json deleted file mode 100644 index 0637a08..0000000 --- a/hubcmdui/config/menu.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/hubcmdui/data/config.json b/hubcmdui/data/config.json deleted file mode 100644 index 968d10a..0000000 --- a/hubcmdui/data/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "logo": "", - "menuItems": [ - { - "text": "首页", - "link": "", - "newTab": false - }, - { - "text": "GitHub", - "link": "https://github.com/dqzboy/Docker-Proxy", - "newTab": true - }, - { - "text": "VPS推荐", - "link": "https://dqzboy.github.io/proxyui/racknerd", - "newTab": true - } - ], - "monitoringConfig": { - "notificationType": "telegram", - "webhookUrl": "", - "telegramToken": "", - "telegramChatId": "", - "monitorInterval": 60, - "isEnabled": false - }, - "proxyDomain": "github.dqzboy.Docker-Proxy" -} \ No newline at end of file diff --git a/hubcmdui/database/database.js b/hubcmdui/database/database.js new file mode 100644 index 0000000..d1b220f --- /dev/null +++ b/hubcmdui/database/database.js @@ -0,0 +1,390 @@ +/** + * SQLite 数据库管理模块 + */ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const fs = require('fs').promises; +const logger = require('../logger'); +const bcrypt = require('bcrypt'); + +// 数据库文件路径 +const DB_PATH = path.join(__dirname, '../data/app.db'); + +class Database { + constructor() { + this.db = null; + } + + /** + * 初始化数据库连接 + */ + async connect() { + try { + // 确保数据目录存在 + const dbDir = path.dirname(DB_PATH); + await fs.mkdir(dbDir, { recursive: true }); + + return new Promise((resolve, reject) => { + this.db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + logger.error('数据库连接失败:', err); + reject(err); + } else { + logger.info('SQLite 数据库连接成功'); + resolve(); + } + }); + }); + } catch (error) { + logger.error('初始化数据库失败:', error); + throw error; + } + } + + /** + * 创建数据表 + */ + async createTables() { + const tables = [ + // 用户表 + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + login_count INTEGER DEFAULT 0, + last_login DATETIME + )`, + + // 配置表 + `CREATE TABLE IF NOT EXISTS configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE NOT NULL, + value TEXT NOT NULL, + type TEXT DEFAULT 'string', + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // 文档表 + `CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + doc_id TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + published BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // 系统日志表 + `CREATE TABLE IF NOT EXISTS system_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL, + message TEXT NOT NULL, + details TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // Session表 - 用于存储用户会话 + `CREATE TABLE IF NOT EXISTS sessions ( + sid TEXT PRIMARY KEY, + sess TEXT NOT NULL, + expire DATETIME NOT NULL + )`, + + // 菜单项表 - 用于存储导航菜单配置 + `CREATE TABLE IF NOT EXISTS menu_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + link TEXT NOT NULL, + new_tab BOOLEAN DEFAULT 0, + sort_order INTEGER DEFAULT 0, + enabled BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )` + ]; + + for (const sql of tables) { + await this.run(sql); + } + + logger.info('数据表创建完成'); + } + + /** + * 执行SQL语句 + */ + async run(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.run(sql, params, function(err) { + if (err) { + logger.error('SQL执行失败:', err); + reject(err); + } else { + resolve({ id: this.lastID, changes: this.changes }); + } + }); + }); + } + + /** + * 查询单条记录 + */ + async get(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.get(sql, params, (err, row) => { + if (err) { + logger.error('SQL查询失败:', err); + reject(err); + } else { + resolve(row); + } + }); + }); + } + + /** + * 查询多条记录 + */ + async all(sql, params = []) { + return new Promise((resolve, reject) => { + this.db.all(sql, params, (err, rows) => { + if (err) { + logger.error('SQL查询失败:', err); + reject(err); + } else { + resolve(rows); + } + }); + }); + } + + + + /** + * 初始化默认管理员用户 + */ + async createDefaultAdmin() { + try { + const adminUser = await this.get('SELECT id FROM users WHERE username = ?', ['root']); + + if (!adminUser) { + const hashedPassword = await bcrypt.hash('admin@123', 10); + await this.run( + 'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)', + ['root', hashedPassword, new Date().toISOString(), 0, null] + ); + logger.info('默认管理员用户创建成功: root/admin@123'); + } + } catch (error) { + logger.error('创建默认管理员用户失败:', error); + } + } + + /** + * 创建默认文档 + */ + async createDefaultDocuments() { + try { + const docCount = await this.get('SELECT COUNT(*) as count FROM documents'); + + if (docCount.count === 0) { + const defaultDocs = [ + { + doc_id: 'welcome', + title: '欢迎使用 Docker 镜像代理加速系统', + content: `## 系统介绍 + +这是一个基于 Nginx 的 Docker 镜像代理加速系统,可以帮助您加速 Docker 镜像的下载和部署。 + +## 主要功能 + +- 🚀 **镜像加速**: 提供多个 Docker 镜像仓库的代理加速 +- 🔧 **配置管理**: 简单易用的 Web 管理界面 +- 📊 **监控统计**: 实时监控代理服务状态 +- 📖 **文档管理**: 内置文档系统,方便管理和分享 + +## 快速开始 + +1. 访问管理面板进行基础配置 +2. 配置 Docker 客户端使用代理地址 +3. 开始享受加速的镜像下载体验 + +## 更多信息 + +如需更多帮助,请查看项目文档或访问 GitHub 仓库。`, + published: 1 + }, + { + doc_id: 'docker-config', + title: 'Docker 客户端配置指南', + content: `## 配置说明 + +使用本代理服务需要配置 Docker 客户端的镜像仓库地址。 + +## Linux/macOS 配置 + +编辑或创建 \`/etc/docker/daemon.json\` 文件: + +\`\`\`json +{ + "registry-mirrors": [ + "http://your-proxy-domain.com" + ] +} +\`\`\` + +重启 Docker 服务: +\`\`\`bash +sudo systemctl restart docker +\`\`\` + +## Windows 配置 + +在 Docker Desktop 设置中: +1. 打开 Settings -> Docker Engine +2. 添加配置到 JSON 文件中 +3. 点击 "Apply & Restart" + +## 验证配置 + +运行以下命令验证配置是否生效: +\`\`\`bash +docker info +\`\`\` + +在输出中查看 "Registry Mirrors" 部分。`, + published: 1 + } + ]; + + for (const doc of defaultDocs) { + await this.run( + 'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', + [doc.doc_id, doc.title, doc.content, doc.published, new Date().toISOString(), new Date().toISOString()] + ); + } + } + } catch (error) { + logger.error('创建默认文档失败:', error); + } + } + + /** + * 检查数据库是否已经初始化 + */ + async isInitialized() { + try { + // 先检查是否有用户表 + const tableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'"); + if (!tableExists) { + return false; + } + + // 检查是否有初始化标记 + const configTableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='configs'"); + if (configTableExists) { + const initFlag = await this.get('SELECT value FROM configs WHERE key = ?', ['db_initialized']); + if (initFlag) { + return true; + } + } + + // 检查是否有用户数据 + const userCount = await this.get('SELECT COUNT(*) as count FROM users'); + return userCount && userCount.count > 0; + } catch (error) { + // 如果查询失败,认为数据库未初始化 + return false; + } + } + + /** + * 标记数据库已初始化 + */ + async markAsInitialized() { + try { + await this.run( + 'INSERT OR REPLACE INTO configs (key, value, type, description) VALUES (?, ?, ?, ?)', + ['db_initialized', 'true', 'boolean', '数据库初始化标记'] + ); + logger.info('数据库已标记为已初始化'); + } catch (error) { + logger.error('标记数据库初始化状态失败:', error); + } + } + + /** + * 关闭数据库连接 + */ + async close() { + return new Promise((resolve, reject) => { + if (this.db) { + this.db.close((err) => { + if (err) { + logger.error('关闭数据库连接失败:', err); + reject(err); + } else { + logger.info('数据库连接已关闭'); + resolve(); + } + }); + } else { + resolve(); + } + }); + } + + /** + * 清理过期的会话 + */ + async cleanExpiredSessions() { + try { + const result = await this.run( + 'DELETE FROM sessions WHERE expire < ?', + [new Date().toISOString()] + ); + if (result.changes > 0) { + logger.info(`清理了 ${result.changes} 个过期会话`); + } + } catch (error) { + logger.error('清理过期会话失败:', error); + } + } + + /** + * 创建默认菜单项 + */ + async createDefaultMenuItems() { + try { + const menuCount = await this.get('SELECT COUNT(*) as count FROM menu_items'); + + if (menuCount.count === 0) { + const defaultMenuItems = [ + { text: '控制台', link: '/admin', new_tab: 0, sort_order: 1 }, + { text: '镜像搜索', link: '/', new_tab: 0, sort_order: 2 }, + { text: '文档', link: '/docs', new_tab: 0, sort_order: 3 }, + { text: 'GitHub', link: 'https://github.com/dqzboy/hubcmdui', new_tab: 1, sort_order: 4 } + ]; + + for (const item of defaultMenuItems) { + await this.run( + 'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', + [item.text, item.link, item.new_tab, item.sort_order, 1, new Date().toISOString(), new Date().toISOString()] + ); + } + } + } catch (error) { + logger.error('创建默认菜单项失败:', error); + } + } +} + +// 创建数据库实例 +const database = new Database(); + +module.exports = database; diff --git a/hubcmdui/docker-compose.yaml b/hubcmdui/docker-compose.yaml index 12d64e2..8510f7c 100644 --- a/hubcmdui/docker-compose.yaml +++ b/hubcmdui/docker-compose.yaml @@ -4,13 +4,15 @@ services: container_name: hubcmd-ui image: dqzboy/hubcmd-ui:latest restart: always + environment: + # HTTP代理配置(可选) + #- HTTP_PROXY=http://proxy.example.com:8080 + #- HTTPS_PROXY=https://proxy.example.com:8080 + #- NO_PROXY=localhost,127.0.0.1,.local + volumes: - /var/run/docker.sock:/var/run/docker.sock - # 配置目录 - - ./data/config:/app/data - # 文档目录 - - ./data/docs:/app/documentation - # 用户数据,需提前在宿主机上创建并把项目users.json内容放入 - - ./data/user/users.json:/app/users.json + # SQLite数据库文件 + - ./data:/app/data ports: - 30080:3000 diff --git a/hubcmdui/documentation/.DS_Store b/hubcmdui/documentation/.DS_Store deleted file mode 100644 index 2b64041..0000000 Binary files a/hubcmdui/documentation/.DS_Store and /dev/null differ diff --git a/hubcmdui/documentation/1743542841590.json b/hubcmdui/documentation/1743542841590.json deleted file mode 100644 index 2a9b100..0000000 --- a/hubcmdui/documentation/1743542841590.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Docker 配置镜像加速", - "content": "# Docker 配置镜像加速\n\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```", - "published": true, - "createdAt": "2025-04-01T21:27:21.591Z", - "updatedAt": "2025-05-10T06:21:33.539Z" -} \ No newline at end of file diff --git a/hubcmdui/documentation/1743543376091.json b/hubcmdui/documentation/1743543376091.json deleted file mode 100644 index 43907e5..0000000 --- a/hubcmdui/documentation/1743543376091.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Containerd 配置镜像加速", - "content": "# Containerd 配置镜像加速\n\n\n* `/etc/containerd/config.toml`,添加如下的配置:\n\n```bash\n [plugins.\"io.containerd.grpc.v1.cri\".registry]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n```", - "published": true, - "createdAt": "2025-04-01T21:36:16.092Z", - "updatedAt": "2025-05-10T06:21:38.920Z" -} \ No newline at end of file diff --git a/hubcmdui/documentation/1743543400369.json b/hubcmdui/documentation/1743543400369.json deleted file mode 100644 index 8aa066f..0000000 --- a/hubcmdui/documentation/1743543400369.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Podman 配置镜像加速", - "content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```", - "published": true, - "createdAt": "2025-04-01T21:36:40.369Z", - "updatedAt": "2025-05-08T15:16:47.900Z" -} \ No newline at end of file diff --git a/hubcmdui/init-dirs.js b/hubcmdui/init-dirs.js index c784b34..a621681 100644 --- a/hubcmdui/init-dirs.js +++ b/hubcmdui/init-dirs.js @@ -19,14 +19,12 @@ async function ensureDirectoriesExist() { path.join(__dirname, 'logs'), // 图片目录 path.join(__dirname, 'web', 'images'), - // 数据目录 + // 数据目录(SQLite数据库文件) path.join(__dirname, 'data'), // 配置目录 path.join(__dirname, 'config'), // 临时文件目录 path.join(__dirname, 'temp'), - // session 目录 - path.join(__dirname, 'data', 'sessions'), // 文档数据目录 path.join(__dirname, 'web', 'data', 'documentation') ]; diff --git a/hubcmdui/package.json b/hubcmdui/package.json index 1d17120..27e09d6 100644 --- a/hubcmdui/package.json +++ b/hubcmdui/package.json @@ -4,11 +4,12 @@ "description": "Docker镜像代理加速系统", "main": "server.js", "scripts": { - "start": "node server.js", + "start": "node scripts/auto-setup.js", + "start-only": "node server.js", "dev": "nodemon server.js", "test": "jest", - "init": "node scripts/init-system.js", - "setup": "npm install && node scripts/init-system.js && echo '系统安装完成,请使用 npm start 启动服务'" + "init": "node scripts/init-complete.js", + "setup": "npm install && node scripts/init-complete.js && echo 'SQLite系统安装完成,请使用 npm start 启动服务'" }, "keywords": [ "docker", @@ -23,13 +24,15 @@ "bcrypt": "^5.0.1", "body-parser": "^1.20.0", "chalk": "^4.1.2", + "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dockerode": "^3.3.4", + "editor.md": "^1.5.0", "express": "^4.21.2", "express-session": "^1.18.1", "node-cache": "^5.1.2", "p-limit": "^4.0.0", - "session-file-store": "^1.5.0", + "sqlite3": "^5.1.7", "systeminformation": "^5.25.11", "validator": "^13.7.0", "ws": "^8.8.1" diff --git a/hubcmdui/routes/auth.js b/hubcmdui/routes/auth.js index 8541aea..8d013b4 100644 --- a/hubcmdui/routes/auth.js +++ b/hubcmdui/routes/auth.js @@ -1,51 +1,48 @@ /** - * 认证相关路由 + * 认证相关路由 - 使用SQLite数据库 */ const express = require('express'); const router = express.Router(); -const bcrypt = require('bcrypt'); -const userService = require('../services/userService'); +const userServiceDB = require('../services/userServiceDB'); const logger = require('../logger'); const { requireLogin } = require('../middleware/auth'); // 登录验证 router.post('/login', async (req, res) => { const { username, password, captcha } = req.body; + + // 验证码检查 if (req.session.captcha !== parseInt(captcha)) { logger.warn(`Captcha verification failed for user: ${username}`); return res.status(401).json({ error: '验证码错误' }); } try { - const users = await userService.getUsers(); - const user = users.users.find(u => u.username === username); + // 使用数据库认证 + const user = await userServiceDB.validateUser(username, password); if (!user) { - logger.warn(`User ${username} not found`); + logger.warn(`Login failed for user: ${username}`); return res.status(401).json({ error: '用户名或密码错误' }); } - if (bcrypt.compareSync(req.body.password, user.password)) { - req.session.user = { username: user.username }; - - // 更新用户登录信息 - await userService.updateUserLoginInfo(username); - - // 确保服务器启动时间已设置 - if (!global.serverStartTime) { - global.serverStartTime = Date.now(); - logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`); - } - - logger.info(`User ${username} logged in successfully`); - res.json({ - success: true, - serverStartTime: global.serverStartTime - }); - } else { - logger.warn(`Login failed for user: ${username}`); - res.status(401).json({ error: '用户名或密码错误' }); + // 更新登录信息 + await userServiceDB.updateUserLoginInfo(username); + logger.info(`用户 ${username} 登录成功`); + + // 设置会话 + req.session.user = { username: user.username }; + + // 确保服务器启动时间已设置 + if (!global.serverStartTime) { + global.serverStartTime = Date.now(); + logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`); } + + res.json({ + success: true, + serverStartTime: global.serverStartTime + }); } catch (error) { logger.error('登录失败:', error); res.status(500).json({ error: '登录处理失败', details: error.message }); @@ -76,16 +73,9 @@ router.post('/change-password', requireLogin, async (req, res) => { } try { - const { users } = await userService.getUsers(); - const user = users.find(u => u.username === req.session.user.username); - - if (user && bcrypt.compareSync(currentPassword, user.password)) { - user.password = bcrypt.hashSync(newPassword, 10); - await userService.saveUsers(users); - res.json({ success: true }); - } else { - res.status(401).json({ error: 'Invalid current password' }); - } + // 使用SQLite数据库服务修改密码 + await userServiceDB.changePassword(req.session.user.username, currentPassword, newPassword); + res.json({ success: true }); } catch (error) { logger.error('修改密码失败:', error); res.status(500).json({ error: '修改密码失败', details: error.message }); @@ -95,8 +85,7 @@ router.post('/change-password', requireLogin, async (req, res) => { // 获取用户信息 router.get('/user-info', requireLogin, async (req, res) => { try { - const userService = require('../services/userService'); - const userStats = await userService.getUserStats(req.session.user.username); + const userStats = await userServiceDB.getUserStats(req.session.user.username); res.json(userStats); } catch (error) { diff --git a/hubcmdui/routes/config.js b/hubcmdui/routes/config.js index 017f1e7..ceba562 100644 --- a/hubcmdui/routes/config.js +++ b/hubcmdui/routes/config.js @@ -1,63 +1,32 @@ /** - * 配置路由模块 + * 配置路由模块 - 使用SQLite数据库 */ const express = require('express'); const router = express.Router(); -const fs = require('fs').promises; -const path = require('path'); const logger = require('../logger'); const { requireLogin } = require('../middleware/auth'); -const configService = require('../services/configService'); - -// 修改配置文件路径,使用独立的配置文件 -const configFilePath = path.join(__dirname, '../data/config.json'); - -// 默认配置 -const DEFAULT_CONFIG = { - proxyDomain: 'registry-1.docker.io', - logo: '', - theme: 'light' -}; - -// 确保配置文件存在 -async function ensureConfigFile() { - try { - // 确保目录存在 - const dir = path.dirname(configFilePath); - try { - await fs.access(dir); - } catch (error) { - await fs.mkdir(dir, { recursive: true }); - logger.info(`创建目录: ${dir}`); - } - - // 检查文件是否存在 - try { - await fs.access(configFilePath); - const data = await fs.readFile(configFilePath, 'utf8'); - return JSON.parse(data); - } catch (error) { - // 文件不存在或JSON解析错误,创建默认配置 - await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2)); - logger.info(`创建默认配置文件: ${configFilePath}`); - return DEFAULT_CONFIG; - } - } catch (error) { - logger.error(`配置文件操作失败: ${error.message}`); - // 出错时返回默认配置以确保API不会失败 - return DEFAULT_CONFIG; - } -} +const configServiceDB = require('../services/configServiceDB'); // 获取配置 router.get('/config', async (req, res) => { try { - const config = await ensureConfigFile(); - res.json(config); + const config = await configServiceDB.getConfig(); + + // 如果配置为空,使用默认配置 + if (!config || Object.keys(config).length === 0) { + const defaultConfig = configServiceDB.getDefaultConfig(); + res.json(defaultConfig); + } else { + // 合并默认配置和数据库配置,数据库配置优先 + const defaultConfig = configServiceDB.getDefaultConfig(); + const mergedConfig = { ...defaultConfig, ...config }; + res.json(mergedConfig); + } } catch (error) { logger.error('获取配置失败:', error); - // 即使失败也返回默认配置 - res.json(DEFAULT_CONFIG); + // 只在真正出错时返回默认配置 + const defaultConfig = configServiceDB.getDefaultConfig(); + res.json(defaultConfig); } }); @@ -74,19 +43,8 @@ router.post('/config', async (req, res) => { }); } - // 读取现有配置 - let existingConfig; - try { - existingConfig = await ensureConfigFile(); - } catch (error) { - existingConfig = DEFAULT_CONFIG; - } - - // 合并配置 - const mergedConfig = { ...existingConfig, ...newConfig }; - - // 保存到文件 - await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2)); + // 保存配置到数据库 + await configServiceDB.saveConfigs(newConfig); res.json({ success: true, message: '配置已保存' }); } catch (error) { @@ -98,127 +56,70 @@ router.post('/config', async (req, res) => { } }); -// 获取监控配置 -router.get('/monitoring-config', async (req, res) => { - logger.info('收到监控配置请求'); - +// 获取配置 - 兼容旧路由 +router.get('/', async (req, res) => { try { - logger.info('读取监控配置...'); - const config = await configService.getConfig(); + const config = await configServiceDB.getConfig(); + res.json(config); + } catch (error) { + logger.error('读取配置失败:', error); + const defaultConfig = configServiceDB.getDefaultConfig(); + res.json(defaultConfig); + } +}); + +// 保存配置 - 兼容旧路由 +router.post('/', async (req, res) => { + try { + const newConfig = req.body; - if (!config.monitoringConfig) { - logger.info('监控配置不存在,创建默认配置'); - config.monitoringConfig = { - notificationType: 'wechat', - webhookUrl: '', - telegramToken: '', - telegramChatId: '', - monitorInterval: 60, - isEnabled: false - }; - await configService.saveConfig(config); + if (!newConfig || typeof newConfig !== 'object') { + return res.status(400).json({ + error: '无效的配置数据', + details: '配置必须是一个对象' + }); } - logger.info('返回监控配置'); - res.json({ - notificationType: config.monitoringConfig.notificationType || 'wechat', - webhookUrl: config.monitoringConfig.webhookUrl || '', - telegramToken: config.monitoringConfig.telegramToken || '', - telegramChatId: config.monitoringConfig.telegramChatId || '', - monitorInterval: config.monitoringConfig.monitorInterval || 60, - isEnabled: config.monitoringConfig.isEnabled || false + await configServiceDB.saveConfigs(newConfig); + res.json({ success: true, message: '配置已保存' }); + } catch (error) { + logger.error('保存配置失败:', error); + res.status(500).json({ + error: '保存配置失败', + details: error.message }); - } catch (error) { - logger.error('获取监控配置失败:', error); - res.status(500).json({ error: '获取监控配置失败', details: error.message }); } }); -// 保存监控配置 -router.post('/monitoring-config', requireLogin, async (req, res) => { - try { - const { - notificationType, - webhookUrl, - telegramToken, - telegramChatId, - monitorInterval, - isEnabled - } = req.body; - - // 验证必填字段 - if (!notificationType) { - return res.status(400).json({ error: '通知类型不能为空' }); +// 获取菜单项配置 +router.get('/menu-items', async (req, res) => { + try { + const menuItems = await configServiceDB.getMenuItems(); + res.json(menuItems); + } catch (error) { + logger.error('获取菜单项失败:', error); + res.status(500).json({ error: '获取菜单项失败', details: error.message }); } - - // 根据通知类型验证对应的字段 - if (notificationType === 'wechat' && !webhookUrl) { - return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' }); - } - - if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { - return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' }); - } - - // 保存配置 - const config = await configService.getConfig(); - config.monitoringConfig = { - notificationType, - webhookUrl: webhookUrl || '', - telegramToken: telegramToken || '', - telegramChatId: telegramChatId || '', - monitorInterval: parseInt(monitorInterval) || 60, - isEnabled: !!isEnabled - }; - - await configService.saveConfig(config); - logger.info('监控配置已更新'); - - res.json({ success: true, message: '监控配置已保存' }); - } catch (error) { - logger.error('保存监控配置失败:', error); - res.status(500).json({ error: '保存监控配置失败', details: error.message }); - } }); -// 测试通知 -router.post('/test-notification', requireLogin, async (req, res) => { - try { - const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body; - - // 验证参数 - if (!notificationType) { - return res.status(400).json({ error: '通知类型不能为空' }); +// 保存菜单项配置 +router.post('/menu-items', requireLogin, async (req, res) => { + try { + const { menuItems } = req.body; + + if (!Array.isArray(menuItems)) { + return res.status(400).json({ + error: '无效的菜单项数据', + details: '菜单项必须是一个数组' + }); + } + + await configServiceDB.saveMenuItems(menuItems); + res.json({ success: true, message: '菜单项配置已保存' }); + } catch (error) { + logger.error('保存菜单项失败:', error); + res.status(500).json({ error: '保存菜单项失败', details: error.message }); } - - if (notificationType === 'wechat' && !webhookUrl) { - return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' }); - } - - if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) { - return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' }); - } - - // 构造测试消息 - const testMessage = { - title: '测试通知', - content: `这是一条测试通知消息,发送时间: ${new Date().toLocaleString('zh-CN')}`, - type: 'info' - }; - - // 模拟发送通知 - logger.info('发送测试通知:', testMessage); - - // TODO: 实际发送通知的逻辑 - // 这里仅做模拟,实际应用中需要实现真正的通知发送逻辑 - - // 返回成功 - res.json({ success: true, message: '测试通知已发送' }); - } catch (error) { - logger.error('发送测试通知失败:', error); - res.status(500).json({ error: '发送测试通知失败', details: error.message }); - } }); -// 导出路由 -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/hubcmdui/routes/documentation.js b/hubcmdui/routes/documentation.js index e07afec..de8df1a 100644 --- a/hubcmdui/routes/documentation.js +++ b/hubcmdui/routes/documentation.js @@ -1,108 +1,16 @@ /** - * 文档管理路由 + * 文档管理路由 - 使用SQLite数据库 */ const express = require('express'); const router = express.Router(); -const fs = require('fs').promises; -const path = require('path'); const logger = require('../logger'); const { requireLogin } = require('../middleware/auth'); +const documentationServiceDB = require('../services/documentationServiceDB'); -// 确保文档目录存在 -const docsDir = path.join(__dirname, '../documentation'); -const metaDir = path.join(docsDir, 'meta'); - -// 文档文件扩展名 -const FILE_EXTENSION = '.md'; -const META_EXTENSION = '.json'; - -// 确保目录存在 -async function ensureDirectories() { +// 获取所有文档列表(管理员) +router.get('/documents', requireLogin, async (req, res) => { try { - await fs.mkdir(docsDir, { recursive: true }); - await fs.mkdir(metaDir, { recursive: true }); - } catch (err) { - logger.error('创建文档目录失败:', err); - } -} - -// 读取文档元数据 -async function getDocumentMeta(id) { - const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`); - try { - const metaContent = await fs.readFile(metaPath, 'utf8'); - return JSON.parse(metaContent); - } catch (err) { - // 如果元数据文件不存在,返回默认值 - return { - id, - published: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - } -} - -// 保存文档元数据 -async function saveDocumentMeta(id, metadata) { - await ensureDirectories(); - const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`); - const metaContent = JSON.stringify(metadata, null, 2); - await fs.writeFile(metaPath, metaContent); -} - -// 初始化确保目录存在,但不再创建默认文档 -(async function() { - try { - await ensureDirectories(); - logger.info('文档目录已初始化'); - } catch (error) { - logger.error('初始化文档目录失败:', error); - } -})(); - -// 获取所有文档列表 -router.get('/documents', async (req, res) => { - try { - let files; - try { - files = await fs.readdir(docsDir); - } catch (err) { - // 如果目录不存在,尝试创建它并返回空列表 - if (err.code === 'ENOENT') { - await fs.mkdir(docsDir, { recursive: true }); - files = []; - } else { - throw err; - } - } - - const documents = []; - for (const file of files) { - if (file.endsWith(FILE_EXTENSION)) { - const filePath = path.join(docsDir, file); - const stats = await fs.stat(filePath); - const content = await fs.readFile(filePath, 'utf8'); - const id = file.replace(FILE_EXTENSION, ''); - - // 读取元数据 - const metadata = await getDocumentMeta(id); - - // 解析文档元数据(简单实现) - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : id; - - documents.push({ - id, - title, - content, - createdAt: metadata.createdAt || stats.birthtime, - updatedAt: metadata.updatedAt || stats.mtime, - published: metadata.published || false - }); - } - } - + const documents = await documentationServiceDB.getDocumentationList(); res.json(documents); } catch (err) { logger.error('获取文档列表失败:', err); @@ -110,6 +18,39 @@ router.get('/documents', async (req, res) => { } }); +// 获取已发布文档列表(公开) +router.get('/published', async (req, res) => { + try { + const documents = await documentationServiceDB.getPublishedDocuments(); + res.json(documents); + } catch (err) { + logger.error('获取已发布文档列表失败:', err); + res.status(500).json({ error: '获取已发布文档列表失败' }); + } +}); + +// 获取单个文档 +router.get('/documents/:id', async (req, res) => { + try { + const { id } = req.params; + const document = await documentationServiceDB.getDocument(id); + + if (!document) { + return res.status(404).json({ error: '文档不存在' }); + } + + // 如果文档未发布,需要登录权限 + if (!document.published && !req.session.user) { + return res.status(403).json({ error: '没有权限访问该文档' }); + } + + res.json(document); + } catch (err) { + logger.error('获取文档失败:', err); + res.status(500).json({ error: '获取文档失败' }); + } +}); + // 保存文档 router.put('/documents/:id', requireLogin, async (req, res) => { try { @@ -120,38 +61,11 @@ router.put('/documents/:id', requireLogin, async (req, res) => { return res.status(400).json({ error: '标题和内容为必填项' }); } - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - - // 确保文档目录存在 - await ensureDirectories(); - - // 获取或创建元数据 - const metadata = await getDocumentMeta(id); - metadata.title = title; - metadata.published = typeof published === 'boolean' ? published : metadata.published; - metadata.updatedAt = new Date().toISOString(); - - // 保存文档内容 - await fs.writeFile(filePath, content); - - // 保存元数据 - await saveDocumentMeta(id, metadata); - - // 获取文件状态 - const stats = await fs.stat(filePath); - const document = { - id, - title, - content, - createdAt: metadata.createdAt, - updatedAt: new Date().toISOString(), - published: metadata.published - }; - + const document = await documentationServiceDB.saveDocument(id, title, content, published); res.json(document); } catch (err) { logger.error('保存文档失败:', err); - res.status(500).json({ error: '保存文档失败', details: err.message }); + res.status(500).json({ error: '保存文档失败' }); } }); @@ -164,42 +78,12 @@ router.post('/documents', requireLogin, async (req, res) => { return res.status(400).json({ error: '标题和内容为必填项' }); } - // 生成唯一ID const id = Date.now().toString(); - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - - // 确保文档目录存在 - await ensureDirectories(); - - // 创建元数据 - const now = new Date().toISOString(); - const metadata = { - id, - title, - published: typeof published === 'boolean' ? published : false, - createdAt: now, - updatedAt: now - }; - - // 保存文档内容 - await fs.writeFile(filePath, content); - - // 保存元数据 - await saveDocumentMeta(id, metadata); - - const document = { - id, - title, - content, - createdAt: now, - updatedAt: now, - published: metadata.published - }; - + const document = await documentationServiceDB.saveDocument(id, title, content, published); res.status(201).json(document); } catch (err) { logger.error('创建文档失败:', err); - res.status(500).json({ error: '创建文档失败', details: err.message }); + res.status(500).json({ error: '创建文档失败' }); } }); @@ -207,331 +91,30 @@ router.post('/documents', requireLogin, async (req, res) => { router.delete('/documents/:id', requireLogin, async (req, res) => { try { const { id } = req.params; - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`); - let success = false; - - // 尝试删除主文档文件 - try { - await fs.access(filePath); - await fs.unlink(filePath); - success = true; - logger.info(`文档 ${id} 已成功删除`); - } catch (err) { - logger.warn(`删除文档文件 ${id} 失败:`, err); - } - - // 尝试删除元数据文件 - try { - await fs.access(metaPath); - await fs.unlink(metaPath); - success = true; - logger.info(`文档元数据 ${id} 已成功删除`); - } catch (err) { - logger.warn(`删除文档元数据 ${id} 失败:`, err); - } - - if (success) { - res.json({ success: true }); - } else { - throw new Error('文档和元数据均不存在或无法删除'); - } - } catch (err) { - logger.error(`删除文档 ${req.params.id} 失败:`, err); - res.status(500).json({ error: '删除文档失败', details: err.message }); - } -}); - -// 获取单个文档 -router.get('/documents/:id', async (req, res) => { - try { - const { id } = req.params; - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - - // 检查文件是否存在 - try { - await fs.access(filePath); - } catch (err) { + const success = await documentationServiceDB.deleteDocument(id); + if (!success) { return res.status(404).json({ error: '文档不存在' }); } - // 读取文件内容和元数据 - const content = await fs.readFile(filePath, 'utf8'); - const metadata = await getDocumentMeta(id); - - // 解析文档标题 - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : metadata.title || id; - - const document = { - id, - title, - content, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - published: metadata.published - }; - - res.json(document); + res.json({ success: true, message: '文档已删除' }); } catch (err) { - logger.error(`获取文档 ${req.params.id} 失败:`, err); - res.status(500).json({ error: '获取文档失败', details: err.message }); + logger.error('删除文档失败:', err); + res.status(500).json({ error: '删除文档失败' }); } }); -// 更新文档发布状态 -router.put('/documentation/toggle-publish/:id', requireLogin, async (req, res) => { +// 切换文档发布状态 +router.put('/toggle-publish/:id', requireLogin, async (req, res) => { try { const { id } = req.params; - const { published } = req.body; - - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - - // 检查文件是否存在 - try { - await fs.access(filePath); - } catch (err) { - return res.status(404).json({ error: '文档不存在' }); - } - - // 读取文件内容和元数据 - const content = await fs.readFile(filePath, 'utf8'); - const metadata = await getDocumentMeta(id); - - // 更新元数据 - metadata.published = typeof published === 'boolean' ? published : !metadata.published; - metadata.updatedAt = new Date().toISOString(); - - // 保存元数据 - await saveDocumentMeta(id, metadata); - - // 解析文档标题 - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : metadata.title || id; - - const document = { - id, - title, - content, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - published: metadata.published - }; + const document = await documentationServiceDB.toggleDocumentPublish(id); res.json(document); } catch (err) { - logger.error(`更新文档状态 ${req.params.id} 失败:`, err); - res.status(500).json({ error: '更新文档状态失败', details: err.message }); + logger.error('切换文档发布状态失败:', err); + res.status(500).json({ error: '切换文档发布状态失败' }); } }); -// 为前端添加获取已发布文档列表的路由 -router.get('/documentation', async (req, res) => { - try { - let files; - try { - files = await fs.readdir(docsDir); - } catch (err) { - if (err.code === 'ENOENT') { - await fs.mkdir(docsDir, { recursive: true }); - files = []; - } else { - throw err; - } - } - - const documents = []; - for (const file of files) { - if (file.endsWith(FILE_EXTENSION)) { - const filePath = path.join(docsDir, file); - const stats = await fs.stat(filePath); - const id = file.replace(FILE_EXTENSION, ''); - - // 读取元数据 - const metadata = await getDocumentMeta(id); - - // 如果发布状态为true,则包含在返回结果中 - if (metadata.published === true) { - const content = await fs.readFile(filePath, 'utf8'); - - // 解析文档标题 - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : metadata.title || id; - - documents.push({ - id, - title, - createdAt: metadata.createdAt || stats.birthtime, - updatedAt: metadata.updatedAt || stats.mtime, - published: true - }); - } - } - } - - logger.info(`前端请求文档列表,返回 ${documents.length} 个已发布文档`); - res.json(documents); - } catch (err) { - logger.error('获取前端文档列表失败:', err); - res.status(500).json({ error: '获取文档列表失败' }); - } -}); - -// 前端获取单个文档内容 -router.get('/documentation/:id', async (req, res) => { - try { - const { id } = req.params; - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - - // 检查文件是否存在 - try { - await fs.access(filePath); - } catch (err) { - return res.status(404).json({ error: '文档不存在' }); - } - - // 读取文件内容和元数据 - const content = await fs.readFile(filePath, 'utf8'); - const metadata = await getDocumentMeta(id); - - // 如果文档未发布,则不允许前端访问 - if (metadata.published !== true) { - return res.status(403).json({ error: '该文档未发布,无法访问' }); - } - - // 解析文档标题 - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : metadata.title || id; - - const document = { - id, - title, - content, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - published: true - }; - - logger.info(`前端请求文档: ${id} - ${title}`); - res.json(document); - } catch (err) { - logger.error(`获取前端文档内容 ${req.params.id} 失败:`, err); - res.status(500).json({ error: '获取文档内容失败', details: err.message }); - } -}); - -// 修改发布状态 -router.patch('/documents/:id/publish', requireLogin, async (req, res) => { - try { - const { id } = req.params; - const { published } = req.body; - - if (typeof published !== 'boolean') { - return res.status(400).json({ error: '发布状态必须为布尔值' }); - } - - // 获取或创建元数据 - const metadata = await getDocumentMeta(id); - metadata.published = published; - metadata.updatedAt = new Date().toISOString(); - - // 保存元数据 - await saveDocumentMeta(id, metadata); - - // 获取文档内容 - const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); - let content; - try { - content = await fs.readFile(filePath, 'utf8'); - } catch (err) { - if (err.code === 'ENOENT') { - return res.status(404).json({ error: '文档不存在' }); - } - throw err; - } - - // 解析标题 - const titleMatch = content.match(/^#\s+(.*)$/m); - const title = titleMatch ? titleMatch[1] : id; - - // 返回更新后的文档信息 - const document = { - id, - title, - content, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - published: metadata.published - }; - - res.json(document); - } catch (err) { - logger.error('修改发布状态失败:', err); - res.status(500).json({ error: '修改发布状态失败', details: err.message }); - } -}); - -// 获取单个文档文件内容 -router.get('/file', async (req, res) => { - try { - const filePath = req.query.path; - - if (!filePath) { - return res.status(400).json({ error: '文件路径不能为空' }); - } - - logger.info(`请求获取文档文件: ${filePath}`); - - // 尝试直接从文件系统读取文件 - try { - // 安全检查,确保只能访问documentation目录下的文件 - const fileName = path.basename(filePath); - const fileDir = path.dirname(filePath); - - // 构建完整路径(只允许访问documentation目录) - const fullPath = path.join(__dirname, '..', 'documentation', fileName); - - logger.info(`尝试读取文件: ${fullPath}`); - - // 检查文件是否存在 - const fileExists = await fs.access(fullPath) - .then(() => true) - .catch(() => false); - - // 如果文件不存在,则返回错误 - if (!fileExists) { - logger.warn(`文件不存在: ${fullPath}`); - return res.status(404).json({ error: '文档不存在' }); - } - - logger.info(`文件存在,开始读取: ${fullPath}`); - - // 读取文件内容 - const fileContent = await fs.readFile(fullPath, 'utf8'); - - // 设置适当的Content-Type - if (fileName.endsWith('.md')) { - res.setHeader('Content-Type', 'text/markdown; charset=utf-8'); - } else if (fileName.endsWith('.json')) { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - } else { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - } - - logger.info(`成功读取文件,内容长度: ${fileContent.length}`); - return res.send(fileContent); - } catch (error) { - logger.error(`读取文件失败: ${error.message}`, error); - return res.status(500).json({ error: `读取文件失败: ${error.message}` }); - } - - } catch (error) { - logger.error('获取文档文件失败:', error); - res.status(500).json({ error: '获取文档文件失败', details: error.message }); - } -}); - -// 导出路由 -logger.success('✓ 文档管理路由已加载'); module.exports = router; diff --git a/hubcmdui/routes/httpProxy.js b/hubcmdui/routes/httpProxy.js new file mode 100644 index 0000000..a8abd18 --- /dev/null +++ b/hubcmdui/routes/httpProxy.js @@ -0,0 +1,167 @@ +/** + * HTTP代理管理路由 + */ +const express = require('express'); +const router = express.Router(); +const logger = require('../logger'); +const { requireLogin } = require('../middleware/auth'); +const httpProxyService = require('../services/httpProxyService'); + +// 获取代理状态 +router.get('/proxy/status', requireLogin, async (req, res) => { + try { + const status = httpProxyService.getStatus(); + res.json(status); + } catch (error) { + logger.error('获取代理状态失败:', error); + res.status(500).json({ + error: '获取代理状态失败', + details: error.message + }); + } +}); + +// 启动代理服务 +router.post('/proxy/start', requireLogin, async (req, res) => { + try { + const config = req.body; + await httpProxyService.start(config); + res.json({ + success: true, + message: '代理服务已启动', + status: httpProxyService.getStatus() + }); + } catch (error) { + logger.error('启动代理服务失败:', error); + res.status(500).json({ + error: '启动代理服务失败', + details: error.message + }); + } +}); + +// 停止代理服务 +router.post('/proxy/stop', requireLogin, async (req, res) => { + try { + await httpProxyService.stop(); + res.json({ + success: true, + message: '代理服务已停止', + status: httpProxyService.getStatus() + }); + } catch (error) { + logger.error('停止代理服务失败:', error); + res.status(500).json({ + error: '停止代理服务失败', + details: error.message + }); + } +}); + +// 重启代理服务 +router.post('/proxy/restart', requireLogin, async (req, res) => { + try { + await httpProxyService.stop(); + await httpProxyService.start(req.body); + res.json({ + success: true, + message: '代理服务已重启', + status: httpProxyService.getStatus() + }); + } catch (error) { + logger.error('重启代理服务失败:', error); + res.status(500).json({ + error: '重启代理服务失败', + details: error.message + }); + } +}); + +// 更新代理配置 +router.put('/proxy/config', requireLogin, async (req, res) => { + try { + const config = req.body; + + // 验证配置 + if (config.port && (config.port < 1 || config.port > 65535)) { + return res.status(400).json({ error: '端口号必须在1-65535之间' }); + } + + if (config.enableAuth && (!config.username || !config.password)) { + return res.status(400).json({ error: '启用认证时必须提供用户名和密码' }); + } + + await httpProxyService.updateConfig(config); + res.json({ + success: true, + message: '代理配置已更新', + status: httpProxyService.getStatus() + }); + } catch (error) { + logger.error('更新代理配置失败:', error); + res.status(500).json({ + error: '更新代理配置失败', + details: error.message + }); + } +}); + +// 获取代理配置 +router.get('/proxy/config', requireLogin, async (req, res) => { + try { + const status = httpProxyService.getStatus(); + res.json({ + success: true, + config: status.config + }); + } catch (error) { + logger.error('获取代理配置失败:', error); + res.status(500).json({ + error: '获取代理配置失败', + details: error.message + }); + } +}); + +// 测试代理连接 +router.post('/proxy/test', requireLogin, async (req, res) => { + try { + const { testUrl = 'http://httpbin.org/ip' } = req.body; + const axios = require('axios'); + const status = httpProxyService.getStatus(); + + if (!status.isRunning) { + return res.status(400).json({ error: '代理服务未运行' }); + } + + // 通过代理测试连接 + const proxyConfig = { + host: status.config.host === '0.0.0.0' ? 'localhost' : status.config.host, + port: status.config.port + }; + + const startTime = Date.now(); + const response = await axios.get(testUrl, { + proxy: proxyConfig, + timeout: 10000 + }); + const responseTime = Date.now() - startTime; + + res.json({ + success: true, + message: '代理连接测试成功', + testUrl, + responseTime: `${responseTime}ms`, + statusCode: response.status, + proxyConfig + }); + } catch (error) { + logger.error('代理连接测试失败:', error); + res.status(500).json({ + error: '代理连接测试失败', + details: error.message + }); + } +}); + +module.exports = router; diff --git a/hubcmdui/routes/system.js b/hubcmdui/routes/system.js index 99cc06d..671bf2a 100644 --- a/hubcmdui/routes/system.js +++ b/hubcmdui/routes/system.js @@ -1,5 +1,5 @@ /** - * 系统相关路由 + * 系统相关路由 - 使用SQLite数据库 */ const express = require('express'); const router = express.Router(); @@ -9,7 +9,7 @@ const { exec } = require('child_process'); const execPromise = util.promisify(exec); // 只在这里定义一次 const logger = require('../logger'); const { requireLogin } = require('../middleware/auth'); -const configService = require('../services/configService'); +const configServiceDB = require('../services/configServiceDB'); const { execCommand, getSystemInfo } = require('../server-utils'); const dockerService = require('../services/dockerService'); const path = require('path'); @@ -115,7 +115,7 @@ async function getSystemStats(req, res) { // 获取系统配置 - 修改版本,避免与其他路由冲突 router.get('/system-config', async (req, res) => { try { - const config = await configService.getConfig(); + const config = await configServiceDB.getConfig(); res.json(config); } catch (error) { logger.error('读取配置失败:', error); @@ -129,9 +129,9 @@ router.get('/system-config', async (req, res) => { // 保存系统配置 - 修改版本,避免与其他路由冲突 router.post('/system-config', requireLogin, async (req, res) => { try { - const currentConfig = await configService.getConfig(); + const currentConfig = await configServiceDB.getConfig(); const newConfig = { ...currentConfig, ...req.body }; - await configService.saveConfig(newConfig); + await configServiceDB.saveConfigs(newConfig); logger.info('系统配置已更新'); res.json({ success: true }); } catch (error) { diff --git a/hubcmdui/scripts/auto-setup.js b/hubcmdui/scripts/auto-setup.js new file mode 100755 index 0000000..15ddbe9 --- /dev/null +++ b/hubcmdui/scripts/auto-setup.js @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const { isDatabaseReady, getDatabaseStats } = require('../utils/database-checker'); + +// 检查是否需要安装依赖 +function needsInstall() { + const nodeModulesPath = path.join(process.cwd(), 'node_modules'); + const packageLockPath = path.join(process.cwd(), 'package-lock.json'); + + if (!fs.existsSync(nodeModulesPath)) { + return true; + } + + // 检查package.json是否比package-lock.json新 + const packageJsonPath = path.join(process.cwd(), 'package.json'); + if (fs.existsSync(packageJsonPath) && fs.existsSync(packageLockPath)) { + const packageStat = fs.statSync(packageJsonPath); + const lockStat = fs.statSync(packageLockPath); + if (packageStat.mtime > lockStat.mtime) { + return true; + } + } + + return false; +} + +// 检查是否需要初始化数据库 +async function needsInit() { + const dataDir = path.join(process.cwd(), 'data'); + + // 如果data目录不存在,需要初始化 + if (!fs.existsSync(dataDir)) { + return true; + } + + // 使用专门的数据库检查器 + const isReady = await isDatabaseReady(); + return !isReady; +} + +// 执行命令并显示输出 +function runCommand(command, description) { + console.log(`\n🔄 ${description}...`); + try { + execSync(command, { stdio: 'inherit', cwd: process.cwd() }); + console.log(`✅ ${description}完成`); + return true; + } catch (error) { + console.error(`❌ ${description}失败:`, error.message); + return false; + } +} + +async function autoSetup() { + console.log('🚀 HubCmdUI 自动设置检查...\n'); + + let needsSetup = false; + + // 检查是否需要安装依赖 + if (needsInstall()) { + console.log('📦 检测到需要安装依赖包'); + needsSetup = true; + + if (!runCommand('npm install', '安装依赖包')) { + process.exit(1); + } + } else { + console.log('✅ 依赖包已安装'); + } + + // 检查是否需要初始化 + const needsInitialization = await needsInit(); + if (needsInitialization) { + console.log('🗄️ 检测到需要初始化数据库'); + needsSetup = true; + + if (!runCommand('node scripts/init-complete.js', '初始化SQLite数据库')) { + process.exit(1); + } + } else { + console.log('✅ 数据库已初始化'); + } + + if (needsSetup) { + console.log('\n🎉 系统设置完成!正在启动服务...\n'); + } else { + console.log('\n🎯 系统已就绪,正在启动服务...\n'); + } + + // 启动服务器 + console.log('🌐 启动 HubCmdUI 服务器...'); + console.log('📍 访问地址: http://localhost:3000'); + console.log('🔧 管理面板: http://localhost:3000/admin'); + console.log('👤 默认账户: root / admin@123\n'); + + // 启动主服务器 + try { + require('../server.js'); + } catch (error) { + console.error('❌ 服务器启动失败:', error.message); + console.error('💡 尝试运行: npm run init 重新初始化'); + process.exit(1); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + autoSetup().catch(error => { + console.error('❌ 自动设置失败:', error.message); + process.exit(1); + }); +} + +module.exports = { autoSetup, needsInstall, needsInit }; diff --git a/hubcmdui/scripts/init-complete.js b/hubcmdui/scripts/init-complete.js new file mode 100644 index 0000000..27ea369 --- /dev/null +++ b/hubcmdui/scripts/init-complete.js @@ -0,0 +1,199 @@ +#!/usr/bin/env node +/** + * 系统初始化和配置脚本 + */ +const fs = require('fs').promises; +const path = require('path'); +const logger = require('../logger'); + +// 颜色输出 +const chalk = require('chalk'); + +async function initializeSystem() { + console.log(chalk.blue('🚀 正在初始化 HubCmdUI 系统...\n')); + + try { + // 1. 检查并创建必要目录 + console.log(chalk.yellow('📁 创建必要目录...')); + await createDirectories(); + + // 2. 检查数据库是否已初始化 + const database = require('../database/database'); + try { + await database.connect(); + const isInitialized = await database.isInitialized(); + + if (isInitialized) { + console.log(chalk.green(' ✓ 数据库已初始化,跳过初始化步骤')); + console.log(chalk.green('\n✅ 系统检查完成!')); + console.log(chalk.cyan('💡 使用 npm start 启动服务')); + console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000')); + return; + } + } catch (error) { + // 数据库连接失败,继续初始化流程 + } + + // 3. 检查配置文件 + console.log(chalk.yellow('⚙️ 检查配置文件...')); + await checkConfigFiles(); + + // 4. 询问用户是否要启用SQLite + const useDatabase = await askUserChoice(); + + if (useDatabase) { + // 5. 迁移数据到SQLite + console.log(chalk.yellow('📊 初始化SQLite数据库...')); + await initializeSQLite(); + + // 6. 设置环境变量 + console.log(chalk.yellow('🔧 配置数据库模式...')); + await setDatabaseMode(true); + } else { + console.log(chalk.yellow('📁 使用文件存储模式...')); + await setDatabaseMode(false); + } + + // 7. 创建默认用户 + console.log(chalk.yellow('👤 创建默认用户...')); + await createDefaultUser(); + + // 8. 配置HTTP代理 + console.log(chalk.yellow('🌐 配置HTTP代理服务...')); + await configureHttpProxy(); + + console.log(chalk.green('\n✅ 系统初始化完成!')); + console.log(chalk.cyan('💡 使用 npm start 启动服务')); + console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000')); + console.log(chalk.cyan('👤 默认用户: root / admin@123')); + + } catch (error) { + console.error(chalk.red('❌ 初始化失败:'), error.message); + process.exit(1); + } +} + +/** + * 创建必要目录 + */ +async function createDirectories() { + const dirs = [ + 'data', // 数据库文件目录 + 'documentation', // 文档目录(静态文件) + 'logs', // 日志目录 + 'temp' // 临时文件目录 + ]; + + for (const dir of dirs) { + const dirPath = path.join(__dirname, '..', dir); + try { + await fs.mkdir(dirPath, { recursive: true }); + } catch (error) { + if (error.code !== 'EEXIST') { + throw error; + } + } + } + console.log(chalk.green(' ✓ 目录创建完成')); +} + +/** + * 检查配置文件 - 简化版,不再创建config.json + */ +async function checkConfigFiles() { + console.log(chalk.green(' ✓ 使用SQLite数据库存储配置')); +} + +/** + * 询问用户选择 + */ +async function askUserChoice() { + // 简化处理,默认使用SQLite + const useDatabase = process.env.USE_SQLITE !== 'false'; + + if (useDatabase) { + console.log(chalk.green(' ✓ 将使用SQLite数据库存储')); + } else { + console.log(chalk.yellow(' ⚠ 将使用文件存储模式')); + } + + return useDatabase; +} + +/** + * 初始化SQLite数据库 + */ +async function initializeSQLite() { + try { + const database = require('../database/database'); + await database.connect(); + await database.createTables(); + + // 初始化数据库(创建默认数据) + await database.createDefaultAdmin(); + await database.createDefaultDocuments(); + await database.createDefaultMenuItems(); + + // 初始化默认配置 + const configServiceDB = require('../services/configServiceDB'); + await configServiceDB.initializeDefaultConfig(); + + // 标记数据库已初始化 + await database.markAsInitialized(); + + console.log(chalk.green(' ✓ SQLite数据库初始化完成')); + } catch (error) { + console.log(chalk.red(' ❌ SQLite初始化失败:'), error.message); + throw error; + } +} + +/** + * 设置数据库模式 + */ +async function setDatabaseMode(useDatabase) { + const envPath = path.join(__dirname, '../.env'); + const envContent = `# 数据库配置 +USE_DATABASE=${useDatabase} +AUTO_MIGRATE=true + +# HTTP代理配置 +PROXY_PORT=8080 +PROXY_HOST=0.0.0.0 +`; + + await fs.writeFile(envPath, envContent); + console.log(chalk.green(` ✓ 数据库模式已设置为: ${useDatabase ? 'SQLite' : '文件存储'}`)); +} + +/** + * 创建默认用户 - 简化版,数据库已处理 + */ +async function createDefaultUser() { + console.log(chalk.green(' ✓ 默认管理员用户由数据库处理 (root/admin@123)')); +} + +/** + * 配置HTTP代理服务信息 + */ +async function configureHttpProxy() { + try { + console.log(chalk.green(' ✓ HTTP代理服务需要通过环境变量配置')); + console.log(chalk.cyan(' 配置方式: 设置 PROXY_PORT 和 PROXY_HOST 环境变量')); + console.log(chalk.cyan(' 示例: PROXY_PORT=8080 PROXY_HOST=0.0.0.0 npm start')); + } catch (error) { + console.log(chalk.yellow(' ⚠ HTTP代理服务配置提示显示失败')); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + initializeSystem().then(() => { + process.exit(0); + }).catch((error) => { + console.error(chalk.red('初始化失败:'), error); + process.exit(1); + }); +} + +module.exports = { initializeSystem }; diff --git a/hubcmdui/scripts/init-database.js b/hubcmdui/scripts/init-database.js new file mode 100644 index 0000000..4f7c301 --- /dev/null +++ b/hubcmdui/scripts/init-database.js @@ -0,0 +1,92 @@ +/** + * 数据库初始化脚本 + */ +const database = require('../database/database'); +const userServiceDB = require('../services/userServiceDB'); +const configServiceDB = require('../services/configServiceDB'); +const logger = require('../logger'); + +async function initializeDatabase() { + try { + logger.info('开始初始化数据库...'); + + // 连接数据库 + await database.connect(); + + // 检查数据库是否已经初始化 + const isInitialized = await database.isInitialized(); + if (isInitialized) { + logger.info('数据库已经初始化,跳过重复初始化'); + return; + } + + // 创建数据表 + await database.createTables(); + + // 创建默认管理员用户(如果不存在) + await database.createDefaultAdmin(); + + // 创建默认文档 + await database.createDefaultDocuments(); + + // 初始化默认配置 + await configServiceDB.initializeDefaultConfig(); + + // 标记数据库已初始化 + await database.markAsInitialized(); + + logger.info('数据库初始化完成!'); + + // 显示数据库信息 + const userCount = await database.get('SELECT COUNT(*) as count FROM users'); + const configCount = await database.get('SELECT COUNT(*) as count FROM configs'); + const docCount = await database.get('SELECT COUNT(*) as count FROM documents'); + + logger.info(`数据库统计:`); + logger.info(` 用户数量: ${userCount.count}`); + logger.info(` 配置项数量: ${configCount.count}`); + logger.info(` 文档数量: ${docCount.count}`); + + } catch (error) { + logger.error('数据库初始化失败:', error); + process.exit(1); + } +} + +/** + * 检查数据库是否已经初始化 + */ +async function checkDatabaseInitialized() { + try { + // 检查用户表是否有数据 + const userCount = await database.get('SELECT COUNT(*) as count FROM users'); + if (userCount && userCount.count > 0) { + return true; + } + + // 检查配置表是否有数据 + const configCount = await database.get('SELECT COUNT(*) as count FROM configs'); + if (configCount && configCount.count > 0) { + return true; + } + + return false; + } catch (error) { + // 如果查询失败,认为数据库未初始化 + return false; + } +} + +// 如果直接运行此脚本,则执行初始化 +if (require.main === module) { + initializeDatabase().then(() => { + process.exit(0); + }).catch((error) => { + logger.error('初始化过程出错:', error); + process.exit(1); + }); +} + +module.exports = { + initializeDatabase +}; diff --git a/hubcmdui/scripts/init-system.js b/hubcmdui/scripts/init-system.js deleted file mode 100644 index bb9cb0d..0000000 --- a/hubcmdui/scripts/init-system.js +++ /dev/null @@ -1,315 +0,0 @@ -/** - * 系统初始化脚本 - 首次运行时执行 - */ -const fs = require('fs').promises; -const path = require('path'); -const bcrypt = require('bcrypt'); -const { execSync } = require('child_process'); -const logger = require('../logger'); -const { ensureDirectoriesExist } = require('../init-dirs'); -const { downloadImages } = require('../download-images'); -const configService = require('../services/configService'); - -// 用户文件路径 -const USERS_FILE = path.join(__dirname, '..', 'users.json'); - -/** - * 创建管理员用户 - * @param {string} username 用户名 - * @param {string} password 密码 - */ -async function createAdminUser(username = 'root', password = 'admin') { - try { - // 检查用户文件是否已存在 - try { - await fs.access(USERS_FILE); - logger.info('用户文件已存在,跳过创建管理员用户'); - return; - } catch (err) { - if (err.code !== 'ENOENT') throw err; - } - - // 创建默认管理员用户 - const defaultUser = { - username, - password: bcrypt.hashSync(password, 10), - createdAt: new Date().toISOString(), - loginCount: 0, - lastLogin: null - }; - - await fs.writeFile(USERS_FILE, JSON.stringify({ users: [defaultUser] }, null, 2)); - logger.success(`创建默认管理员用户: ${username}/${password}`); - logger.warn('请在首次登录后立即修改默认密码'); - } catch (error) { - logger.error('创建管理员用户失败:', error); - throw error; - } -} - -/** - * 创建默认配置 - */ -async function createDefaultConfig() { - try { - // 检查配置是否已存在 - const config = await configService.getConfig(); - - // 如果菜单项为空,添加默认菜单项 - if (!config.menuItems || config.menuItems.length === 0) { - config.menuItems = [ - { - text: "控制台", - link: "/admin", - newTab: false - }, - { - text: "镜像搜索", - link: "/", - newTab: false - }, - { - text: "文档", - link: "/docs", - newTab: false - }, - { - text: "GitHub", - link: "https://github.com/dqzboy/hubcmdui", - newTab: true - } - ]; - - await configService.saveConfig(config); - logger.success('创建默认菜单配置'); - } - - return config; - } catch (error) { - logger.error('初始化配置失败:', error); - throw error; - } -} - -/** - * 创建示例文档 - 现已禁用 - */ -async function createSampleDocumentation() { - logger.info('示例文档创建功能已禁用'); - return; // 不再创建默认文档 - - /* 旧代码保留注释,已禁用 - const docService = require('../services/documentationService'); - - try { - await docService.ensureDocumentationDir(); - - // 检查是否有现有文档 - const docs = await docService.getDocumentationList(); - if (docs && docs.length > 0) { - logger.info('文档已存在,跳过创建示例文档'); - return; - } - - // 创建示例文档 - const welcomeDoc = { - title: "欢迎使用 Docker 镜像代理加速系统", - content: `# 欢迎使用 Docker 镜像代理加速系统 - -## 系统简介 - -Docker 镜像代理加速系统是一个帮助用户快速搜索、拉取 Docker 镜像的工具。本系统提供了以下功能: - -- 快速搜索 Docker Hub 上的镜像 -- 查看镜像的详细信息和标签 -- 管理本地 Docker 容器 -- 监控容器状态并发送通知 - -## 快速开始 - -1. 在首页搜索框中输入要查找的镜像名称 -2. 点击搜索结果查看详细信息 -3. 使用提供的命令拉取镜像 - -## 管理功能 - -管理员可以通过控制面板管理系统: - -- 查看所有容器状态 -- 启动/停止/重启容器 -- 更新容器镜像 -- 配置监控告警 - -祝您使用愉快! -`, - published: true - }; - - const aboutDoc = { - title: "关于系统", - content: `# 关于 Docker 镜像代理加速系统 - -## 系统版本 - -当前版本: v1.0.0 - -## 技术栈 - -- 前端: HTML, CSS, JavaScript -- 后端: Node.js, Express -- 容器: Docker, Dockerode -- 数据存储: 文件系统 - -## 联系方式 - -如有问题,请通过以下方式联系我们: - -- GitHub Issues -- 电子邮件: example@example.com - -## 许可证 - -本项目采用 MIT 许可证 -`, - published: true - }; - - await docService.saveDocument(Date.now().toString(), welcomeDoc.title, welcomeDoc.content); - await docService.saveDocument((Date.now() + 1000).toString(), aboutDoc.title, aboutDoc.content); - - logger.success('创建示例文档成功'); - } catch (error) { - logger.error('创建示例文档失败:', error); - } - */ -} - -/** - * 检查必要依赖 - */ -async function checkDependencies() { - try { - logger.info('正在检查系统依赖...'); - - // 检查 Node.js 版本 - const nodeVersion = process.version; - const minNodeVersion = 'v14.0.0'; - if (compareVersions(nodeVersion, minNodeVersion) < 0) { - logger.warn(`当前 Node.js 版本 ${nodeVersion} 低于推荐的最低版本 ${minNodeVersion}`); - } else { - logger.success(`Node.js 版本 ${nodeVersion} 满足要求`); - } - - // 检查必要的 npm 包 - try { - const packageJson = require('../package.json'); - const requiredDeps = Object.keys(packageJson.dependencies); - - logger.info(`系统依赖共 ${requiredDeps.length} 个包`); - - // 检查是否有 node_modules 目录 - try { - await fs.access(path.join(__dirname, '..', 'node_modules')); - } catch (err) { - if (err.code === 'ENOENT') { - logger.warn('未找到 node_modules 目录,请运行 npm install 安装依赖'); - return false; - } - } - } catch (err) { - logger.warn('无法读取 package.json:', err.message); - } - - // 检查 Docker - try { - execSync('docker --version', { stdio: ['ignore', 'ignore', 'ignore'] }); - logger.success('Docker 已安装'); - } catch (err) { - logger.warn('未检测到 Docker,部分功能可能无法正常使用'); - } - - return true; - } catch (error) { - logger.error('依赖检查失败:', error); - return false; - } -} - -/** - * 比较版本号 - */ -function compareVersions(v1, v2) { - const v1parts = v1.replace('v', '').split('.'); - const v2parts = v2.replace('v', '').split('.'); - - for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) { - const v1part = parseInt(v1parts[i] || 0); - const v2part = parseInt(v2parts[i] || 0); - - if (v1part > v2part) return 1; - if (v1part < v2part) return -1; - } - - return 0; -} - -/** - * 主初始化函数 - */ -async function initialize() { - logger.info('开始系统初始化...'); - - try { - // 1. 检查系统依赖 - await checkDependencies(); - - // 2. 确保目录结构存在 - await ensureDirectoriesExist(); - logger.success('目录结构初始化完成'); - - // 3. 下载必要图片 - await downloadImages(); - - // 4. 创建默认用户 - await createAdminUser(); - - // 5. 创建默认配置 - await createDefaultConfig(); - - // 6. 创建示例文档 - await createSampleDocumentation(); - - logger.success('系统初始化完成!'); - // 移除敏感的账户信息日志 - logger.warn('首次登录后请立即修改默认密码!'); - - return { success: true }; - } catch (error) { - logger.error('系统初始化失败:', error); - return { success: false, error: error.message }; - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - initialize() - .then((result) => { - if (result.success) { - process.exit(0); - } else { - process.exit(1); - } - }) - .catch((error) => { - logger.fatal('初始化过程中发生错误:', error); - process.exit(1); - }); -} - -module.exports = { - initialize, - createAdminUser, - createDefaultConfig, - createSampleDocumentation, - checkDependencies -}; diff --git a/hubcmdui/server.js b/hubcmdui/server.js index 23f93d5..49bdbdb 100644 --- a/hubcmdui/server.js +++ b/hubcmdui/server.js @@ -15,7 +15,9 @@ const { gracefulShutdown } = require('./cleanup'); const os = require('os'); const { requireLogin } = require('./middleware/auth'); const compatibilityLayer = require('./compatibility-layer'); -const initSystem = require('./scripts/init-system'); +const { initializeDatabase } = require('./scripts/init-database'); +const database = require('./database/database'); +const httpProxyService = require('./services/httpProxyService'); // 设置日志级别 (默认INFO, 可通过环境变量设置) const logLevel = process.env.LOG_LEVEL || 'WARN'; @@ -146,6 +148,15 @@ async function startServer() { logger.info(`服务器已启动并监听端口 ${PORT}`); try { + // 初始化数据库 + try { + await initializeDatabase(); + logger.success('数据库初始化完成'); + } catch (dbError) { + logger.error('数据库初始化失败:', dbError); + logger.warn('将使用文件存储作为备用方案'); + } + // 确保目录存在 await ensureDirectoriesExist(); logger.success('系统目录初始化完成'); @@ -153,15 +164,42 @@ async function startServer() { // 下载必要资源 await downloadImages(); logger.success('资源下载完成'); - - // 初始化系统 + + // 默认使用SQLite数据库模式 try { - const { initialize } = require('./scripts/init-system'); - await initialize(); - logger.success('系统初始化完成'); + logger.info('正在检查SQLite数据库...'); + const { isDatabaseReady } = require('./utils/database-checker'); + const dbReady = await isDatabaseReady(); + + if (!dbReady) { + logger.warn('数据库未完全初始化,正在初始化...'); + await initializeDatabase(); + } else { + logger.info('SQLite数据库已就绪'); + } + + logger.success('SQLite数据库初始化完成'); + } catch (dbError) { + logger.error('SQLite数据库初始化失败:', dbError.message); + throw dbError; // 数据库初始化失败时直接退出 + } + + // 初始化系统配置 + try { + // 系统配置已在数据库初始化时完成 + logger.info('系统配置初始化完成'); } catch (initError) { - logger.warn('系统初始化遇到问题:', initError.message); - logger.warn('某些功能可能无法正常工作'); + logger.warn('系统配置初始化遇到问题:', initError.message); + } + + // 初始化HTTP代理服务 + try { + await httpProxyService.loadConfig(); + // 检查环境变量并自动启动代理 + await httpProxyService.checkEnvironmentAndAutoStart(); + logger.success('HTTP代理服务配置已加载'); + } catch (proxyError) { + logger.warn('HTTP代理服务初始化失败:', proxyError.message); } // 尝试启动监控 diff --git a/hubcmdui/services/configService.js b/hubcmdui/services/configService.js deleted file mode 100644 index 26999ed..0000000 --- a/hubcmdui/services/configService.js +++ /dev/null @@ -1,47 +0,0 @@ -const fs = require('fs').promises; -const path = require('path'); -const logger = require('../logger'); - -const CONFIG_FILE = path.join(__dirname, '../config.json'); -const DEFAULT_CONFIG = { - theme: 'light', - language: 'zh_CN', - notifications: true, - autoRefresh: true, - refreshInterval: 30000, - dockerHost: 'localhost', - dockerPort: 2375, - useHttps: false -}; - -async function ensureConfigFile() { - try { - await fs.access(CONFIG_FILE); - } catch (error) { - if (error.code === 'ENOENT') { - await fs.writeFile(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2)); - } else { - throw error; - } - } -} - -async function getConfig() { - try { - await ensureConfigFile(); - const data = await fs.readFile(CONFIG_FILE, 'utf8'); - return JSON.parse(data); - } catch (error) { - logger.error('读取配置文件失败:', error); - return { ...DEFAULT_CONFIG, error: true }; - } -} - -module.exports = { - getConfig, - saveConfig: async (config) => { - await ensureConfigFile(); - await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); - }, - DEFAULT_CONFIG -}; diff --git a/hubcmdui/services/configServiceDB.js b/hubcmdui/services/configServiceDB.js new file mode 100644 index 0000000..803d716 --- /dev/null +++ b/hubcmdui/services/configServiceDB.js @@ -0,0 +1,233 @@ +/** + * 基于SQLite的配置服务模块 + */ +const logger = require('../logger'); +const database = require('../database/database'); + +class ConfigServiceDB { + /** + * 获取配置项 + */ + async getConfig(key = null) { + try { + if (key) { + const config = await database.get('SELECT * FROM configs WHERE key = ?', [key]); + if (config) { + return JSON.parse(config.value); + } + return null; + } else { + // 获取所有配置 + const configs = await database.all('SELECT * FROM configs'); + const result = {}; + for (const config of configs) { + result[config.key] = JSON.parse(config.value); + } + return result; + } + } catch (error) { + logger.error('获取配置失败:', error); + throw error; + } + } + + /** + * 保存配置项 + */ + async saveConfig(key, value, description = null) { + try { + const valueString = JSON.stringify(value); + const valueType = typeof value; + + const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]); + + if (existingConfig) { + // 更新现有配置 + await database.run( + 'UPDATE configs SET value = ?, type = ?, description = ?, updated_at = ? WHERE key = ?', + [valueString, valueType, description, new Date().toISOString(), key] + ); + } else { + // 创建新配置 + await database.run( + 'INSERT INTO configs (key, value, type, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', + [key, valueString, valueType, description, new Date().toISOString(), new Date().toISOString()] + ); + } + + // 移除详细的配置保存日志,减少日志噪音 + } catch (error) { + logger.error('保存配置失败:', error); + throw error; + } + } + + /** + * 批量保存配置 + */ + async saveConfigs(configs) { + try { + const configCount = Object.keys(configs).length; + for (const [key, value] of Object.entries(configs)) { + await this.saveConfig(key, value); + } + logger.info(`批量保存配置完成,共 ${configCount} 项配置`); + } catch (error) { + logger.error('批量保存配置失败:', error); + throw error; + } + } + + /** + * 删除配置项 + */ + async deleteConfig(key) { + try { + await database.run('DELETE FROM configs WHERE key = ?', [key]); + // 删除配置时仍保留日志,因为这是重要操作 + logger.info(`配置 ${key} 已删除`); + } catch (error) { + logger.error('删除配置失败:', error); + throw error; + } + } + + /** + * 获取系统默认配置 + */ + getDefaultConfig() { + return { + theme: 'light', + language: 'zh_CN', + notifications: true, + autoRefresh: true, + refreshInterval: 30000, + dockerHost: 'localhost', + dockerPort: 2375, + useHttps: false, + proxyDomain: 'registry-1.docker.io', + logo: '', + menuItems: [ + { + text: "首页", + link: "/", + newTab: false + }, + { + text: "文档", + link: "https://dqzboy.github.io/docs/", + newTab: true + }, + { + text: "推广", + link: "https://dqzboy.github.io/proxyui/zanzhu", + newTab: true + }, + { + text: "GitHub", + link: "https://github.com/dqzboy/Docker-Proxy", + newTab: true + } + ], + monitoringConfig: { + notificationType: 'wechat', + webhookUrl: '', + telegramToken: '', + telegramChatId: '', + monitorInterval: 60, + isEnabled: false + } + }; + } + + /** + * 初始化默认配置 + */ + async initializeDefaultConfig() { + try { + const defaultConfig = this.getDefaultConfig(); + let newConfigCount = 0; + + for (const [key, value] of Object.entries(defaultConfig)) { + const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]); + if (!existingConfig) { + await this.saveConfig(key, value, `默认${key}配置`); + newConfigCount++; + } + } + } catch (error) { + logger.error('初始化默认配置失败:', error); + throw error; + } + } + + /** + * 获取监控配置 + */ + async getMonitoringConfig() { + try { + return await this.getConfig('monitoringConfig') || this.getDefaultConfig().monitoringConfig; + } catch (error) { + logger.error('获取监控配置失败:', error); + return this.getDefaultConfig().monitoringConfig; + } + } + + /** + * 保存监控配置 + */ + async saveMonitoringConfig(config) { + try { + await this.saveConfig('monitoringConfig', config, '监控系统配置'); + } catch (error) { + logger.error('保存监控配置失败:', error); + throw error; + } + } + + /** + * 获取菜单项配置 + */ + async getMenuItems() { + try { + const menuItems = await database.all( + 'SELECT text, link, new_tab, sort_order, enabled FROM menu_items WHERE enabled = 1 ORDER BY sort_order' + ); + + return menuItems.map(item => ({ + text: item.text, + link: item.link, + newTab: Boolean(item.new_tab) + })); + } catch (error) { + logger.error('获取菜单项失败:', error); + return []; + } + } + + /** + * 保存菜单项配置 + */ + async saveMenuItems(menuItems) { + try { + // 先清空现有菜单项 + await database.run('DELETE FROM menu_items'); + + // 插入新的菜单项 + for (let i = 0; i < menuItems.length; i++) { + const item = menuItems[i]; + await database.run( + 'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', + [item.text, item.link, item.newTab ? 1 : 0, i + 1, 1, new Date().toISOString(), new Date().toISOString()] + ); + } + + logger.info('菜单项配置保存成功'); + } catch (error) { + logger.error('保存菜单项失败:', error); + throw error; + } + } +} + +module.exports = new ConfigServiceDB(); diff --git a/hubcmdui/services/documentationService.js b/hubcmdui/services/documentationService.js deleted file mode 100644 index 23078fd..0000000 --- a/hubcmdui/services/documentationService.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * 文档服务模块 - 处理文档管理功能 - */ -const fs = require('fs').promises; -const path = require('path'); -const logger = require('../logger'); - -const DOCUMENTATION_DIR = path.join(__dirname, '..', 'documentation'); -const META_DIR = path.join(DOCUMENTATION_DIR, 'meta'); - -// 确保文档目录存在 -async function ensureDocumentationDir() { - try { - await fs.access(DOCUMENTATION_DIR); - logger.debug('文档目录已存在'); - - // 确保meta目录存在 - try { - await fs.access(META_DIR); - logger.debug('文档meta目录已存在'); - } catch (error) { - if (error.code === 'ENOENT') { - await fs.mkdir(META_DIR, { recursive: true }); - logger.success('文档meta目录已创建'); - } else { - throw error; - } - } - } catch (error) { - if (error.code === 'ENOENT') { - await fs.mkdir(DOCUMENTATION_DIR, { recursive: true }); - logger.success('文档目录已创建'); - - // 创建meta目录 - await fs.mkdir(META_DIR, { recursive: true }); - logger.success('文档meta目录已创建'); - } else { - throw error; - } - } -} - -// 获取文档列表 -async function getDocumentationList() { - try { - await ensureDocumentationDir(); - const files = await fs.readdir(DOCUMENTATION_DIR); - - const documents = await Promise.all(files.map(async file => { - // 跳过目录和非文档文件 - if (file === 'meta' || file.startsWith('.')) return null; - - // 处理JSON文件 - if (file.endsWith('.json')) { - try { - const filePath = path.join(DOCUMENTATION_DIR, file); - const content = await fs.readFile(filePath, 'utf8'); - const doc = JSON.parse(content); - return { - id: path.parse(file).name, - title: doc.title, - published: doc.published, - createdAt: doc.createdAt || new Date().toISOString(), - updatedAt: doc.updatedAt || new Date().toISOString() - }; - } catch (fileError) { - logger.error(`读取JSON文档文件 ${file} 失败:`, fileError); - return null; - } - } - - // 处理MD文件 - if (file.endsWith('.md')) { - try { - const id = path.parse(file).name; - let metaData = { published: true, title: path.parse(file).name }; - - // 尝试读取meta数据 - try { - const metaPath = path.join(META_DIR, `${id}.json`); - const metaContent = await fs.readFile(metaPath, 'utf8'); - metaData = { ...metaData, ...JSON.parse(metaContent) }; - } catch (metaError) { - // meta文件不存在或无法解析,使用默认值 - logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message); - } - - // 确保有发布状态 - if (typeof metaData.published !== 'boolean') { - metaData.published = true; - } - - return { - id, - title: metaData.title || id, - path: file, // 不直接加载内容,而是提供路径 - published: metaData.published, - createdAt: metaData.createdAt || new Date().toISOString(), - updatedAt: metaData.updatedAt || new Date().toISOString() - }; - } catch (mdError) { - logger.error(`处理MD文档 ${file} 失败:`, mdError); - return null; - } - } - - return null; - })); - - return documents.filter(doc => doc !== null); - } catch (error) { - logger.error('获取文档列表失败:', error); - throw error; - } -} - -// 获取已发布文档 -async function getPublishedDocuments() { - const documents = await getDocumentationList(); - return documents.filter(doc => doc.published); -} - -// 获取单个文档 -async function getDocument(id) { - try { - await ensureDocumentationDir(); - - // 首先尝试读取JSON文件 - try { - const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`); - const jsonContent = await fs.readFile(jsonPath, 'utf8'); - return JSON.parse(jsonContent); - } catch (jsonError) { - // JSON文件不存在,尝试读取MD文件 - if (jsonError.code === 'ENOENT') { - const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`); - const mdContent = await fs.readFile(mdPath, 'utf8'); - - // 读取meta数据 - let metaData = { published: true, title: id }; - try { - const metaPath = path.join(META_DIR, `${id}.json`); - const metaContent = await fs.readFile(metaPath, 'utf8'); - metaData = { ...metaData, ...JSON.parse(metaContent) }; - } catch (metaError) { - // meta文件不存在或无法解析,使用默认值 - logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message); - } - - return { - id, - title: metaData.title || id, - content: mdContent, - published: metaData.published, - createdAt: metaData.createdAt || new Date().toISOString(), - updatedAt: metaData.updatedAt || new Date().toISOString() - }; - } - - // 其他错误,直接抛出 - throw jsonError; - } - } catch (error) { - logger.error(`获取文档 ${id} 失败:`, error); - throw error; - } -} - -// 保存文档 -async function saveDocument(id, title, content) { - try { - await ensureDocumentationDir(); - const docId = id || Date.now().toString(); - const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`); - - // 检查是否已存在,保留发布状态 - let published = false; - try { - const existingDoc = await fs.readFile(docPath, 'utf8'); - published = JSON.parse(existingDoc).published || false; - } catch (error) { - // 文件不存在,使用默认值 - } - - const now = new Date().toISOString(); - const docData = { - title, - content, - published, - createdAt: now, - updatedAt: now - }; - - await fs.writeFile( - docPath, - JSON.stringify(docData, null, 2), - 'utf8' - ); - - return { id: docId, ...docData }; - } catch (error) { - logger.error('保存文档失败:', error); - throw error; - } -} - -// 删除文档 -async function deleteDocument(id) { - try { - await ensureDocumentationDir(); - - // 删除JSON文件(如果存在) - try { - const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`); - await fs.unlink(jsonPath); - } catch (error) { - if (error.code !== 'ENOENT') { - logger.warn(`删除JSON文档 ${id} 失败:`, error); - } - } - - // 删除MD文件(如果存在) - try { - const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`); - await fs.unlink(mdPath); - } catch (error) { - if (error.code !== 'ENOENT') { - logger.warn(`删除MD文档 ${id} 失败:`, error); - } - } - - // 删除meta文件(如果存在) - try { - const metaPath = path.join(META_DIR, `${id}.json`); - await fs.unlink(metaPath); - } catch (error) { - if (error.code !== 'ENOENT') { - logger.warn(`删除文档 ${id} 的meta数据失败:`, error); - } - } - - return { success: true }; - } catch (error) { - logger.error(`删除文档 ${id} 失败:`, error); - throw error; - } -} - -// 切换文档发布状态 -async function toggleDocumentPublish(id) { - try { - await ensureDocumentationDir(); - - // 尝试读取JSON文件 - try { - const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`); - const content = await fs.readFile(jsonPath, 'utf8'); - const doc = JSON.parse(content); - doc.published = !doc.published; - doc.updatedAt = new Date().toISOString(); - - await fs.writeFile(jsonPath, JSON.stringify(doc, null, 2), 'utf8'); - return doc; - } catch (jsonError) { - // 如果JSON文件不存在,尝试处理MD文件的meta数据 - if (jsonError.code === 'ENOENT') { - const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`); - - // 确认MD文件存在 - try { - await fs.access(mdPath); - } catch (mdError) { - throw new Error(`文档 ${id} 不存在`); - } - - // 获取或创建meta数据 - const metaPath = path.join(META_DIR, `${id}.json`); - let metaData = { published: true, title: id }; - - try { - const metaContent = await fs.readFile(metaPath, 'utf8'); - metaData = { ...metaData, ...JSON.parse(metaContent) }; - } catch (metaError) { - // meta文件不存在,使用默认值 - } - - // 切换发布状态 - metaData.published = !metaData.published; - metaData.updatedAt = new Date().toISOString(); - - // 保存meta数据 - await fs.writeFile(metaPath, JSON.stringify(metaData, null, 2), 'utf8'); - - // 获取MD文件内容 - const mdContent = await fs.readFile(mdPath, 'utf8'); - - return { - id, - title: metaData.title, - content: mdContent, - published: metaData.published, - createdAt: metaData.createdAt, - updatedAt: metaData.updatedAt - }; - } - - // 其他错误,直接抛出 - throw jsonError; - } - } catch (error) { - logger.error(`切换文档 ${id} 发布状态失败:`, error); - throw error; - } -} - -module.exports = { - ensureDocumentationDir, - getDocumentationList, - getPublishedDocuments, - getDocument, - saveDocument, - deleteDocument, - toggleDocumentPublish -}; diff --git a/hubcmdui/services/documentationServiceDB.js b/hubcmdui/services/documentationServiceDB.js new file mode 100644 index 0000000..2fcc966 --- /dev/null +++ b/hubcmdui/services/documentationServiceDB.js @@ -0,0 +1,242 @@ +/** + * 基于SQLite的文档服务模块 + */ +const logger = require('../logger'); +const database = require('../database/database'); + +class DocumentationServiceDB { + /** + * 获取文档列表 + */ + async getDocumentationList() { + try { + const documents = await database.all( + 'SELECT doc_id, title, published, created_at, updated_at FROM documents ORDER BY updated_at DESC' + ); + + return documents.map(doc => ({ + id: doc.doc_id, + title: doc.title, + published: Boolean(doc.published), + createdAt: doc.created_at, + updatedAt: doc.updated_at + })); + } catch (error) { + logger.error('获取文档列表失败:', error); + throw error; + } + } + + /** + * 获取已发布文档列表 + */ + async getPublishedDocuments() { + try { + const documents = await database.all( + 'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE published = 1 ORDER BY updated_at DESC' + ); + + return documents.map(doc => ({ + id: doc.doc_id, + title: doc.title, + published: Boolean(doc.published), + createdAt: doc.created_at, + updatedAt: doc.updated_at + })); + } catch (error) { + logger.error('获取已发布文档列表失败:', error); + throw error; + } + } + + /** + * 获取单个文档 + */ + async getDocument(docId) { + try { + const document = await database.get( + 'SELECT * FROM documents WHERE doc_id = ?', + [docId] + ); + + if (!document) { + throw new Error(`文档 ${docId} 不存在`); + } + + return { + id: document.doc_id, + title: document.title, + content: document.content, + published: Boolean(document.published), + createdAt: document.created_at, + updatedAt: document.updated_at + }; + } catch (error) { + logger.error(`获取文档 ${docId} 失败:`, error); + throw error; + } + } + + /** + * 保存文档 + */ + async saveDocument(docId, title, content, published = false) { + try { + const id = docId || Date.now().toString(); + const now = new Date().toISOString(); + + const existingDoc = await database.get( + 'SELECT id FROM documents WHERE doc_id = ?', + [id] + ); + + if (existingDoc) { + // 更新现有文档 + await database.run( + 'UPDATE documents SET title = ?, content = ?, published = ?, updated_at = ? WHERE doc_id = ?', + [title, content, published ? 1 : 0, now, id] + ); + logger.info(`文档 ${id} 已更新`); + } else { + // 创建新文档 + await database.run( + 'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', + [id, title, content, published ? 1 : 0, now, now] + ); + logger.info(`文档 ${id} 已创建`); + } + + return { + id, + title, + content, + published: Boolean(published), + createdAt: existingDoc ? existingDoc.created_at : now, + updatedAt: now + }; + } catch (error) { + logger.error('保存文档失败:', error); + throw error; + } + } + + /** + * 删除文档 + */ + async deleteDocument(docId) { + try { + const result = await database.run( + 'DELETE FROM documents WHERE doc_id = ?', + [docId] + ); + + if (result.changes === 0) { + throw new Error(`文档 ${docId} 不存在`); + } + + logger.info(`文档 ${docId} 已删除`); + return true; + } catch (error) { + logger.error(`删除文档 ${docId} 失败:`, error); + throw error; + } + } + + /** + * 切换文档发布状态 + */ + async toggleDocumentPublish(docId) { + try { + const document = await this.getDocument(docId); + const newPublishedStatus = !document.published; + + await database.run( + 'UPDATE documents SET published = ?, updated_at = ? WHERE doc_id = ?', + [newPublishedStatus ? 1 : 0, new Date().toISOString(), docId] + ); + + logger.info(`文档 ${docId} 发布状态已切换为: ${newPublishedStatus}`); + + return { + ...document, + published: newPublishedStatus, + updatedAt: new Date().toISOString() + }; + } catch (error) { + logger.error(`切换文档 ${docId} 发布状态失败:`, error); + throw error; + } + } + + /** + * 批量更新文档发布状态 + */ + async batchUpdatePublishStatus(docIds, published) { + try { + const placeholders = docIds.map(() => '?').join(','); + const params = [...docIds, published ? 1 : 0, new Date().toISOString()]; + + const result = await database.run( + `UPDATE documents SET published = ?, updated_at = ? WHERE doc_id IN (${placeholders})`, + params + ); + + logger.info(`批量更新 ${result.changes} 个文档的发布状态`); + return result.changes; + } catch (error) { + logger.error('批量更新文档发布状态失败:', error); + throw error; + } + } + + /** + * 搜索文档 + */ + async searchDocuments(keyword, publishedOnly = false) { + try { + let sql = 'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE (title LIKE ? OR content LIKE ?)'; + const params = [`%${keyword}%`, `%${keyword}%`]; + + if (publishedOnly) { + sql += ' AND published = 1'; + } + + sql += ' ORDER BY updated_at DESC'; + + const documents = await database.all(sql, params); + + return documents.map(doc => ({ + id: doc.doc_id, + title: doc.title, + published: Boolean(doc.published), + createdAt: doc.created_at, + updatedAt: doc.updated_at + })); + } catch (error) { + logger.error('搜索文档失败:', error); + throw error; + } + } + + /** + * 获取文档统计信息 + */ + async getDocumentStats() { + try { + const totalCount = await database.get('SELECT COUNT(*) as count FROM documents'); + const publishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 1'); + const unpublishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 0'); + + return { + total: totalCount.count, + published: publishedCount.count, + unpublished: unpublishedCount.count + }; + } catch (error) { + logger.error('获取文档统计信息失败:', error); + throw error; + } + } +} + +module.exports = new DocumentationServiceDB(); diff --git a/hubcmdui/services/httpProxyService.js b/hubcmdui/services/httpProxyService.js new file mode 100644 index 0000000..b898f0d --- /dev/null +++ b/hubcmdui/services/httpProxyService.js @@ -0,0 +1,418 @@ +/** + * HTTP代理服务模块 + */ +const http = require('http'); +const https = require('https'); +const url = require('url'); +const net = require('net'); +const logger = require('../logger'); +const configServiceDB = require('./configServiceDB'); + +class HttpProxyService { + constructor() { + this.proxyServer = null; + this.isRunning = false; + this.config = { + port: 8080, + host: '0.0.0.0', + enableHttps: true, + enableAuth: false, + username: '', + password: '', + allowedHosts: [], + blockedHosts: [], + logRequests: true + }; + } + + /** + * 启动代理服务器 + */ + async start(config = {}) { + try { + this.config = { ...this.config, ...config }; + + if (this.isRunning) { + logger.warn('HTTP代理服务器已在运行'); + return; + } + + this.proxyServer = http.createServer(); + + // 处理HTTP请求 + this.proxyServer.on('request', this.handleHttpRequest.bind(this)); + + // 处理HTTPS CONNECT请求 + this.proxyServer.on('connect', this.handleHttpsConnect.bind(this)); + + // 错误处理 + this.proxyServer.on('error', this.handleServerError.bind(this)); + + return new Promise((resolve, reject) => { + this.proxyServer.listen(this.config.port, this.config.host, (err) => { + if (err) { + reject(err); + } else { + this.isRunning = true; + logger.info(`HTTP代理服务器已启动,监听 ${this.config.host}:${this.config.port}`); + resolve(); + } + }); + }); + } catch (error) { + logger.error('启动HTTP代理服务器失败:', error); + throw error; + } + } + + /** + * 停止代理服务器 + */ + async stop() { + return new Promise((resolve) => { + if (this.proxyServer && this.isRunning) { + this.proxyServer.close(() => { + this.isRunning = false; + logger.info('HTTP代理服务器已停止'); + resolve(); + }); + } else { + resolve(); + } + }); + } + + /** + * 处理HTTP请求 + */ + handleHttpRequest(clientReq, clientRes) { + try { + const targetUrl = clientReq.url; + const parsedUrl = url.parse(targetUrl); + + // 记录请求日志 + if (this.config.logRequests) { + logger.info(`HTTP代理请求: ${clientReq.method} ${targetUrl}`); + } + + // 认证检查 + if (this.config.enableAuth && !this.checkAuth(clientReq)) { + this.sendAuthRequired(clientRes); + return; + } + + // 主机检查 + if (!this.isHostAllowed(parsedUrl.hostname)) { + this.sendForbidden(clientRes, '主机不在允许列表中'); + return; + } + + if (this.isHostBlocked(parsedUrl.hostname)) { + this.sendForbidden(clientRes, '主机已被阻止'); + return; + } + + // 创建目标请求选项 + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), + path: parsedUrl.path, + method: clientReq.method, + headers: { ...clientReq.headers } + }; + + // 移除代理相关的头部 + delete options.headers['proxy-connection']; + delete options.headers['proxy-authorization']; + + // 选择HTTP或HTTPS + const httpModule = parsedUrl.protocol === 'https:' ? https : http; + + // 发送请求到目标服务器 + const proxyReq = httpModule.request(options, (proxyRes) => { + // 复制响应头 + clientRes.writeHead(proxyRes.statusCode, proxyRes.headers); + + // 管道传输响应数据 + proxyRes.pipe(clientRes); + }); + + // 错误处理 + proxyReq.on('error', (err) => { + logger.error('代理请求错误:', err); + if (!clientRes.headersSent) { + clientRes.writeHead(500, { 'Content-Type': 'text/plain' }); + clientRes.end('代理服务器错误'); + } + }); + + // 管道传输请求数据 + clientReq.pipe(proxyReq); + + } catch (error) { + logger.error('处理HTTP请求失败:', error); + if (!clientRes.headersSent) { + clientRes.writeHead(500, { 'Content-Type': 'text/plain' }); + clientRes.end('内部服务器错误'); + } + } + } + + /** + * 处理HTTPS CONNECT请求 + */ + handleHttpsConnect(clientReq, clientSocket, head) { + try { + const { hostname, port } = this.parseConnectUrl(clientReq.url); + + // 记录请求日志 + if (this.config.logRequests) { + logger.info(`HTTPS代理请求: CONNECT ${hostname}:${port}`); + } + + // 认证检查 + if (this.config.enableAuth && !this.checkAuth(clientReq)) { + clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'); + clientSocket.end(); + return; + } + + // 主机检查 + if (!this.isHostAllowed(hostname)) { + clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); + clientSocket.end(); + return; + } + + if (this.isHostBlocked(hostname)) { + clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); + clientSocket.end(); + return; + } + + // 连接到目标服务器 + const serverSocket = net.connect(port, hostname, () => { + // 发送连接成功响应 + clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); + + // 建立隧道 + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); + + // 错误处理 + serverSocket.on('error', (err) => { + logger.error('服务器连接错误:', err); + clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); + clientSocket.end(); + }); + + clientSocket.on('error', (err) => { + logger.error('客户端连接错误:', err); + serverSocket.end(); + }); + + } catch (error) { + logger.error('处理HTTPS CONNECT请求失败:', error); + clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n'); + clientSocket.end(); + } + } + + /** + * 解析CONNECT请求URL + */ + parseConnectUrl(connectUrl) { + const [hostname, port] = connectUrl.split(':'); + return { + hostname, + port: parseInt(port) || 443 + }; + } + + /** + * 检查认证 + */ + checkAuth(req) { + if (!this.config.enableAuth) { + return true; + } + + const auth = req.headers['proxy-authorization']; + if (!auth) { + return false; + } + + const [type, credentials] = auth.split(' '); + if (type !== 'Basic') { + return false; + } + + const decoded = Buffer.from(credentials, 'base64').toString(); + const [username, password] = decoded.split(':'); + + return username === this.config.username && password === this.config.password; + } + + /** + * 检查主机是否允许 + */ + isHostAllowed(hostname) { + if (this.config.allowedHosts.length === 0) { + return true; // 如果没有设置允许列表,则允许所有 + } + + return this.config.allowedHosts.some(allowed => { + if (allowed.startsWith('*.')) { + const domain = allowed.substring(2); + return hostname.endsWith(domain); + } + return hostname === allowed; + }); + } + + /** + * 检查主机是否被阻止 + */ + isHostBlocked(hostname) { + return this.config.blockedHosts.some(blocked => { + if (blocked.startsWith('*.')) { + const domain = blocked.substring(2); + return hostname.endsWith(domain); + } + return hostname === blocked; + }); + } + + /** + * 发送认证要求响应 + */ + sendAuthRequired(res) { + res.writeHead(407, { + 'Proxy-Authenticate': 'Basic realm="Proxy"', + 'Content-Type': 'text/plain' + }); + res.end('需要代理认证'); + } + + /** + * 发送禁止访问响应 + */ + sendForbidden(res, message) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end(message || '禁止访问'); + } + + /** + * 处理服务器错误 + */ + handleServerError(error) { + logger.error('HTTP代理服务器错误:', error); + } + + /** + * 获取代理状态 + */ + getStatus() { + return { + isRunning: this.isRunning, + config: this.config, + port: this.config.port, + host: this.config.host + }; + } + + /** + * 更新配置 + */ + async updateConfig(newConfig) { + try { + const needRestart = this.isRunning && ( + newConfig.port !== this.config.port || + newConfig.host !== this.config.host + ); + + this.config = { ...this.config, ...newConfig }; + + if (needRestart) { + await this.stop(); + await this.start(); + logger.info('HTTP代理服务器配置已更新并重启'); + } else { + logger.info('HTTP代理服务器配置已更新'); + } + + // 保存配置到数据库 + await configServiceDB.saveConfig('httpProxyConfig', this.config); + } catch (error) { + logger.error('更新HTTP代理配置失败:', error); + throw error; + } + } + + /** + * 从数据库加载配置 + */ + async loadConfig() { + try { + const savedConfig = await configServiceDB.getConfig('httpProxyConfig'); + if (savedConfig) { + this.config = { ...this.config, ...savedConfig }; + logger.info('HTTP代理配置已从数据库加载'); + } else { + // 从环境变量加载默认配置 + this.config = { + ...this.config, + port: parseInt(process.env.PROXY_PORT) || this.config.port, + host: process.env.PROXY_HOST || this.config.host + }; + logger.info('使用默认HTTP代理配置'); + } + } catch (error) { + logger.error('加载HTTP代理配置失败:', error); + // 使用默认配置 + logger.info('使用默认HTTP代理配置'); + } + } + + /** + * 检查环境变量并自动启动代理 + */ + async checkEnvironmentAndAutoStart() { + const autoStart = process.env.PROXY_AUTO_START; + const proxyPort = process.env.PROXY_PORT; + const proxyHost = process.env.PROXY_HOST; + const enableAuth = process.env.PROXY_ENABLE_AUTH; + const username = process.env.PROXY_USERNAME; + const password = process.env.PROXY_PASSWORD; + + // 检查是否应该自动启动代理 + if (autoStart === 'true' || proxyPort || proxyHost) { + logger.info('检测到代理环境变量,尝试自动启动HTTP代理服务...'); + + const envConfig = {}; + if (proxyPort) envConfig.port = parseInt(proxyPort); + if (proxyHost) envConfig.host = proxyHost; + if (enableAuth === 'true') { + envConfig.enableAuth = true; + if (username) envConfig.username = username; + if (password) envConfig.password = password; + } + + try { + await this.start(envConfig); + logger.info(`HTTP代理服务已自动启动 - ${envConfig.host || '0.0.0.0'}:${envConfig.port || 8080}`); + } catch (error) { + logger.warn('自动启动HTTP代理服务失败:', error.message); + } + } else { + logger.info('未检测到代理自动启动环境变量'); + } + } +} + +// 创建单例实例 +const httpProxyService = new HttpProxyService(); + +module.exports = httpProxyService; diff --git a/hubcmdui/services/monitoringService.js b/hubcmdui/services/monitoringService.js index ec7190d..24c1207 100644 --- a/hubcmdui/services/monitoringService.js +++ b/hubcmdui/services/monitoringService.js @@ -3,7 +3,7 @@ */ const axios = require('axios'); const logger = require('../logger'); -const configService = require('./configService'); +const configServiceDB = require('./configServiceDB'); const dockerService = require('./dockerService'); // 监控相关状态映射 @@ -15,13 +15,13 @@ let monitoringInterval = null; // 更新监控配置 async function updateMonitoringConfig(config) { try { - const currentConfig = await configService.getConfig(); + const currentConfig = await configServiceDB.getConfig(); currentConfig.monitoringConfig = { ...currentConfig.monitoringConfig, ...config }; - await configService.saveConfig(currentConfig); + await configServiceDB.saveConfig(currentConfig); // 重新启动监控 await startMonitoring(); @@ -36,7 +36,7 @@ async function updateMonitoringConfig(config) { // 启动监控 async function startMonitoring() { try { - const config = await configService.getConfig(); + const config = await configServiceDB.getConfig(); const { isEnabled, monitorInterval } = config.monitoringConfig || {}; // 如果监控已启用 @@ -308,9 +308,9 @@ async function testNotification(config) { // 切换监控状态 async function toggleMonitoring(isEnabled) { - const config = await configService.getConfig(); + const config = await configServiceDB.getConfig(); config.monitoringConfig.isEnabled = isEnabled; - await configService.saveConfig(config); + await configServiceDB.saveConfig(config); return startMonitoring(); } diff --git a/hubcmdui/services/userService.js b/hubcmdui/services/userService.js deleted file mode 100644 index 8087c5f..0000000 --- a/hubcmdui/services/userService.js +++ /dev/null @@ -1,175 +0,0 @@ -/** - * 用户服务模块 - */ -const fs = require('fs').promises; -const path = require('path'); -const bcrypt = require('bcrypt'); -const logger = require('../logger'); - -const USERS_FILE = path.join(__dirname, '..', 'users.json'); - -// 获取所有用户 -async function getUsers() { - try { - const data = await fs.readFile(USERS_FILE, 'utf8'); - return JSON.parse(data); - } catch (error) { - if (error.code === 'ENOENT') { - logger.warn('Users file does not exist, creating default user'); - const defaultUser = { - username: 'root', - password: bcrypt.hashSync('admin', 10), - createdAt: new Date().toISOString(), - loginCount: 0, - lastLogin: null - }; - await saveUsers([defaultUser]); - return { users: [defaultUser] }; - } - throw error; - } -} - -// 保存用户 -async function saveUsers(users) { - await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8'); -} - -// 更新用户登录信息 -async function updateUserLoginInfo(username) { - try { - const { users } = await getUsers(); - const user = users.find(u => u.username === username); - - if (user) { - user.loginCount = (user.loginCount || 0) + 1; - user.lastLogin = new Date().toISOString(); - await saveUsers(users); - } - } catch (error) { - logger.error('更新用户登录信息失败:', error); - } -} - -// 获取用户统计信息 -async function getUserStats(username) { - try { - const { users } = await getUsers(); - const user = users.find(u => u.username === username); - - if (!user) { - return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; - } - - // 计算账户年龄(如果有创建日期) - let accountAge = '0'; - if (user.createdAt) { - const createdDate = new Date(user.createdAt); - const currentDate = new Date(); - const diffTime = Math.abs(currentDate - createdDate); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - accountAge = diffDays.toString(); - } - - // 格式化最后登录时间 - let lastLogin = '未知'; - if (user.lastLogin) { - const lastLoginDate = new Date(user.lastLogin); - const now = new Date(); - const isToday = lastLoginDate.toDateString() === now.toDateString(); - - if (isToday) { - lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } - } - - return { - username: user.username, - loginCount: (user.loginCount || 0).toString(), - lastLogin, - accountAge - }; - } catch (error) { - logger.error('获取用户统计信息失败:', error); - return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; - } -} - -// 创建新用户 -async function createUser(username, password) { - try { - const { users } = await getUsers(); - - // 检查用户是否已存在 - if (users.some(u => u.username === username)) { - throw new Error('用户名已存在'); - } - - const hashedPassword = bcrypt.hashSync(password, 10); - const newUser = { - username, - password: hashedPassword, - createdAt: new Date().toISOString(), - loginCount: 0, - lastLogin: null - }; - - users.push(newUser); - await saveUsers(users); - - return { success: true, username }; - } catch (error) { - logger.error('创建用户失败:', error); - throw error; - } -} - -// 修改用户密码 -async function changePassword(username, currentPassword, newPassword) { - try { - const { users } = await getUsers(); - const user = users.find(u => u.username === username); - - if (!user) { - throw new Error('用户不存在'); - } - - // 验证当前密码 - const isMatch = await bcrypt.compare(currentPassword, user.password); - if (!isMatch) { - throw new Error('当前密码不正确'); - } - - // 验证新密码复杂度(虽然前端做了,后端再做一层保险) - if (!isPasswordComplex(newPassword)) { - throw new Error('新密码不符合复杂度要求'); - } - - // 更新密码 - user.password = await bcrypt.hash(newPassword, 10); - await saveUsers(users); - - logger.info(`用户 ${username} 密码已成功修改`); - } catch (error) { - logger.error('修改密码失败:', error); - throw error; - } -} - -// 验证密码复杂度 (从 userCenter.js 复制过来并调整) -function isPasswordComplex(password) { - // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间 - const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/; - return passwordRegex.test(password); -} - -module.exports = { - getUsers, - saveUsers, - updateUserLoginInfo, - getUserStats, - createUser, - changePassword -}; diff --git a/hubcmdui/services/userServiceDB.js b/hubcmdui/services/userServiceDB.js new file mode 100644 index 0000000..e5b1f95 --- /dev/null +++ b/hubcmdui/services/userServiceDB.js @@ -0,0 +1,190 @@ +/** + * 基于SQLite的用户服务模块 + */ +const bcrypt = require('bcrypt'); +const logger = require('../logger'); +const database = require('../database/database'); + +class UserServiceDB { + /** + * 获取所有用户 + */ + async getUsers() { + try { + const users = await database.all('SELECT * FROM users ORDER BY created_at DESC'); + return { users }; + } catch (error) { + logger.error('获取用户列表失败:', error); + throw error; + } + } + + /** + * 通过用户名获取用户 + */ + async getUserByUsername(username) { + try { + return await database.get('SELECT * FROM users WHERE username = ?', [username]); + } catch (error) { + logger.error('获取用户失败:', error); + throw error; + } + } + + /** + * 创建新用户 + */ + async createUser(username, password) { + try { + // 检查用户是否已存在 + const existingUser = await this.getUserByUsername(username); + if (existingUser) { + throw new Error('用户名已存在'); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const result = await database.run( + 'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)', + [username, hashedPassword, new Date().toISOString(), 0, null] + ); + + return { success: true, username, id: result.id }; + } catch (error) { + logger.error('创建用户失败:', error); + throw error; + } + } + + /** + * 更新用户登录信息 + */ + async updateUserLoginInfo(username) { + try { + const user = await this.getUserByUsername(username); + if (user) { + await database.run( + 'UPDATE users SET login_count = login_count + 1, last_login = ? WHERE username = ?', + [new Date().toISOString(), username] + ); + } + } catch (error) { + logger.error('更新用户登录信息失败:', error); + } + } + + /** + * 获取用户统计信息 + */ + async getUserStats(username) { + try { + const user = await this.getUserByUsername(username); + + if (!user) { + return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; + } + + // 计算账户年龄 + let accountAge = '0'; + if (user.created_at) { + const createdDate = new Date(user.created_at); + const currentDate = new Date(); + const diffTime = Math.abs(currentDate - createdDate); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + accountAge = diffDays.toString(); + } + + // 格式化最后登录时间 + let lastLogin = '未知'; + if (user.last_login) { + const lastLoginDate = new Date(user.last_login); + const now = new Date(); + const isToday = lastLoginDate.toDateString() === now.toDateString(); + + if (isToday) { + lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else { + lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + } + + return { + username: user.username, + loginCount: (user.login_count || 0).toString(), + lastLogin, + accountAge + }; + } catch (error) { + logger.error('获取用户统计信息失败:', error); + return { loginCount: '0', lastLogin: '未知', accountAge: '0' }; + } + } + + /** + * 修改用户密码 + */ + async changePassword(username, currentPassword, newPassword) { + try { + const user = await this.getUserByUsername(username); + + if (!user) { + throw new Error('用户不存在'); + } + + // 验证当前密码 + const isMatch = await bcrypt.compare(currentPassword, user.password); + if (!isMatch) { + throw new Error('当前密码不正确'); + } + + // 验证新密码复杂度 + if (!this.isPasswordComplex(newPassword)) { + throw new Error('新密码不符合复杂度要求'); + } + + // 更新密码 + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + await database.run( + 'UPDATE users SET password = ?, updated_at = ? WHERE username = ?', + [hashedNewPassword, new Date().toISOString(), username] + ); + + logger.info(`用户 ${username} 密码已成功修改`); + } catch (error) { + logger.error('修改密码失败:', error); + throw error; + } + } + + /** + * 验证密码复杂度 + */ + isPasswordComplex(password) { + // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间 + const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/; + return passwordRegex.test(password); + } + + /** + * 验证用户登录 + */ + async validateUser(username, password) { + try { + const user = await this.getUserByUsername(username); + if (!user) { + return null; + } + + const isMatch = await bcrypt.compare(password, user.password); + if (isMatch) { + return user; + } + + return null; + } catch (error) { + logger.error('验证用户失败:', error); + throw error; + } + } +} + +module.exports = new UserServiceDB(); diff --git a/hubcmdui/users.json b/hubcmdui/users.json deleted file mode 100644 index de1cb5c..0000000 --- a/hubcmdui/users.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "users": [ - { - "username": "root", - "password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW", - "loginCount": 2, - "lastLogin": "2025-07-11T02:17:50.457Z" - } - ] -} \ No newline at end of file diff --git a/hubcmdui/utils/database-checker.js b/hubcmdui/utils/database-checker.js new file mode 100644 index 0000000..1d49917 --- /dev/null +++ b/hubcmdui/utils/database-checker.js @@ -0,0 +1,131 @@ +/** + * 数据库状态检查工具 + */ +const sqlite3 = require('sqlite3'); +const path = require('path'); +const fs = require('fs'); + +const DB_PATH = path.join(__dirname, '../data/app.db'); + +/** + * 检查数据库是否已完全初始化 + */ +async function isDatabaseReady() { + return new Promise((resolve) => { + // 检查数据库文件是否存在 + if (!fs.existsSync(DB_PATH)) { + resolve(false); + return; + } + + // 检查文件大小 + try { + const stats = fs.statSync(DB_PATH); + if (stats.size < 1024) { + resolve(false); + return; + } + } catch (error) { + resolve(false); + return; + } + + // 检查数据库结构和数据 + const db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + resolve(false); + return; + } + + // 检查必要的表是否存在 + const requiredTables = ['users', 'configs', 'documents']; + let checkedTables = 0; + let allTablesReady = true; + let tablesWithData = 0; + + requiredTables.forEach(tableName => { + db.get( + `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, + [tableName], + (err, row) => { + if (err || !row) { + allTablesReady = false; + checkedTables++; + checkComplete(); + return; + } + + // 检查表是否有数据(至少用户表和配置表应该有数据) + if (tableName === 'users' || tableName === 'configs') { + db.get(`SELECT COUNT(*) as count FROM ${tableName}`, (err, countRow) => { + if (err || !countRow || countRow.count === 0) { + allTablesReady = false; + } else { + tablesWithData++; + } + checkedTables++; + checkComplete(); + }); + } else { + checkedTables++; + checkComplete(); + } + } + ); + }); + + function checkComplete() { + if (checkedTables === requiredTables.length) { + db.close((err) => { + // 需要至少用户表和配置表有数据 + resolve(allTablesReady && tablesWithData >= 2); + }); + } + } + }); + }); +} + +/** + * 获取数据库统计信息 + */ +async function getDatabaseStats() { + return new Promise((resolve) => { + if (!fs.existsSync(DB_PATH)) { + resolve(null); + return; + } + + const db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + resolve(null); + return; + } + + const stats = {}; + let completedQueries = 0; + const tables = ['users', 'configs', 'documents']; + + tables.forEach(table => { + db.get(`SELECT COUNT(*) as count FROM ${table}`, (err, row) => { + completedQueries++; + if (!err && row) { + stats[table] = row.count; + } else { + stats[table] = 0; + } + + if (completedQueries === tables.length) { + db.close(); + resolve(stats); + } + }); + }); + }); + }); +} + +module.exports = { + isDatabaseReady, + getDatabaseStats +}; diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index c938e1a..e1e7b51 100644 --- a/hubcmdui/web/admin.html +++ b/hubcmdui/web/admin.html @@ -12,12 +12,24 @@ + + + + - - + + + + + + + + + + + + +
+
+

+ + 新建文档 +

+
+ + 返回管理面板 + + +
+
+ + + +
+ +
+
+ + + + + + + diff --git a/hubcmdui/web/index.html b/hubcmdui/web/index.html index 21a3ea6..03f14b7 100644 --- a/hubcmdui/web/index.html +++ b/hubcmdui/web/index.html @@ -13,7 +13,7 @@
- +