fix: code review — 4 critical bugs in KnowledgeItem/KB modules
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 42s

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 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-06 12:04:24 +08:00
parent 4b8653080e
commit 4b21c98835
4 changed files with 39 additions and 7 deletions

View File

@ -59,8 +59,12 @@ export class KnowledgeBaseController {
@Post(':id/folders') @Post(':id/folders')
@ApiOperation({ summary: '创建文件夹' }) @ApiOperation({ summary: '创建文件夹' })
async createFolder(@Param('id') id: string, @Body() dto: { name: string; parentId?: string }) { async createFolder(
return this.service.createFolder(id, dto); @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') @Patch(':id/folders/:folderId')

View File

@ -119,10 +119,18 @@ export class KnowledgeBaseService {
// ── Folder CRUD ── // ── Folder CRUD ──
async createFolder(kbId: string, dto: { name: string; parentId?: string }) { async createFolder(userId: string, kbId: string, dto: { name: string; parentId?: string }) {
// Also create a KnowledgeItem with itemType='folder' for iOS list compatibility
const kb = await this.repository.findById(kbId); 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([ const [folder] = await Promise.all([
this.prisma.knowledgeFolder.create({ this.prisma.knowledgeFolder.create({
@ -155,11 +163,26 @@ export class KnowledgeBaseService {
} }
async deleteFolder(folderId: string) { async deleteFolder(folderId: string) {
const folder = await this.prisma.knowledgeFolder.findUnique({ where: { id: folderId } });
if (!folder) throw new NotFoundException('文件夹不存在');
// Soft-delete folder and children // Soft-delete folder and children
await this.prisma.knowledgeFolder.updateMany({ await this.prisma.knowledgeFolder.updateMany({
where: { OR: [{ id: folderId }, { parentId: folderId }] }, where: { OR: [{ id: folderId }, { parentId: folderId }] },
data: { deletedAt: new Date() }, 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 }; return { success: true };
} }
} }

View File

@ -110,9 +110,15 @@ export class KnowledgeItemsRepository {
} }
async update(id: string, dto: Record<string, any>) { async update(id: string, dto: Record<string, any>) {
// Whitelist allowed fields to prevent mass assignment
const allowed = ['title', 'content', 'summary', 'parentId', 'itemType', 'sourceType', 'orderIndex', 'status', 'durationSeconds'];
const data: Record<string, any> = {};
for (const key of allowed) {
if (dto[key] !== undefined) data[key] = dto[key];
}
return this.prisma.knowledgeItem.update({ return this.prisma.knowledgeItem.update({
where: { id }, where: { id },
data: dto, data,
}); });
} }

View File

@ -47,7 +47,6 @@ export class KnowledgeItemsService {
const enriched = { ...item, content: freshUrl }; const enriched = { ...item, content: freshUrl };
if (headInfo) { if (headInfo) {
enriched.fileSize = Number(headInfo.size); enriched.fileSize = Number(headInfo.size);
enriched.sourceType = enriched.sourceType || headInfo.contentType;
} }
return enriched; return enriched;
} catch { } catch {