import { BadRequestException, Controller, ForbiddenException, Get, HttpCode, HttpStatus, Logger, NotFoundException, Param, Post, Req, Res, UseGuards, UseInterceptors, } from '@nestjs/common'; import { AttachmentService } from './services/attachment.service'; import { FastifyReply } from 'fastify'; import { AttachmentInterceptor } from './interceptors/attachment.interceptor'; import * as bytes from 'bytes'; import { AuthUser } from '../../decorators/auth-user.decorator'; import { AuthWorkspace } from '../../decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { User, Workspace } from '@docmost/db/types/entity.types'; import { StorageService } from '../../integrations/storage/storage.service'; import { getAttachmentFolderPath, validAttachmentTypes, } from './attachment.utils'; import { getMimeType } from '../../helpers'; import { AttachmentType, MAX_AVATAR_SIZE, MAX_FILE_SIZE, } from './attachment.constants'; import { SpaceCaslAction, SpaceCaslSubject, } from '../casl/interfaces/space-ability.type'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { WorkspaceCaslAction, WorkspaceCaslSubject, } from '../casl/interfaces/workspace-ability.type'; import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory'; @Controller('attachments') export class AttachmentController { private readonly logger = new Logger(AttachmentController.name); constructor( private readonly attachmentService: AttachmentService, private readonly storageService: StorageService, private readonly workspaceAbility: WorkspaceAbilityFactory, private readonly spaceAbility: SpaceAbilityFactory, ) {} @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('upload-file') @UseInterceptors(AttachmentInterceptor) async uploadFile( @Req() req: any, @Res() res: FastifyReply, @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { const maxFileSize = bytes(MAX_FILE_SIZE); let file = null; try { file = await req.file({ limits: { fileSize: maxFileSize, fields: 1, files: 1 }, }); } catch (err: any) { if (err?.statusCode === 413) { throw new BadRequestException( `File too large. Exceeds the ${MAX_FILE_SIZE} limit`, ); } } if (!file) { throw new BadRequestException('Invalid file upload'); } const pageId = file.fields?.pageId.value; if (!pageId) { throw new BadRequestException('PageId is required'); } try { const fileResponse = await this.attachmentService.uploadFile( file, pageId, user.id, workspace.id, ); return res.send(fileResponse); } catch (err: any) { throw new BadRequestException('Error processing file upload.'); } } @Get('/:fileId/:fileName') async getFile( @Req() req: any, @Res() res: FastifyReply, @Param('fileId') fileId: string, @Param('fileName') fileName?: string, ) { // TODO } @UseGuards(JwtAuthGuard) @HttpCode(HttpStatus.OK) @Post('upload-image') @UseInterceptors(AttachmentInterceptor) async uploadAvatarOrLogo( @Req() req: any, @Res() res: FastifyReply, @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { const maxFileSize = bytes(MAX_AVATAR_SIZE); let file = null; try { file = await req.file({ limits: { fileSize: maxFileSize, fields: 3, files: 1 }, }); } catch (err: any) { if (err?.statusCode === 413) { throw new BadRequestException( `File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`, ); } } if (!file) { throw new BadRequestException('Invalid file upload'); } const attachmentType = file.fields?.type?.value; const spaceId = file.fields?.spaceId?.value; if (!attachmentType) { throw new BadRequestException('attachment type is required'); } if ( !validAttachmentTypes.includes(attachmentType) || attachmentType === AttachmentType.File ) { throw new BadRequestException('Invalid image attachment type'); } if (attachmentType === AttachmentType.WorkspaceLogo) { const ability = this.workspaceAbility.createForUser(user, workspace); if ( ability.cannot( WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings, ) ) { throw new ForbiddenException(); } } if (attachmentType === AttachmentType.SpaceLogo) { if (!spaceId) { throw new BadRequestException('spaceId is required'); } const spaceAbility = await this.spaceAbility.createForUser(user, spaceId); if ( spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings) ) { throw new ForbiddenException(); } } try { const fileResponse = await this.attachmentService.uploadImage( file, attachmentType, user.id, workspace.id, spaceId, ); return res.send(fileResponse); } catch (err: any) { this.logger.error(err); throw new BadRequestException('Error processing file upload.'); } } @Get('/img/:attachmentType/:fileName') async getLogoOrAvatar( @Req() req: any, @Res() res: FastifyReply, @Param('attachmentType') attachmentType: AttachmentType, @Param('fileName') fileName?: string, ) { const workspaceId = req.raw?.workspaceId; if (!workspaceId) { throw new BadRequestException('Invalid workspace'); } if ( !validAttachmentTypes.includes(attachmentType) || attachmentType === AttachmentType.File ) { throw new BadRequestException('Invalid image attachment type'); } const buildFilePath = `${getAttachmentFolderPath(attachmentType, workspaceId)}/${fileName}`; try { const fileStream = await this.storageService.read(buildFilePath); res.headers({ 'Content-Type': getMimeType(buildFilePath), }); return res.send(fileStream); } catch (err) { this.logger.error(err); throw new NotFoundException('File not found'); } } }