Fix - Remove signFileUrl method (#19121)

`signFileUrl` is the old way to resolve file url. Replaced by
`signFileByIdUrl` which needs `FileFolder` and `fileId`.

Fix also avatar for person and workspaceMember. To be continued with
imageIdentifier refactor.


Fix : 
- https://discord.com/channels/1130383047699738754/1486723854910099576
- https://discord.com/channels/1130383047699738754/1484566445584285870
- https://discord.com/channels/1130383047699738754/1487124612889313420

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Etienne
2026-03-30 17:14:54 +02:00
committed by GitHub
parent 3d1c53ec9d
commit ecf8161d0e
20 changed files with 175 additions and 195 deletions
@@ -12,11 +12,8 @@ import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/w
import { FilesFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/files-field-query-result-getter.handler';
import { RichTextFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-field-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 { 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 { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@@ -39,18 +36,13 @@ export class CommonResultGettersService {
QueryResultGetterHandlerInterface
>;
constructor(
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
) {
constructor(private readonly fileUrlService: FileUrlService) {
this.initializeObjectHandlers();
this.initializeFieldHandlers();
}
private initializeObjectHandlers() {
this.objectHandlers = new Map<string, QueryResultGetterHandlerInterface>([
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
['person', new PersonQueryResultGetterHandler(this.fileService)],
[
'workspaceMember',
new WorkspaceMemberQueryResultGetterHandler(this.fileUrlService),
@@ -19,7 +19,6 @@ const richTextFieldMetadata = [
] as FlatFieldMetadata[];
const mockFileUrlService = {
signFileUrl: jest.fn().mockReturnValue('signed-path'),
signFileByIdUrl: jest.fn().mockReturnValue('signed-path'),
} as unknown as FileUrlService;
@@ -1,31 +0,0 @@
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { type FileService } from 'src/engine/core-modules/file/services/file.service';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
export class AttachmentQueryResultGetterHandler
implements QueryResultGetterHandlerInterface
{
constructor(private readonly fileService: FileService) {}
async handle(
attachment: AttachmentWorkspaceEntity,
workspaceId: string,
): Promise<AttachmentWorkspaceEntity> {
if (!attachment.id || !attachment?.fullPath) {
return attachment;
}
const signedPath = this.fileService.signFileUrl({
url: attachment.fullPath,
workspaceId,
});
const fullPath = `${process.env.SERVER_URL}/files/${signedPath}`;
return {
...attachment,
fullPath,
};
}
}
@@ -1,29 +0,0 @@
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { type FileService } from 'src/engine/core-modules/file/services/file.service';
import { type 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<PersonWorkspaceEntity> {
if (!person.id || !person?.avatarUrl) {
return person;
}
const signedPath = this.fileService.signFileUrl({
url: person.avatarUrl,
workspaceId,
});
return {
...person,
avatarUrl: signedPath,
};
}
}
@@ -5,7 +5,7 @@ import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-pan
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -85,7 +85,7 @@ describe('AdminPanelService', () => {
},
},
{
provide: FileService,
provide: FileUrlService,
useValue: {},
},
{
@@ -3,10 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import semver from 'semver';
import { FeatureFlagKey, FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import * as z from 'zod';
import { FeatureFlagKey } from 'twenty-shared/types';
import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto';
@@ -19,7 +19,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { type FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
@@ -33,7 +33,7 @@ export class AdminPanelService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
private readonly secureHttpClientService: SecureHttpClientService,
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
@@ -81,12 +81,13 @@ export class AdminPanelService {
id: userWorkspace.workspace.id,
name: userWorkspace.workspace.displayName ?? '',
totalUsers: userWorkspace.workspace.workspaceUsers.length,
logo: userWorkspace.workspace.logo
? this.fileService.signFileUrl({
url: userWorkspace.workspace.logo,
logo: isDefined(userWorkspace.workspace.logoFileId)
? this.fileUrlService.signFileByIdUrl({
fileId: userWorkspace.workspace.logoFileId,
workspaceId: userWorkspace.workspace.id,
fileFolder: FileFolder.CorePicture,
})
: userWorkspace.workspace.logo,
: undefined,
allowImpersonation: userWorkspace.workspace.allowImpersonation,
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
subdomain: userWorkspace.workspace.subdomain,
@@ -6,7 +6,7 @@ import crypto from 'crypto';
import { msg } from '@lingui/core/macro';
import { render } from '@react-email/render';
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
import { SettingsPath } from 'twenty-shared/types';
import { FileFolder, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -18,7 +18,7 @@ import {
import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -31,7 +31,7 @@ export class ApprovedAccessDomainService {
private readonly approvedAccessDomainRepository: Repository<ApprovedAccessDomainEntity>,
private readonly emailService: EmailService,
private readonly twentyConfigService: TwentyConfigService,
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
@@ -78,12 +78,13 @@ export class ApprovedAccessDomainService {
link: link.toString(),
workspace: {
name: workspace.displayName,
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
logo: isDefined(workspace.logoFileId)
? this.fileUrlService.signFileByIdUrl({
fileId: workspace.logoFileId,
workspaceId: workspace.id,
fileFolder: FileFolder.CorePicture,
})
: workspace.logo,
: undefined,
},
domain: approvedAccessDomain.domain,
sender: {
@@ -12,11 +12,11 @@ import {
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { ApprovedAccessDomainService } from './approved-access-domain.service';
// To avoid dynamic import issues in Jest
@@ -69,9 +69,9 @@ describe('ApprovedAccessDomainService', () => {
},
},
{
provide: FileService,
provide: FileUrlService,
useValue: {
signFileUrl: jest
signFileByIdUrl: jest
.fn()
.mockReturnValue('https://signed-url.com/logo.png'),
},
@@ -3,24 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm';
import { type Readable } from 'stream';
import { isNonEmptyString } from '@sniptt/guards';
import { FileFolder } from 'twenty-shared/types';
import {
buildSignedPath,
extractFolderPathFilenameAndTypeOrThrow,
} from 'twenty-shared/utils';
import { Like, Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
type FileTokenJwtPayloadLegacy,
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 { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type';
import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils';
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.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 { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -198,44 +189,7 @@ export class FileService {
};
}
signFileUrl({ url, workspaceId }: { url: string; workspaceId: string }) {
if (!isNonEmptyString(url)) {
return url;
}
return buildSignedPath({
path: url,
token: this.encodeFileToken({
filename: extractFolderPathFilenameAndTypeOrThrow(url).filename,
workspaceId,
}),
});
}
encodeFileToken(
payloadToEncode: Omit<FileTokenJwtPayloadLegacy, 'type' | 'sub'>,
) {
const fileTokenExpiresIn = this.twentyConfigService.get(
'FILE_TOKEN_EXPIRES_IN',
);
const payload: FileTokenJwtPayloadLegacy = {
...payloadToEncode,
sub: payloadToEncode.workspaceId,
type: JwtTokenTypeEnum.FILE,
};
const secret = this.jwtWrapperService.generateAppSecret(
payload.type,
payloadToEncode.workspaceId,
);
return this.jwtWrapperService.sign(payload, {
secret,
expiresIn: fileTokenExpiresIn,
});
}
/** @deprecated Use FileStorageService.deleteByFileId instead */
async deleteFile({
folderPath,
filename,
@@ -253,6 +207,7 @@ export class FileService {
});
}
/** @deprecated */
async deleteWorkspaceFolder(workspaceId: string) {
const workspaceFolderPath = `workspace-${workspaceId}`;
@@ -1,16 +1,19 @@
import { isNonEmptyString } from '@sniptt/guards';
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
import { FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { FileFolder } from 'twenty-shared/types';
type GetRecordImageIdentifierOptions = {
record: Record<string, unknown>;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
signUrl?: (url: string) => string | null;
signUrl?: (fileId: string, fileFolder: FileFolder) => string | null;
};
export const getRecordImageIdentifier = ({
@@ -30,6 +33,30 @@ export const getRecordImageIdentifier = ({
: null;
}
//TODO: Temporary solution before imageIdentifier refactor
if (signUrl && flatObjectMetadata.nameSingular === 'person') {
const avatarFileId = (record.avatarFile as FileOutput[])?.[0]?.fileId;
if (!isDefined(avatarFileId)) {
return null;
}
return signUrl(avatarFileId, FileFolder.FilesField);
}
if (
signUrl &&
flatObjectMetadata.nameSingular === 'workspaceMember' &&
isDefined(record.avatarUrl)
) {
const avatarFileId = extractFileIdFromUrl(
record.avatarUrl as string,
FileFolder.CorePicture,
);
if (!isDefined(avatarFileId)) {
return null;
}
return signUrl(avatarFileId, FileFolder.CorePicture);
}
if (!isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) {
return null;
}
@@ -55,12 +82,8 @@ export const getRecordImageIdentifier = ({
return null;
}
if (
signUrl &&
(flatObjectMetadata.nameSingular === 'person' ||
flatObjectMetadata.nameSingular === 'workspaceMember')
) {
return signUrl(rawImageValue);
if (signUrl && flatObjectMetadata.nameSingular === 'workspaceMember') {
return signUrl(rawImageValue, FileFolder.FilesField);
}
return rawImageValue;
@@ -5,7 +5,7 @@ import {
mockFlatFieldMetadataMaps,
mockFlatObjectMetadatas,
} from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -18,7 +18,7 @@ describe('SearchService', () => {
providers: [
SearchService,
{ provide: GlobalWorkspaceOrmManager, useValue: {} },
{ provide: FileService, useValue: {} },
{ provide: FileUrlService, useValue: {} },
{ provide: TwentyConfigService, useValue: { get: () => false } },
],
}).compile();
@@ -105,13 +105,13 @@ describe('SearchService', () => {
});
describe('getImageIdentifierColumn', () => {
it('should return null if the object metadata item does not have an image identifier', () => {
it('should return `avatarFile` if the object metadata item is a person', () => {
const imageIdentifierColumn = service.getImageIdentifierColumn(
mockFlatObjectMetadatas[0],
mockFlatFieldMetadataMaps,
);
expect(imageIdentifierColumn).toBeNull();
expect(imageIdentifierColumn).toEqual('avatarFile');
});
it('should return `domainNamePrimaryLinkUrl` column for a company object metadata item', () => {
const imageIdentifierColumn = service.getImageIdentifierColumn(
@@ -3,18 +3,24 @@ import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import chunk from 'lodash.chunk';
import { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from 'twenty-shared/constants';
import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types';
import {
FieldMetadataType,
FileFolder,
ObjectRecord,
} from 'twenty-shared/types';
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
import { Brackets, type ObjectLiteral } from 'typeorm';
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import {
decodeCursor,
encodeCursorData,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util';
import { STANDARD_OBJECTS_BY_PRIORITY_RANK } from 'src/engine/core-modules/search/constants/standard-objects-by-priority-rank';
import { type ObjectRecordFilterInput } from 'src/engine/core-modules/search/dtos/object-record-filter-input';
import { type SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
@@ -52,7 +58,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5;
export class SearchService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
private readonly twentyConfigService: TwentyConfigService,
) {}
@@ -512,6 +518,15 @@ export class SearchService {
return 'domainNamePrimaryLinkUrl';
}
//TODO: Temporary solution before imageIdentifier refactor
if (flatObjectMetadata.nameSingular === 'person') {
return 'avatarFile';
}
if (flatObjectMetadata.nameSingular === 'workspaceMember') {
return 'avatarUrl';
}
if (!flatObjectMetadata.imageIdentifierFieldMetadataId) {
return null;
}
@@ -528,10 +543,15 @@ export class SearchService {
return imageIdentifierField.name;
}
private getImageUrlWithToken(avatarUrl: string, workspaceId: string): string {
return this.fileService.signFileUrl({
url: avatarUrl,
private getImageUrlWithToken(
avatarFileId: string,
fileFolder: FileFolder,
workspaceId: string,
): string {
return this.fileUrlService.signFileByIdUrl({
fileId: avatarFileId,
workspaceId,
fileFolder,
});
}
@@ -553,9 +573,41 @@ export class SearchService {
return getLogoUrlFromDomainName(record.domainNamePrimaryLinkUrl) || '';
}
//TODO: Temporary solution before imageIdentifier refactor
if (flatObjectMetadata.nameSingular === 'person') {
const avatarFileId = (record.avatarFile as FileOutput[])?.[0]?.fileId;
if (!isDefined(avatarFileId)) {
return '';
}
return this.getImageUrlWithToken(
avatarFileId,
FileFolder.FilesField,
workspaceId,
);
}
if (flatObjectMetadata.nameSingular === 'workspaceMember') {
const avatarFileId = extractFileIdFromUrl(
record.avatarUrl,
FileFolder.CorePicture,
);
if (!isDefined(avatarFileId)) {
return '';
}
return this.getImageUrlWithToken(
avatarFileId,
FileFolder.CorePicture,
workspaceId,
);
}
return imageIdentifierField &&
isNonEmptyString(record[imageIdentifierField])
? this.getImageUrlWithToken(record[imageIdentifierField], workspaceId)
? this.getImageUrlWithToken(
record[imageIdentifierField],
FileFolder.FilesField,
workspaceId,
)
: '';
}
@@ -6,13 +6,13 @@ import { type DataSource, type Repository } from 'typeorm';
import { type ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@@ -126,7 +126,7 @@ describe('UserWorkspaceService', () => {
useValue: {},
},
{
provide: FileService,
provide: FileUrlService,
useValue: {},
},
{
@@ -19,8 +19,8 @@ import { type AvailableWorkspace } from 'src/engine/core-modules/auth/dto/availa
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -60,7 +60,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly userRoleService: UserRoleService,
private readonly fileCorePictureService: FileCorePictureService,
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
private readonly onboardingService: OnboardingService,
) {
super(userWorkspaceRepository);
@@ -503,12 +503,13 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
id: workspace.id,
displayName: workspace.displayName,
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
logo: isDefined(workspace.logoFileId)
? this.fileUrlService.signFileByIdUrl({
fileId: workspace.logoFileId,
workspaceId: workspace.id,
fileFolder: FileFolder.CorePicture,
})
: workspace.logo,
: '',
sso:
workspace.workspaceSSOIdentityProviders?.reduce(
(acc, identityProvider) =>
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { type DeletedWorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
import { type WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
@@ -15,6 +16,7 @@ import {
type WorkspaceMemberTimeFormatEnum,
type WorkspaceMemberWorkspaceEntity,
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { FileFolder } from 'twenty-shared/types';
export type ToWorkspaceMemberDtoArgs = {
workspaceMemberEntity: WorkspaceMemberWorkspaceEntity;
@@ -24,7 +26,7 @@ export type ToWorkspaceMemberDtoArgs = {
@Injectable()
export class WorkspaceMemberTranspiler {
constructor(private readonly fileService: FileService) {}
constructor(private readonly fileUrlService: FileUrlService) {}
generateSignedAvatarUrl({
workspaceId,
@@ -40,9 +42,19 @@ export class WorkspaceMemberTranspiler {
return '';
}
return this.fileService.signFileUrl({
url: workspaceMember.avatarUrl,
const fileId = extractFileIdFromUrl(
workspaceMember.avatarUrl,
FileFolder.CorePicture,
);
if (!isDefined(fileId)) {
return '';
}
return this.fileUrlService.signFileByIdUrl({
fileId,
workspaceId,
fileFolder: FileFolder.CorePicture,
});
}
@@ -9,18 +9,18 @@ import {
} from 'src/engine/core-modules/app-token/app-token.entity';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception';
import { RoleValidationService } from 'src/engine/metadata-modules/role-validation/services/role-validation.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { RoleValidationService } from 'src/engine/metadata-modules/role-validation/services/role-validation.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { WorkspaceInvitationService } from './workspace-invitation.service';
// To fix a circular dependency issue
@@ -113,9 +113,9 @@ describe('WorkspaceInvitationService', () => {
},
},
{
provide: FileService,
provide: FileUrlService,
useValue: {
signFileUrl: jest
signFileByIdUrl: jest
.fn()
.mockReturnValue('https://signed-url.com/logo.png'),
},
@@ -8,7 +8,7 @@ import { render } from '@react-email/render';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { SendInviteLinkEmail } from 'twenty-emails';
import { AppPath } from 'twenty-shared/types';
import { AppPath, FileFolder } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
@@ -22,7 +22,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
@@ -52,8 +52,8 @@ export class WorkspaceInvitationService {
private readonly onboardingService: OnboardingService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly i18nService: I18nService,
private readonly fileService: FileService,
private readonly throttlerService: ThrottlerService,
private readonly fileUrlService: FileUrlService,
) {}
async validatePersonalInvitation({
@@ -318,12 +318,13 @@ export class WorkspaceInvitationService {
link: link.toString(),
workspace: {
name: workspace.displayName,
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
logo: isDefined(workspace.logoFileId)
? this.fileUrlService.signFileByIdUrl({
fileId: workspace.logoFileId,
workspaceId: workspace.id,
fileFolder: FileFolder.CorePicture,
})
: workspace.logo,
: undefined,
},
sender: {
email: sender.userEmail,
@@ -15,9 +15,10 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util';
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto';
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
@@ -34,7 +35,6 @@ import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
import {
type AuthProvidersDTO,
@@ -391,15 +391,12 @@ export class WorkspaceResolver {
let workspaceLogoWithToken = '';
if (workspace.logo) {
try {
workspaceLogoWithToken = this.fileService.signFileUrl({
url: workspace.logo,
workspaceId: workspace.id,
});
} catch {
workspaceLogoWithToken = workspace.logo;
}
if (isDefined(workspace.logoFileId)) {
workspaceLogoWithToken = this.fileUrlService.signFileByIdUrl({
fileId: workspace.logoFileId,
workspaceId: workspace.id,
fileFolder: FileFolder.CorePicture,
});
}
return {
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { getRecordDisplayName } from 'src/engine/core-modules/record-crud/utils/get-record-display-name.util';
import { getRecordImageIdentifier } from 'src/engine/core-modules/record-crud/utils/get-record-image-identifier.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
@@ -14,13 +14,14 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util';
import { FileFolder } from 'twenty-shared/types';
@Injectable()
export class NavigationMenuItemRecordIdentifierService {
constructor(
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly fileService: FileService,
private readonly fileUrlService: FileUrlService,
) {}
async resolveRecordIdentifier({
@@ -124,10 +125,11 @@ export class NavigationMenuItemRecordIdentifierService {
record,
flatObjectMetadata: objectMetadata,
flatFieldMetadataMaps,
signUrl: (url: string) =>
this.fileService.signFileUrl({
url,
signUrl: (fileId: string, fileFolder: FileFolder) =>
this.fileUrlService.signFileByIdUrl({
fileId,
workspaceId,
fileFolder,
}),
});
@@ -11,6 +11,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
const ID_FIELD = 'id' as const;
const COMPANY_AVATAR_COLUMN = 'domainNamePrimaryLinkUrl' as const;
const PERSON_AVATAR_COLUMN = 'avatarFile' as const;
export const getMinimalSelectForRecordIdentifier = ({
flatObjectMetadata,
@@ -48,6 +49,9 @@ export const getMinimalSelectForRecordIdentifier = ({
if (flatObjectMetadata.nameSingular === 'company') {
selectColumns.push(COMPANY_AVATAR_COLUMN);
//TODO: Temporary solution before imageIdentifier refactor
} else if (flatObjectMetadata.nameSingular === 'person') {
selectColumns.push(PERSON_AVATAR_COLUMN);
} else if (isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) {
const imageField = findFlatEntityByIdInFlatEntityMaps({
flatEntityMaps: flatFieldMetadataMaps,