Add default role to workspace (#10444)

## Context
Adding a defaultRole to each workspace, this role will be automatically
added when a member joins a workspace via invite link or public link
(seeds work differently though).
Took the occasion to refactor a bit the frontend components, splitting
them in smaller components for more readability.

## Test
<img width="948" alt="Screenshot 2025-02-24 at 14 54 02"
src="https://github.com/user-attachments/assets/13ef1452-d3c9-4385-940c-2ced0f0b05ef"
/>
This commit is contained in:
Weiko
2025-02-25 11:26:35 +01:00
committed by GitHub
parent a1eea40cf7
commit 0220672fa9
29 changed files with 538 additions and 273 deletions

View File

@ -15,9 +15,9 @@ import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.ser
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
@ -46,6 +46,7 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
@ -91,6 +92,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
GuardRedirectModule,
HealthModule,
PermissionsModule,
UserRoleModule,
],
controllers: [
GoogleAuthController,

View File

@ -6,6 +6,10 @@ import { WorkspaceActivationStatus } from 'twenty-shared';
import { Repository } from 'typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import {
AuthProviderWithPasswordType,
@ -14,18 +18,16 @@ import {
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
jest.mock('src/utils/image', () => {
return {
@ -42,6 +44,8 @@ describe('SignInUpService', () => {
let userWorkspaceService: UserWorkspaceService;
let environmentService: EnvironmentService;
let domainManagerService: DomainManagerService;
let userRoleService: UserRoleService;
let featureFlagService: FeatureFlagService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -117,6 +121,18 @@ describe('SignInUpService', () => {
generateSubdomain: jest.fn(),
},
},
{
provide: UserRoleService,
useValue: {
assignRoleToUserWorkspace: jest.fn(),
},
},
{
provide: FeatureFlagService,
useValue: {
isFeatureEnabled: jest.fn(),
},
},
],
}).compile();
@ -132,6 +148,8 @@ describe('SignInUpService', () => {
environmentService = module.get<EnvironmentService>(EnvironmentService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
userRoleService = module.get<UserRoleService>(UserRoleService);
featureFlagService = module.get<FeatureFlagService>(FeatureFlagService);
});
it('should handle signInUp with valid personal invitation', async () => {
@ -161,9 +179,10 @@ describe('SignInUpService', () => {
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
.mockResolvedValue(undefined);
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
const result = await service.signInUp(params);
@ -198,9 +217,10 @@ describe('SignInUpService', () => {
},
};
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
const result = await service.signInUp(params);
@ -271,9 +291,10 @@ describe('SignInUpService', () => {
};
jest.spyOn(environmentService, 'get').mockReturnValue(false);
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
jest
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
.mockResolvedValue({} as UserWorkspace);
@ -312,4 +333,38 @@ describe('SignInUpService', () => {
),
);
});
it('should assign default role when permissions are enabled', async () => {
const params: SignInUpBaseParams &
ExistingUserOrPartialUserWithPicture &
AuthProviderWithPasswordType = {
workspace: {
id: 'workspaceId',
defaultRoleId: 'defaultRoleId',
activationStatus: WorkspaceActivationStatus.ACTIVE,
} as Workspace,
authParams: { provider: 'password', password: 'validPassword' },
userData: {
type: 'existingUser',
existingUser: { email: 'test@example.com' } as User,
},
};
const mockUserWorkspace = { id: 'userWorkspaceId' };
jest.spyOn(featureFlagService, 'isFeatureEnabled').mockResolvedValue(true);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: mockUserWorkspace as UserWorkspace,
});
await service.signInUp(params);
expect(params.workspace).toBeDefined();
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
workspaceId: params.workspace!.id,
userWorkspaceId: mockUserWorkspace.id,
roleId: params.workspace!.defaultRoleId,
});
});
});

View File

@ -31,6 +31,8 @@ import {
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -38,6 +40,7 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { getImageBufferFromUrl } from 'src/utils/image';
import { isWorkEmail } from 'src/utils/is-work-email';
@ -58,6 +61,8 @@ export class SignInUpService {
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
private readonly userService: UserService,
private readonly userRoleService: UserRoleService,
private readonly featureFlagService: FeatureFlagService,
) {}
async computeParamsForNewUser(
@ -256,10 +261,11 @@ export class SignInUpService {
)
: params.userData.existingUser;
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
currentUser,
params.workspace,
);
const { user: updatedUser, userWorkspace } =
await this.userWorkspaceService.addUserToWorkspace(
currentUser,
params.workspace,
);
const user = Object.assign(currentUser, updatedUser);
@ -267,6 +273,19 @@ export class SignInUpService {
await this.activateOnboardingForUser(user, params.workspace);
}
const isPermissionsEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
params.workspace.id,
);
if (isPermissionsEnabled && params.workspace.defaultRoleId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId: params.workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: params.workspace.defaultRoleId,
});
}
return user;
}