feat(auth): collect the workspace logo on the sign-up creation step (#21723)

## What & why

A single, consistent **workspace-creation step** for both
multi-workspace and single-workspace self-host — collecting **name +
logo** (and the **subdomain** in multi-workspace) — which **removes the
duplicate name/logo prompt** that previously reappeared on the workspace
subdomain (reported after #21641).

## Changes

**One creation form for both modes**
- With 0 workspaces, both multi-workspace and single-workspace route to
the shared `SignInUpWorkspaceCreationForm`; `SignInUp` renders it for
the `WorkspaceCreation` step regardless of domain/scope.
- The subdomain field shows only in multi-workspace; single-workspace
keeps its fixed address.

**Logo on the creation step**
- New scoped `uploadNewWorkspaceLogo(workspaceId, file)` mutation: the
creator sets a logo on their just-created `PENDING_CREATION` workspace
via the workspace-agnostic token (membership enforced — only the creator
is a member at that point), reusing `uploadWorkspacePicture`. Upload
size is capped via `settings.storage.maxFileSize` (also applied to the
existing logo / profile-picture uploads).
- The picked file is held locally (object-URL preview, revoked on
unmount) and uploaded right after creation (non-fatal on failure).

**Onboarding step → pure activation loader**
- The old "Create your workspace" form (name + logo) is removed. The
onboarding step now activates the pending workspace on mount and shows
the loader, with a **Retry** action on failure.

## Testing
- typecheck (front + server) ; oxlint + oxfmt clean on changed files 
- Unit tests: `auth.resolver.spec`, `useWorkspaceSubdomainField`,
`SignInUpWorkspaceCreationForm` (multi + single-workspace), `useAuth` 
- Metadata GraphQL + `twenty-client-sdk` schema regenerated.

Follow-up to #21641.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Xw37hR5seiCyWnppG9z4op

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-06-18 17:56:14 +02:00
committed by GitHub
parent 880e7144ec
commit 6a1b28bc12
55 changed files with 1136 additions and 569 deletions
@@ -320,6 +320,15 @@ export class ApplicationService {
},
queryRunner?: QueryRunner,
) {
const existingApplication = await this.findByUniversalIdentifier({
universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
workspaceId,
});
if (isDefined(existingApplication)) {
return existingApplication;
}
const defaultPackageFields = await getDefaultApplicationPackageFields();
const twentyStandardApplication = await this.create(
@@ -15,6 +15,7 @@ import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomai
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { TwoFactorAuthenticationService } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.service';
@@ -77,6 +78,10 @@ describe('AuthResolver', () => {
provide: SubdomainManagerService,
useValue: {},
},
{
provide: FileCorePictureService,
useValue: {},
},
{
provide: UserWorkspaceService,
useValue: {},
@@ -2,6 +2,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Context, Mutation, Query } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import bytes from 'bytes';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import omit from 'lodash.omit';
import { PermissionFlagType } from 'twenty-shared/constants';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
@@ -9,7 +11,10 @@ import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { settings } from 'src/engine/constants/settings';
import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service';
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
import { EventLogEmitterService } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.service';
@@ -57,6 +62,8 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac
import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util';
import { EmailVerificationTrigger } from 'src/engine/core-modules/email-verification/email-verification.constants';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
@@ -84,6 +91,7 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { UserAuthGuard } from 'src/engine/guards/user-auth.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';
import { ApiKeyToken } from './dto/api-key-token.dto';
import { AuthToken } from './dto/auth-token.dto';
@@ -136,6 +144,7 @@ export class AuthResolver {
private readonly eventLogEmitterService: EventLogEmitterService,
private readonly impersonationAuthorizationService: ImpersonationAuthorizationService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly fileCorePictureService: FileCorePictureService,
) {}
@UseGuards(CaptchaGuard, PublicEndpointGuard, NoPermissionGuard)
@@ -557,6 +566,34 @@ export class AuthResolver {
};
}
@Mutation(() => FileWithSignedUrlDTO)
@UseGuards(UserAuthGuard, NoPermissionGuard)
async uploadNewWorkspaceLogo(
@AuthUser() currentUser: AuthContextUser,
@Args('workspaceId') workspaceId: string,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<FileWithSignedUrlDTO> {
const workspace =
await this.fileCorePictureService.getPendingWorkspaceForLogoUploadOrThrow(
{
userId: currentUser.id,
workspaceId,
},
);
const buffer = await streamToBuffer(
createReadStream(),
bytes(settings.storage.maxFileSize) ?? undefined,
);
return this.fileCorePictureService.uploadWorkspacePicture({
file: buffer,
filename,
workspace,
});
}
@Mutation(() => TransientTokenDTO)
@UseGuards(UserAuthGuard, NoPermissionGuard)
async generateTransientToken(
@@ -951,7 +951,6 @@ export class AuthService {
workspaceInviteHash,
workspaceId,
billingCheckoutSessionState,
action,
locale,
returnToPath,
}: MicrosoftRequest['user'] | GoogleRequest['user'],
@@ -964,11 +963,7 @@ export class AuthService {
// Route SSO sign-ins through the same create-or-select flow as credentials
// instead of landing straight on a workspace subdomain.
if (
!workspaceId &&
!workspaceInviteHash &&
action === 'list-available-workspaces'
) {
if (!workspaceId && !workspaceInviteHash) {
const user =
existingUser ??
(await this.signInUpService.signUpWithoutWorkspace(
@@ -1010,15 +1005,12 @@ export class AuthService {
return url.toString();
}
const currentWorkspace =
action === 'create-new-workspace'
? undefined
: await this.findWorkspaceForSignInUp({
workspaceId,
workspaceInviteHash,
email,
authProvider,
});
const currentWorkspace = await this.findWorkspaceForSignInUp({
workspaceId,
workspaceInviteHash,
email,
authProvider,
});
try {
const invitation =
@@ -516,6 +516,18 @@ export class SignInUpService {
await this.assertWorkspaceCreationAllowed(userData);
const displayName = options?.displayName?.trim();
if (!displayName) {
throw new AuthException(
'Workspace name is required',
AuthExceptionCode.INVALID_INPUT,
{
userFriendlyMessage: msg`Workspace name is required`,
},
);
}
const requestedSubdomain = options?.subdomain;
if (isDefined(requestedSubdomain)) {
@@ -544,7 +556,7 @@ export class SignInUpService {
isWorkEmailFound ? { userEmail: email } : {},
),
workspaceCustomApplicationId,
displayName: options?.displayName ?? '',
displayName,
inviteHash: v4(),
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
});
@@ -8,13 +8,18 @@ import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-p
import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository';
@Module({
imports: [
JwtModule,
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity]),
TypeOrmModule.forFeature([
FileEntity,
WorkspaceEntity,
UserWorkspaceEntity,
]),
PermissionsModule,
FileStorageModule,
FileUrlModule,
@@ -1,12 +1,14 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation } from '@nestjs/graphql';
import bytes from 'bytes';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { settings } from 'src/engine/constants/settings';
import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto';
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
@@ -41,7 +43,10 @@ export class FileCorePictureResolver {
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<FileWithSignedUrlDTO> {
const buffer = await streamToBuffer(createReadStream());
const buffer = await streamToBuffer(
createReadStream(),
bytes(settings.storage.maxFileSize) ?? undefined,
);
return await this.fileCorePictureService.uploadWorkspacePicture({
file: buffer,
@@ -57,7 +62,10 @@ export class FileCorePictureResolver {
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload,
): Promise<FileWithSignedUrlDTO> {
const buffer = await streamToBuffer(createReadStream());
const buffer = await streamToBuffer(
createReadStream(),
bytes(settings.storage.maxFileSize) ?? undefined,
);
return await this.fileCorePictureService.uploadWorkspaceMemberProfilePicture(
{
@@ -8,6 +8,7 @@ import { FileTypeParser } from 'file-type';
import { detectPdf } from '@file-type/pdf';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Like, type QueryRunner, Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -15,6 +16,10 @@ import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
@@ -22,6 +27,7 @@ import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.s
import { extractFileInfoOrThrow } from 'src/engine/core-modules/file/utils/extract-file-info-or-throw.utils';
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
@@ -35,6 +41,8 @@ export class FileCorePictureService {
private readonly fileStorageService: FileStorageService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
@InjectWorkspaceScopedRepository(FileEntity)
private readonly fileRepository: WorkspaceScopedRepository<FileEntity>,
private readonly fileUrlService: FileUrlService,
@@ -137,6 +145,35 @@ export class FileCorePictureService {
};
}
async getPendingWorkspaceForLogoUploadOrThrow({
userId,
workspaceId,
}: {
userId: string;
workspaceId: string;
}): Promise<WorkspaceEntity> {
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
});
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId, workspaceId },
});
if (
!isDefined(workspace) ||
!isDefined(userWorkspace) ||
workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION
) {
throw new AuthException(
'Cannot set a logo for this workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
return workspace;
}
async uploadWorkspaceMemberProfilePicture({
file,
filename,
@@ -39,7 +39,9 @@ export class OnboardingService {
private isWorkspaceActivationPending(workspace: WorkspaceEntity) {
return (
workspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION
workspace.activationStatus ===
WorkspaceActivationStatus.PENDING_CREATION ||
workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION
);
}
@@ -166,6 +166,14 @@ export class UpgradeMigrationService {
? queryRunner.manager.getRepository(UpgradeMigrationEntity)
: this.upgradeMigrationRepository;
const existingInitialMigration = await repository.findOne({
where: { name, attempt: 1, workspaceId, isInitial: true },
});
if (isDefined(existingInitialMigration)) {
return;
}
await repository.save({
name,
status,
@@ -275,7 +275,10 @@ describe('UserWorkspaceService', () => {
];
const workspaceMemberRepository = {
insert: jest.fn(),
find: jest.fn().mockResolvedValue(workspaceMember),
find: jest
.fn()
.mockResolvedValueOnce([])
.mockResolvedValue(workspaceMember),
};
jest
@@ -304,6 +307,31 @@ describe('UserWorkspaceService', () => {
avatarUrl: 'userWorkspace-avatar-url',
});
});
it('should not create a workspace member when one already exists', async () => {
const workspaceId = 'workspace-id';
const user = {
id: 'user-id',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
locale: 'en',
} as unknown as AuthContextUser;
const workspaceMemberRepository = {
insert: jest.fn(),
find: jest
.fn()
.mockResolvedValue([{ id: 'existing-member-id', userId: 'user-id' }]),
};
jest
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
await service.createWorkspaceMember(workspaceId, user);
expect(workspaceMemberRepository.insert).not.toHaveBeenCalled();
});
});
describe('addUserToWorkspaceIfUserNotInWorkspace', () => {
@@ -149,6 +149,14 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
{ shouldBypassPermissionChecks: true },
);
const existingWorkspaceMembers = await workspaceMemberRepository.find({
where: { userId: user.id },
});
if (existingWorkspaceMembers.length > 0) {
return;
}
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: {
userId: user.id,
@@ -4,7 +4,13 @@ import { IsOptional, IsString } from 'class-validator';
@InputType()
export class ActivateWorkspaceInput {
@Field({ nullable: true })
// The workspace name is set at creation (signUpInNewWorkspace). This field is
// ignored during activation and kept only for backward compatibility.
@Field({
nullable: true,
description:
'Deprecated: the workspace name is set at creation (signUpInNewWorkspace) and this field is ignored during activation. Kept for backward compatibility.',
})
@IsString()
@IsOptional()
displayName?: string;
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import assert from 'assert';
@@ -8,7 +8,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, QueryRunner, Repository } from 'typeorm';
import { DataSource, LessThan, QueryRunner, Repository } from 'typeorm';
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
@@ -42,7 +42,6 @@ import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/se
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { type ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
WorkspaceException,
@@ -76,6 +75,12 @@ import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
// A workspace stuck in ONGOING_CREATION for longer than this is treated as a
// crashed activation (the process died before the catch block could reset it to
// PENDING_CREATION) and may be retried. It is far longer than a real activation
// takes, so a genuinely in-progress activation is never reclaimed.
const WORKSPACE_ACTIVATION_STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1000;
@Injectable()
// oxlint-disable-next-line twenty/inject-workspace-repository
export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
@@ -324,57 +329,90 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
return updatedWorkspace;
}
async activateWorkspace(
user: AuthContextUser,
workspace: WorkspaceEntity,
data: ActivateWorkspaceInput,
) {
if (!data.displayName || !data.displayName.length) {
throw new BadRequestException("'displayName' not provided");
async activateWorkspace(user: AuthContextUser, workspace: WorkspaceEntity) {
// Acquire the activation lock by atomically moving the workspace to
// ONGOING_CREATION. First try the normal case (PENDING_CREATION). If nothing
// matches, the workspace may be stuck in ONGOING_CREATION from a prior
// attempt that was killed before the catch block could reset it — reclaim it,
// but only once the lock is stale, so a genuinely concurrent activation is
// never interrupted. Postgres row locking serializes concurrent reclaims, and
// repository.update bumps updatedAt, so a reclaimed lock is immediately fresh.
let activationLockResult = await this.workspaceRepository.update(
{
id: workspace.id,
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
},
{ activationStatus: WorkspaceActivationStatus.ONGOING_CREATION },
);
if ((activationLockResult.affected ?? 0) === 0) {
activationLockResult = await this.workspaceRepository.update(
{
id: workspace.id,
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
updatedAt: LessThan(
new Date(Date.now() - WORKSPACE_ACTIVATION_STALE_LOCK_TIMEOUT_MS),
),
},
{ activationStatus: WorkspaceActivationStatus.ONGOING_CREATION },
);
}
if (
workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION
) {
if ((activationLockResult.affected ?? 0) === 0) {
// Activation is idempotent for the terminal state: if a prior attempt
// already completed (e.g. the client lost the response and retried),
// return the active workspace instead of failing. Otherwise another
// activation is genuinely in progress and must not be interrupted.
const existingWorkspace = await this.workspaceRepository.findOneBy({
id: workspace.id,
});
if (
existingWorkspace?.activationStatus === WorkspaceActivationStatus.ACTIVE
) {
return existingWorkspace;
}
throw new Error('Workspace is already being created');
}
if (
workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION
) {
throw new Error('Workspace is not pending creation');
}
await this.workspaceRepository.update(workspace.id, {
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
});
await this.coreEntityCacheService.invalidate(
'workspaceEntity',
workspace.id,
);
await this.workspaceManagerService.init({
workspace,
userId: user.id,
});
try {
await this.workspaceManagerService.init({
workspace,
userId: user.id,
});
await this.featureFlagService.enableFeatureFlags(
DEFAULT_FEATURE_FLAGS,
workspace.id,
);
await this.featureFlagService.enableFeatureFlags(
DEFAULT_FEATURE_FLAGS,
workspace.id,
);
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
await this.prefillCreatedWorkspaceRecords({
workspaceId: workspace.id,
schemaName: getWorkspaceSchemaName(workspace.id),
});
await this.prefillCreatedWorkspaceRecords({
workspaceId: workspace.id,
schemaName: getWorkspaceSchemaName(workspace.id),
});
await this.activateAndInitializeUpgradeState({
workspaceId: workspace.id,
displayName: data.displayName,
});
await this.activateAndInitializeUpgradeState({
workspaceId: workspace.id,
});
} catch (error) {
await this.workspaceRepository.update(workspace.id, {
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
});
await this.coreEntityCacheService.invalidate(
'workspaceEntity',
workspace.id,
);
throw error;
}
try {
await this.sdkClientGenerationService.enqueueSdkClientGenerationForWorkspace(
@@ -399,11 +437,9 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
}
private async activateAndInitializeUpgradeState({
displayName,
workspaceId,
}: {
workspaceId: string;
displayName: string;
}): Promise<void> {
const lastAttemptedInstanceCommand =
await this.upgradeMigrationService.getLastAttemptedInstanceCommandOrThrow();
@@ -423,7 +459,6 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
try {
await queryRunner.manager.update(WorkspaceEntity, workspaceId, {
displayName,
activationStatus: WorkspaceActivationStatus.ACTIVE,
});
@@ -115,11 +115,14 @@ export class WorkspaceResolver {
@Mutation(() => WorkspaceEntity)
@UseGuards(UserAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
async activateWorkspace(
@Args('data') data: ActivateWorkspaceInput,
// Deprecated: the workspace name is set at creation. This argument is kept
// for backward compatibility (removing it would be a breaking schema change)
// but is ignored.
@Args('data') _data: ActivateWorkspaceInput,
@AuthUser() user: AuthContextUser,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
return await this.workspaceService.activateWorkspace(user, workspace, data);
return await this.workspaceService.activateWorkspace(user, workspace);
}
@Mutation(() => WorkspaceEntity)