Files
twenty/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts
Paul Rastoin 442f8dbe3c [QRQC_2] No implicitAny in twenty-server (#12075)
# Introduction
Following https://github.com/twentyhq/twenty/pull/12068
Related with https://github.com/twentyhq/core-team-issues/issues/975

We're enabling `noImplicitAny` handled few use case manually, added a
`ts-expect-error` to the others, we should plan to handle them in the
future
2025-05-15 18:23:22 +02:00

366 lines
9.6 KiB
TypeScript

import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
import {
UpdateRoleInput,
UpdateRolePayload,
} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
export class RoleService {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
private readonly userRoleService: UserRoleService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
return this.roleRepository.find({
where: {
workspaceId,
},
relations: [
'userWorkspaceRoles',
'settingPermissions',
'objectPermissions',
],
});
}
public async getRoleById(
id: string,
workspaceId: string,
): Promise<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id,
workspaceId,
},
relations: ['userWorkspaceRoles', 'settingPermissions'],
});
}
public async createRole({
input,
workspaceId,
}: {
input: CreateRoleInput;
workspaceId: string;
}): Promise<RoleEntity> {
await this.validateRoleInput({ input, workspaceId });
const role = await this.roleRepository.save({
label: input.label,
description: input.description,
icon: input.icon,
canUpdateAllSettings: input.canUpdateAllSettings,
canReadAllObjectRecords: input.canReadAllObjectRecords,
canUpdateAllObjectRecords: input.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: input.canDestroyAllObjectRecords,
isEditable: true,
workspaceId,
});
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [role.id],
ignoreLock: true,
});
return role;
}
public async updateRole({
input,
workspaceId,
}: {
input: UpdateRoleInput;
workspaceId: string;
}): Promise<RoleEntity> {
await this.validateRoleIsEditableOrThrow({
roleId: input.id,
workspaceId,
});
const existingRole = await this.roleRepository.findOne({
where: {
id: input.id,
workspaceId,
},
});
if (!isDefined(existingRole)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
);
}
await this.validateRoleInput({
input: input.update,
workspaceId,
roleId: input.id,
});
const updatedRole = await this.roleRepository.save({
id: input.id,
...input.update,
});
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [input.id],
ignoreLock: true,
});
return { ...existingRole, ...updatedRole };
}
public async createAdminRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
icon: 'IconUserCog',
canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});
}
public async deleteRole(
roleId: string,
workspaceId: string,
): Promise<string> {
await this.validateRoleIsEditableOrThrow({
roleId,
workspaceId,
});
const defaultRole = await this.workspaceRepository.findOne({
where: {
id: workspaceId,
},
});
const defaultRoleId = defaultRole?.defaultRoleId;
if (!isDefined(defaultRoleId)) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_NOT_FOUND,
PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND,
);
}
await this.validateRoleIsNotDefaultRoleOrThrow({
roleId,
defaultRoleId,
});
await this.assignDefaultRoleToMembersWithRoleToDelete({
roleId,
workspaceId,
defaultRoleId,
});
await this.roleRepository.delete({
id: roleId,
workspaceId,
});
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
ignoreLock: true,
});
return roleId;
}
public async createMemberRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: MEMBER_ROLE_LABEL,
description: 'Member role',
icon: 'IconUser',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});
}
// Only used for dev seeding and testing
public async createGuestRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: 'Guest',
description: 'Guest role',
icon: 'IconUser',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
isEditable: false,
workspaceId,
});
}
private async validateRoleInput({
input,
workspaceId,
roleId,
}: {
input: CreateRoleInput | UpdateRolePayload;
workspaceId: string;
roleId?: string;
}): Promise<void> {
const keysToValidate = [
'label',
'canUpdateAllSettings',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
];
for (const key of keysToValidate) {
try {
isArgDefinedIfProvidedOrThrow({
input,
key,
// @ts-expect-error legacy noImplicitAny
value: input[key],
});
} catch (error) {
throw new PermissionsException(
error.message,
PermissionsExceptionCode.INVALID_ARG,
);
}
}
if (isDefined(input.label)) {
let workspaceRoles = await this.getWorkspaceRoles(workspaceId);
if (isDefined(roleId)) {
workspaceRoles = workspaceRoles.filter((role) => role.id !== roleId);
}
if (workspaceRoles.some((role) => role.label === input.label)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
);
}
}
}
private async validateRoleIsNotDefaultRoleOrThrow({
roleId,
defaultRoleId,
}: {
roleId: string;
defaultRoleId: string;
}): Promise<void> {
if (defaultRoleId === roleId) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
);
}
}
private async assignDefaultRoleToMembersWithRoleToDelete({
roleId,
workspaceId,
defaultRoleId,
}: {
roleId: string;
workspaceId: string;
defaultRoleId: string;
}): Promise<void> {
const userWorkspaceIds =
await this.userRoleService.getUserWorkspaceIdsAssignedToRole(
roleId,
workspaceId,
);
await Promise.all(
userWorkspaceIds.map((userWorkspaceId) =>
this.userRoleService.assignRoleToUserWorkspace({
userWorkspaceId,
roleId: defaultRoleId,
workspaceId,
}),
),
);
}
private async getRole(
roleId: string,
workspaceId: string,
): Promise<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
}
private async validateRoleIsEditableOrThrow({
roleId,
workspaceId,
}: {
roleId: string;
workspaceId: string;
}) {
const role = await this.getRole(roleId, workspaceId);
if (!role?.isEditable) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
);
}
}
}