Files
twenty/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts
T
Félix Malfait 6a1b28bc12 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>
2026-06-18 17:56:14 +02:00

137 lines
4.8 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
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';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
@Injectable()
export class WorkspaceManagerService {
private readonly logger = new Logger(WorkspaceManagerService.name);
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly roleService: RoleService,
private readonly userRoleService: UserRoleService,
private readonly twentyStandardApplicationService: TwentyStandardApplicationService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectWorkspaceScopedRepository(RoleEntity)
private readonly roleRepository: WorkspaceScopedRepository<RoleEntity>,
private readonly applicationService: ApplicationService,
) {}
public async init({
workspace,
userId,
}: {
workspace: WorkspaceEntity;
userId: string;
}): Promise<void> {
const workspaceId = workspace.id;
const schemaCreationStart = performance.now();
const schemaName =
await this.workspaceDataSourceService.createWorkspaceDBSchema(
workspaceId,
);
const schemaCreationEnd = performance.now();
this.logger.log(
`Schema creation took ${schemaCreationEnd - schemaCreationStart}ms`,
);
const dataSourceMetadataCreationStart = performance.now();
await this.workspaceRepository.update(workspaceId, {
databaseSchema: schemaName,
});
await this.applicationService.createTwentyStandardApplication({
workspaceId,
});
await this.twentyStandardApplicationService.synchronizeTwentyStandardApplicationOrThrow(
{
workspaceId,
},
);
const dataSourceMetadataCreationEnd = performance.now();
this.logger.log(
`Metadata creation took ${dataSourceMetadataCreationEnd - dataSourceMetadataCreationStart}ms`,
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.setupDefaultRoles({
workspaceId,
userId,
workspaceCustomFlatApplication,
});
}
private async setupDefaultRoles({
userId,
workspaceId,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
userId: string;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const adminRole = await this.roleRepository.findOne(workspaceId, {
where: {
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
},
});
if (adminRole) {
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: { workspaceId, userId },
});
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [userWorkspace.id],
roleId: adminRole.id,
});
}
const existingMemberRole = await this.roleRepository.findOne(workspaceId, {
where: { label: MEMBER_ROLE_LABEL },
});
const memberRole =
existingMemberRole ??
(await this.roleService.createMemberRole({
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
}));
await this.workspaceRepository.update(workspaceId, {
defaultRoleId: memberRole.id,
});
}
}