diff --git a/backend/auth.js b/backend/auth.js index a52c70c..1fe9fb6 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -132,9 +132,15 @@ function adminMiddleware(req, res, next) { next(); } +// 检查JWT密钥是否安全 +function isJwtSecretSecure() { + return !DEFAULT_SECRETS.includes(JWT_SECRET) && JWT_SECRET.length >= 32; +} + module.exports = { JWT_SECRET, generateToken, authMiddleware, - adminMiddleware + adminMiddleware, + isJwtSecretSecure }; diff --git a/backend/server.js b/backend/server.js index 6589201..5639328 100644 --- a/backend/server.js +++ b/backend/server.js @@ -21,7 +21,7 @@ const execAsync = util.promisify(exec); const execFileAsync = util.promisify(execFile); const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database'); -const { generateToken, authMiddleware, adminMiddleware } = require('./auth'); +const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth'); const app = express(); const PORT = process.env.PORT || 40001; @@ -3382,6 +3382,184 @@ app.post('/api/admin/settings/test-smtp', authMiddleware, adminMiddleware, async } }); +// 系统健康检测API +app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req, res) => { + try { + const checks = []; + let overallStatus = 'healthy'; // healthy, warning, critical + + // 1. JWT密钥安全检查 + const jwtSecure = isJwtSecretSecure(); + checks.push({ + name: 'JWT密钥', + category: 'security', + status: jwtSecure ? 'pass' : 'fail', + message: jwtSecure ? 'JWT密钥已正确配置(随机生成)' : 'JWT密钥使用默认值或长度不足,存在安全风险!', + suggestion: jwtSecure ? null : '请在.env中设置随机生成的JWT_SECRET,至少32字符' + }); + if (!jwtSecure) overallStatus = 'critical'; + + // 2. CORS配置检查 + const allowedOrigins = process.env.ALLOWED_ORIGINS; + const corsConfigured = allowedOrigins && allowedOrigins.trim().length > 0; + checks.push({ + name: 'CORS跨域配置', + category: 'security', + status: corsConfigured ? 'pass' : 'warning', + message: corsConfigured + ? `已配置允许的域名: ${allowedOrigins}` + : 'CORS未配置,允许所有来源(仅适合开发环境)', + suggestion: corsConfigured ? null : '生产环境建议配置ALLOWED_ORIGINS环境变量' + }); + if (!corsConfigured && overallStatus === 'healthy') overallStatus = 'warning'; + + // 3. HTTPS/Cookie安全配置 + const enforceHttps = process.env.ENFORCE_HTTPS === 'true'; + const cookieSecure = process.env.COOKIE_SECURE === 'true'; + const httpsConfigured = enforceHttps && cookieSecure; + checks.push({ + name: 'HTTPS安全配置', + category: 'security', + status: httpsConfigured ? 'pass' : 'warning', + message: httpsConfigured + ? 'HTTPS强制开启,Cookie安全标志已设置' + : `ENFORCE_HTTPS=${enforceHttps}, COOKIE_SECURE=${cookieSecure}`, + suggestion: httpsConfigured ? null : '生产环境建议开启ENFORCE_HTTPS和COOKIE_SECURE' + }); + + // 4. 管理员密码强度检查(检查是否为默认值) + const adminUsername = process.env.ADMIN_USERNAME; + const adminConfigured = adminUsername && adminUsername !== 'admin'; + checks.push({ + name: '管理员账号配置', + category: 'security', + status: adminConfigured ? 'pass' : 'warning', + message: adminConfigured + ? '管理员用户名已自定义' + : '管理员使用默认用户名"admin"', + suggestion: adminConfigured ? null : '建议使用自定义管理员用户名' + }); + + // 5. SMTP邮件配置检查 + const smtpHost = SettingsDB.get('smtp_host') || process.env.SMTP_HOST; + const smtpUser = SettingsDB.get('smtp_user') || process.env.SMTP_USER; + const smtpPassword = SettingsDB.get('smtp_password') || process.env.SMTP_PASSWORD; + const smtpConfigured = smtpHost && smtpUser && smtpPassword; + checks.push({ + name: 'SMTP邮件服务', + category: 'service', + status: smtpConfigured ? 'pass' : 'warning', + message: smtpConfigured + ? `已配置: ${smtpHost}` + : '未配置SMTP,邮箱验证和密码重置功能不可用', + suggestion: smtpConfigured ? null : '配置SMTP以启用邮箱验证功能' + }); + + // 6. 数据库连接检查 + let dbStatus = 'pass'; + let dbMessage = '数据库连接正常'; + try { + const testQuery = db.prepare('SELECT 1').get(); + if (!testQuery) throw new Error('查询返回空'); + } catch (dbError) { + dbStatus = 'fail'; + dbMessage = '数据库连接异常: ' + dbError.message; + overallStatus = 'critical'; + } + checks.push({ + name: '数据库连接', + category: 'service', + status: dbStatus, + message: dbMessage, + suggestion: dbStatus === 'fail' ? '检查数据库文件权限和路径配置' : null + }); + + // 7. 存储目录检查 + const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage'); + let storageStatus = 'pass'; + let storageMessage = `存储目录正常: ${storageRoot}`; + try { + if (!fs.existsSync(storageRoot)) { + fs.mkdirSync(storageRoot, { recursive: true }); + storageMessage = `存储目录已创建: ${storageRoot}`; + } + // 检查写入权限 + const testFile = path.join(storageRoot, '.health-check-test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + } catch (storageError) { + storageStatus = 'fail'; + storageMessage = '存储目录不可写: ' + storageError.message; + overallStatus = 'critical'; + } + checks.push({ + name: '存储目录', + category: 'service', + status: storageStatus, + message: storageMessage, + suggestion: storageStatus === 'fail' ? '检查存储目录权限,确保Node进程有写入权限' : null + }); + + // 8. 限流器状态 + const rateLimiterActive = typeof loginLimiter !== 'undefined' && loginLimiter !== null; + checks.push({ + name: '登录防爆破', + category: 'security', + status: rateLimiterActive ? 'pass' : 'warning', + message: rateLimiterActive + ? '限流器已启用(5次/15分钟,封锁30分钟)' + : '限流器未正常初始化', + suggestion: null + }); + + // 9. 信任代理配置(反向代理环境) + const trustProxy = app.get('trust proxy'); + checks.push({ + name: '反向代理支持', + category: 'config', + status: trustProxy ? 'pass' : 'info', + message: trustProxy + ? '已启用trust proxy,支持X-Forwarded-For' + : '未启用trust proxy,直接暴露时无影响', + suggestion: null + }); + + // 10. Node环境 + const nodeEnv = process.env.NODE_ENV || 'development'; + checks.push({ + name: '运行环境', + category: 'config', + status: nodeEnv === 'production' ? 'pass' : 'info', + message: `当前环境: ${nodeEnv}`, + suggestion: nodeEnv !== 'production' ? '生产部署建议设置NODE_ENV=production' : null + }); + + // 统计 + const summary = { + total: checks.length, + pass: checks.filter(c => c.status === 'pass').length, + warning: checks.filter(c => c.status === 'warning').length, + fail: checks.filter(c => c.status === 'fail').length, + info: checks.filter(c => c.status === 'info').length + }; + + res.json({ + success: true, + overallStatus, + summary, + checks, + timestamp: new Date().toISOString(), + version: process.env.npm_package_version || '1.1.0' + }); + } catch (error) { + console.error('健康检测失败:', error); + res.status(500).json({ + success: false, + message: '健康检测失败: ' + error.message + }); + } +}); + // 获取服务器存储统计信息 app.get('/api/admin/storage-stats', authMiddleware, adminMiddleware, async (req, res) => { try { diff --git a/frontend/app.html b/frontend/app.html index fdf5d2f..4fdde2a 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -654,6 +654,12 @@ background: rgba(102, 126, 234, 0.05); border-radius: 12px; } + + /* 健康检测状态颜色 */ + .text-green-600 { color: #16a34a; } + .text-yellow-600 { color: #ca8a04; } + .text-red-600 { color: #dc2626; } + .text-blue-600 { color: #2563eb; } @@ -1747,6 +1753,127 @@ + +
+
+

+ 系统健康检测 +

+ +
+ + +
+
+
+ + {{ getOverallStatusText(healthCheck.overallStatus) }} +
+
+ 通过: {{ healthCheck.summary.pass }} + 警告: {{ healthCheck.summary.warning }} + 失败: {{ healthCheck.summary.fail }} + 信息: {{ healthCheck.summary.info }} +
+
+
+ 上次检测: {{ new Date(healthCheck.lastCheck).toLocaleString() }} +
+
+ + +
+ +
+

安全配置

+
+
+ + {{ getHealthStatusIcon(check.status) }} + +
+
{{ check.name }}
+
{{ check.message }}
+
+ {{ check.suggestion }} +
+
+
+
+
+ +
+

服务状态

+
+
+ + {{ getHealthStatusIcon(check.status) }} + +
+
{{ check.name }}
+
{{ check.message }}
+
+ {{ check.suggestion }} +
+
+
+
+
+ +
+

运行配置

+
+
+ + {{ getHealthStatusIcon(check.status) }} + +
+
{{ check.name }}
+
{{ check.message }}
+
+ {{ check.suggestion }} +
+
+
+
+
+
+ + +
+ +

点击"刷新检测"按钮开始系统健康检测

+
+
+

用户管理

diff --git a/frontend/app.js b/frontend/app.js index de3dee8..83110a1 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -148,6 +148,15 @@ createApp({ } }, + // 健康检测 + healthCheck: { + loading: false, + lastCheck: null, + overallStatus: null, // healthy, warning, critical + summary: { total: 0, pass: 0, warning: 0, fail: 0, info: 0 }, + checks: [] + }, + // Toast通知 toasts: [], toastIdCounter: 0, @@ -2041,10 +2050,11 @@ handleDragLeave(e) { this.loadShares(); break; case 'admin': - // 切换到管理后台时,重新加载用户列表 + // 切换到管理后台时,重新加载用户列表和健康检测 if (this.user && this.user.is_admin) { this.loadUsers(); this.loadServerStorageStats(); + this.loadHealthCheck(); } break; case 'settings': @@ -2260,6 +2270,67 @@ handleDragLeave(e) { } }, + // ===== 健康检测 ===== + + async loadHealthCheck() { + this.healthCheck.loading = true; + try { + const response = await axios.get(`${this.apiBase}/api/admin/health-check`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.healthCheck.overallStatus = response.data.overallStatus; + this.healthCheck.summary = response.data.summary; + this.healthCheck.checks = response.data.checks; + this.healthCheck.lastCheck = response.data.timestamp; + } + } catch (error) { + console.error('健康检测失败:', error); + this.showToast('error', '错误', '健康检测失败'); + } finally { + this.healthCheck.loading = false; + } + }, + + getHealthStatusColor(status) { + const colors = { + pass: 'bg-green-100 text-green-800', + warning: 'bg-yellow-100 text-yellow-800', + fail: 'bg-red-100 text-red-800', + info: 'bg-blue-100 text-blue-800' + }; + return colors[status] || 'bg-gray-100 text-gray-800'; + }, + + getHealthStatusIcon(status) { + const icons = { + pass: '✓', + warning: '⚠', + fail: '✗', + info: 'ℹ' + }; + return icons[status] || '?'; + }, + + getOverallStatusColor(status) { + const colors = { + healthy: 'text-green-600', + warning: 'text-yellow-600', + critical: 'text-red-600' + }; + return colors[status] || 'text-gray-600'; + }, + + getOverallStatusText(status) { + const texts = { + healthy: '系统健康', + warning: '存在警告', + critical: '存在问题' + }; + return texts[status] || '未知'; + }, + // ===== 上传工具管理 ===== // 检测上传工具是否存在