159 lines
9.3 KiB
Markdown
159 lines
9.3 KiB
Markdown
## 视频下载同源修复 + 安全漏洞第一批修复 (Day 32)
|
||
|
||
### 概述
|
||
|
||
今天的工作聚焦四件事:
|
||
|
||
1. 修复首页与发布成功弹窗点击下载时被浏览器当作在线播放(新开标签页)的问题。
|
||
2. 将下载修复开始后的开发内容从 `Day31` 拆分到 `Day32`,保持日志按天清晰归档。
|
||
3. 根据安全审计报告(`Temp/安全审计报告.md`),实施第一批 6 项无功能风险的安全修复。
|
||
4. 统一弹窗关闭交互:默认支持点空白关闭,发布成功清理弹窗保持强制留存。
|
||
|
||
---
|
||
|
||
## ✅ 1) 视频下载链路修复(避免新开标签页播放)
|
||
|
||
### 问题现象
|
||
|
||
- 首页“下载视频”与发布成功弹窗“下载视频备份”在部分浏览器会打开新标签页播放视频,而不是直接触发下载。
|
||
- 根因是跨域签名 URL 场景下,浏览器可能忽略 `<a download>`。
|
||
|
||
### 修复方案
|
||
|
||
- 后端新增同源下载接口:`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` ✅
|