diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts index c24cd55218..d1e72fb162 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/index.ts @@ -1,7 +1,9 @@ +import { QueryResultGettersFactory } from './query-result-getters.factory'; import { RecordPositionFactory } from './record-position.factory'; import { QueryRunnerArgsFactory } from './query-runner-args.factory'; export const workspaceQueryRunnerFactories = [ QueryRunnerArgsFactory, RecordPositionFactory, + QueryResultGettersFactory, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory.ts new file mode 100644 index 0000000000..6ea3a300ea --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; + +import { addMilliseconds } from 'date-fns'; +import ms from 'ms'; + +import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface'; + +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { TokenService } from 'src/engine/modules/auth/services/token.service'; + +@Injectable() +export class QueryResultGettersFactory { + constructor( + private readonly tokenService: TokenService, + private readonly environmentService: EnvironmentService, + ) {} + + async create( + result: Result, + objectMetadataItem: ObjectMetadataInterface, + ): Promise { + // TODO: look for file type once implemented + switch (objectMetadataItem.nameSingular) { + case 'attachment': + return this.applyAttachmentGetters(result); + default: + return result; + } + } + + private async applyAttachmentGetters( + attachments: any, + ): Promise { + if (!attachments || !attachments.edges) { + return attachments; + } + + const fileTokenExpiresIn = this.environmentService.get( + 'FILE_TOKEN_EXPIRES_IN', + ); + const secret = this.environmentService.get('FILE_TOKEN_SECRET'); + + const mappedEdges = await Promise.all( + attachments.edges.map(async (attachment: any) => { + if (!attachment.node.id || !attachment?.node?.fullPath) { + return attachment; + } + + const expirationDate = addMilliseconds( + new Date(), + ms(fileTokenExpiresIn), + ); + + const signedPayload = await this.tokenService.encodePayload( + { + expiration_date: expirationDate, + attachment_id: attachment.node.id, + }, + { + secret, + }, + ); + + attachment.node.fullPath = `${attachment.node.fullPath}?token=${signedPayload}`; + + return attachment; + }), + ); + + return { + ...attachments, + edges: mappedEdges, + } as Result; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 86384c7b14..e96db3e364 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -5,11 +5,13 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module'; import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener'; +import { AuthModule } from 'src/engine/modules/auth/auth.module'; import { WorkspaceQueryRunnerService } from './workspace-query-runner.service'; @Module({ imports: [ + AuthModule, WorkspaceQueryBuilderModule, WorkspaceDataSourceModule, WorkspacePreQueryHookModule, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index cc75735714..620008a6a5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -45,6 +45,7 @@ import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-q import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { NotFoundError } from 'src/engine/filters/utils/graphql-errors.util'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; +import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { @@ -61,6 +62,7 @@ export class WorkspaceQueryRunnerService { private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, + private readonly queryResultGettersFactory: QueryResultGettersFactory, @Inject(MessageQueue.webhookQueue) private readonly messageQueueService: MessageQueueService, private readonly eventEmitter: EventEmitter2, @@ -133,7 +135,7 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - const parsedResult = this.parseResult>( + const parsedResult = await this.parseResult>( result, objectMetadataItem, '', @@ -174,7 +176,7 @@ export class WorkspaceQueryRunnerService { workspaceId, ); - const parsedResult = this.parseResult>( + const parsedResult = await this.parseResult>( existingRecordResult, objectMetadataItem, '', @@ -227,10 +229,12 @@ export class WorkspaceQueryRunnerService { const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'insertInto', + const parsedResults = ( + await this.parseResult>( + result, + objectMetadataItem, + 'insertInto', + ) )?.records; await this.triggerWebhooks( @@ -280,10 +284,12 @@ export class WorkspaceQueryRunnerService { const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'update', + const parsedResults = ( + await this.parseResult>( + result, + objectMetadataItem, + 'update', + ) )?.records; await this.triggerWebhooks( @@ -316,10 +322,12 @@ export class WorkspaceQueryRunnerService { const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'update', + const parsedResults = ( + await this.parseResult>( + result, + objectMetadataItem, + 'update', + ) )?.records; await this.triggerWebhooks( @@ -349,10 +357,12 @@ export class WorkspaceQueryRunnerService { const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', + const parsedResults = ( + await this.parseResult>( + result, + objectMetadataItem, + 'deleteFrom', + ) )?.records; await this.triggerWebhooks( @@ -382,10 +392,12 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', + const parsedResults = ( + await this.parseResult>( + result, + objectMetadataItem, + 'deleteFrom', + ) )?.records; await this.triggerWebhooks( @@ -445,11 +457,11 @@ export class WorkspaceQueryRunnerService { return results; } - private parseResult( + private async parseResult( graphqlResult: PGGraphQLResult | undefined, objectMetadataItem: ObjectMetadataInterface, command: string, - ): Result { + ): Promise { const entityKey = `${command}${computeObjectTargetTable( objectMetadataItem, )}Collection`; @@ -481,7 +493,12 @@ export class WorkspaceQueryRunnerService { throw error; } - return parseResult(result); + const resultWithGetters = await this.queryResultGettersFactory.create( + result, + objectMetadataItem, + ); + + return parseResult(resultWithGetters); } async executeAndParse( diff --git a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts index 9280550fc4..1afb6e784e 100644 --- a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts @@ -129,6 +129,13 @@ export class EnvironmentVariables { @IsOptional() LOGIN_TOKEN_EXPIRES_IN: string = '15m'; + @IsString() + FILE_TOKEN_SECRET: string; + + @IsDuration() + @IsOptional() + FILE_TOKEN_EXPIRES_IN: string; + // Auth @IsUrl({ require_tld: false }) @IsOptional() diff --git a/packages/twenty-server/src/engine/integrations/environment/environment.default.ts b/packages/twenty-server/src/engine/integrations/environment/environment.default.ts new file mode 100644 index 0000000000..3b3632d4fb --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/environment/environment.default.ts @@ -0,0 +1,79 @@ +import { EmailDriver } from 'src/engine/integrations/email/interfaces/email.interface'; +import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface'; + +import { ExceptionHandlerDriver } from 'src/engine/integrations/exception-handler/interfaces'; +import { StorageDriverType } from 'src/engine/integrations/file-storage/interfaces'; +import { LoggerDriverType } from 'src/engine/integrations/logger/interfaces'; +import { MessageQueueDriverType } from 'src/engine/integrations/message-queue/interfaces'; +import { EnvironmentVariables } from 'src/engine/integrations/environment/environment-variables'; + +const EnvironmentDefault = new EnvironmentVariables(); + +EnvironmentDefault.DEBUG_MODE = false; +EnvironmentDefault.SIGN_IN_PREFILLED = false; +EnvironmentDefault.IS_BILLING_ENABLED = false; +EnvironmentDefault.BILLING_PLAN_REQUIRED_LINK = ''; +EnvironmentDefault.BILLING_STRIPE_BASE_PLAN_PRODUCT_ID = ''; +EnvironmentDefault.BILLING_FREE_TRIAL_DURATION_IN_DAYS = 7; +EnvironmentDefault.BILLING_STRIPE_API_KEY = ''; +EnvironmentDefault.BILLING_STRIPE_WEBHOOK_SECRET = ''; +EnvironmentDefault.TELEMETRY_ENABLED = true; +EnvironmentDefault.TELEMETRY_ANONYMIZATION_ENABLED = true; +EnvironmentDefault.PORT = 3000; +EnvironmentDefault.REDIS_HOST = '127.0.0.1'; +EnvironmentDefault.REDIS_PORT = 6379; +EnvironmentDefault.PG_DATABASE_URL = ''; +EnvironmentDefault.FRONT_BASE_URL = ''; +EnvironmentDefault.SERVER_URL = ''; +EnvironmentDefault.ACCESS_TOKEN_SECRET = 'random_string'; +EnvironmentDefault.ACCESS_TOKEN_EXPIRES_IN = '30m'; +EnvironmentDefault.REFRESH_TOKEN_SECRET = 'random_string'; +EnvironmentDefault.REFRESH_TOKEN_EXPIRES_IN = '30m'; +EnvironmentDefault.REFRESH_TOKEN_COOL_DOWN = '1m'; +EnvironmentDefault.LOGIN_TOKEN_SECRET = 'random_string'; +EnvironmentDefault.LOGIN_TOKEN_EXPIRES_IN = '30m'; +EnvironmentDefault.FILE_TOKEN_SECRET = 'random_string'; +EnvironmentDefault.FILE_TOKEN_EXPIRES_IN = '1d'; +EnvironmentDefault.API_TOKEN_EXPIRES_IN = '100y'; +EnvironmentDefault.SHORT_TERM_TOKEN_EXPIRES_IN = '5m'; +EnvironmentDefault.FRONT_AUTH_CALLBACK_URL = ''; +EnvironmentDefault.MESSAGING_PROVIDER_GMAIL_ENABLED = false; +EnvironmentDefault.MESSAGING_PROVIDER_GMAIL_CALLBACK_URL = ''; +EnvironmentDefault.AUTH_GOOGLE_ENABLED = false; +EnvironmentDefault.AUTH_GOOGLE_CLIENT_ID = ''; +EnvironmentDefault.AUTH_GOOGLE_CLIENT_SECRET = ''; +EnvironmentDefault.AUTH_GOOGLE_CALLBACK_URL = ''; +EnvironmentDefault.STORAGE_TYPE = StorageDriverType.Local; +EnvironmentDefault.STORAGE_S3_REGION = 'aws-east-1'; +EnvironmentDefault.STORAGE_S3_NAME = ''; +EnvironmentDefault.STORAGE_S3_ENDPOINT = ''; +EnvironmentDefault.STORAGE_LOCAL_PATH = '.local-storage'; +EnvironmentDefault.MESSAGE_QUEUE_TYPE = MessageQueueDriverType.Sync; +EnvironmentDefault.EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com'; +EnvironmentDefault.EMAIL_SYSTEM_ADDRESS = 'system@yourdomain.com'; +EnvironmentDefault.EMAIL_FROM_NAME = 'John from Twenty'; +EnvironmentDefault.EMAIL_DRIVER = EmailDriver.Logger; +EnvironmentDefault.EMAIL_SMTP_HOST = ''; +EnvironmentDefault.EMAIL_SMTP_PORT = 587; +EnvironmentDefault.EMAIL_SMTP_USER = ''; +EnvironmentDefault.EMAIL_SMTP_PASSWORD = ''; +EnvironmentDefault.SUPPORT_DRIVER = SupportDriver.None; +EnvironmentDefault.SUPPORT_FRONT_CHAT_ID = ''; +EnvironmentDefault.SUPPORT_FRONT_HMAC_KEY = ''; +EnvironmentDefault.LOGGER_DRIVER = LoggerDriverType.Console; +EnvironmentDefault.EXCEPTION_HANDLER_DRIVER = ExceptionHandlerDriver.Console; +EnvironmentDefault.LOG_LEVELS = ['log', 'error', 'warn']; +EnvironmentDefault.SENTRY_DSN = ''; +EnvironmentDefault.DEMO_WORKSPACE_IDS = []; +EnvironmentDefault.OPENROUTER_API_KEY = ''; +EnvironmentDefault.PASSWORD_RESET_TOKEN_EXPIRES_IN = '5m'; +EnvironmentDefault.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 30; +EnvironmentDefault.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 60; +EnvironmentDefault.IS_SIGN_UP_DISABLED = false; +EnvironmentDefault.API_RATE_LIMITING_TTL = 100; +EnvironmentDefault.API_RATE_LIMITING_LIMIT = 500; +EnvironmentDefault.MUTATION_MAXIMUM_RECORD_AFFECTED = 100; +EnvironmentDefault.CACHE_STORAGE_TYPE = 'memory'; +EnvironmentDefault.CACHE_STORAGE_TTL = 3600 * 24 * 7; + +export { EnvironmentDefault }; diff --git a/packages/twenty-server/src/engine/modules/auth/auth.module.ts b/packages/twenty-server/src/engine/modules/auth/auth.module.ts index 5d7995fd31..a33776122b 100644 --- a/packages/twenty-server/src/engine/modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/modules/auth/auth.module.ts @@ -5,7 +5,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { HttpModule } from '@nestjs/axios'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { FileModule } from 'src/engine/modules/file/file.module'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; import { User } from 'src/engine/modules/user/user.entity'; import { RefreshToken } from 'src/engine/modules/refresh-token/refresh-token.entity'; @@ -22,6 +21,7 @@ import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-work import { SignUpService } from 'src/engine/modules/auth/services/sign-up.service'; import { GoogleGmailAuthController } from 'src/engine/modules/auth/controllers/google-gmail-auth.controller'; import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity'; +import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module'; import { AuthResolver } from './auth.resolver'; @@ -42,7 +42,7 @@ const jwtModule = JwtModule.registerAsync({ @Module({ imports: [ jwtModule, - FileModule, + FileUploadModule, DataSourceModule, UserModule, WorkspaceManagerModule, diff --git a/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.spec.ts b/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.spec.ts index 2fe72ca969..af3b2ea6cc 100644 --- a/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.spec.ts +++ b/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.spec.ts @@ -6,7 +6,7 @@ import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; import { User } from 'src/engine/modules/user/user.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { SignUpService } from 'src/engine/modules/auth/services/sign-up.service'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; describe('SignUpService', () => { diff --git a/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.ts b/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.ts index f19f5cadd9..9c49e61be7 100644 --- a/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.ts +++ b/packages/twenty-server/src/engine/modules/auth/services/sign-up.service.ts @@ -19,7 +19,7 @@ import { } from 'src/engine/modules/auth/auth.util'; import { User } from 'src/engine/modules/user/user.entity'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { getImageBufferFromUrl } from 'src/utils/image'; import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; diff --git a/packages/twenty-server/src/engine/modules/auth/services/token.service.spec.ts b/packages/twenty-server/src/engine/modules/auth/services/token.service.spec.ts index 5fc239cae1..697359de9a 100644 --- a/packages/twenty-server/src/engine/modules/auth/services/token.service.spec.ts +++ b/packages/twenty-server/src/engine/modules/auth/services/token.service.spec.ts @@ -7,7 +7,6 @@ import { RefreshToken } from 'src/engine/modules/refresh-token/refresh-token.ent import { User } from 'src/engine/modules/user/user.entity'; import { JwtAuthStrategy } from 'src/engine/modules/auth/strategies/jwt.auth.strategy'; import { EmailService } from 'src/engine/integrations/email/email.service'; -import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; import { TokenService } from './token.service'; @@ -35,10 +34,6 @@ describe('TokenService', () => { provide: EmailService, useValue: {}, }, - { - provide: UserWorkspaceService, - useValue: {}, - }, { provide: getRepositoryToken(User, 'core'), useValue: {}, diff --git a/packages/twenty-server/src/engine/modules/auth/services/token.service.ts b/packages/twenty-server/src/engine/modules/auth/services/token.service.ts index 72aa8fc83c..76ac9e9d70 100644 --- a/packages/twenty-server/src/engine/modules/auth/services/token.service.ts +++ b/packages/twenty-server/src/engine/modules/auth/services/token.service.ts @@ -40,7 +40,6 @@ import { EmailService } from 'src/engine/integrations/email/email.service'; import { InvalidatePassword } from 'src/engine/modules/auth/dto/invalidate-password.entity'; import { EmailPasswordResetLink } from 'src/engine/modules/auth/dto/email-password-reset-link.entity'; import { JwtData } from 'src/engine/modules/auth/types/jwt-data.type'; -import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; @Injectable() @@ -56,7 +55,6 @@ export class TokenService { @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private readonly emailService: EmailService, - private readonly userWorkspaceService: UserWorkspaceService, ) {} async generateAccessToken( @@ -528,4 +526,12 @@ export class TokenService { return { success: true }; } + + async encodePayload(payload: any, options?: any): Promise { + return this.jwtService.sign(payload, options); + } + + async decodePayload(payload: any, options?: any): Promise { + return this.jwtService.decode(payload, options); + } } diff --git a/packages/twenty-server/src/engine/modules/file/controllers/file.controller.spec.ts b/packages/twenty-server/src/engine/modules/file/controllers/file.controller.spec.ts index b7048b4166..7005425ab3 100644 --- a/packages/twenty-server/src/engine/modules/file/controllers/file.controller.spec.ts +++ b/packages/twenty-server/src/engine/modules/file/controllers/file.controller.spec.ts @@ -1,11 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { CanActivate } from '@nestjs/common'; import { FileService } from 'src/engine/modules/file/services/file.service'; +import { FilePathGuard } from 'src/engine/modules/file/guards/file-path-guard'; import { FileController } from './file.controller'; describe('FileController', () => { let controller: FileController; + const mock_FilePathGuard: CanActivate = { canActivate: jest.fn(() => true) }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,7 +19,10 @@ describe('FileController', () => { useValue: {}, }, ], - }).compile(); + }) + .overrideGuard(FilePathGuard) + .useValue(mock_FilePathGuard) + .compile(); controller = module.get(FileController); }); diff --git a/packages/twenty-server/src/engine/modules/file/controllers/file.controller.ts b/packages/twenty-server/src/engine/modules/file/controllers/file.controller.ts index 3e9d231445..ad3547de41 100644 --- a/packages/twenty-server/src/engine/modules/file/controllers/file.controller.ts +++ b/packages/twenty-server/src/engine/modules/file/controllers/file.controller.ts @@ -1,7 +1,8 @@ -import { Controller, Get, Param, Res } from '@nestjs/common'; +import { Controller, Get, Param, Res, UseGuards } from '@nestjs/common'; import { Response } from 'express'; +import { FilePathGuard } from 'src/engine/modules/file/guards/file-path-guard'; import { checkFilePath, checkFilename, @@ -10,6 +11,7 @@ import { FileService } from 'src/engine/modules/file/services/file.service'; // TODO: Add cookie authentication @Controller('files') +@UseGuards(FilePathGuard) export class FileController { constructor(private readonly fileService: FileService) {} diff --git a/packages/twenty-server/src/engine/modules/file/file-upload/file-upload.module.ts b/packages/twenty-server/src/engine/modules/file/file-upload/file-upload.module.ts new file mode 100644 index 0000000000..4c27162db7 --- /dev/null +++ b/packages/twenty-server/src/engine/modules/file/file-upload/file-upload.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { FileUploadResolver } from 'src/engine/modules/file/file-upload/resolvers/file-upload.resolver'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; + +@Module({ + providers: [FileUploadService, FileUploadResolver, EnvironmentService], + exports: [FileUploadService, FileUploadResolver], +}) +export class FileUploadModule {} diff --git a/packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.spec.ts b/packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.spec.ts similarity index 85% rename from packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.spec.ts rename to packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.spec.ts index b5ec1c02fa..242907bee0 100644 --- a/packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.spec.ts +++ b/packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { FileUploadResolver } from './file-upload.resolver'; diff --git a/packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.ts b/packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.ts similarity index 94% rename from packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.ts rename to packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.ts index fc73123714..8b4a1f1a45 100644 --- a/packages/twenty-server/src/engine/modules/file/resolvers/file-upload.resolver.ts +++ b/packages/twenty-server/src/engine/modules/file/file-upload/resolvers/file-upload.resolver.ts @@ -5,7 +5,7 @@ import { GraphQLUpload, FileUpload } from 'graphql-upload'; import { FileFolder } from 'src/engine/modules/file/interfaces/file-folder.interface'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; diff --git a/packages/twenty-server/src/engine/modules/file/services/file-upload.service.spec.ts b/packages/twenty-server/src/engine/modules/file/file-upload/services/file-upload.service.spec.ts similarity index 100% rename from packages/twenty-server/src/engine/modules/file/services/file-upload.service.spec.ts rename to packages/twenty-server/src/engine/modules/file/file-upload/services/file-upload.service.spec.ts diff --git a/packages/twenty-server/src/engine/modules/file/services/file-upload.service.ts b/packages/twenty-server/src/engine/modules/file/file-upload/services/file-upload.service.ts similarity index 100% rename from packages/twenty-server/src/engine/modules/file/services/file-upload.service.ts rename to packages/twenty-server/src/engine/modules/file/file-upload/services/file-upload.service.ts diff --git a/packages/twenty-server/src/engine/modules/file/file.module.ts b/packages/twenty-server/src/engine/modules/file/file.module.ts index 0c0a42f153..dfbed11bf1 100644 --- a/packages/twenty-server/src/engine/modules/file/file.module.ts +++ b/packages/twenty-server/src/engine/modules/file/file.module.ts @@ -1,20 +1,17 @@ import { Module } from '@nestjs/common'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { FilePathGuard } from 'src/engine/modules/file/guards/file-path-guard'; +import { AuthModule } from 'src/engine/modules/auth/auth.module'; +import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module'; import { FileService } from './services/file.service'; -import { FileUploadService } from './services/file-upload.service'; -import { FileUploadResolver } from './resolvers/file-upload.resolver'; import { FileController } from './controllers/file.controller'; @Module({ - providers: [ - FileService, - FileUploadService, - FileUploadResolver, - EnvironmentService, - ], - exports: [FileService, FileUploadService], + imports: [FileUploadModule, AuthModule], + providers: [FileService, EnvironmentService, FilePathGuard], + exports: [FileService], controllers: [FileController], }) export class FileModule {} diff --git a/packages/twenty-server/src/engine/modules/file/guards/file-path-guard.ts b/packages/twenty-server/src/engine/modules/file/guards/file-path-guard.ts new file mode 100644 index 0000000000..93ede5dc7c --- /dev/null +++ b/packages/twenty-server/src/engine/modules/file/guards/file-path-guard.ts @@ -0,0 +1,52 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, +} from '@nestjs/common'; + +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { TokenService } from 'src/engine/modules/auth/services/token.service'; + +@Injectable() +export class FilePathGuard implements CanActivate { + constructor( + private readonly tokenService: TokenService, + private readonly environmentService: EnvironmentService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const query = context.switchToHttp().getRequest().query; + + if (query && query['token']) { + return !(await this.isExpired(query['token'])); + } + + return true; + } + + private async isExpired(signedExpirationDate: string): Promise { + const decodedPayload = await this.tokenService.decodePayload( + signedExpirationDate, + { + secret: this.environmentService.get('FILE_TOKEN_SECRET'), + }, + ); + + const expirationDate = decodedPayload?.['expiration_date']; + + if (!expirationDate) { + return true; + } + + if (new Date(expirationDate) < new Date()) { + throw new HttpException( + 'This url has expired. Please reload twenty page and open file again.', + HttpStatus.FORBIDDEN, + ); + } + + return false; + } +} diff --git a/packages/twenty-server/src/engine/modules/user/user.module.ts b/packages/twenty-server/src/engine/modules/user/user.module.ts index 3edc028e66..3265e620da 100644 --- a/packages/twenty-server/src/engine/modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/modules/user/user.module.ts @@ -4,7 +4,6 @@ import { Module } from '@nestjs/common'; import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; -import { FileModule } from 'src/engine/modules/file/file.module'; import { User } from 'src/engine/modules/user/user.entity'; import { UserResolver } from 'src/engine/modules/user/user.resolver'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; @@ -12,6 +11,7 @@ import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.mo import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module'; import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; +import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module'; import { userAutoResolverOpts } from './user.auto-resolver-opts'; @@ -27,7 +27,7 @@ import { UserService } from './services/user.service'; resolvers: userAutoResolverOpts, }), DataSourceModule, - FileModule, + FileUploadModule, UserWorkspaceModule, ], exports: [UserService], diff --git a/packages/twenty-server/src/engine/modules/user/user.resolver.ts b/packages/twenty-server/src/engine/modules/user/user.resolver.ts index b9b051edd6..9485875d40 100644 --- a/packages/twenty-server/src/engine/modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/modules/user/user.resolver.ts @@ -20,7 +20,7 @@ import { FileFolder } from 'src/engine/modules/file/interfaces/file-folder.inter import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { assert } from 'src/utils/assert'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; diff --git a/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts index 65dc8e1430..a2fa95f231 100644 --- a/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; -import { FileModule } from 'src/engine/modules/file/file.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { WorkspaceResolver } from 'src/engine/modules/workspace/workspace.resolver'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; @@ -12,6 +11,7 @@ import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace. import { User } from 'src/engine/modules/user/user.entity'; import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module'; import { BillingModule } from 'src/engine/modules/billing/billing.module'; +import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module'; import { Workspace } from './workspace.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; @@ -24,7 +24,7 @@ import { WorkspaceService } from './services/workspace.service'; NestjsQueryGraphQLModule.forFeature({ imports: [ BillingModule, - FileModule, + FileUploadModule, NestjsQueryTypeOrmModule.forFeature( [User, Workspace, UserWorkspace, FeatureFlagEntity], 'core', diff --git a/packages/twenty-server/src/engine/modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/modules/workspace/workspace.resolver.ts index 061f4a8bdd..db02e13842 100644 --- a/packages/twenty-server/src/engine/modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/modules/workspace/workspace.resolver.ts @@ -13,7 +13,7 @@ import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileFolder } from 'src/engine/modules/file/interfaces/file-folder.interface'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { FileUploadService } from 'src/engine/modules/file/services/file-upload.service'; +import { FileUploadService } from 'src/engine/modules/file/file-upload/services/file-upload.service'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { assert } from 'src/utils/assert'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';