From 4b21c98835cd15f398ff90ea5d8abc64c1d74006 Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 6 Jun 2026 12:04:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20code=20review=20=E2=80=94=204=20critical?= =?UTF-8?q?=20bugs=20in=20KnowledgeItem/KB=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. enrichItem: remove MIME type → sourceType mapping (contentType is text/markdown not markdown) 2. update(): add field whitelist to prevent mass assignment (userId/deletedAt/etc) 3. createFolder: add userId permission check + parent folder validation 4. deleteFolder: cascade soft-delete to corresponding KnowledgeItem (itemType=folder) Co-Authored-By: Claude Opus 4.7 --- .../knowledge-base.controller.ts | 8 +++-- .../knowledge-base/knowledge-base.service.ts | 29 +++++++++++++++++-- .../knowledge-items.repository.ts | 8 ++++- .../knowledge-items.service.ts | 1 - 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/modules/knowledge-base/knowledge-base.controller.ts b/src/modules/knowledge-base/knowledge-base.controller.ts index c843341..298c716 100644 --- a/src/modules/knowledge-base/knowledge-base.controller.ts +++ b/src/modules/knowledge-base/knowledge-base.controller.ts @@ -59,8 +59,12 @@ export class KnowledgeBaseController { @Post(':id/folders') @ApiOperation({ summary: '创建文件夹' }) - async createFolder(@Param('id') id: string, @Body() dto: { name: string; parentId?: string }) { - return this.service.createFolder(id, dto); + async createFolder( + @CurrentUser() user: UserPayload, + @Param('id') id: string, + @Body() dto: { name: string; parentId?: string }, + ) { + return this.service.createFolder(String(user?.id || 'anonymous'), id, dto); } @Patch(':id/folders/:folderId') diff --git a/src/modules/knowledge-base/knowledge-base.service.ts b/src/modules/knowledge-base/knowledge-base.service.ts index aafc01c..2fd4927 100644 --- a/src/modules/knowledge-base/knowledge-base.service.ts +++ b/src/modules/knowledge-base/knowledge-base.service.ts @@ -119,10 +119,18 @@ export class KnowledgeBaseService { // ── Folder CRUD ── - async createFolder(kbId: string, dto: { name: string; parentId?: string }) { - // Also create a KnowledgeItem with itemType='folder' for iOS list compatibility + async createFolder(userId: string, kbId: string, dto: { name: string; parentId?: string }) { const kb = await this.repository.findById(kbId); - if (!kb) throw new NotFoundException('知识库不存在'); + if (!kb || kb.deletedAt) throw new NotFoundException('知识库不存在'); + if (String(kb.userId) !== userId) throw new ForbiddenException('无权操作该知识库'); + + // Validate parent folder belongs to same KB + if (dto.parentId) { + const parentFolder = await this.prisma.knowledgeFolder.findUnique({ where: { id: dto.parentId } }); + if (!parentFolder || parentFolder.knowledgeBaseId !== kbId) { + throw new BadRequestException('父文件夹不存在或不属于该知识库'); + } + } const [folder] = await Promise.all([ this.prisma.knowledgeFolder.create({ @@ -155,11 +163,26 @@ export class KnowledgeBaseService { } async deleteFolder(folderId: string) { + const folder = await this.prisma.knowledgeFolder.findUnique({ where: { id: folderId } }); + if (!folder) throw new NotFoundException('文件夹不存在'); + // Soft-delete folder and children await this.prisma.knowledgeFolder.updateMany({ where: { OR: [{ id: folderId }, { parentId: folderId }] }, data: { deletedAt: new Date() }, }); + + // Also soft-delete the corresponding KnowledgeItem (itemType='folder') + await this.prisma.knowledgeItem.updateMany({ + where: { + knowledgeBaseId: folder.knowledgeBaseId, + title: folder.name, + itemType: 'folder', + deletedAt: null, + }, + data: { deletedAt: new Date() }, + }); + return { success: true }; } } diff --git a/src/modules/knowledge-items/knowledge-items.repository.ts b/src/modules/knowledge-items/knowledge-items.repository.ts index acdf88a..f799783 100644 --- a/src/modules/knowledge-items/knowledge-items.repository.ts +++ b/src/modules/knowledge-items/knowledge-items.repository.ts @@ -110,9 +110,15 @@ export class KnowledgeItemsRepository { } async update(id: string, dto: Record) { + // Whitelist allowed fields to prevent mass assignment + const allowed = ['title', 'content', 'summary', 'parentId', 'itemType', 'sourceType', 'orderIndex', 'status', 'durationSeconds']; + const data: Record = {}; + for (const key of allowed) { + if (dto[key] !== undefined) data[key] = dto[key]; + } return this.prisma.knowledgeItem.update({ where: { id }, - data: dto, + data, }); } diff --git a/src/modules/knowledge-items/knowledge-items.service.ts b/src/modules/knowledge-items/knowledge-items.service.ts index 9d6f492..21cf44d 100644 --- a/src/modules/knowledge-items/knowledge-items.service.ts +++ b/src/modules/knowledge-items/knowledge-items.service.ts @@ -47,7 +47,6 @@ export class KnowledgeItemsService { const enriched = { ...item, content: freshUrl }; if (headInfo) { enriched.fileSize = Number(headInfo.size); - enriched.sourceType = enriched.sourceType || headInfo.contentType; } return enriched; } catch {