Fix missing encoding in workspace-logo, members, person (#6510)

This commit is contained in:
Weiko
2024-08-02 15:18:48 +02:00
committed by GitHub
parent 0c036efcc4
commit 5870979bfa
13 changed files with 176 additions and 70 deletions

View File

@ -5,7 +5,7 @@ export const getImageAbsoluteURI = (imageUrl?: string | null) => {
return null; return null;
} }
if (imageUrl?.startsWith('https:')) { if (imageUrl?.startsWith('https:') || imageUrl?.startsWith('http:')) {
return imageUrl; return imageUrl;
} }

View File

@ -13,7 +13,10 @@ import {
} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; 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 { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@ -52,9 +55,11 @@ export class UpdateFileFolderStructureCommand extends CommandRunner {
): Promise<void> { ): Promise<void> {
const workspaceIds = options.workspaceId const workspaceIds = options.workspaceId
? [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) { if (!workspaceIds.length) {
this.logger.log(chalk.yellow('No workspace found')); this.logger.log(chalk.yellow('No workspace found'));

View File

@ -1,41 +1,21 @@
import { addMilliseconds } from 'date-fns'; import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import ms from 'ms';
import { QueryResultGuetterHandlerInterface } 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 { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
export class AttachmentQueryResultGetterHandler export class AttachmentQueryResultGetterHandler
implements QueryResultGuetterHandlerInterface implements QueryResultGetterHandlerInterface
{ {
constructor( constructor(private readonly fileService: FileService) {}
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
) {}
async process(attachment: any, workspaceId: string): Promise<any> { async handle(attachment: any, workspaceId: string): Promise<any> {
if (!attachment.id || !attachment?.fullPath) { if (!attachment.id || !attachment?.fullPath) {
return attachment; return attachment;
} }
const fileTokenExpiresIn = this.environmentService.get( const signedPayload = await this.fileService.encodeFileToken({
'FILE_TOKEN_EXPIRES_IN', attachment_id: attachment.id,
); workspace_id: workspaceId,
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,
},
);
return { return {
...attachment, ...attachment,

View File

@ -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<any> {
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}`,
};
}
}

View File

@ -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<any> {
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}`,
};
}
}

View File

@ -1,3 +1,3 @@
export interface QueryResultGuetterHandlerInterface { export interface QueryResultGetterHandlerInterface {
process(result: any, workspaceId: string): Promise<any>; handle(result: any, workspaceId: string): Promise<any>;
} }

View File

@ -1,31 +1,28 @@
import { Injectable } from '@nestjs/common'; 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 { 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 { 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 { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; 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() @Injectable()
export class QueryResultGettersFactory { export class QueryResultGettersFactory {
private handlers: Map<string, QueryResultGuetterHandlerInterface>; private handlers: Map<string, QueryResultGetterHandlerInterface>;
constructor( constructor(private readonly fileService: FileService) {
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
) {
this.initializeHandlers(); this.initializeHandlers();
} }
private initializeHandlers() { private initializeHandlers() {
this.handlers = new Map<string, QueryResultGuetterHandlerInterface>([ this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
['person', new PersonQueryResultGetterHandler(this.fileService)],
[ [
'attachment', 'workspaceMember',
new AttachmentQueryResultGetterHandler( new WorkspaceMemberQueryResultGetterHandler(this.fileService),
this.tokenService,
this.environmentService,
),
], ],
]); ]);
} }
@ -43,7 +40,7 @@ export class QueryResultGettersFactory {
edges: await Promise.all( edges: await Promise.all(
result.edges.map(async (edge: any) => ({ result.edges.map(async (edge: any) => ({
...edge, ...edge,
node: await handler.process(edge.node, workspaceId), node: await handler.handle(edge.node, workspaceId),
})), })),
), ),
}; };
@ -54,19 +51,19 @@ export class QueryResultGettersFactory {
...result, ...result,
records: await Promise.all( records: await Promise.all(
result.records.map( 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 ( return (
this.handlers.get(objectType) || { this.handlers.get(objectType) || {
process: (result: any) => result, handle: (result: any) => result,
} }
); );
} }

View File

@ -1,16 +1,17 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; 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 { 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 { 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'; import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@ -25,6 +26,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
AnalyticsModule, AnalyticsModule,
DuplicateModule, DuplicateModule,
FileModule,
], ],
providers: [ providers: [
WorkspaceQueryRunnerService, WorkspaceQueryRunnerService,

View File

@ -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 { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.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 { FileController } from './controllers/file.controller';
import { FileService } from './services/file.service';
@Module({ @Module({
imports: [FileUploadModule, AuthModule], imports: [FileUploadModule, forwardRef(() => AuthModule)],
providers: [FileService, EnvironmentService, FilePathGuard], providers: [FileService, EnvironmentService, FilePathGuard],
exports: [FileService], exports: [FileService],
controllers: [FileController], controllers: [FileController],

View File

@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; 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 { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
@ -20,6 +21,10 @@ describe('FileService', () => {
provide: EnvironmentService, provide: EnvironmentService,
useValue: {}, useValue: {},
}, },
{
provide: TokenService,
useValue: {},
},
], ],
}).compile(); }).compile();

View File

@ -2,16 +2,25 @@ import { Injectable } from '@nestjs/common';
import { Stream } from 'stream'; import { Stream } from 'stream';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { import {
FileStorageException, FileStorageException,
FileStorageExceptionCode, FileStorageExceptionCode,
} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception'; } 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'; import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
@Injectable() @Injectable()
export class FileService { export class FileService {
constructor(private readonly fileStorageService: FileStorageService) {} constructor(
private readonly fileStorageService: FileStorageService,
private readonly environmentService: EnvironmentService,
private readonly tokenService: TokenService,
) {}
async getFileStream( async getFileStream(
folderPath: string, folderPath: string,
@ -39,4 +48,25 @@ export class FileService {
throw error; throw error;
} }
} }
async encodeFileToken(payloadToEncode: Record<string, any>) {
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;
}
} }

View File

@ -8,6 +8,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; 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 { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.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: [ imports: [
NestjsQueryTypeOrmModule.forFeature([User], 'core'), NestjsQueryTypeOrmModule.forFeature([User], 'core'),
TypeORMModule, TypeORMModule,
FileModule,
], ],
resolvers: userAutoResolverOpts, resolvers: userAutoResolverOpts,
}), }),

View File

@ -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 { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; 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 { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; 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 onboardingService: OnboardingService,
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext, private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
private readonly userVarService: UserVarsService, private readonly userVarService: UserVarsService,
private readonly fileService: FileService,
) {} ) {}
@Query(() => User) @Query(() => User)
@ -68,6 +70,20 @@ export class UserResolver {
assert(user, 'User not found'); 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; return user;
} }
@ -99,7 +115,18 @@ export class UserResolver {
async workspaceMember( async workspaceMember(
@Parent() user: User, @Parent() user: User,
): Promise<WorkspaceMember | undefined> { ): Promise<WorkspaceMember | undefined> {
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, { @ResolveField(() => String, {