diff --git a/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts deleted file mode 100644 index d4d916db8..000000000 --- a/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -@Injectable() -export class DeletedWorkspaceMemberTranspiler { - constructor(private readonly fileService: FileService) {} - - generateSignedAvatarUrl({ - workspaceId, - workspaceMember, - }: { - workspaceMember: Pick; - workspaceId: string; - }): string { - return this.fileService.signFileUrl({ - url: workspaceMember.avatarUrl, - workspaceId, - }); - } - - toDeletedWorkspaceMemberDto( - workspaceMember: WorkspaceMemberWorkspaceEntity, - userWorkspaceId?: string, - ): DeletedWorkspaceMember { - const { - avatarUrl: avatarUrlFromEntity, - id, - name, - userEmail, - } = workspaceMember; - - const avatarUrl = - userWorkspaceId && avatarUrlFromEntity - ? this.generateSignedAvatarUrl({ - workspaceId: userWorkspaceId, - workspaceMember: { - avatarUrl: avatarUrlFromEntity, - id, - }, - }) - : null; - - return { - id, - name, - userEmail, - avatarUrl, - userWorkspaceId: userWorkspaceId ?? null, - } satisfies DeletedWorkspaceMember; - } - - toDeletedWorkspaceMemberDtos( - workspaceMembers: WorkspaceMemberWorkspaceEntity[], - userWorkspaceId?: string, - ): DeletedWorkspaceMember[] { - return workspaceMembers.map((workspaceMember) => - this.toDeletedWorkspaceMemberDto(workspaceMember, userWorkspaceId), - ); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts new file mode 100644 index 000000000..2e2359eee --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@nestjs/common'; + +import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; + +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; +import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { fromRoleEntitiesToRoleDtos } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util'; +import { + WorkspaceMemberDateFormatEnum, + WorkspaceMemberTimeFormatEnum, + WorkspaceMemberWorkspaceEntity, +} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +export type ToWorkspaceMemberDtoArgs = { + workspaceMemberEntity: WorkspaceMemberWorkspaceEntity; + userWorkspaceRoles: RoleEntity[]; + userWorkspace: UserWorkspace; +}; + +@Injectable() +export class WorkspaceMemberTranspiler { + constructor(private readonly fileService: FileService) {} + + generateSignedAvatarUrl({ + workspaceId, + workspaceMember, + }: { + workspaceMember: Pick; + workspaceId: string; + }): string { + if ( + !isDefined(workspaceMember.avatarUrl) || + !isNonEmptyString(workspaceMember.avatarUrl) + ) { + return ''; + } + + return this.fileService.signFileUrl({ + url: workspaceMember.avatarUrl, + workspaceId, + }); + } + + toWorkspaceMemberDto({ + userWorkspace, + workspaceMemberEntity, + userWorkspaceRoles, + }: ToWorkspaceMemberDtoArgs): WorkspaceMember { + const { + avatarUrl: avatarUrlFromEntity, + id, + name, + userEmail, + colorScheme, + locale, + timeFormat, + timeZone, + dateFormat, + } = workspaceMemberEntity; + + const avatarUrl = this.generateSignedAvatarUrl({ + workspaceId: userWorkspace.id, + workspaceMember: { + avatarUrl: avatarUrlFromEntity, + id, + }, + }); + + const roles = fromRoleEntitiesToRoleDtos(userWorkspaceRoles); + + return { + id, + name, + userEmail, + avatarUrl, + userWorkspaceId: userWorkspace.id, + colorScheme, + dateFormat: dateFormat as WorkspaceMemberDateFormatEnum, + locale, + timeFormat: timeFormat as WorkspaceMemberTimeFormatEnum, + timeZone, + roles, + } satisfies WorkspaceMember; + } + + toWorkspaceMemberDtos( + allWorkspaceEntitiesBundles: ToWorkspaceMemberDtoArgs[], + ) { + return allWorkspaceEntitiesBundles.map((bundle) => + this.toWorkspaceMemberDto(bundle), + ); + } + + toDeletedWorkspaceMemberDto( + workspaceMember: WorkspaceMemberWorkspaceEntity, + userWorkspaceId?: string, + ): DeletedWorkspaceMember { + const { + avatarUrl: avatarUrlFromEntity, + id, + name, + userEmail, + } = workspaceMember; + + const avatarUrl = userWorkspaceId + ? this.generateSignedAvatarUrl({ + workspaceId: userWorkspaceId, + workspaceMember: { + avatarUrl: avatarUrlFromEntity, + id, + }, + }) + : null; + + return { + id, + name, + userEmail, + avatarUrl, + userWorkspaceId: userWorkspaceId ?? null, + } satisfies DeletedWorkspaceMember; + } + + toDeletedWorkspaceMemberDtos( + workspaceMembers: WorkspaceMemberWorkspaceEntity[], + userWorkspaceId?: string, + ): DeletedWorkspaceMember[] { + return workspaceMembers.map((workspaceMember) => + this.toDeletedWorkspaceMemberDto(workspaceMember, userWorkspaceId), + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 4cc468a65..d2f475e44 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -16,7 +16,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service'; +import { WorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/workspace-member-transpiler.service'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { User } from 'src/engine/core-modules/user/user.entity'; import { UserResolver } from 'src/engine/core-modules/user/user.resolver'; @@ -54,12 +54,12 @@ import { UserService } from './services/user.service'; PermissionsModule, UserWorkspaceModule, ], - exports: [UserService, DeletedWorkspaceMemberTranspiler], + exports: [UserService, WorkspaceMemberTranspiler], providers: [ UserService, UserResolver, TypeORMService, - DeletedWorkspaceMemberTranspiler, + WorkspaceMemberTranspiler, ], }) export class UserModule {} diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index 447efd2a8..6bc0c3fa4 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -13,7 +13,7 @@ import crypto from 'crypto'; import { GraphQLJSONObject } from 'graphql-type-json'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; -import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { In, Repository } from 'typeorm'; @@ -24,12 +24,10 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.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 { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; import { OnboardingService, @@ -37,10 +35,14 @@ import { } from 'src/engine/core-modules/onboarding/onboarding.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.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 { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; -import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { + ToWorkspaceMemberDtoArgs, + WorkspaceMemberTranspiler, +} from 'src/engine/core-modules/user/services/workspace-member-transpiler.service'; import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; @@ -48,11 +50,10 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; +import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; @@ -77,16 +78,50 @@ export class UserResolver { private readonly fileUploadService: FileUploadService, private readonly onboardingService: OnboardingService, private readonly userVarService: UserVarsService, - private readonly fileService: FileService, - private readonly domainManagerService: DomainManagerService, @InjectRepository(UserWorkspace, 'core') private readonly userWorkspaceRepository: Repository, private readonly userRoleService: UserRoleService, private readonly permissionsService: PermissionsService, - private readonly deletedWorkspaceMemberTranspiler: DeletedWorkspaceMemberTranspiler, private readonly featureFlagService: FeatureFlagService, + private readonly workspaceMemberTranspiler: WorkspaceMemberTranspiler, + private readonly userWorkspaceService: UserWorkspaceService, ) {} + private async getUserWorkspacePermissions({ + currentUserWorkspace, + workspace, + }: { + workspace: Workspace; + currentUserWorkspace: UserWorkspace; + }): Promise { + const workspaceIsPendingOrOngoingCreation = [ + WorkspaceActivationStatus.PENDING_CREATION, + WorkspaceActivationStatus.ONGOING_CREATION, + ].includes(workspace.activationStatus); + + if (workspaceIsPendingOrOngoingCreation) { + return this.permissionsService.getDefaultUserWorkspacePermissions(); + } + + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED, + workspace.id, + ); + + if (!isPermissionsV2Enabled) { + return await this.permissionsService.getUserWorkspacePermissions({ + userWorkspaceId: currentUserWorkspace.id, + workspaceId: workspace.id, + }); + } + + return await this.permissionsService.getUserWorkspacePermissionsV2({ + userWorkspaceId: currentUserWorkspace.id, + workspaceId: workspace.id, + }); + } + @Query(() => User) async currentUser( @AuthUser() { id: userId }: User, @@ -108,75 +143,24 @@ export class UserResolver { (userWorkspace) => userWorkspace.workspace.id === workspace.id, ); - if (!currentUserWorkspace) { + if (!isDefined(currentUserWorkspace)) { throw new Error('Current user workspace not found'); } - let settingsPermissions = {}; - let objectRecordsPermissions = {}; - let objectPermissions: ObjectPermissionDTO[] = []; - if ( - ![ - WorkspaceActivationStatus.PENDING_CREATION, - WorkspaceActivationStatus.ONGOING_CREATION, - ].includes(workspace.activationStatus) - ) { - const isPermissionsV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED, - workspace.id, - ); - - if (isPermissionsV2Enabled) { - const permissions = - await this.permissionsService.getUserWorkspacePermissionsV2({ - userWorkspaceId: currentUserWorkspace.id, - workspaceId: workspace.id, - }); - - settingsPermissions = permissions.settingsPermissions; - objectPermissions = Object.entries(permissions.objectPermissions).map( - ([objectMetadataId, permissions]) => ({ - objectMetadataId, - canReadObjectRecords: permissions.canRead, - canUpdateObjectRecords: permissions.canUpdate, - canSoftDeleteObjectRecords: permissions.canSoftDelete, - canDestroyObjectRecords: permissions.canDestroy, - }), - ); - objectRecordsPermissions = permissions.objectRecordsPermissions; - } else { - const permissions = - await this.permissionsService.getUserWorkspacePermissions({ - userWorkspaceId: currentUserWorkspace.id, - workspaceId: workspace.id, - }); - - settingsPermissions = permissions.settingsPermissions; - objectRecordsPermissions = permissions.objectRecordsPermissions; - } - } - - const grantedSettingsPermissions: SettingPermissionType[] = ( - Object.keys(settingsPermissions) as SettingPermissionType[] - ) - // @ts-expect-error legacy noImplicitAny - .filter((feature) => settingsPermissions[feature] === true); - - const grantedObjectRecordsPermissions = ( - Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[] - ) - // @ts-expect-error legacy noImplicitAny - .filter((permission) => objectRecordsPermissions[permission] === true); - - currentUserWorkspace.settingsPermissions = grantedSettingsPermissions; - currentUserWorkspace.objectRecordsPermissions = - grantedObjectRecordsPermissions; - currentUserWorkspace.objectPermissions = objectPermissions; - user.currentUserWorkspace = currentUserWorkspace; + const userWorkspacePermissions = + fromUserWorkspacePermissionsToUserWorkspacePermissionsDto( + await this.getUserWorkspacePermissions({ + currentUserWorkspace, + workspace, + }), + ); return { ...user, + currentUserWorkspace: { + ...currentUserWorkspace, + ...userWorkspacePermissions, + }, currentWorkspace: workspace, }; } @@ -185,18 +169,17 @@ export class UserResolver { async userVars( @Parent() user: User, @AuthWorkspace() workspace: Workspace, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise> { + ): Promise> { const userVars = await this.userVarService.getAll({ userId: user.id, workspaceId: workspace.id, }); - const userVarAllowList = [ + const userVarAllowList: string[] = [ OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING, AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES, - ] as string[]; + ]; const filteredMap = new Map( [...userVars].filter(([key]) => userVarAllowList.includes(key)), @@ -212,20 +195,39 @@ export class UserResolver { @Parent() user: User, @AuthWorkspace() workspace: Workspace, ): Promise { - const workspaceMember = await this.userService.loadWorkspaceMember( + const workspaceMemberEntity = await this.userService.loadWorkspaceMember( user, workspace, ); - if (workspaceMember && workspaceMember.avatarUrl) { - workspaceMember.avatarUrl = this.fileService.signFileUrl({ - url: workspaceMember.avatarUrl, - workspaceId: workspace.id, - }); + if (!isDefined(workspaceMemberEntity)) { + throw new Error('Workspace member not found'); } - // TODO Refactor to be transpiled to WorkspaceMember instead - return workspaceMember as WorkspaceMember | null; + const workspaceId = workspace.id; + const userWorkspace = + await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({ + userId: workspaceMemberEntity.userId, + workspaceId: workspace.id, + }); + + const roleOfUserWorkspace = + await this.userRoleService.getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspace.id], + workspaceId, + }); + + const userWorkspaceRoles = roleOfUserWorkspace.get(userWorkspace.id); + + if (!isDefined(userWorkspaceRoles)) { + throw new Error('User workspace roles not found'); + } + + return this.workspaceMemberTranspiler.toWorkspaceMemberDto({ + workspaceMemberEntity, + userWorkspace, + userWorkspaceRoles, + }); } @ResolveField(() => [WorkspaceMember], { @@ -240,7 +242,6 @@ export class UserResolver { false, ); - const workspaceMembers: WorkspaceMember[] = []; const userWorkspaces = await this.userWorkspaceRepository.find({ where: { userId: In(workspaceMemberEntities.map((entity) => entity.userId)), @@ -248,14 +249,14 @@ export class UserResolver { }, }); - const userWorkspacesByUserId = new Map( + const userWorkspacesByUserIdMap = new Map( userWorkspaces.map((userWorkspace) => [ userWorkspace.userId, userWorkspace, ]), ); - const rolesByUserWorkspaces: Map = + const rolesByUserWorkspacesMap = await this.userRoleService.getRolesByUserWorkspaces({ userWorkspaceIds: userWorkspaces.map( (userWorkspace) => userWorkspace.id, @@ -263,54 +264,36 @@ export class UserResolver { workspaceId: workspace.id, }); - for (const workspaceMemberEntity of workspaceMemberEntities) { - if (workspaceMemberEntity.avatarUrl) { - workspaceMemberEntity.avatarUrl = this.fileService.signFileUrl({ - url: workspaceMemberEntity.avatarUrl, - workspaceId: workspace.id, - }); - } + const toWorkspaceMemberDtoArgs = + workspaceMemberEntities.map( + (workspaceMemberEntity) => { + const userWorkspace = userWorkspacesByUserIdMap.get( + workspaceMemberEntity.userId, + ); - // TODO Refactor to be transpiled to WorkspaceMember instead - const workspaceMember = workspaceMemberEntity as WorkspaceMember; + if (!isDefined(userWorkspace)) { + throw new Error('User workspace not found'); + } - const userWorkspace = userWorkspacesByUserId.get( - workspaceMemberEntity.userId, + const userWorkspaceRoles = rolesByUserWorkspacesMap.get( + userWorkspace.id, + ); + + if (!isDefined(userWorkspaceRoles)) { + throw new Error('User workspace roles not found'); + } + + return { + userWorkspace, + userWorkspaceRoles, + workspaceMemberEntity, + }; + }, ); - // TODO Refactor should not throw ? typed as nullable ? - if (!userWorkspace) { - throw new Error('User workspace not found'); - } - - workspaceMember.userWorkspaceId = userWorkspace.id; - - const workspaceMemberRoles = ( - rolesByUserWorkspaces.get(userWorkspace.id) ?? [] - ).map((roleEntity) => { - return { - id: roleEntity.id, - label: roleEntity.label, - canUpdateAllSettings: roleEntity.canUpdateAllSettings, - description: roleEntity.description, - icon: roleEntity.icon, - isEditable: roleEntity.isEditable, - userWorkspaceRoles: roleEntity.userWorkspaceRoles, - canReadAllObjectRecords: roleEntity.canReadAllObjectRecords, - canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords, - canSoftDeleteAllObjectRecords: - roleEntity.canSoftDeleteAllObjectRecords, - canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords, - }; - }); - - workspaceMember.roles = workspaceMemberRoles; - - workspaceMembers.push(workspaceMember); - } - - // TODO: Fix typing disrepency between Entity and DTO - return workspaceMembers; + return this.workspaceMemberTranspiler.toWorkspaceMemberDtos( + toWorkspaceMemberDtoArgs, + ); } @ResolveField(() => [DeletedWorkspaceMember], { @@ -323,7 +306,7 @@ export class UserResolver { const workspaceMemberEntities = await this.userService.loadDeletedWorkspaceMembersOnly(workspace); - return this.deletedWorkspaceMemberTranspiler.toDeletedWorkspaceMemberDtos( + return this.workspaceMemberTranspiler.toDeletedWorkspaceMemberDtos( workspaceMemberEntities, workspace.id, ); @@ -375,7 +358,6 @@ export class UserResolver { @Mutation(() => User) async deleteUser(@AuthUser() { id: userId }: User) { - // Proceed with user deletion return this.userService.deleteUser(userId); } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index c17583473..36fec92d6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; -import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { @@ -16,6 +15,7 @@ import { PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -33,11 +33,7 @@ export class PermissionsService { }: { userWorkspaceId: string; workspaceId: string; - }): Promise<{ - settingsPermissions: Record; - objectRecordsPermissions: Record; - objectPermissions: ObjectRecordsPermissions; - }> { + }): Promise { const [roleOfUserWorkspace] = await this.userRoleService .getRolesByUserWorkspaces({ userWorkspaceIds: [userWorkspaceId], @@ -60,7 +56,9 @@ export class PermissionsService { const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; - const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce( + const defaultSettingsPermissions = + this.getDefaultUserWorkspacePermissions().settingsPermissions; + const settingsPermissions = Object.keys(SettingPermissionType).reduce( (acc, feature) => ({ ...acc, [feature]: @@ -69,7 +67,7 @@ export class PermissionsService { (settingPermission) => settingPermission.setting === feature, ), }), - {} as Record, + defaultSettingsPermissions, ); const { data: rolesPermissions } = @@ -79,37 +77,53 @@ export class PermissionsService { const objectPermissions = rolesPermissions[roleOfUserWorkspace.id] ?? {}; - const objectRecordsPermissionsMap: Record< - PermissionsOnAllObjectRecords, - boolean - > = { - [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canReadAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canUpdateAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canDestroyAllObjectRecords ?? false, - }; + const objectRecordsPermissions: UserWorkspacePermissions['objectRecordsPermissions'] = + { + [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canReadAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canUpdateAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canDestroyAllObjectRecords ?? false, + }; return { - settingsPermissions: settingsPermissionsMap, - objectRecordsPermissions: objectRecordsPermissionsMap, + settingsPermissions, + objectRecordsPermissions, objectPermissions, }; } + public getDefaultUserWorkspacePermissions = () => + ({ + objectRecordsPermissions: { + [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: false, + [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: false, + [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: false, + [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: false, + }, + settingsPermissions: { + [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false, + [SettingPermissionType.WORKSPACE]: false, + [SettingPermissionType.WORKSPACE_MEMBERS]: false, + [SettingPermissionType.ROLES]: false, + [SettingPermissionType.DATA_MODEL]: false, + [SettingPermissionType.ADMIN_PANEL]: false, + [SettingPermissionType.SECURITY]: false, + [SettingPermissionType.WORKFLOWS]: false, + }, + objectPermissions: {}, + }) as const satisfies UserWorkspacePermissions; + public async getUserWorkspacePermissions({ userWorkspaceId, workspaceId, }: { userWorkspaceId: string; workspaceId: string; - }): Promise<{ - settingsPermissions: Record; - objectRecordsPermissions: Record; - }> { + }): Promise { const [roleOfUserWorkspace] = await this.userRoleService .getRolesByUserWorkspaces({ userWorkspaceIds: [userWorkspaceId], @@ -132,7 +146,9 @@ export class PermissionsService { const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; - const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce( + const defaultSettingsPermissions = + this.getDefaultUserWorkspacePermissions().settingsPermissions; + const settingsPermissions = Object.keys(SettingPermissionType).reduce( (acc, feature) => ({ ...acc, [feature]: @@ -141,26 +157,25 @@ export class PermissionsService { (settingPermission) => settingPermission.setting === feature, ), }), - {} as Record, + defaultSettingsPermissions, ); - const objectRecordsPermissionsMap: Record< - PermissionsOnAllObjectRecords, - boolean - > = { - [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canReadAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canUpdateAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false, - [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: - roleOfUserWorkspace.canDestroyAllObjectRecords ?? false, - }; + const objectRecordsPermissions: UserWorkspacePermissions['objectRecordsPermissions'] = + { + [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canReadAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canUpdateAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace.canDestroyAllObjectRecords ?? false, + }; return { - settingsPermissions: settingsPermissionsMap, - objectRecordsPermissions: objectRecordsPermissionsMap, + settingsPermissions, + objectRecordsPermissions, + objectPermissions: {}, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts new file mode 100644 index 000000000..473873c3f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts @@ -0,0 +1,10 @@ +import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { ObjectRecordsPermissions } from 'twenty-shared/types'; + +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; + +export type UserWorkspacePermissions = { + settingsPermissions: Record; + objectRecordsPermissions: Record; + objectPermissions: ObjectRecordsPermissions; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto.ts new file mode 100644 index 000000000..11f587b45 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto.ts @@ -0,0 +1,6 @@ +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; + +export type UserWorkspacePermissionsDto = Pick< + UserWorkspace, + 'objectPermissions' | 'settingsPermissions' | 'objectRecordsPermissions' +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts new file mode 100644 index 000000000..0a5f6e076 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts @@ -0,0 +1,33 @@ +import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; + +export const fromRoleEntityToRoleDto = ({ + id, + label, + canUpdateAllSettings, + description, + icon, + isEditable, + userWorkspaceRoles, + canReadAllObjectRecords, + canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords, +}: RoleEntity): RoleDTO => { + return { + id, + label, + canUpdateAllSettings, + description, + icon, + isEditable, + userWorkspaceRoles, + canReadAllObjectRecords, + canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords, + }; +}; + +export const fromRoleEntitiesToRoleDtos = (roleEntities: RoleEntity[]) => + roleEntities.map(fromRoleEntityToRoleDto); diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts new file mode 100644 index 000000000..b73b773ec --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts @@ -0,0 +1,35 @@ +import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; + +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; +import { UserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto'; + +export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({ + objectPermissions: rawObjectPermissions, + objectRecordsPermissions: rawObjectRecordsPermissions, + settingsPermissions: rawSettingsPermissions, +}: UserWorkspacePermissions): UserWorkspacePermissionsDto => { + const objectPermissions = Object.entries(rawObjectPermissions).map( + ([objectMetadataId, permissions]) => ({ + objectMetadataId, + canReadObjectRecords: permissions.canRead, + canUpdateObjectRecords: permissions.canUpdate, + canSoftDeleteObjectRecords: permissions.canSoftDelete, + canDestroyObjectRecords: permissions.canDestroy, + }), + ); + + const settingsPermissions = ( + Object.keys(rawSettingsPermissions) as SettingPermissionType[] + ).filter((feature) => rawSettingsPermissions[feature] === true); + + const objectRecordsPermissions = ( + Object.keys(rawObjectRecordsPermissions) as PermissionsOnAllObjectRecords[] + ).filter((feature) => rawObjectRecordsPermissions[feature] === true); + + return { + objectPermissions, + objectRecordsPermissions, + settingsPermissions, + }; +}; diff --git a/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts b/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts index 637965976..1311bb781 100644 --- a/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts +++ b/packages/twenty-shared/src/types/ObjectRecordsPermissions.ts @@ -1,8 +1,7 @@ -export type ObjectRecordsPermissions = { - [objectMetadataId: string]: { +type ObjectMetadataId = string; +export type ObjectRecordsPermissions = Record; diff --git a/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts b/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts index dd13ed8bf..8726c6eca 100644 --- a/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts +++ b/packages/twenty-shared/src/types/ObjectRecordsPermissionsByRoleId.ts @@ -1,5 +1,7 @@ -import { ObjectRecordsPermissions } from '@/types'; +import { ObjectRecordsPermissions } from "@/types/ObjectRecordsPermissions"; -export type ObjectRecordsPermissionsByRoleId = { - [roleId: string]: ObjectRecordsPermissions; -}; +type RoleId = string; +export type ObjectRecordsPermissionsByRoleId = Record< + RoleId, + ObjectRecordsPermissions +>;