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:
+116
-5
@@ -1,6 +1,7 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import FileType from 'file-type';
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
@@ -20,6 +21,7 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
@@ -27,6 +29,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-18:migrate-workspace-pictures',
|
||||
@@ -44,6 +47,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
@@ -135,14 +139,60 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
|
||||
return;
|
||||
}
|
||||
|
||||
const isInWorkspaceLogo = workspace.logo.startsWith(
|
||||
FileFolder.WorkspaceLogo,
|
||||
);
|
||||
|
||||
const isTwentyIconLogo = workspace.logo.includes('twenty-icons');
|
||||
|
||||
if (!isTwentyIconLogo && !isInWorkspaceLogo) {
|
||||
this.logger.log(
|
||||
`Workspace logo is not a twenty icon or a workspace logo, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Migrating workspace logo for workspace ${workspaceId}: ${workspace.logo}`,
|
||||
);
|
||||
|
||||
if (isInWorkspaceLogo) {
|
||||
await this.migrateWorkspaceLogoFromWorkspaceFolder({
|
||||
workspaceId,
|
||||
logoPath: workspace.logo,
|
||||
isDryRun,
|
||||
workspaceCustomFlatApplication,
|
||||
fileRepository,
|
||||
});
|
||||
}
|
||||
|
||||
if (isTwentyIconLogo) {
|
||||
await this.migrateWorkspaceLogoFromTwentyIcons({
|
||||
workspaceId,
|
||||
logoUrl: workspace.logo,
|
||||
isDryRun,
|
||||
workspaceCustomFlatApplication,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateWorkspaceLogoFromWorkspaceFolder({
|
||||
workspaceId,
|
||||
logoPath,
|
||||
isDryRun,
|
||||
workspaceCustomFlatApplication,
|
||||
fileRepository,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
logoPath: string;
|
||||
isDryRun: boolean;
|
||||
workspaceCustomFlatApplication: FlatApplication;
|
||||
fileRepository: Repository<FileEntity>;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
|
||||
workspace.logo,
|
||||
);
|
||||
const { type: fileExtension } =
|
||||
extractFolderPathFilenameAndTypeOrThrow(logoPath);
|
||||
|
||||
const fileId = v4();
|
||||
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
|
||||
@@ -152,7 +202,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
|
||||
await this.fileStorageService.copyLegacy({
|
||||
from: {
|
||||
folderPath: `workspace-${workspaceId}`,
|
||||
filename: workspace.logo,
|
||||
filename: logoPath,
|
||||
},
|
||||
to: {
|
||||
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
|
||||
@@ -181,7 +231,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Migrated workspace logo for workspace ${workspaceId} (${workspace.logo} -> ${newResourcePath})`,
|
||||
`Migrated workspace logo for workspace ${workspaceId} (${logoPath} -> ${newResourcePath})`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -191,6 +241,67 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateWorkspaceLogoFromTwentyIcons({
|
||||
workspaceId,
|
||||
logoUrl,
|
||||
isDryRun,
|
||||
workspaceCustomFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
logoUrl: string;
|
||||
isDryRun: boolean;
|
||||
workspaceCustomFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const httpClient = this.secureHttpClientService.getHttpClient();
|
||||
const buffer = await getImageBufferFromUrl(logoUrl, httpClient);
|
||||
|
||||
const type = await FileType.fromBuffer(buffer);
|
||||
|
||||
if (!isDefined(type) || !type.mime.startsWith('image/')) {
|
||||
this.logger.warn(
|
||||
`Unable to detect image type for workspace logo ${logoUrl}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = v4();
|
||||
const newFilename = `${fileId}.${type.ext}`;
|
||||
const newResourcePath = `${newFilename}`;
|
||||
|
||||
if (!isDryRun) {
|
||||
const fileEntity = await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
resourcePath: newResourcePath,
|
||||
sourceFile: buffer,
|
||||
mimeType: type.mime,
|
||||
fileId,
|
||||
settings: {
|
||||
isTemporaryFile: false,
|
||||
toDelete: false,
|
||||
},
|
||||
});
|
||||
|
||||
await this.workspaceRepository.update(
|
||||
{ id: workspaceId },
|
||||
{ logoFileId: fileEntity.id },
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Migrated workspace logo from twenty-icons for workspace ${workspaceId} (${logoUrl} -> ${FileFolder.CorePicture}/${newResourcePath})`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to migrate workspace logo from twenty-icons for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateWorkspaceMemberAvatars({
|
||||
workspaceId,
|
||||
isDryRun,
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
@@ -51,6 +52,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
FileModule,
|
||||
UserWorkspaceModule,
|
||||
WorkspaceMigrationModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
providers: [
|
||||
MigratePersonAvatarFilesCommand,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+15
@@ -111,6 +111,21 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
+3
@@ -5,4 +5,7 @@ export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_ENABLED,
|
||||
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
+79
-46
@@ -1,10 +1,20 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import request from 'supertest';
|
||||
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
const uploadWorkspaceLogoMutation = gql`
|
||||
mutation UploadWorkspaceLogo($file: Upload!) {
|
||||
uploadWorkspaceLogo(file: $file) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('Security permissions', () => {
|
||||
@@ -506,62 +516,85 @@ describe('Security permissions', () => {
|
||||
});
|
||||
|
||||
describe('logo update', () => {
|
||||
beforeAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should update workspace logo when user has workspace settings permission', async () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
mutation updateWorkspace {
|
||||
updateWorkspace(data: { logo: "new-logo" }) {
|
||||
id
|
||||
const testImageBuffer = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
const uploadResponse = await makeMetadataAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceLogoMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: testImageBuffer,
|
||||
filename: 'test-logo.png',
|
||||
contentType: 'image/png',
|
||||
},
|
||||
APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(uploadResponse.status).toBe(200);
|
||||
expect(uploadResponse.body.errors).toBeUndefined();
|
||||
expect(uploadResponse.body.data).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo.id).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo.url).toBeDefined();
|
||||
|
||||
const getWorkspaceQuery = gql`
|
||||
query GetWorkspace {
|
||||
currentWorkspace {
|
||||
logo
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
`;
|
||||
|
||||
return client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.updateWorkspace;
|
||||
const workspaceResponse = await makeMetadataAPIRequest({
|
||||
query: getWorkspaceQuery,
|
||||
});
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.logo).toContain('new-logo');
|
||||
});
|
||||
expect(workspaceResponse.body.data.currentWorkspace.logo).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (member role)', async () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
mutation updateWorkspace {
|
||||
updateWorkspace(data: { logo: "another-new-logo" }) {
|
||||
id
|
||||
logo
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const testImageBuffer = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
await client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeNull();
|
||||
expect(res.body.errors).toBeDefined();
|
||||
expect(res.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(res.body.errors[0].extensions.code).toBe(
|
||||
ErrorCode.FORBIDDEN,
|
||||
);
|
||||
});
|
||||
const response = await makeMetadataAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceLogoMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: testImageBuffer,
|
||||
filename: 'test-logo.png',
|
||||
contentType: 'image/png',
|
||||
},
|
||||
APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data).toBeNull();
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(
|
||||
ErrorCode.FORBIDDEN,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+81
-48
@@ -1,5 +1,6 @@
|
||||
import gql from 'graphql-tag';
|
||||
import request from 'supertest';
|
||||
import { makeMetadataAPIRequestWithFileUpload } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-file-upload.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
@@ -7,6 +8,15 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
const uploadWorkspaceLogoMutation = gql`
|
||||
mutation UploadWorkspaceLogo($file: Upload!) {
|
||||
uploadWorkspaceLogo(file: $file) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('workspace permissions', () => {
|
||||
@@ -266,62 +276,85 @@ describe('workspace permissions', () => {
|
||||
});
|
||||
|
||||
describe('logo update', () => {
|
||||
beforeAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should update workspace logo when user has workspace settings permission', async () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
mutation updateWorkspace {
|
||||
updateWorkspace(data: { logo: "new-logo" }) {
|
||||
id
|
||||
logo
|
||||
const testImageBuffer = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
const uploadResponse = await makeMetadataAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceLogoMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: testImageBuffer,
|
||||
filename: 'test-logo.png',
|
||||
contentType: 'image/png',
|
||||
},
|
||||
APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(uploadResponse.status).toBe(200);
|
||||
expect(uploadResponse.body.errors).toBeUndefined();
|
||||
expect(uploadResponse.body.data).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo.id).toBeDefined();
|
||||
expect(uploadResponse.body.data.uploadWorkspaceLogo.url).toBeDefined();
|
||||
|
||||
const getWorkspaceQuery = gql`
|
||||
query GetWorkspace {
|
||||
currentWorkspace {
|
||||
logo
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
`;
|
||||
|
||||
return client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.errors).toBeUndefined();
|
||||
})
|
||||
.expect((res) => {
|
||||
const data = res.body.data.updateWorkspace;
|
||||
const workspaceResponse = await makeMetadataAPIRequest({
|
||||
query: getWorkspaceQuery,
|
||||
});
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.logo).toContain('new-logo');
|
||||
});
|
||||
expect(workspaceResponse.body.data.currentWorkspace.logo).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (member role)', async () => {
|
||||
const queryData = {
|
||||
query: `
|
||||
mutation updateWorkspace {
|
||||
updateWorkspace(data: { logo: "another-new-logo" }) {
|
||||
id
|
||||
logo
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const testImageBuffer = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
await client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
|
||||
.send(queryData)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeNull();
|
||||
expect(res.body.errors).toBeDefined();
|
||||
expect(res.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(res.body.errors[0].extensions.code).toBe(
|
||||
ErrorCode.FORBIDDEN,
|
||||
);
|
||||
});
|
||||
const response = await makeMetadataAPIRequestWithFileUpload(
|
||||
{
|
||||
query: uploadWorkspaceLogoMutation,
|
||||
variables: { file: null },
|
||||
},
|
||||
{
|
||||
field: 'file',
|
||||
buffer: testImageBuffer,
|
||||
filename: 'test-logo.png',
|
||||
contentType: 'image/png',
|
||||
},
|
||||
APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data).toBeNull();
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(
|
||||
ErrorCode.FORBIDDEN,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user