فهرست منبع

fix: 个人数据接口改严格鉴权 requireAuth, 修复token过期静默降级user1

根因: token 过期/无效时 optionalAuth 静默兜底成 user1, 导致用户看到
user1 名下积累的全部数据(2818条历史等). 隔离逻辑本身正确, 问题在降级.

修复: 新增 requireAuth 中间件(token 无效/过期返回 body code:401, 不降级),
前端 request.ts 已有 code===401 → 清token跳登录 的处理.

换用 requireAuth 的个人接口:
- history(GET / 和 /list)+ history-batch(批量删)
- drafts / playlist / notifications 全部
- video-generator projects/materials 全部
- langgraph: my-books + 所有写/生成接口 + 个人态读(progress/status/
  full-content/workflow/interactive读) + debug/trigger-audio
- album: /list(我的书) + 改书(POST) + merge-audio

保留 optionalAuth(公开浏览, 已有归属校验): GET /books 列表, GET /books/:id
详情, /books/:id/chapters, chapter-content, POST /books 创建
MyFramework User 2 ماه پیش
والد
کامیت
546b61bf94

+ 31 - 2
server/src/middleware/auth.ts

@@ -49,8 +49,7 @@ export async function authMiddleware(ctx: Context, next: Next): Promise<void> {
 }
 
 // 可选认证(允许未登录访问,自动使用测试用户)
-export async function optionalAuth(ctx: Context, next: Next): Promise<void> {
-  const authHeader = ctx.get('Authorization');
+export async function optionalAuth(ctx: Context, next: Next): Promise<void> {  const authHeader = ctx.get('Authorization');
 
   if (authHeader) {
     const parts = authHeader.split(' ');
@@ -78,3 +77,33 @@ export async function optionalAuth(ctx: Context, next: Next): Promise<void> {
 
   await next();
 }
+
+/**
+ * 严格认证:必须携带有效 token,否则返回 401(不降级为测试用户)
+ *
+ * 用于"只对本人有意义"的个人数据接口(历史/草稿/播放列表/通知/视频项目/我的书等),
+ * 避免 token 过期/缺失时被 optionalAuth 静默兜底成 user1,从而看到 user1 名下数据。
+ *
+ * 返回响应体 code:401(前端 request.ts 按 body.code===401 清 token 并跳登录)。
+ */
+export async function requireAuth(ctx: Context, next: Next): Promise<void> {
+  const authHeader = ctx.get('Authorization');
+  const parts = authHeader ? authHeader.split(' ') : [];
+  if (parts.length !== 2 || parts[0] !== 'Bearer') {
+    ctx.status = 401;
+    ctx.body = { code: 401, message: '请先登录' };
+    return;
+  }
+  try {
+    const payload = jwt.verify(parts[1], config.jwt.secret) as JwtPayload;
+    ctx.state.user = payload;
+    await next();
+  } catch (err: unknown) {
+    const error = err as Error;
+    ctx.status = 401;
+    ctx.body = {
+      code: 401,
+      message: error.name === 'TokenExpiredError' ? '登录已过期,请重新登录' : '登录无效,请重新登录',
+    };
+  }
+}

+ 4 - 4
server/src/modules/book-generator/album-controller.ts

@@ -7,7 +7,7 @@ import { Context } from 'koa';
 import path from 'path';
 import { bookStore } from './book-generator.store';
 import { assertBookAccess, assertChapterAccess } from './access-control';
-import { optionalAuth } from '../../middleware/auth';
+import { optionalAuth, requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 // 开发环境测试用户ID
@@ -103,7 +103,7 @@ router.get('/albums/:id/chapters', optionalAuth, getChapters);
  * GET /api/book-generator/list
  * 获取书籍列表(带分页和日期过滤)
  */
-router.get('/list', optionalAuth, async (ctx: Context) => {
+router.get('/list', requireAuth, async (ctx: Context) => {
   try {
     const { startDate, pageSize = '100' } = ctx.query as { startDate?: string; pageSize?: string };
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -263,7 +263,7 @@ router.get('/books/:id', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/books/:id
  * 更新书籍(书名、描述等)
  */
-router.post('/books/:id', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id', requireAuth, async (ctx: Context) => {
   try {
     const bookId = parseInt(ctx.params.id as string);
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -456,7 +456,7 @@ router.get('/books/:id/chapters', optionalAuth, getChapters);
  * POST /api/book-generator/books/:id/chapters/:chapterId/merge-audio
  * 合并章下所有小节音频为单个文件
  */
-router.post('/books/:id/chapters/:chapterId/merge-audio', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/:chapterId/merge-audio', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const chapterId = parseInt(ctx.params.chapterId as string);

+ 36 - 36
server/src/modules/book-generator/langgraph-controller.ts

@@ -10,7 +10,7 @@ import { bookStore, cancelAudioGeneration } from './book-generator.store';
 import { assertBookAccess, assertChapterAccess } from './access-control';
 import { prisma } from '../../models';
 import { estimateBookWords, estimateAudioMinutesFromWords, checkBookGenerationQuota, checkAudioQuota, atomicReserveQuota, releaseQuota, AUDIO_BILLING_CONFIG } from '../subscription/subscription.service';
-import { optionalAuth } from '../../middleware/auth';
+import { optionalAuth, requireAuth } from '../../middleware/auth';
 import { getAllBookTypes, getDetectableTypes, getBookTypeConfig, BOOK_TYPE_CONFIG, DETECTABLE_TYPES } from './book-type-config';
 import { callLLMWithMessages, callLLM, ChatMessage } from '../../services/llm';
 import { cleanLlmShortText, extractJsonFromResponse } from '../../services/llm/response-cleaner';
@@ -769,7 +769,7 @@ router.get('/public-books', async (ctx: Context) => {
  * 获取当前用户自己的书籍列表(管理页专用)
  * 只返回当前用户创建的书籍
  */
-router.get('/my-books', optionalAuth, async (ctx: Context) => {
+router.get('/my-books', requireAuth, async (ctx: Context) => {
   try {
     const userId = ctx.state.user?.userId;
     if (!userId) {
@@ -791,7 +791,7 @@ router.get('/my-books', optionalAuth, async (ctx: Context) => {
  * PUT /api/book-generator/langgraph/books/:id/publish
  * 切换书籍公开状态
  */
-router.put('/books/:id/publish', optionalAuth, async (ctx: Context) => {
+router.put('/books/:id/publish', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -858,7 +858,7 @@ router.get('/books/:id', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/books/:id/progress
  * 获取书籍生成进度
  */
-router.get('/books/:id/progress', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/progress', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -917,7 +917,7 @@ router.get('/books/:id/progress', optionalAuth, async (ctx: Context) => {
  * DELETE /api/book-generator/langgraph/books/:id
  * 删除书籍
  */
-router.delete('/books/:id', optionalAuth, async (ctx: Context) => {
+router.delete('/books/:id', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -940,7 +940,7 @@ router.delete('/books/:id', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/generate
  * 对已有书籍使用 LangGraph 生成
  */
-router.post('/books/:id/generate', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/generate', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1097,7 +1097,7 @@ router.post('/books/:id/generate', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/audio
  * 批量生成书籍所有小节的音频
  */
-router.post('/books/:id/audio', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/audio', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1180,7 +1180,7 @@ router.post('/books/:id/audio', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/audio/cancel
  * 取消书籍所有章节的音频生成
  */
-router.post('/books/:id/audio/cancel', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/audio/cancel', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1226,7 +1226,7 @@ router.post('/books/:id/audio/cancel', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/books/:id/video-status
  * 获取书籍视频生成状态(用于前端轮询)
  */
-router.get('/books/:id/video-status', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/video-status', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1282,7 +1282,7 @@ router.get('/books/:id/video-status', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/books/:id/audio-status
  * 获取书籍音频生成状态(用于前端轮询)
  */
-router.get('/books/:id/audio-status', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/audio-status', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1338,7 +1338,7 @@ router.get('/books/:id/audio-status', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/books/:id/failed-chapters
  * 获取生成失败的小节列表
  */
-router.get('/books/:id/failed-chapters', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/failed-chapters', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1392,7 +1392,7 @@ router.get('/books/:id/failed-chapters', optionalAuth, async (ctx: Context) => {
  * 逐个调用 generateSingleChapterContent(只补单章,不碰大纲),
  * 音频随后由 AudioScanner 兜底入队。
  */
-router.post('/books/:id/resume', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/resume', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1492,7 +1492,7 @@ router.post('/books/:id/resume', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/retry-chapter
  * 单独重试某个失败的小节
  */
-router.post('/books/:id/retry-chapter', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/retry-chapter', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const { chapterId } = ctx.request.body as { chapterId: number };
@@ -1542,7 +1542,7 @@ router.post('/books/:id/retry-chapter', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/books/:id/full-content
  * 获取完整书籍内容(所有章节内容合并)
  */
-router.get('/books/:id/full-content', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/full-content', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1608,7 +1608,7 @@ router.get('/books/:id/full-content', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/outline
  * 生成书籍大纲
  */
-router.post('/books/:id/outline', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/outline', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1663,7 +1663,7 @@ router.post('/books/:id/outline', optionalAuth, async (ctx: Context) => {
  * 生成单个章节(或全部章节,取决于参数)
  * body: { chapterNumber?: number } - 如果不传chapterNumber,则生成全部
  */
-router.post('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1723,7 +1723,7 @@ router.post('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/foreword
  * 生成前言
  */
-router.post('/books/:id/foreword', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/foreword', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1787,7 +1787,7 @@ router.post('/books/:id/foreword', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/afterword
  * 生成后记
  */
-router.post('/books/:id/afterword', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/afterword', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1851,7 +1851,7 @@ router.post('/books/:id/afterword', optionalAuth, async (ctx: Context) => {
  * GET /api/book-generator/langgraph/workflow/:bookId
  * 获取书籍生成工作流状态
  */
-router.get('/workflow/:bookId', optionalAuth, async (ctx: Context) => {
+router.get('/workflow/:bookId', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.bookId as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -1982,7 +1982,7 @@ router.get('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: C
  * POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/content
  * 保存章节内容(手工编辑后保存)
  */
-router.post('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/:chapterId/content', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
     const chapterIdNum = parseInt(chapterId);
@@ -2043,7 +2043,7 @@ router.post('/books/:id/chapters/:chapterId/content', optionalAuth, async (ctx:
  * POST /api/book-generator/langgraph/books/:id/chapters/batch-regenerate
  * 批量重新生成章节内容
  */
-router.post('/books/:id/chapters/batch-regenerate', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/batch-regenerate', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId } = ctx.params as { id: string };
     const { chapterIds } = ctx.request.body as { chapterIds: number[] };
@@ -2100,7 +2100,7 @@ router.post('/books/:id/chapters/batch-regenerate', optionalAuth, async (ctx: Co
  * POST /api/book-generator/langgraph/books/:id/chapters/batch-delete
  * 批量删除章节
  */
-router.post('/books/:id/chapters/batch-delete', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/batch-delete', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId } = ctx.params as { id: string };
     const { chapterIds } = ctx.request.body as { chapterIds: number[] };
@@ -2161,7 +2161,7 @@ router.post('/books/:id/chapters/batch-delete', optionalAuth, async (ctx: Contex
  * POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/regenerate
  * 重新生成指定章节的内容(仅叶节点允许)
  */
-router.post('/books/:id/chapters/:chapterId/regenerate', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/:chapterId/regenerate', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
     const chapterIdNum = parseInt(chapterId);
@@ -2229,7 +2229,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate', optionalAuth, async (ct
  * POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/regenerate-audio
  * 重新生成章节音频(仅叶节点)
  */
-router.post('/books/:id/chapters/:chapterId/regenerate-audio', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/:chapterId/regenerate-audio', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
     const chapterIdNum = parseInt(chapterId);
@@ -2329,7 +2329,7 @@ router.post('/books/:id/chapters/:chapterId/regenerate-audio', optionalAuth, asy
  * POST /api/book-generator/langgraph/books/:id/chapters/:chapterId/video
  * 生成单个章节视频
  */
-router.post('/books/:id/chapters/:chapterId/video', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/chapters/:chapterId/video', requireAuth, async (ctx: Context) => {
   try {
     const { id: bookId, chapterId } = ctx.params as { id: string; chapterId: string };
     const chapterIdNum = parseInt(chapterId);
@@ -2406,7 +2406,7 @@ router.post('/books/:id/chapters/:chapterId/video', optionalAuth, async (ctx: Co
  * POST /api/book-generator/langgraph/books/:id/videos
  * 批量生成书籍所有章节视频
  */
-router.post('/books/:id/videos', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/videos', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -2531,7 +2531,7 @@ router.post('/books/:id/videos', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/merge-audio
  * 合并叶节点音频到上级章节
  */
-router.post('/books/:id/merge-audio', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/merge-audio', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -2663,7 +2663,7 @@ router.post('/books/:id/merge-audio', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/merge-video
  * 合并叶节点视频到上级章节
  */
-router.post('/books/:id/merge-video', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/merge-video', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -2880,7 +2880,7 @@ async function mergeChapterVideos(parentChapter: any, childChapters: any[], user
  * DEBUG: 手动触发章节音频生成(强制从content_completed重新开始)
  * POST /api/book-generator/debug/trigger-audio
  */
-router.post('/debug/trigger-audio', optionalAuth, async (ctx: Context) => {
+router.post('/debug/trigger-audio', requireAuth, async (ctx: Context) => {
   try {
     const { chapterId } = ctx.request.body as { chapterId?: number };
     if (!chapterId) {
@@ -2927,7 +2927,7 @@ router.post('/debug/trigger-audio', optionalAuth, async (ctx: Context) => {
  * POST /api/book-generator/langgraph/books/:id/interactive/plan
  * 异步启动深度规划,前端通过 GET 同名接口轮询结果
  */
-router.post('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -3004,7 +3004,7 @@ router.post('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
  * GET /api/book-generator/langgraph/books/:id/interactive/plan
  * 轮询获取深度规划结果
  */
-router.get('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -3061,7 +3061,7 @@ router.get('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
  * PUT /api/book-generator/langgraph/books/:id/interactive/plan
  * 保存用户修改后的 plan
  */
-router.put('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) => {
+router.put('/books/:id/interactive/plan', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const { plan } = ctx.request.body as { plan: string };
@@ -3096,7 +3096,7 @@ router.put('/books/:id/interactive/plan', optionalAuth, async (ctx: Context) =>
  * POST /api/book-generator/langgraph/books/:id/interactive/outline
  * 异步启动大纲生成,前端通过 GET 同名接口轮询结果
  */
-router.post('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -3171,7 +3171,7 @@ router.post('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
  * GET /api/book-generator/langgraph/books/:id/interactive/outline
  * 轮询获取大纲生成结果
  */
-router.get('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
+router.get('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
@@ -3226,7 +3226,7 @@ router.get('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
  * PUT /api/book-generator/langgraph/books/:id/interactive/outline
  * 保存用户修改后的大纲,并重建章节树
  */
-router.put('/books/:id/interactive/outline', optionalAuth, async (ctx: Context) => {
+router.put('/books/:id/interactive/outline', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const { outline } = ctx.request.body as { outline: any };
@@ -3309,7 +3309,7 @@ router.put('/books/:id/interactive/outline', optionalAuth, async (ctx: Context)
  * POST /api/book-generator/langgraph/books/:id/interactive/generate
  * 运行并行内容生成 + 连贯性编辑
  */
-router.post('/books/:id/interactive/generate', optionalAuth, async (ctx: Context) => {
+router.post('/books/:id/interactive/generate', requireAuth, async (ctx: Context) => {
   try {
     const bookId = ctx.params.id;
     const accessUserId = ctx.state.user?.userId || TEST_USER_ID;

+ 7 - 7
server/src/modules/drafts/drafts.controller.ts

@@ -1,13 +1,13 @@
 import Router from '@koa/router';
 import { Context } from 'koa';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 const TEST_USER_ID = '1';
 const router = new Router();
 
 // 获取草稿列表(兼容前端 /api/drafts/list)
-router.get('/list', optionalAuth, async (ctx: Context) => {
+router.get('/list', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { type } = ctx.query as { type?: string };
   const where: any = { userId: parseInt(userId) };
@@ -20,7 +20,7 @@ router.get('/list', optionalAuth, async (ctx: Context) => {
 });
 
 // 获取草稿列表
-router.get('/', optionalAuth, async (ctx: Context) => {
+router.get('/', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { type } = ctx.query as { type?: string };
   const where: any = { userId: parseInt(userId) };
@@ -33,7 +33,7 @@ router.get('/', optionalAuth, async (ctx: Context) => {
 });
 
 // 获取单个草稿
-router.get('/:id', optionalAuth, async (ctx: Context) => {
+router.get('/:id', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   // 归属校验:只能读自己的草稿
@@ -49,7 +49,7 @@ router.get('/:id', optionalAuth, async (ctx: Context) => {
 });
 
 // 保存草稿
-router.post('/', optionalAuth, async (ctx: Context) => {
+router.post('/', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const data = ctx.request.body as {
     type: string;
@@ -71,7 +71,7 @@ router.post('/', optionalAuth, async (ctx: Context) => {
 });
 
 // 更新草稿
-router.put('/:id', optionalAuth, async (ctx: Context) => {
+router.put('/:id', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   const data = ctx.request.body as {
@@ -94,7 +94,7 @@ router.put('/:id', optionalAuth, async (ctx: Context) => {
 });
 
 // 删除草稿
-router.delete('/:id', optionalAuth, async (ctx: Context) => {
+router.delete('/:id', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   // 归属校验:只删除属于本人的草稿

+ 3 - 3
server/src/modules/history/history-batch.controller.ts

@@ -1,6 +1,6 @@
 import Router from '@koa/router';
 import { Context } from 'koa';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 const TEST_USER_ID = '1';
@@ -8,7 +8,7 @@ const TEST_USER_ID = '1';
 const router = new Router();
 
 // 批量删除历史记录(使用POST避免DELETE body问题)
-router.post('/batch-delete', optionalAuth, async (ctx: Context) => {
+router.post('/batch-delete', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const body = ctx.request.body as {
     ids: string[];
@@ -53,7 +53,7 @@ router.post('/batch-delete', optionalAuth, async (ctx: Context) => {
 });
 
 // 保留DELETE方法作为兼容
-router.delete('/batch', optionalAuth, async (ctx: Context) => {
+router.delete('/batch', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const body = ctx.request.body as {
     ids: string[];

+ 3 - 3
server/src/modules/history/history.controller.ts

@@ -1,6 +1,6 @@
 import Router from '@koa/router';
 import { Context } from 'koa';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 const TEST_USER_ID = '1';
@@ -8,7 +8,7 @@ const TEST_USER_ID = '1';
 const router = new Router();
 
 // 获取音频生成历史列表(兼容前端 /api/history/list)
-router.get('/list', optionalAuth, async (ctx: Context) => {
+router.get('/list', requireAuth, async (ctx: Context) => {
   // 开发环境使用测试用户ID
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const userIdNum = parseInt(userId as string);
@@ -71,7 +71,7 @@ router.get('/list', optionalAuth, async (ctx: Context) => {
 });
 
 // 获取音频生成历史列表
-router.get('/', optionalAuth, async (ctx: Context) => {
+router.get('/', requireAuth, async (ctx: Context) => {
   // 开发环境使用测试用户ID
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const userIdNum = parseInt(userId as string);

+ 5 - 5
server/src/modules/notifications/notifications.controller.ts

@@ -1,6 +1,6 @@
 import Router from '@koa/router';
 import { notificationsService } from './notifications.service';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 const router = new Router();
@@ -12,7 +12,7 @@ const TEST_USER_ID = '1';
  * 获取通知列表(兼容前端 /api/notifications/list)
  * GET /api/notifications/list
  */
-router.get('/list', optionalAuth, async (ctx) => {
+router.get('/list', requireAuth, async (ctx) => {
   try {
     const userId = ctx.state.user?.userId || TEST_USER_ID;
     const notifications = await notificationsService.getNotifications(String(userId));
@@ -34,7 +34,7 @@ router.get('/list', optionalAuth, async (ctx) => {
  * 获取通知列表
  * GET /api/notifications
  */
-router.get('/', optionalAuth, async (ctx) => {
+router.get('/', requireAuth, async (ctx) => {
   try {
     const userId = ctx.state.user?.userId || TEST_USER_ID;
     const notifications = await notificationsService.getNotifications(String(userId));
@@ -56,7 +56,7 @@ router.get('/', optionalAuth, async (ctx) => {
  * 标记通知为已读
  * POST /api/notifications/read
  */
-router.post('/read', optionalAuth, async (ctx) => {
+router.post('/read', requireAuth, async (ctx) => {
   try {
     const userId = ctx.state.user?.userId || TEST_USER_ID;
     const { id } = ctx.request.body as { id: string };
@@ -86,7 +86,7 @@ router.post('/read', optionalAuth, async (ctx) => {
  * 全部标记为已读
  * PUT /api/notifications/read-all
  */
-router.put('/read-all', optionalAuth, async (ctx) => {
+router.put('/read-all', requireAuth, async (ctx) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   await prisma.notification.updateMany({
     where: { userId: parseInt(String(userId)), isRead: false },

+ 7 - 7
server/src/modules/player/playlist.controller.ts

@@ -1,13 +1,13 @@
 import Router from '@koa/router';
 import { Context } from 'koa';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import { prisma } from '../../models';
 
 const TEST_USER_ID = '1';
 const router = new Router();
 
 // 获取播放列表
-router.get('/', optionalAuth, async (ctx: Context) => {
+router.get('/', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const playlists = await prisma.playlist.findMany({
     where: { userId: parseInt(userId) },
@@ -18,7 +18,7 @@ router.get('/', optionalAuth, async (ctx: Context) => {
 });
 
 // 创建播放列表
-router.post('/', optionalAuth, async (ctx: Context) => {
+router.post('/', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { name, description } = ctx.request.body as { name: string; description?: string };
   const playlist = await prisma.playlist.create({
@@ -32,7 +32,7 @@ router.post('/', optionalAuth, async (ctx: Context) => {
 });
 
 // 获取单个播放列表详情
-router.get('/:id', optionalAuth, async (ctx: Context) => {
+router.get('/:id', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   // 归属校验:只能看自己的播放列表
@@ -49,7 +49,7 @@ router.get('/:id', optionalAuth, async (ctx: Context) => {
 });
 
 // 添加项目到播放列表
-router.post('/:id/items', optionalAuth, async (ctx: Context) => {
+router.post('/:id/items', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   const { chapterId, audioId } = ctx.request.body as { chapterId?: number; audioId?: string };
@@ -83,7 +83,7 @@ router.post('/:id/items', optionalAuth, async (ctx: Context) => {
 });
 
 // 重新排序项目
-router.put('/:id/items/reorder', optionalAuth, async (ctx: Context) => {
+router.put('/:id/items/reorder', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id } = ctx.params;
   const { items } = ctx.request.body as { items: { id: number; order: number }[] };
@@ -108,7 +108,7 @@ router.put('/:id/items/reorder', optionalAuth, async (ctx: Context) => {
 });
 
 // 删除项目
-router.delete('/:id/items/:itemId', optionalAuth, async (ctx: Context) => {
+router.delete('/:id/items/:itemId', requireAuth, async (ctx: Context) => {
   const userId = ctx.state.user?.userId || TEST_USER_ID;
   const { id, itemId } = ctx.params;
   // 归属校验:条目必须属于当前用户名下的该播放列表

+ 12 - 12
server/src/modules/video-generator/video-generator.controller.ts

@@ -3,7 +3,7 @@
  */
 
 import Router from '@koa/router';
-import { optionalAuth } from '../../middleware/auth';
+import { requireAuth } from '../../middleware/auth';
 import {
   createVideoProject,
   getVideoProjects,
@@ -33,7 +33,7 @@ function currentUserId(ctx: any): number {
  * GET /api/video/projects
  * 获取视频项目列表(只返回当前用户的项目)
  */
-router.get('/projects', optionalAuth, async (ctx) => {
+router.get('/projects', requireAuth, async (ctx) => {
   const query = {
     userId: currentUserId(ctx),  // 强制用当前登录用户,忽略客户端传入的 userId
     status: ctx.query.status as any,
@@ -49,7 +49,7 @@ router.get('/projects', optionalAuth, async (ctx) => {
  * POST /api/video/projects
  * 创建视频项目
  */
-router.post('/projects', optionalAuth, async (ctx) => {
+router.post('/projects', requireAuth, async (ctx) => {
   const body = ctx.request.body as any;
   const userId = currentUserId(ctx);
 
@@ -61,7 +61,7 @@ router.post('/projects', optionalAuth, async (ctx) => {
  * GET /api/video/projects/:id
  * 获取视频项目详情
  */
-router.get('/projects/:id', optionalAuth, async (ctx) => {
+router.get('/projects/:id', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const project = await getVideoProject(id, currentUserId(ctx));
 
@@ -78,7 +78,7 @@ router.get('/projects/:id', optionalAuth, async (ctx) => {
  * PUT /api/video/projects/:id
  * 更新视频项目
  */
-router.put('/projects/:id', optionalAuth, async (ctx) => {
+router.put('/projects/:id', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const body = ctx.request.body as any;
 
@@ -97,7 +97,7 @@ router.put('/projects/:id', optionalAuth, async (ctx) => {
  * DELETE /api/video/projects/:id
  * 删除视频项目
  */
-router.delete('/projects/:id', optionalAuth, async (ctx) => {
+router.delete('/projects/:id', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const success = await deleteVideoProject(id, currentUserId(ctx));
 
@@ -116,7 +116,7 @@ router.delete('/projects/:id', optionalAuth, async (ctx) => {
  * POST /api/video/projects/:id/generate
  * 开始生成视频
  */
-router.post('/projects/:id/generate', optionalAuth, async (ctx) => {
+router.post('/projects/:id/generate', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const result = await generateVideoForProject(id, currentUserId(ctx));
 
@@ -140,7 +140,7 @@ router.post('/projects/:id/generate', optionalAuth, async (ctx) => {
  * GET /api/video/projects/:id/status
  * 获取生成状态
  */
-router.get('/projects/:id/status', optionalAuth, async (ctx) => {
+router.get('/projects/:id/status', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const status = await getGenerateProgress(id, currentUserId(ctx));
 
@@ -153,7 +153,7 @@ router.get('/projects/:id/status', optionalAuth, async (ctx) => {
  * GET /api/video/materials
  * 获取素材列表
  */
-router.get('/materials', optionalAuth, async (ctx) => {
+router.get('/materials', requireAuth, async (ctx) => {
   const query = {
     userId: currentUserId(ctx),
     type: ctx.query.type as any,
@@ -170,7 +170,7 @@ router.get('/materials', optionalAuth, async (ctx) => {
  * POST /api/video/materials/upload
  * 上传素材(处理 multipart/form-data 文件上传)
  */
-router.post('/materials/upload', optionalAuth, async (ctx) => {
+router.post('/materials/upload', requireAuth, async (ctx) => {
   const userId = currentUserId(ctx);
 
   // 处理 multipart form data
@@ -214,7 +214,7 @@ router.post('/materials/upload', optionalAuth, async (ctx) => {
  * DELETE /api/video/materials/:id
  * 删除素材
  */
-router.delete('/materials/:id', optionalAuth, async (ctx) => {
+router.delete('/materials/:id', requireAuth, async (ctx) => {
   const id = Number(ctx.params.id);
   const success = await deleteMaterial(id);
 
@@ -233,7 +233,7 @@ router.delete('/materials/:id', optionalAuth, async (ctx) => {
  * POST /api/video/books/:bookId/generate
  * 从书籍生成视频项目
  */
-router.post('/books/:bookId/generate', optionalAuth, async (ctx) => {
+router.post('/books/:bookId/generate', requireAuth, async (ctx) => {
   const bookId = Number(ctx.params.bookId);
   const userId = currentUserId(ctx);