From 79394f93f506e048e3eeb8e31f9e305a600a02e7 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:51:06 +0100 Subject: [PATCH] refactor(base): drop dedicated /bases/files/upload endpoint, reuse /files/upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base file-cell uploads were hitting a parallel POST /bases/files/upload endpoint that re-implemented the multipart parse, the size limit handling, and the spaceId resolution that the standard page-attachment endpoint (POST /files/upload) already does. The two diverged on minor points (no audit log, no attachmentId support, slightly different permission check) without good reason — bases are pages (isBase=true), so the existing endpoint already handles them correctly. Server: delete uploadBaseFile and the now-unused BaseRepo injection. Client: route the file-cell uploader through the existing uploadFile helper in page-service. The base's pageId is a valid page id, so the server's pageRepo.findById succeeds and pageAccessService.validateCanEdit runs — which lines up with the Base edit ability at the space-role level (Manage Page and Manage Base track together for admins/writers, Read for readers). --- .../base/components/cells/cell-file.tsx | 22 ++--- .../core/attachment/attachment.controller.ts | 83 ------------------- 2 files changed, 8 insertions(+), 97 deletions(-) diff --git a/apps/client/src/features/base/components/cells/cell-file.tsx b/apps/client/src/features/base/components/cells/cell-file.tsx index ce4d5cb2c..b279fee35 100644 --- a/apps/client/src/features/base/components/cells/cell-file.tsx +++ b/apps/client/src/features/base/components/cells/cell-file.tsx @@ -8,7 +8,7 @@ import { } from "@tabler/icons-react"; import { IBaseProperty } from "@/features/base/types/base.types"; import cellClasses from "@/features/base/styles/cells.module.css"; -import api from "@/lib/api-client"; +import { uploadFile } from "@/features/page/services/page-service"; export type FileValue = { id: string; @@ -68,21 +68,15 @@ export function CellFile({ const newFiles: FileValue[] = [...files]; + // Reuse the page-attachment upload pipeline. A base IS a page + // (isBase=true) — the server's /files/upload endpoint accepts the + // base's pageId, runs the standard pageAccessService.validateCanEdit + // check (which lines up with Base edit at the space-role level per + // the casl rules), and stores the attachment via the same flow as + // any other page attachment. for (const file of Array.from(fileList)) { try { - const formData = new FormData(); - formData.append("file", file); - formData.append("pageId", property.pageId); - - const res = await api.post( - "/bases/files/upload", - formData, - { - headers: { "Content-Type": "multipart/form-data" }, - }, - ); - - const attachment = res as unknown as FileValue; + const attachment = await uploadFile(file, property.pageId); newFiles.push({ id: attachment.id, fileName: attachment.fileName, diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index e2f174145..7b24cc356 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -47,7 +47,6 @@ import { } from '../casl/interfaces/workspace-ability.type'; import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; -import { BaseRepo } from '@docmost/db/repos/base/base.repo'; import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; import { validate as isValidUUID } from 'uuid'; import { EnvironmentService } from '../../integrations/environment/environment.service'; @@ -72,7 +71,6 @@ export class AttachmentController { private readonly workspaceAbility: WorkspaceAbilityFactory, private readonly spaceAbility: SpaceAbilityFactory, private readonly pageRepo: PageRepo, - private readonly baseRepo: BaseRepo, private readonly attachmentRepo: AttachmentRepo, private readonly environmentService: EnvironmentService, private readonly tokenService: TokenService, @@ -165,87 +163,6 @@ export class AttachmentController { } } - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.OK) - @Post('bases/files/upload') - @UseInterceptors(FileInterceptor) - async uploadBaseFile( - @Req() req: any, - @Res() res: FastifyReply, - @AuthUser() user: User, - @AuthWorkspace() workspace: Workspace, - ) { - const maxFileSize = bytes(this.environmentService.getFileUploadSizeLimit()); - - let file = null; - try { - file = await req.file({ - limits: { fileSize: maxFileSize, fields: 3, files: 1 }, - }); - } catch (err: any) { - this.logger.error(err.message); - if (err?.statusCode === 413) { - throw new BadRequestException( - `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`, - ); - } - } - - if (!file) { - throw new BadRequestException('Failed to upload file'); - } - - const pageId = file.fields?.pageId?.value; - - if (!pageId) { - throw new BadRequestException('pageId is required'); - } - - if (!isValidUUID(pageId)) { - throw new BadRequestException('Invalid pageId'); - } - - const base = await this.baseRepo.findById(pageId); - - if (!base) { - throw new NotFoundException('Base not found'); - } - - const spaceId = base.spaceId; - - const spaceAbilityCheck = await this.spaceAbility.createForUser( - user, - spaceId, - ); - if ( - spaceAbilityCheck.cannot( - SpaceCaslAction.Edit, - SpaceCaslSubject.Base, - ) - ) { - throw new ForbiddenException(); - } - - try { - const fileResponse = await this.attachmentService.uploadFile({ - filePromise: file, - spaceId: spaceId, - userId: user.id, - workspaceId: workspace.id, - }); - - return res.send(fileResponse); - } catch (err: any) { - if (err?.statusCode === 413) { - const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`; - this.logger.error(errMessage); - throw new BadRequestException(errMessage); - } - this.logger.error(err); - throw new BadRequestException('Error processing file upload.'); - } - } - @UseGuards(JwtAuthGuard) @Get('/files/:fileId/:fileName') async getFile(