Fixes - Workspace logo migration (#18035)
- Update migration command to handle case where workspace logo is originated from workspace email and point to twenty-icons.com - Update same logic for new workspaces - Add feature-flag for all newly created workspaces
This commit is contained in:
@@ -38,13 +38,14 @@ import { EmailVerificationModule } from 'src/engine/core-modules/email-verificat
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.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 { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { TwoFactorAuthenticationMethodEntity } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity';
|
||||
import { TwoFactorAuthenticationModule } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.module';
|
||||
@@ -117,6 +118,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
SecureHttpClientModule,
|
||||
FileModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
|
||||
+3
@@ -97,6 +97,9 @@ const createSignInUpServiceForTests = () => {
|
||||
{
|
||||
createWorkspaceCustomApplication: jest.fn(),
|
||||
} as any,
|
||||
{
|
||||
uploadWorkspaceLogoFromUrl: jest.fn(),
|
||||
} as any,
|
||||
{
|
||||
createQueryRunner: jest.fn(() => queryRunnerMock),
|
||||
} as any,
|
||||
|
||||
+23
-15
@@ -3,6 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository, type DataSource, type QueryRunner } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
type SignInUpNewUserPayload,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
|
||||
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
@@ -62,6 +64,7 @@ export class SignInUpService {
|
||||
private readonly metricsService: MetricsService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly fileCorePictureService: FileCorePictureService,
|
||||
@InjectDataSource()
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
@@ -463,21 +466,7 @@ export class SignInUpService {
|
||||
|
||||
const shouldGrantServerAdmin = !(await this.hasServerAdmin());
|
||||
|
||||
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
|
||||
const isLogoUrlValid = async () => {
|
||||
try {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
const response = await httpClient.get(logoUrl, { timeout: 600 });
|
||||
|
||||
return response.status === 200;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isWorkEmailFound = isWorkEmail(email);
|
||||
const logo =
|
||||
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
|
||||
const workspaceId = v4();
|
||||
const workspaceCustomApplicationId = v4();
|
||||
@@ -496,7 +485,6 @@ export class SignInUpService {
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
logo,
|
||||
});
|
||||
|
||||
const workspace = await queryRunner.manager.save(
|
||||
@@ -513,6 +501,26 @@ export class SignInUpService {
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (isWorkEmailFound) {
|
||||
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
|
||||
const logoFile =
|
||||
await this.fileCorePictureService.uploadWorkspaceLogoFromUrl({
|
||||
imageUrl: logoUrl,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
customApplication.universalIdentifier,
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
if (isDefined(logoFile)) {
|
||||
await queryRunner.manager.update(
|
||||
WorkspaceEntity,
|
||||
{ id: workspaceId },
|
||||
{ logoFileId: logoFile.id },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
|
||||
+5
-1
@@ -16,11 +16,15 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
|
||||
@UseFilters(
|
||||
PermissionsGraphqlApiExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
)
|
||||
@MetadataResolver()
|
||||
export class FileCorePictureResolver {
|
||||
constructor(
|
||||
|
||||
+51
-9
@@ -64,7 +64,7 @@ export class FileCorePictureService {
|
||||
|
||||
const savedFile = await this.fileStorageService.writeFile({
|
||||
sourceFile: sanitizedFile,
|
||||
resourcePath: `${FileFolder.CorePicture}/${finalName}`,
|
||||
resourcePath: finalName,
|
||||
mimeType,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
applicationUniversalIdentifier: universalIdentifier,
|
||||
@@ -182,6 +182,25 @@ export class FileCorePictureService {
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchImageBufferFromUrl(
|
||||
imageUrl: string,
|
||||
): Promise<{ buffer: Buffer; extension: string } | undefined> {
|
||||
try {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
const buffer = await getImageBufferFromUrl(imageUrl, httpClient);
|
||||
|
||||
const type = await FileType.fromBuffer(buffer);
|
||||
|
||||
if (!isDefined(type) || !type.mime.startsWith('image/')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { buffer, extension: type.ext };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadWorkspaceMemberProfilePictureFromUrl({
|
||||
imageUrl,
|
||||
workspaceId,
|
||||
@@ -193,18 +212,41 @@ export class FileCorePictureService {
|
||||
applicationUniversalIdentifier?: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<FileWithSignedUrlDto | undefined> {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
const buffer = await getImageBufferFromUrl(imageUrl, httpClient);
|
||||
const imageData = await this.fetchImageBufferFromUrl(imageUrl);
|
||||
|
||||
const type = await FileType.fromBuffer(buffer);
|
||||
|
||||
if (!isDefined(type) || !type.mime.startsWith('image/')) {
|
||||
return;
|
||||
if (!isDefined(imageData)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.uploadWorkspaceMemberProfilePicture({
|
||||
file: buffer,
|
||||
filename: `avatar.${type.ext}`,
|
||||
file: imageData.buffer,
|
||||
filename: `avatar.${imageData.extension}`,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
|
||||
async uploadWorkspaceLogoFromUrl({
|
||||
imageUrl,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
queryRunner,
|
||||
}: {
|
||||
imageUrl: string;
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier?: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<FileEntity | undefined> {
|
||||
const imageData = await this.fetchImageBufferFromUrl(imageUrl);
|
||||
|
||||
if (!isDefined(imageData)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.uploadCorePicture({
|
||||
file: imageData.buffer,
|
||||
filename: `logo.${imageData.extension}`,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
queryRunner,
|
||||
|
||||
Reference in New Issue
Block a user