From 62ec22a335fd5fe5591e7baf459d795ab8ee34c1 Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Wed, 3 Sep 2025 22:33:30 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix=E5=A4=A7=E5=B0=8F=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/development.sqlite3-shm | Bin 32768 -> 32768 bytes src/middlewares/ErrorHandler/index.js | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/middlewares/ErrorHandler/index.js diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index e8d9ef559001896f529ae0a9a0fba79b9f91002a..005896d90f85968b9e5c239d9c28c992d68f53fb 100644 GIT binary patch delta 55 ucmZo@U}|V!;*@x#%K!!wIpqZftEMKhq_3NgEXWELoEWRjxMX8veLVnk`woZz delta 55 vcmZo@U}|V!;*@x#%K!o_6FKDtU8|-hMx?Ksk1WUv7MvKX%s78zV|_gUj5ZIO diff --git a/src/middlewares/ErrorHandler/index.js b/src/middlewares/ErrorHandler/index.js new file mode 100644 index 0000000..816dce4 --- /dev/null +++ b/src/middlewares/ErrorHandler/index.js @@ -0,0 +1,43 @@ +import { logger } from "@/logger" +// src/plugins/errorHandler.js +// 错误处理中间件插件 + +async function formatError(ctx, status, message, stack) { + const accept = ctx.accepts("json", "html", "text") + const isDev = process.env.NODE_ENV === "development" + if (accept === "json") { + ctx.type = "application/json" + ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message } + } else if (accept === "html") { + ctx.type = "html" + await ctx.render("error/index", { status, message, stack, isDev }) + } else { + ctx.type = "text" + ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}` + } + ctx.status = status +} + +export default function errorHandler() { + return async (ctx, next) => { + // 拦截 Chrome DevTools 探测请求,直接返回 204 + if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") { + ctx.status = 204 + ctx.body = "" + return + } + try { + await next() + if (ctx.status === 404) { + await formatError(ctx, 404, "Resource not found") + } + } catch (err) { + logger.error(err) + const isDev = process.env.NODE_ENV === "development" + if (isDev && err.stack) { + console.error(err.stack) + } + await formatError(ctx, err.statusCode || 500, err.message || err || "Internal server error", isDev ? err.stack : undefined) + } + } +} From 014ed6cc87f696b247b1af0d9b70ade0d3e398c9 Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Wed, 3 Sep 2025 23:31:51 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 163991 -> 178932 bytes database/development.sqlite3-shm | Bin 32768 -> 32768 bytes database/development.sqlite3-wal | Bin 675712 -> 696312 bytes package.json | 1 + src/controllers/Page/PageController.js | 24 +++++++++++++++++++----- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/bun.lockb b/bun.lockb index 649d0f4de3582154657cb09a264e5ade76387bb4..271c91e2262ca78b1d41aeed27281acd3bb7603e 100644 GIT binary patch delta 38516 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 diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index 005896d90f85968b9e5c239d9c28c992d68f53fb..3827c87fbc700252d5582640f1f27d0c7b99f50e 100644 GIT binary patch delta 179 zcmZo@U}|V!s+V}A%K!pQK+MR%Ag~fh3jy)F<%U`TKiypJ|B#;l=KTTn2R`@qn&sSH zPO5sKQDCqFNd89xP+=y9KA}W|__ZoYZ(W8!=yC+WaMuM-Kq$NjC8S diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index e19f5cacdb865abcddd97f750a474b49608ef911..bc8a3141e21f468f988386721233ea38b81eabe1 100644 GIT binary patch delta 366 zcmZpeulZxYRznM83sVbo3rh=Y3tJ0&3r7p*7On<=2?hoRRv;F-|3iBIoA(FQ&CLT% z)aPDg++q)(`D~*uHdvZG&D9bFgG%st|!kW#o<$-Bl*0iQx07n2T+}< zxskc)bX|EaX^|~+78U<_XTrh@w1bh~k%8ZlPhfMQ0t?^v3n$r?8A0ar2{ziW@H0s2 zH?oQ{Fc=#PGQ>{TyT>`57if|>5L=q)7bTX1Z55dqdVX`SErS4=##tI$TACUmjMFk) h{?5hiX8;+-iCY>P0Yk&W0PH%wg1q#^ t.trim()).filter(Boolean) + const allowed = ctx.query.allowedTypes + .split(",") + .map(t => t.trim()) + .filter(Boolean) typeList = defaultTypeList.filter(item => allowed.includes(item.mime)) } @@ -308,7 +312,7 @@ class PageController { ext = path.extname(picked.originalFilename || picked.newFilename || "") || fallbackExt } // 文件名 - const filename = `${ctx.session.user.id}-${Date.now()}-${Math.random().toString(36).slice(2,8)}${ext}` + const filename = `${ctx.session.user.id}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}` const destPath = path.join(uploadsDir, filename) // 如果设置了 uploadDir 且 keepExtensions,文件可能已在该目录,仍统一重命名 if (oldPath && oldPath !== destPath) { @@ -368,18 +372,27 @@ class PageController { // formidable v2 的文件对象 const oldPath = picked.filepath || picked.path + const result = { url: "", thumb: "" } const ext = path.extname(picked.originalFilename || picked.newFilename || "") || path.extname(oldPath || "") || ".jpg" const safeExt = [".jpg", ".jpeg", ".png", ".webp", ".gif"].includes(ext.toLowerCase()) ? ext : ".jpg" - const filename = `${ctx.session.user.id}-${Date.now()}${safeExt}` + const filename = `${ctx.session.user.id}-${Date.now()}/raw${safeExt}` const destPath = path.join(avatarsDir, filename) // 如果设置了 uploadDir 且 keepExtensions,文件可能已在该目录,仍统一重命名 if (oldPath && oldPath !== destPath) { + await fs.mkdir(path.parse(destPath).dir, { recursive: true }) await fs.rename(oldPath, destPath) + try { + const thumbnail = await imageThumbnail(destPath) + fs.writeFile(destPath.replace(/raw\./, "thumb."), thumbnail) + } catch (err) { + console.error(err) + } } const url = `/uploads/avatars/${filename}` - + result.url = url + result.thumb = url.replace(/raw\./, "thumb.") const updatedUser = await this.userService.updateUser(ctx.session.user.id, { avatar: url }) ctx.session.user = { ...ctx.session.user, ...updatedUser } @@ -387,6 +400,7 @@ class PageController { success: true, message: "头像上传成功", url, + thumb: result.thumb, user: updatedUser, } } catch (error) { From a10a97da4f273cd50df0893beba423e615473402 Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Fri, 5 Sep 2025 00:58:29 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(env):=20=E5=A2=9E=E5=BC=BA=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=E9=85=8D=E7=BD=AE=E5=92=8C=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完善 .env.example 模板,增加详细注释和安全提示 - 创建环境变量验证模块,校验必需变量和格式正确性 - 移除 JWT_SECRET 默认值,强制必须配置 - 在应用启动时进行环境变量验证,验证失败则退出 - 更新 README.md,补充快速启动和环境配置指南 - 修改 docker-compose.yml,支持从环境变量读取敏感配置 - 添加环境变量验证测试脚本,覆盖缺失、格式和正确配置场景 - 提供环境变量脱敏显示功能,保护敏感信息安全 - 完善项目文档,新增环境配置、安全规范和改进任务跟踪文档 --- .env.example | 57 +++++++- README.md | 70 +++++++++- database/development.sqlite3-shm | Bin 32768 -> 32768 bytes docker-compose.yml | 4 + docs/environment-setup.md | 141 +++++++++++++++++++ docs/improvement-tasks.md | 195 +++++++++++++++++++++++++++ docs/project-standards-review.md | 285 +++++++++++++++++++++++++++++++++++++++ package.json | 3 +- scripts/test-env-validation.js | 50 +++++++ src/global.js | 19 +-- src/middlewares/Auth/auth.js | 2 +- src/utils/envValidator.js | 165 +++++++++++++++++++++++ 12 files changed, 978 insertions(+), 13 deletions(-) create mode 100644 docs/environment-setup.md create mode 100644 docs/improvement-tasks.md create mode 100644 docs/project-standards-review.md create mode 100644 scripts/test-env-validation.js create mode 100644 src/utils/envValidator.js diff --git a/.env.example b/.env.example index 6ac9f01..c11cbbf 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,55 @@ -SESSION_SECRET=随机字符串 -HTTPS_ENABLE=on \ No newline at end of file +# ======================================== +# koa3-demo 环境变量配置模板 +# ======================================== +# 复制此文件为 .env 并设置实际值 + +# ======================================== +# 必需环境变量 (Required) +# ======================================== + +# 会话密钥,用于cookie签名,多个密钥用逗号分隔,支持密钥轮换 +# Session secrets for cookie signing, comma-separated for key rotation +SESSION_SECRET=your-super-secret-session-key-at-least-32-chars,backup-secret-key + +# JWT密钥,用于生成和验证JWT令牌,至少32个字符 +# JWT secret for token generation and verification, minimum 32 characters +JWT_SECRET=your-super-secret-jwt-key-must-be-at-least-32-characters-long + +# ======================================== +# 可选环境变量 (Optional) +# ======================================== + +# 运行环境: development | production | test +# Application environment +NODE_ENV=development + +# 服务器端口 +# Server port +PORT=3000 + +# 日志文件目录 +# Log files directory +LOG_DIR=logs + +# 是否启用HTTPS (生产环境推荐): on | off +# Enable HTTPS in production environment +HTTPS_ENABLE=off + +# ======================================== +# 生产环境额外配置建议 +# ======================================== + +# 生产环境示例配置: +# NODE_ENV=production +# PORT=3000 +# HTTPS_ENABLE=on +# SESSION_SECRET=生产环境强密钥1,生产环境强密钥2 +# JWT_SECRET=生产环境JWT强密钥至少32字符 + +# ======================================== +# 安全提示 +# ======================================== +# 1. 永远不要将真实的密钥提交到版本控制系统 +# 2. 生产环境的密钥应该使用安全的随机字符串 +# 3. 定期轮换密钥 +# 4. SESSION_SECRET 支持多个密钥,便于无缝密钥更新 \ No newline at end of file diff --git a/README.md b/README.md index 4a07b2e..5764c61 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,51 @@ - [ ] 界面 - [ ] 定时任务 - [ ] htmx + - [x] 环境变量验证 + +## 快速开始 + +### 1. 环境配置 + +复制环境变量模板文件: +```bash +cp .env.example .env +``` + +编辑 `.env` 文件,设置必需的环境变量: +- `SESSION_SECRET`: 会话密钥(至少32个字符) +- `JWT_SECRET`: JWT密钥(至少32个字符) + +### 2. 安装依赖 +```bash +bun install +``` + +### 3. 初始化数据库 +```bash +bun run dev:init +``` + +### 4. 启动开发服务器 +```bash +bun run dev +``` + +## 环境变量说明 + +项目启动时会自动验证环境变量配置,确保应用安全性: + +### 必需环境变量 +- `SESSION_SECRET`: 会话密钥,支持多密钥轮换(逗号分隔) +- `JWT_SECRET`: JWT令牌密钥(最少32字符) + +### 可选环境变量 +- `NODE_ENV`: 运行环境 (development/production/test) +- `PORT`: 服务器端口 (默认: 3000) +- `LOG_DIR`: 日志目录 (默认: logs) +- `HTTPS_ENABLE`: 是否启用HTTPS (默认: off) + +详细配置请参考 `.env.example` 文件。 ### 数据库查询缓存(QueryBuilder 扩展) @@ -58,4 +103,27 @@ const stats = DbQueryCache.stats() // { size, valid, expired } - 该实现为进程内内存缓存,适合单实例与读多写少场景;多实例下需用外部缓存(如 Redis)替换/扩展。 - TTL 为可选,未设置则永久有效,直到被失效或进程重启。 -- 自定义 key 建议使用明确的命名空间前缀(如 `users:`、`site:`),以便使用前缀批量失效。 \ No newline at end of file +- 自定义 key 建议使用明确的命名空间前缀(如 `users:`、`site:`),以便使用前缀批量失效。 + +## 📚 项目文档 + +- [环境变量配置指南](./docs/environment-setup.md) - 详细的环境配置说明 +- [项目规范检查报告](./docs/project-standards-review.md) - 完整的规范评估和改进记录 +- [改进任务跟踪](./docs/improvement-tasks.md) - 项目改进任务的执行状态 + +## 🔧 开发和部署 + +### 测试环境变量配置 +```bash +bun run test:env +``` + +### 生产环境部署 +```bash +# 使用Docker +docker-compose up -d + +# 或直接构建 +docker build -t koa3-demo . +docker run -p 3000:3000 koa3-demo +``` \ No newline at end of file diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index 3827c87fbc700252d5582640f1f27d0c7b99f50e..3b94e9ba73ee083da6936dea1f86cb0a5393c796 100644 GIT binary patch delta 86 zcmZo@U}|V!;*@x#%K!!wIpqb9`P|#flXH7HvY;kR5TcBMVI`399|^$4H#XMS0|1ha B9?Sp$ delta 86 zcmZo@U}|V!;*@x#%K!pQ6FKDtANbtcYnF3+IkKQ8Oc1OLNUQ+j|40BPzI { - app.keys.push(secret) - }) -} +// SESSION_SECRET 已通过环境变量验证确保存在 +process.env.SESSION_SECRET.split(",").forEach(secret => { + app.keys.push(secret.trim()) +}) export { app } export default app \ No newline at end of file diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js index 8d1cc5f..81bfc70 100644 --- a/src/middlewares/Auth/auth.js +++ b/src/middlewares/Auth/auth.js @@ -2,7 +2,7 @@ import { logger } from "@/logger" import jwt from "./jwt" import { minimatch } from "minimatch" -export const JWT_SECRET = process.env.JWT_SECRET || "jwt-demo-secret" +export const JWT_SECRET = process.env.JWT_SECRET function matchList(list, path) { for (const item of list) { diff --git a/src/utils/envValidator.js b/src/utils/envValidator.js new file mode 100644 index 0000000..fc9fb03 --- /dev/null +++ b/src/utils/envValidator.js @@ -0,0 +1,165 @@ +import { logger } from "@/logger.js" + +/** + * 环境变量验证配置 + * required: 必需的环境变量 + * optional: 可选的环境变量(提供默认值) + */ +const ENV_CONFIG = { + required: [ + "SESSION_SECRET", + "JWT_SECRET" + ], + optional: { + "NODE_ENV": "development", + "PORT": "3000", + "LOG_DIR": "logs", + "HTTPS_ENABLE": "off" + } +} + +/** + * 验证必需的环境变量 + * @returns {Object} 验证结果 + */ +function validateRequiredEnv() { + const missing = [] + const valid = {} + + for (const key of ENV_CONFIG.required) { + const value = process.env[key] + if (!value || value.trim() === '') { + missing.push(key) + } else { + valid[key] = value + } + } + + return { missing, valid } +} + +/** + * 设置可选环境变量的默认值 + * @returns {Object} 设置的默认值 + */ +function setOptionalDefaults() { + const defaults = {} + + for (const [key, defaultValue] of Object.entries(ENV_CONFIG.optional)) { + if (!process.env[key]) { + process.env[key] = defaultValue + defaults[key] = defaultValue + } + } + + return defaults +} + +/** + * 验证环境变量的格式和有效性 + * @param {Object} env 环境变量对象 + * @returns {Array} 错误列表 + */ +function validateEnvFormat(env) { + const errors = [] + + // 验证 PORT 是数字 + if (env.PORT && isNaN(parseInt(env.PORT))) { + errors.push("PORT must be a valid number") + } + + // 验证 NODE_ENV 的值 + const validNodeEnvs = ['development', 'production', 'test'] + if (env.NODE_ENV && !validNodeEnvs.includes(env.NODE_ENV)) { + errors.push(`NODE_ENV must be one of: ${validNodeEnvs.join(', ')}`) + } + + // 验证 SESSION_SECRET 至少包含一个密钥 + if (env.SESSION_SECRET) { + const secrets = env.SESSION_SECRET.split(',').filter(s => s.trim()) + if (secrets.length === 0) { + errors.push("SESSION_SECRET must contain at least one non-empty secret") + } + } + + // 验证 JWT_SECRET 长度 + if (env.JWT_SECRET && env.JWT_SECRET.length < 32) { + errors.push("JWT_SECRET must be at least 32 characters long for security") + } + + return errors +} + +/** + * 初始化和验证所有环境变量 + * @returns {boolean} 验证是否成功 + */ +export function validateEnvironment() { + logger.info("🔍 开始验证环境变量...") + + // 1. 验证必需的环境变量 + const { missing, valid } = validateRequiredEnv() + + if (missing.length > 0) { + logger.error("❌ 缺少必需的环境变量:") + missing.forEach(key => { + logger.error(` - ${key}`) + }) + logger.error("请设置这些环境变量后重新启动应用") + return false + } + + // 2. 设置可选环境变量的默认值 + const defaults = setOptionalDefaults() + if (Object.keys(defaults).length > 0) { + logger.info("⚙️ 设置默认环境变量:") + Object.entries(defaults).forEach(([key, value]) => { + logger.info(` - ${key}=${value}`) + }) + } + + // 3. 验证环境变量格式 + const formatErrors = validateEnvFormat(process.env) + if (formatErrors.length > 0) { + logger.error("❌ 环境变量格式错误:") + formatErrors.forEach(error => { + logger.error(` - ${error}`) + }) + return false + } + + // 4. 记录有效的环境变量(敏感信息脱敏) + logger.info("✅ 环境变量验证成功:") + logger.info(` - NODE_ENV=${process.env.NODE_ENV}`) + logger.info(` - PORT=${process.env.PORT}`) + logger.info(` - LOG_DIR=${process.env.LOG_DIR}`) + logger.info(` - SESSION_SECRET=${maskSecret(process.env.SESSION_SECRET)}`) + logger.info(` - JWT_SECRET=${maskSecret(process.env.JWT_SECRET)}`) + + return true +} + +/** + * 脱敏显示敏感信息 + * @param {string} secret 敏感字符串 + * @returns {string} 脱敏后的字符串 + */ +export function maskSecret(secret) { + if (!secret) return "未设置" + if (secret.length <= 8) return "*".repeat(secret.length) + return secret.substring(0, 4) + "*".repeat(secret.length - 8) + secret.substring(secret.length - 4) +} + +/** + * 获取环境变量配置(用于生成 .env.example) + * @returns {Object} 环境变量配置 + */ +export function getEnvConfig() { + return ENV_CONFIG +} + +export default { + validateEnvironment, + getEnvConfig, + maskSecret +} \ No newline at end of file