## 视频下载同源修复 + 安全漏洞第一批修复 (Day 32) ### 概述 今天的工作聚焦四件事: 1. 修复首页与发布成功弹窗点击下载时被浏览器当作在线播放(新开标签页)的问题。 2. 将下载修复开始后的开发内容从 `Day31` 拆分到 `Day32`,保持日志按天清晰归档。 3. 根据安全审计报告(`Temp/安全审计报告.md`),实施第一批 6 项无功能风险的安全修复。 4. 统一弹窗关闭交互:默认支持点空白关闭,发布成功清理弹窗保持强制留存。 --- ## ✅ 1) 视频下载链路修复(避免新开标签页播放) ### 问题现象 - 首页“下载视频”与发布成功弹窗“下载视频备份”在部分浏览器会打开新标签页播放视频,而不是直接触发下载。 - 根因是跨域签名 URL 场景下,浏览器可能忽略 ``。 ### 修复方案 - 后端新增同源下载接口:`GET /api/videos/generated/{video_id}/download` - 使用 `FileResponse` 返回本地视频文件 - 显式返回 `Content-Disposition: attachment` - 浏览器直接进入保存文件流程 - 发布成功弹窗下载改为传 `videoId`,不再依赖签名 URL。 - 首页作品预览下载同步改为同源下载接口,下载行为与发布弹窗统一。 - 兼容旧清理状态:`CleanupContext` 对旧 `videoDownloadUrl` 持久化字段做 `videoId` 解析回填。 --- ## ✅ 2) 配套调整与文档拆分 ### 前端联动 - `CleanupContext` 继续沿用“清理失败不关弹窗、不清本地”的逻辑,下载链路仅替换为同源接口。 - 首页 `PreviewPanel` 支持传入 `generatedVideoId`,下载按钮优先走 `/api/videos/generated/{id}/download`。 ### 日志归档 - 将“下载修复开始后的内容”从 `Day31` 移出并归档到 `Day32`。 - `Day31` 保留 Day31 当日核心内容(到 cleanup 链路加固为止)。 --- ## ✅ 3) 安全漏洞第一批修复(6 项,无功能风险) 根据安全审计报告,实施第一批 6 项可直接修复的安全加固项。 ### 3.1 JWT 默认密钥启动拦截 - **文件**:`backend/app/main.py` - 新增 `check_jwt_secret` startup 事件(在 `init_admin` 之前) - 当 `JWT_SECRET_KEY` 仍为默认值 `"your-secret-key-change-in-production"` 时: - **生产环境**(`DEBUG=False`):`raise RuntimeError` 直接阻止服务启动 - **开发环境**(`DEBUG=True`):输出 `CRITICAL` 级别日志告警,不阻止启动 ### 3.2 AI / Tools 接口加认证 - **文件**:`backend/app/modules/ai/router.py`、`backend/app/modules/tools/router.py` - AI 路由 3 个端点(`/translate`、`/generate-meta`、`/rewrite`)均增加 `Depends(get_current_user)` - Tools 路由 1 个端点(`/extract-script`)增加 `Depends(get_current_user)` - 前端 axios 已有 `withCredentials: true`,401 自动跳登录页,无需前端改动 ### 3.3 素材路径穿越修复 - **文件**:`backend/app/modules/materials/router.py`、`backend/app/modules/materials/service.py` - `stream`、`delete_material`、`rename_material` 三处在 `startswith(user_id)` 校验之前新增 `..` 拒绝 - 含 `..` 的 `material_id` 直接返回 400 - `delete_material` 路由补充 `except ValueError` → 400(原先仅 catch `PermissionError`,`ValueError` 会被 `Exception` 兜底返回 500) ### 3.4 video_id 白名单校验 - **文件**:`backend/app/modules/videos/router.py` - `download_generated` 和 `delete_generated` 两个端点在函数开头增加正则校验 - 仅允许 `^[A-Za-z0-9_-]+$`,不符合直接返回 400 ### 3.5 上传/下载大小限制 - **materials/service.py**(流式上传):在 chunk 累加后检查 `MAX_UPLOAD_SIZE_MB`(默认 500MB),超限抛 `ValueError` - **ref_audios/service.py**(参考音频):`await file.read()` 后检查 5MB 上限 - **tools/service.py**(文案提取文件上传):将 `shutil.copyfileobj` 替换为分块拷贝 + 500MB 限制 - **tools/service.py**(URL 下载分支):`_download_video` 返回后检查文件体积,超 500MB 删除临时文件并拒绝 ### 3.6 错误信息通用化 - **ai/router.py**:3 处 `detail=str(e)` 分别改为"翻译服务暂时不可用"、"生成标题标签失败"、"改写服务暂时不可用" - **tools/router.py**:保留 "Fresh cookies" 特定分支提示,fallback 改为"文案提取失败,请稍后重试" - **generated_audios/service.py**:任务失败 `error` 字段从 `traceback.format_exc()` 改为 `str(e)`,traceback 仅写入服务端日志 --- ## ✅ 4) 弹窗关闭交互统一(UX) ### 目标 - 保持统一交互预期:业务弹窗默认可通过 `X` 与点击遮罩关闭。 - 保留关键流程保护:发布成功清理弹窗继续禁止遮罩关闭,避免误触导致流程中断。 ### 调整内容 - 文案提取弹窗(`ScriptExtractionModal`)支持点击遮罩关闭。 - AI 改写弹窗(`RewriteModal`)支持点击遮罩关闭。 - 发布页扫码登录弹窗支持点击遮罩关闭。 - 修改密码弹窗支持点击遮罩关闭。 - 录音弹窗采用动态策略:`closeOnOverlay={!isRecording}` - 未录音:允许遮罩关闭 - 录音中:禁止遮罩关闭(防误触);`X` 关闭仍可用,且会先停止录音再关闭 - 发布成功清理弹窗维持 `closeOnOverlay=false`,并且不提供 `onClose`(无右上角关闭按钮)。 --- ## 📁 今日主要修改文件 | 文件 | 改动 | |------|------| | `backend/app/modules/videos/router.py` | 新增 `GET /api/videos/generated/{video_id}/download`,返回 `attachment` 下载响应;新增 `video_id` 白名单正则校验(`^[A-Za-z0-9_-]+$`) | | `frontend/src/features/publish/model/usePublishController.ts` | 发布成功后 `triggerCleanup()` 传 `video.id`(替换签名 URL) | | `frontend/src/shared/contexts/CleanupContext.tsx` | 下载字段改为 `videoId`;兼容旧 `videoDownloadUrl` 回填;下载按钮改同源路径 | | `frontend/src/features/home/ui/PreviewPanel.tsx` | 首页下载改为同源下载接口 | | `frontend/src/features/home/ui/HomePage.tsx` | 透传 `generatedVideoId` 给 `PreviewPanel` | | `frontend/src/features/home/ui/ScriptExtractionModal.tsx` | 弹窗支持点击遮罩关闭(`closeOnOverlay`) | | `frontend/src/features/home/ui/RewriteModal.tsx` | 弹窗支持点击遮罩关闭(`closeOnOverlay`) | | `frontend/src/features/publish/ui/PublishPage.tsx` | 扫码登录弹窗支持点击遮罩关闭 | | `frontend/src/components/AccountSettingsDropdown.tsx` | 修改密码弹窗支持点击遮罩关闭 | | `frontend/src/features/home/ui/RefAudioPanel.tsx` | 录音弹窗改为 `closeOnOverlay={!isRecording}`(录音中禁遮罩关闭) | | `Docs/DevLogs/Day31.md` | 移除下载修复章节与对应验证/覆盖项(迁入 Day32) | | `Docs/TASK_COMPLETE.md` | 新增 Day32 Current 区块,Day31 取消 Current | | `Docs/BACKEND_README.md` | 补充 `/api/videos/generated/{video_id}/download` 接口说明 | | `Docs/BACKEND_DEV.md` | 补充下载接口 `attachment` 约定 | | `Docs/FRONTEND_README.md` | 补充首页/发布弹窗下载统一同源接口说明 | | `Docs/FRONTEND_DEV.md` | 补充 CleanupContext 下载策略规范 | | `Docs/PUBLISH_DEPLOY.md` | 补充发布成功后同源下载联动说明 | | `README.md` | 补充”一键下载直达(同源 attachment)”能力描述 | | `backend/app/main.py` | `check_jwt_secret` startup 事件:生产环境(`DEBUG=False`)强拦截启动,开发环境 `CRITICAL` 告警 | | `backend/app/modules/ai/router.py` | 3 个端点加 `Depends(get_current_user)` 认证;错误返回改为通用消息 | | `backend/app/modules/tools/router.py` | `extract-script` 端点加 `Depends(get_current_user)` 认证;错误返回改为通用消息 | | `backend/app/modules/materials/router.py` | `stream` 端点新增 `..` 路径穿越拒绝;`delete` 端点补充 `except ValueError` → 400 | | `backend/app/modules/materials/service.py` | `delete_material` / `rename_material` 新增 `..` 路径穿越拒绝;流式上传增加 `MAX_UPLOAD_SIZE_MB` 大小限制 | | `backend/app/modules/ref_audios/service.py` | 参考音频上传增加 5MB 大小限制 | | `backend/app/modules/tools/service.py` | 文案提取文件上传替换为限大小分块拷贝(500MB);URL 下载分支增加下载后体积检查(500MB) | | `backend/app/modules/generated_audios/service.py` | 任务失败错误字段从 `traceback.format_exc()` 改为 `str(e)`,避免泄露内部路径 | --- ## 🔍 验证记录 - `python -m py_compile backend/app/modules/videos/router.py` ✅ - `npm run build`(frontend)✅ - `npm run build`(frontend,弹窗关闭策略调整后复验)✅ - `pm2 restart vigent2-frontend` ✅ - `pm2 restart vigent2-backend` ✅ - `curl http://127.0.0.1:8006/health` 返回 `{"status":"ok"}` ✅ - 安全修复第一批语法验证:`python -m py_compile backend/app/main.py backend/app/modules/materials/router.py backend/app/modules/tools/service.py backend/app/modules/ai/router.py backend/app/modules/tools/router.py backend/app/modules/materials/service.py backend/app/modules/ref_audios/service.py backend/app/modules/videos/router.py backend/app/modules/generated_audios/service.py` ✅ - 未登录调用 `/api/ai/translate` → 返回 401 ✅ - 未登录调用 `/api/tools/extract-script` → 返回 401 ✅ - 收尾三刀语法验证:`python -m py_compile backend/app/main.py backend/app/modules/materials/router.py backend/app/modules/tools/service.py` ✅