Files Field - Add new controller (#17561)
Add new files field controller to fetch files from FILES field
This commit is contained in:
+1
-8
@@ -1,20 +1,13 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
FileModule,
|
||||
HttpModule,
|
||||
PermissionsModule,
|
||||
TypeOrmModule.forFeature([ApplicationEntity]),
|
||||
],
|
||||
imports: [FileModule, HttpModule, PermissionsModule],
|
||||
providers: [FileUploadService, FileUploadResolver],
|
||||
exports: [FileUploadService, FileUploadResolver],
|
||||
})
|
||||
|
||||
-23
@@ -7,7 +7,6 @@ import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
@@ -54,28 +53,6 @@ export class FileUploadResolver {
|
||||
return files[0];
|
||||
}
|
||||
|
||||
@Mutation(() => FileDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadFilesFieldFile(
|
||||
@AuthWorkspace()
|
||||
{ id: workspaceId, workspaceCustomApplicationId }: WorkspaceEntity,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
): Promise<FileDTO> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
|
||||
const fileEntity = await this.fileUploadService.uploadFilesFieldFile({
|
||||
file: buffer,
|
||||
filename,
|
||||
declaredMimeType: mimetype,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
return fileEntity;
|
||||
}
|
||||
|
||||
@Mutation(() => SignedFileDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadImage(
|
||||
|
||||
+2
-75
@@ -1,21 +1,16 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import FileType from 'file-type';
|
||||
import sharp from 'sharp';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
|
||||
import { getCropSize, getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
export type SignedFile = { path: string; token: string };
|
||||
@@ -32,8 +27,6 @@ export class FileUploadService {
|
||||
private readonly fileStorage: FileStorageService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly httpService: HttpService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
) {}
|
||||
|
||||
private async _uploadFile({
|
||||
@@ -55,26 +48,6 @@ export class FileUploadService {
|
||||
});
|
||||
}
|
||||
|
||||
private _sanitizeFile({
|
||||
file,
|
||||
ext,
|
||||
mimeType,
|
||||
}: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
ext: string;
|
||||
mimeType: string | undefined;
|
||||
}): Buffer | Uint8Array | string {
|
||||
if (ext === 'svg' || mimeType === 'image/svg+xml') {
|
||||
const { JSDOM } = require('jsdom');
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
|
||||
return purify.sanitize(file.toString());
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use uploadWorkspaceRecordFile if uploading workspace records-scoped files. Or create your dedicated upload file service.
|
||||
*/
|
||||
@@ -95,7 +68,7 @@ export class FileUploadService {
|
||||
const folder = this.getWorkspaceFolderName(workspaceId, fileFolder);
|
||||
|
||||
await this._uploadFile({
|
||||
file: this._sanitizeFile({ file, ext, mimeType }),
|
||||
file: sanitizeFile({ file, ext, mimeType }),
|
||||
filename: name,
|
||||
mimeType,
|
||||
folder,
|
||||
@@ -216,50 +189,4 @@ export class FileUploadService {
|
||||
private getWorkspaceFolderName(workspaceId: string, fileFolder: FileFolder) {
|
||||
return `workspace-${workspaceId}/${fileFolder}`;
|
||||
}
|
||||
|
||||
async uploadFilesFieldFile({
|
||||
file,
|
||||
filename,
|
||||
declaredMimeType,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
declaredMimeType: string | undefined;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): Promise<FileEntity> {
|
||||
const { mimeType, ext } = await extractFileInfo({
|
||||
file,
|
||||
declaredMimeType,
|
||||
filename,
|
||||
});
|
||||
|
||||
const sanitizedFile = this._sanitizeFile({ file, ext, mimeType });
|
||||
|
||||
const fileId = v4();
|
||||
const name = `${fileId}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
id: applicationId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fileStorage.write_v2({
|
||||
sourceFile: sanitizedFile,
|
||||
destinationPath: name,
|
||||
mimeType,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
fileId,
|
||||
settings: {
|
||||
isTemporaryFile: true,
|
||||
toDelete: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FilesFieldDeletionJob } from 'src/engine/core-modules/file/files-field/jobs/files-field-deletion.job';
|
||||
import { FilesFieldDeletionListener } from 'src/engine/core-modules/file/files-field/listeners/files-field-deletion.listener';
|
||||
import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard';
|
||||
import { FileDeletionJob } from 'src/engine/core-modules/file/jobs/file-deletion.job';
|
||||
import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/jobs/file-workspace-folder-deletion.job';
|
||||
@@ -13,13 +11,12 @@ import { FileAttachmentListener } from 'src/engine/core-modules/file/listeners/f
|
||||
import { FileWorkspaceMemberListener } from 'src/engine/core-modules/file/listeners/file-workspace-member.listener';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
import { FileController } from './controllers/file.controller';
|
||||
import { FileEntity } from './entities/file.entity';
|
||||
import { FileUploadService } from './file-upload/services/file-upload.service';
|
||||
import { FilesFieldService } from './files-field/files-field.service';
|
||||
import { FilesFieldModule } from './files-field/files-field.module';
|
||||
import { FileResolver } from './resolvers/file.resolver';
|
||||
import { FileMetadataService } from './services/file-metadata.service';
|
||||
import { FileService } from './services/file.service';
|
||||
@@ -31,23 +28,20 @@ import { FileService } from './services/file.service';
|
||||
HttpModule,
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
FilesFieldModule,
|
||||
],
|
||||
providers: [
|
||||
FileService,
|
||||
FileMetadataService,
|
||||
FilesFieldService,
|
||||
FileResolver,
|
||||
FilePathGuard,
|
||||
FileAttachmentListener,
|
||||
FileWorkspaceMemberListener,
|
||||
FilesFieldDeletionListener,
|
||||
FileWorkspaceFolderDeletionJob,
|
||||
FileDeletionJob,
|
||||
FilesFieldDeletionJob,
|
||||
FileUploadService,
|
||||
],
|
||||
exports: [FileService, FileMetadataService, FilesFieldService],
|
||||
exports: [FileService, FileMetadataService],
|
||||
controllers: [FileController],
|
||||
})
|
||||
export class FileModule {}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Req,
|
||||
Res,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import {
|
||||
FileException,
|
||||
FileExceptionCode,
|
||||
} from 'src/engine/core-modules/file/file.exception';
|
||||
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
|
||||
import { FilesFieldGuard } from 'src/engine/core-modules/file/files-field/guards/files-field.guard';
|
||||
import { FileApiExceptionFilter } from 'src/engine/core-modules/file/filters/file-api-exception.filter';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
|
||||
@Controller('files-field')
|
||||
@UseFilters(FileApiExceptionFilter)
|
||||
export class FilesFieldController {
|
||||
constructor(private readonly filesFieldService: FilesFieldService) {}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(FilesFieldGuard, NoPermissionGuard)
|
||||
async getFileById(
|
||||
@Res() res: Response,
|
||||
@Req() req: Request,
|
||||
@Param('id') fileId: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const workspaceId = (req as any)?.workspaceId;
|
||||
|
||||
try {
|
||||
const fileStream = await this.filesFieldService.getFileStream({
|
||||
fileId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
fileStream.on('error', () => {
|
||||
throw new FileException(
|
||||
'Error streaming file from storage',
|
||||
FileExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
fileStream.pipe(res);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new FileException(
|
||||
'File not found',
|
||||
FileExceptionCode.FILE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
throw new FileException(
|
||||
`Error retrieving file: ${error.message}`,
|
||||
FileExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -4,6 +4,7 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum FilesFieldExceptionCode {
|
||||
FILE_DELETION_FAILED = 'FILE_DELETION_FAILED',
|
||||
TEMPORARY_FILE_NOT_ALLOWED = 'TEMPORARY_FILE_NOT_ALLOWED',
|
||||
}
|
||||
|
||||
export class FilesFieldException extends CustomException<FilesFieldExceptionCode> {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FilesFieldController } from 'src/engine/core-modules/file/files-field/controllers/files-field.controller';
|
||||
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
|
||||
import { FilesFieldGuard } from 'src/engine/core-modules/file/files-field/guards/files-field.guard';
|
||||
import { FilesFieldDeletionJob } from 'src/engine/core-modules/file/files-field/jobs/files-field-deletion.job';
|
||||
import { FilesFieldDeletionListener } from 'src/engine/core-modules/file/files-field/listeners/files-field-deletion.listener';
|
||||
import { FilesFieldResolver } from 'src/engine/core-modules/file/files-field/resolvers/files-field.resolver';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule,
|
||||
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity, ApplicationEntity]),
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
FilesFieldService,
|
||||
FilesFieldResolver,
|
||||
FilesFieldGuard,
|
||||
FilesFieldDeletionListener,
|
||||
FilesFieldDeletionJob,
|
||||
],
|
||||
exports: [FilesFieldService],
|
||||
controllers: [FilesFieldController],
|
||||
})
|
||||
export class FilesFieldModule {}
|
||||
+139
-1
@@ -1,9 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import {
|
||||
FilesFieldTokenJwtPayload,
|
||||
JwtTokenTypeEnum,
|
||||
} from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
|
||||
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
import {
|
||||
FilesFieldException,
|
||||
@@ -12,7 +28,61 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class FilesFieldService {
|
||||
constructor(private readonly fileStorageService: FileStorageService) {}
|
||||
constructor(
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
) {}
|
||||
|
||||
async uploadFile({
|
||||
file,
|
||||
filename,
|
||||
declaredMimeType,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
declaredMimeType: string | undefined;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): Promise<FileEntity> {
|
||||
const { mimeType, ext } = await extractFileInfo({
|
||||
file,
|
||||
declaredMimeType,
|
||||
filename,
|
||||
});
|
||||
|
||||
const sanitizedFile = sanitizeFile({ file, ext, mimeType });
|
||||
|
||||
const fileId = v4();
|
||||
const name = `${fileId}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
id: applicationId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fileStorageService.write_v2({
|
||||
sourceFile: sanitizedFile,
|
||||
destinationPath: name,
|
||||
mimeType,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
fileId,
|
||||
settings: {
|
||||
isTemporaryFile: true,
|
||||
toDelete: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteFilesFieldFile({
|
||||
fileId,
|
||||
@@ -37,4 +107,72 @@ export class FilesFieldService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getFileStream({
|
||||
fileId,
|
||||
workspaceId,
|
||||
}: {
|
||||
fileId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Readable> {
|
||||
const file = await this.fileRepository.findOneOrFail({
|
||||
where: {
|
||||
id: fileId,
|
||||
workspaceId,
|
||||
application: {
|
||||
workspaceId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (file.settings?.isTemporaryFile === true) {
|
||||
throw new FilesFieldException(
|
||||
`File ${fileId} is not associated with a permanent files field`,
|
||||
FilesFieldExceptionCode.TEMPORARY_FILE_NOT_ALLOWED,
|
||||
{
|
||||
userFriendlyMessage: msg`File ${fileId} is not associated with a files field. It can't be downloaded.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
id: file.applicationId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fileStorageService.read_v2({
|
||||
destinationPath: removeFileFolderFromFileEntityPath(file.path),
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
signFileUrl(
|
||||
payloadToEncode: Omit<FilesFieldTokenJwtPayload, 'type' | 'sub'>,
|
||||
) {
|
||||
const fileTokenExpiresIn = this.twentyConfigService.get(
|
||||
'FILE_TOKEN_EXPIRES_IN',
|
||||
);
|
||||
|
||||
const payload: FilesFieldTokenJwtPayload = {
|
||||
...payloadToEncode,
|
||||
sub: payloadToEncode.workspaceId,
|
||||
type: JwtTokenTypeEnum.FILE,
|
||||
};
|
||||
|
||||
const secret = this.jwtWrapperService.generateAppSecret(
|
||||
payload.type,
|
||||
payloadToEncode.workspaceId,
|
||||
);
|
||||
|
||||
const token = this.jwtWrapperService.sign(payload, {
|
||||
secret,
|
||||
expiresIn: fileTokenExpiresIn,
|
||||
});
|
||||
|
||||
return `${process.env.SERVER_URL}/files-field/${payloadToEncode.fileId}?token=${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
type CanActivate,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { FilesFieldTokenJwtPayload } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
|
||||
@Injectable()
|
||||
export class FilesFieldGuard implements CanActivate {
|
||||
constructor(private readonly jwtWrapperService: JwtWrapperService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const fileToken = request.query.token;
|
||||
const fileId = request.params.id;
|
||||
|
||||
if (!fileToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtWrapperService.verifyJwtToken(fileToken, {
|
||||
ignoreExpiration:
|
||||
fileFolderConfigs[FileFolder.FilesField].ignoreExpirationToken,
|
||||
});
|
||||
|
||||
if (!payload.workspaceId) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const decodedPayload =
|
||||
this.jwtWrapperService.decode<FilesFieldTokenJwtPayload>(fileToken, {
|
||||
json: true,
|
||||
});
|
||||
|
||||
request.workspaceId = decodedPayload.workspaceId;
|
||||
|
||||
if (decodedPayload.fileId !== fileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
|
||||
@Resolver()
|
||||
export class FilesFieldResolver {
|
||||
constructor(private readonly filesFieldService: FilesFieldService) {}
|
||||
|
||||
@Mutation(() => FileDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadFilesFieldFile(
|
||||
@AuthWorkspace()
|
||||
{ id: workspaceId, workspaceCustomApplicationId }: WorkspaceEntity,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
): Promise<FileDTO> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
|
||||
const fileEntity = await this.filesFieldService.uploadFile({
|
||||
file: buffer,
|
||||
filename,
|
||||
declaredMimeType: mimetype,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
return fileEntity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
export const sanitizeFile = ({
|
||||
file,
|
||||
ext,
|
||||
mimeType,
|
||||
}: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
ext: string;
|
||||
mimeType: string | undefined;
|
||||
}): Buffer | Uint8Array | string => {
|
||||
if (ext === 'svg' || mimeType === 'image/svg+xml') {
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
|
||||
let fileString: string;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
fileString = file;
|
||||
} else if (Buffer.isBuffer(file)) {
|
||||
fileString = file.toString('utf-8');
|
||||
} else {
|
||||
fileString = Buffer.from(file).toString('utf-8');
|
||||
}
|
||||
|
||||
return purify.sanitize(fileString);
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
||||
Reference in New Issue
Block a user