|
@@ -6,6 +6,7 @@ import Router from '@koa/router';
|
|
|
import { Context } from 'koa';
|
|
import { Context } from 'koa';
|
|
|
import path from 'path';
|
|
import path from 'path';
|
|
|
import { bookStore } from './book-generator.store';
|
|
import { bookStore } from './book-generator.store';
|
|
|
|
|
+import { assertBookAccess, assertChapterAccess } from './access-control';
|
|
|
import { optionalAuth } from '../../middleware/auth';
|
|
import { optionalAuth } from '../../middleware/auth';
|
|
|
import { prisma } from '../../models';
|
|
import { prisma } from '../../models';
|
|
|
|
|
|
|
@@ -19,7 +20,9 @@ const router = new Router();
|
|
|
*/
|
|
*/
|
|
|
async function getAlbums(ctx: Context) {
|
|
async function getAlbums(ctx: Context) {
|
|
|
try {
|
|
try {
|
|
|
- const books = await bookStore.getAllByUser();
|
|
|
|
|
|
|
+ // 用户隔离:只返回当前用户(或游客)的书,避免列出他人专辑
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
+ const books = await bookStore.getAllByUser(parseInt(userId as string), false);
|
|
|
const albums = books.map(book => ({
|
|
const albums = books.map(book => ({
|
|
|
id: book.id,
|
|
id: book.id,
|
|
|
title: book.title,
|
|
title: book.title,
|
|
@@ -38,15 +41,22 @@ async function getAlbums(ctx: Context) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// GET /api/book-generator/books
|
|
// GET /api/book-generator/books
|
|
|
-router.get('/books', getAlbums);
|
|
|
|
|
|
|
+router.get('/books', optionalAuth, getAlbums);
|
|
|
|
|
|
|
|
// GET /api/book-generator/albums (与 /books 同样的内容,作为 AL01 用例路径)
|
|
// GET /api/book-generator/albums (与 /books 同样的内容,作为 AL01 用例路径)
|
|
|
-router.get('/albums', getAlbums);
|
|
|
|
|
|
|
+router.get('/albums', optionalAuth, getAlbums);
|
|
|
|
|
|
|
|
// GET /api/book-generator/albums/:id (单独书籍详情,便于前端跳转)
|
|
// GET /api/book-generator/albums/:id (单独书籍详情,便于前端跳转)
|
|
|
router.get('/albums/:id', optionalAuth, async (ctx: Context) => {
|
|
router.get('/albums/:id', optionalAuth, async (ctx: Context) => {
|
|
|
try {
|
|
try {
|
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
+ // 归属校验:非本人且未公开 → 404
|
|
|
|
|
+ if (!(await assertBookAccess(bookId, userId, 'read'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '专辑不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const book = await prisma.book.findUnique({
|
|
const book = await prisma.book.findUnique({
|
|
|
where: { id: bookId },
|
|
where: { id: bookId },
|
|
|
include: { chapters: { orderBy: { number: 'asc' } } },
|
|
include: { chapters: { orderBy: { number: 'asc' } } },
|
|
@@ -201,6 +211,12 @@ router.post('/books', createAlbum);
|
|
|
router.get('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
router.get('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
try {
|
|
try {
|
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
+ if (!(await assertBookAccess(bookId, userId, 'read'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const book = await prisma.book.findUnique({
|
|
const book = await prisma.book.findUnique({
|
|
|
where: { id: bookId },
|
|
where: { id: bookId },
|
|
|
include: { chapters: { orderBy: { number: 'asc' } } },
|
|
include: { chapters: { orderBy: { number: 'asc' } } },
|
|
@@ -250,6 +266,7 @@ router.get('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
router.post('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
router.post('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
try {
|
|
try {
|
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
const bookId = parseInt(ctx.params.id as string);
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
const body = ctx.request.body as {
|
|
const body = ctx.request.body as {
|
|
|
title?: string;
|
|
title?: string;
|
|
|
description?: string;
|
|
description?: string;
|
|
@@ -258,6 +275,13 @@ router.post('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
targetAudience?: string;
|
|
targetAudience?: string;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 归属校验:只能改自己的书(写操作,已公开也不放行)
|
|
|
|
|
+ if (!(await assertBookAccess(bookId, userId, 'write'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '书籍不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const existing = await prisma.book.findUnique({ where: { id: bookId } });
|
|
const existing = await prisma.book.findUnique({ where: { id: bookId } });
|
|
|
if (!existing) {
|
|
if (!existing) {
|
|
|
ctx.status = 404;
|
|
ctx.status = 404;
|
|
@@ -299,6 +323,13 @@ async function getChapters(ctx: Context) {
|
|
|
const bookId = ctx.params.id as string;
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
|
|
|
|
|
+ // 归属校验:非本人且未公开 → 404(公开书允许读,音频可见性下面再细化)
|
|
|
|
|
+ if (!(await assertBookAccess(bookId, userId, 'read'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '专辑不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const book = await prisma.book.findUnique({
|
|
const book = await prisma.book.findUnique({
|
|
|
where: { id: parseInt(bookId) },
|
|
where: { id: parseInt(bookId) },
|
|
|
});
|
|
});
|
|
@@ -429,6 +460,14 @@ router.post('/books/:id/chapters/:chapterId/merge-audio', optionalAuth, async (c
|
|
|
try {
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
const bookId = ctx.params.id as string;
|
|
|
const chapterId = parseInt(ctx.params.chapterId as string);
|
|
const chapterId = parseInt(ctx.params.chapterId as string);
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
+
|
|
|
|
|
+ // 归属校验:章节须属于该书,且该书属于当前用户(写操作)
|
|
|
|
|
+ if (!(await assertChapterAccess(bookId, chapterId, userId, 'write'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '章节不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 获取章信息
|
|
// 获取章信息
|
|
|
const chapter = await prisma.bookChapter.findFirst({
|
|
const chapter = await prisma.bookChapter.findFirst({
|
|
@@ -514,6 +553,7 @@ router.post('/books/:id/chapters/:chapterId/merge-audio', optionalAuth, async (c
|
|
|
async function getChapterDetail(ctx: Context) {
|
|
async function getChapterDetail(ctx: Context) {
|
|
|
try {
|
|
try {
|
|
|
const chapterId = parseInt(ctx.params.id as string);
|
|
const chapterId = parseInt(ctx.params.id as string);
|
|
|
|
|
+ const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
const chapter = await prisma.bookChapter.findUnique({
|
|
const chapter = await prisma.bookChapter.findUnique({
|
|
|
where: { id: chapterId },
|
|
where: { id: chapterId },
|
|
|
});
|
|
});
|
|
@@ -524,6 +564,13 @@ async function getChapterDetail(ctx: Context) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 归属校验:章节所属书须本人或已公开(读)
|
|
|
|
|
+ if (!(await assertBookAccess(chapter.bookId, userId, 'read'))) {
|
|
|
|
|
+ ctx.status = 404;
|
|
|
|
|
+ ctx.body = { code: 1, message: '章节不存在' };
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
ctx.body = {
|
|
ctx.body = {
|
|
|
code: 0,
|
|
code: 0,
|
|
|
message: 'success',
|
|
message: 'success',
|