From 5870979bfaef03ce5ea0cda03ba91932d4826532 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 2 Aug 2024 15:18:48 +0200 Subject: [PATCH] Fix missing encoding in workspace-logo, members, person (#6510) --- .../src/utils/image/getImageAbsoluteURI.ts | 2 +- ...23-update-file-folder-structure.command.ts | 13 +++++-- .../attachment-query-result-getter.handler.ts | 38 +++++-------------- .../person-query-result-getter.handler.ts | 29 ++++++++++++++ ...pace-member-query-result-getter.handler.ts | 29 ++++++++++++++ .../query-result-getter-handler.interface.ts | 4 +- .../query-result-getters.factory.ts | 35 ++++++++--------- .../workspace-query-runner.module.ts | 18 +++++---- .../engine/core-modules/file/file.module.ts | 10 ++--- .../file/services/file.service.spec.ts | 5 +++ .../file/services/file.service.ts | 32 +++++++++++++++- .../engine/core-modules/user/user.module.ts | 2 + .../engine/core-modules/user/user.resolver.ts | 29 +++++++++++++- 13 files changed, 176 insertions(+), 70 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts diff --git a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts index 6237813b7..819567796 100644 --- a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts +++ b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts @@ -5,7 +5,7 @@ export const getImageAbsoluteURI = (imageUrl?: string | null) => { return null; } - if (imageUrl?.startsWith('https:')) { + if (imageUrl?.startsWith('https:') || imageUrl?.startsWith('http:')) { return imageUrl; } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts index be890ed77..bc5494611 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command.ts @@ -13,7 +13,10 @@ import { } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -52,9 +55,11 @@ export class UpdateFileFolderStructureCommand extends CommandRunner { ): Promise { const workspaceIds = options.workspaceId ? [options.workspaceId] - : (await this.workspaceRepository.find()).map( - (workspace) => workspace.id, - ); + : ( + await this.workspaceRepository.find({ + where: { activationStatus: WorkspaceActivationStatus.ACTIVE }, + }) + ).map((workspace) => workspace.id); if (!workspaceIds.length) { this.logger.log(chalk.yellow('No workspace found')); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts index f2c00bbb0..909c313cc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts @@ -1,41 +1,21 @@ -import { addMilliseconds } from 'date-fns'; -import ms from 'ms'; +import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; -import { QueryResultGuetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; - -import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; export class AttachmentQueryResultGetterHandler - implements QueryResultGuetterHandlerInterface + implements QueryResultGetterHandlerInterface { - constructor( - private readonly tokenService: TokenService, - private readonly environmentService: EnvironmentService, - ) {} + constructor(private readonly fileService: FileService) {} - async process(attachment: any, workspaceId: string): Promise { + async handle(attachment: any, workspaceId: string): Promise { if (!attachment.id || !attachment?.fullPath) { return attachment; } - const fileTokenExpiresIn = this.environmentService.get( - 'FILE_TOKEN_EXPIRES_IN', - ); - const secret = this.environmentService.get('FILE_TOKEN_SECRET'); - - const expirationDate = addMilliseconds(new Date(), ms(fileTokenExpiresIn)); - - const signedPayload = await this.tokenService.encodePayload( - { - expiration_date: expirationDate, - attachment_id: attachment.id, - workspace_id: workspaceId, - }, - { - secret, - }, - ); + const signedPayload = await this.fileService.encodeFileToken({ + attachment_id: attachment.id, + workspace_id: workspaceId, + }); return { ...attachment, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts new file mode 100644 index 000000000..17410286a --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts @@ -0,0 +1,29 @@ +import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; + +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; + +export class PersonQueryResultGetterHandler + implements QueryResultGetterHandlerInterface +{ + constructor(private readonly fileService: FileService) {} + + async handle( + person: PersonWorkspaceEntity, + workspaceId: string, + ): Promise { + if (!person.id || !person?.avatarUrl) { + return person; + } + + const signedPayload = await this.fileService.encodeFileToken({ + person_id: person.id, + workspace_id: workspaceId, + }); + + return { + ...person, + avatarUrl: `${person.avatarUrl}?token=${signedPayload}`, + }; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts new file mode 100644 index 000000000..06dead059 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts @@ -0,0 +1,29 @@ +import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; + +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +export class WorkspaceMemberQueryResultGetterHandler + implements QueryResultGetterHandlerInterface +{ + constructor(private readonly fileService: FileService) {} + + async handle( + workspaceMember: WorkspaceMemberWorkspaceEntity, + workspaceId: string, + ): Promise { + if (!workspaceMember.id || !workspaceMember?.avatarUrl) { + return workspaceMember; + } + + const signedPayload = await this.fileService.encodeFileToken({ + workspace_member_id: workspaceMember.id, + workspace_id: workspaceId, + }); + + return { + ...workspaceMember, + avatarUrl: `${workspaceMember.avatarUrl}?token=${signedPayload}`, + }; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts index 742a3fe32..2218afcde 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts @@ -1,3 +1,3 @@ -export interface QueryResultGuetterHandlerInterface { - process(result: any, workspaceId: string): Promise; +export interface QueryResultGetterHandlerInterface { + handle(result: any, workspaceId: string): Promise; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index eaed388df..047217f42 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -1,31 +1,28 @@ import { Injectable } from '@nestjs/common'; -import { QueryResultGuetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; +import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler'; -import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; +import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; @Injectable() export class QueryResultGettersFactory { - private handlers: Map; + private handlers: Map; - constructor( - private readonly tokenService: TokenService, - private readonly environmentService: EnvironmentService, - ) { + constructor(private readonly fileService: FileService) { this.initializeHandlers(); } private initializeHandlers() { - this.handlers = new Map([ + this.handlers = new Map([ + ['attachment', new AttachmentQueryResultGetterHandler(this.fileService)], + ['person', new PersonQueryResultGetterHandler(this.fileService)], [ - 'attachment', - new AttachmentQueryResultGetterHandler( - this.tokenService, - this.environmentService, - ), + 'workspaceMember', + new WorkspaceMemberQueryResultGetterHandler(this.fileService), ], ]); } @@ -43,7 +40,7 @@ export class QueryResultGettersFactory { edges: await Promise.all( result.edges.map(async (edge: any) => ({ ...edge, - node: await handler.process(edge.node, workspaceId), + node: await handler.handle(edge.node, workspaceId), })), ), }; @@ -54,19 +51,19 @@ export class QueryResultGettersFactory { ...result, records: await Promise.all( result.records.map( - async (item: any) => await handler.process(item, workspaceId), + async (item: any) => await handler.handle(item, workspaceId), ), ), }; } - return await handler.process(result, workspaceId); + return await handler.handle(result, workspaceId); } - private getHandler(objectType: string): QueryResultGuetterHandlerInterface { + private getHandler(objectType: string): QueryResultGetterHandlerInterface { return ( this.handlers.get(objectType) || { - process: (result: any) => result, + handle: (result: any) => 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 99c9b7a6e..bd8153261 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 @@ -1,16 +1,17 @@ import { Module } from '@nestjs/common'; import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; -import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; -import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener'; -import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { RecordPositionBackfillCommand } from 'src/engine/api/graphql/workspace-query-runner/commands/0-20-record-position-backfill.command'; +import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; +import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener'; +import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module'; +import { FileModule } from 'src/engine/core-modules/file/file.module'; +import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceQueryRunnerService } from './workspace-query-runner.service'; @@ -25,6 +26,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), AnalyticsModule, DuplicateModule, + FileModule, ], providers: [ WorkspaceQueryRunnerService, diff --git a/packages/twenty-server/src/engine/core-modules/file/file.module.ts b/packages/twenty-server/src/engine/core-modules/file/file.module.ts index 521cde04b..0169906f0 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file.module.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file.module.ts @@ -1,15 +1,15 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; +import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { FileService } from './services/file.service'; import { FileController } from './controllers/file.controller'; +import { FileService } from './services/file.service'; @Module({ - imports: [FileUploadModule, AuthModule], + imports: [FileUploadModule, forwardRef(() => AuthModule)], providers: [FileService, EnvironmentService, FilePathGuard], exports: [FileService], controllers: [FileController], diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts index da68c62a4..5d2df71e1 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; @@ -20,6 +21,10 @@ describe('FileService', () => { provide: EnvironmentService, useValue: {}, }, + { + provide: TokenService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts index d4b1b021d..dcce3f7d4 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts @@ -2,16 +2,25 @@ import { Injectable } from '@nestjs/common'; import { Stream } from 'stream'; +import { addMilliseconds } from 'date-fns'; +import ms from 'ms'; + import { FileStorageException, FileStorageExceptionCode, } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; +import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; @Injectable() export class FileService { - constructor(private readonly fileStorageService: FileStorageService) {} + constructor( + private readonly fileStorageService: FileStorageService, + private readonly environmentService: EnvironmentService, + private readonly tokenService: TokenService, + ) {} async getFileStream( folderPath: string, @@ -39,4 +48,25 @@ export class FileService { throw error; } } + + async encodeFileToken(payloadToEncode: Record) { + const fileTokenExpiresIn = this.environmentService.get( + 'FILE_TOKEN_EXPIRES_IN', + ); + const secret = this.environmentService.get('FILE_TOKEN_SECRET'); + + const expirationDate = addMilliseconds(new Date(), ms(fileTokenExpiresIn)); + + const signedPayload = await this.tokenService.encodePayload( + { + expiration_date: expirationDate, + ...payloadToEncode, + }, + { + secret, + }, + ); + + return signedPayload; + } } diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index d9abb3d4f..7a776cca2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -8,6 +8,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; +import { FileModule } from 'src/engine/core-modules/file/file.module'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; @@ -26,6 +27,7 @@ import { UserService } from './services/user.service'; imports: [ NestjsQueryTypeOrmModule.forFeature([User], 'core'), TypeORMModule, + FileModule, ], resolvers: userAutoResolverOpts, }), diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index 12627dc64..68453d8f4 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -20,6 +20,7 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; @@ -55,6 +56,7 @@ export class UserResolver { private readonly onboardingService: OnboardingService, private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext, private readonly userVarService: UserVarsService, + private readonly fileService: FileService, ) {} @Query(() => User) @@ -68,6 +70,20 @@ export class UserResolver { assert(user, 'User not found'); + user.workspaces = await Promise.all( + user.workspaces.map(async (userWorkspace) => { + if (userWorkspace.workspace.logo) { + const workspaceLogoToken = await this.fileService.encodeFileToken({ + workspace_id: userWorkspace.workspace.id, + }); + + userWorkspace.workspace.logo = `${userWorkspace.workspace.logo}?token=${workspaceLogoToken}`; + } + + return userWorkspace; + }), + ); + return user; } @@ -99,7 +115,18 @@ export class UserResolver { async workspaceMember( @Parent() user: User, ): Promise { - return this.userService.loadWorkspaceMember(user); + const workspaceMember = await this.userService.loadWorkspaceMember(user); + + if (workspaceMember && workspaceMember.avatarUrl) { + const avatarUrlToken = await this.fileService.encodeFileToken({ + workspace_member_id: workspaceMember.id, + workspace_id: user.defaultWorkspace.id, + }); + + workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`; + } + + return workspaceMember; } @ResolveField(() => String, {