Implement updateRole (#10009)
In this PR, we are implementing the updateRole endpoint with the following rules 1. A user can only update a member's role if they have the permission (= the admin role) 2. Admin role can't be unassigned if there are no other admin in the workspace 3. (For now) as members can only have one role for now, when they are assigned a new role, they are first unassigned the other role (if any) 4. (For now) removing a member's admin role = leaving the member with no role = calling updateRole with a null roleId
This commit is contained in:
@ -13,6 +13,7 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
@ -28,6 +29,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
DataSourceModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceInvitationModule,
|
||||
TwentyORMModule,
|
||||
],
|
||||
services: [UserWorkspaceService],
|
||||
}),
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
@ -19,7 +20,9 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
@ -34,6 +37,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(userWorkspaceRepository);
|
||||
}
|
||||
@ -196,4 +200,51 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
async getUserWorkspaceForUserOrThrow({
|
||||
userId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<UserWorkspace> {
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
throw new Error('User workspace not found');
|
||||
}
|
||||
|
||||
return userWorkspace;
|
||||
}
|
||||
|
||||
async getWorkspaceMemberOrThrow({
|
||||
workspaceMemberId,
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceMemberId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkspaceMemberWorkspaceEntity> {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
id: workspaceMemberId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workspaceMember)) {
|
||||
throw new Error('Workspace member not found');
|
||||
}
|
||||
|
||||
return workspaceMember;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import {
|
||||
WorkspaceMemberDateFormatEnum,
|
||||
WorkspaceMemberTimeFormatEnum,
|
||||
@ -42,4 +43,10 @@ export class WorkspaceMember {
|
||||
|
||||
@Field(() => WorkspaceMemberTimeFormatEnum, { nullable: true })
|
||||
timeFormat: WorkspaceMemberTimeFormatEnum;
|
||||
|
||||
@Field(() => [RoleDTO], { nullable: true })
|
||||
roles?: RoleDTO[];
|
||||
|
||||
@Field(() => String)
|
||||
userWorkspaceId: string;
|
||||
}
|
||||
|
||||
@ -8,17 +8,20 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
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';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
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 { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -39,10 +42,12 @@ import { UserService } from './services/user.service';
|
||||
FileUploadModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||
TypeOrmModule.forFeature([KeyValuePair, UserWorkspace], 'core'),
|
||||
UserVarsModule,
|
||||
AnalyticsModule,
|
||||
DomainManagerModule,
|
||||
UserRoleModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
providers: [UserService, UserResolver, TypeORMService],
|
||||
|
||||
@ -13,14 +13,22 @@ import crypto from 'crypto';
|
||||
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
|
||||
import { AnalyticsTinybirdJwtMap } from 'src/engine/core-modules/analytics/entities/analytics-tinybird-jwts.entity';
|
||||
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 { 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 { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
@ -28,25 +36,22 @@ import {
|
||||
OnboardingService,
|
||||
OnboardingStepKeys,
|
||||
} from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.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';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
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 {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -70,6 +75,10 @@ export class UserResolver {
|
||||
private readonly fileService: FileService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
@ -159,22 +168,81 @@ export class UserResolver {
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<WorkspaceMember[]> {
|
||||
const workspaceMembers =
|
||||
const workspaceMemberEntities =
|
||||
await this.userService.loadWorkspaceMembers(workspace);
|
||||
|
||||
for (const workspaceMember of workspaceMembers) {
|
||||
if (workspaceMember.avatarUrl) {
|
||||
const workspaceMembers: WorkspaceMember[] = [];
|
||||
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: {
|
||||
userId: In(workspaceMemberEntities.map((entity) => entity.userId)),
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const userWorkspacesByUserId = new Map(
|
||||
userWorkspaces.map((userWorkspace) => [
|
||||
userWorkspace.userId,
|
||||
userWorkspace,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const workspaceMemberEntity of workspaceMemberEntities) {
|
||||
if (workspaceMemberEntity.avatarUrl) {
|
||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
workspaceMemberId: workspaceMemberEntity.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||
workspaceMemberEntity.avatarUrl = `${workspaceMemberEntity.avatarUrl}?token=${avatarUrlToken}`;
|
||||
}
|
||||
|
||||
const userWorkspace = userWorkspacesByUserId.get(
|
||||
workspaceMemberEntity.userId,
|
||||
);
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new Error('User workspace not found');
|
||||
}
|
||||
|
||||
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const workspaceMember: WorkspaceMember = {
|
||||
...workspaceMemberEntity,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
} as WorkspaceMember;
|
||||
|
||||
if (permissionsEnabled === true) {
|
||||
const roles = await this.userRoleService
|
||||
.getRolesForUserWorkspace(userWorkspace.id)
|
||||
.then(([roleEntity]) => {
|
||||
if (!isDefined(roleEntity)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: roleEntity.id,
|
||||
label: roleEntity.label,
|
||||
canUpdateAllSettings: roleEntity.canUpdateAllSettings,
|
||||
description: roleEntity.description,
|
||||
isEditable: roleEntity.isEditable,
|
||||
userWorkspaceRoles: roleEntity.userWorkspaceRoles,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
workspaceMember.roles = roles;
|
||||
}
|
||||
|
||||
workspaceMembers.push(workspaceMember);
|
||||
}
|
||||
|
||||
// TODO: Fix typing disrepency between Entity and DTO
|
||||
return workspaceMembers as WorkspaceMember[];
|
||||
return workspaceMembers;
|
||||
}
|
||||
|
||||
@ResolveField(() => String, {
|
||||
|
||||
Reference in New Issue
Block a user