feat: delete workspace member (#987)

* add delete user endpoint (server)

* delete user (UI)

* prevent token generation

* more checks
This commit is contained in:
Philip Okugbe
2025-04-07 19:26:03 +01:00
committed by GitHub
parent 3559358d14
commit 7431804a46
15 changed files with 250 additions and 23 deletions
@@ -34,6 +34,7 @@ import { addDays } from 'date-fns';
import { FastifyReply } from 'fastify';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { CheckHostnameDto } from '../dto/check-hostname.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
@UseGuards(JwtAuthGuard)
@Controller('workspace')
@@ -120,6 +121,22 @@ export class WorkspaceController {
}
}
@HttpCode(HttpStatus.OK)
@Post('members/delete')
async deleteWorkspaceMember(
@Body() dto: RemoveWorkspaceUserDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const ability = this.workspaceAbility.createForUser(user, workspace);
if (
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
) {
throw new ForbiddenException();
}
await this.workspaceService.deleteUser(user, dto.userId, workspace.id);
}
@HttpCode(HttpStatus.OK)
@Post('members/change-role')
async updateWorkspaceMemberRole(
@@ -27,6 +27,8 @@ import { DomainService } from '../../../integrations/environment/domain.service'
import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { addDays } from 'date-fns';
import { DISALLOWED_HOSTNAMES, WorkspaceStatus } from '../workspace.constants';
import { v4 } from 'uuid';
import { AttachmentType } from 'src/core/attachment/attachment.constants';
import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq';
@@ -45,6 +47,7 @@ export class WorkspaceService {
private environmentService: EnvironmentService,
private domainService: DomainService,
@InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
) {}
@@ -419,4 +422,66 @@ export class WorkspaceService {
}
return { hostname: this.domainService.getUrl(hostname) };
}
async deleteUser(
authUser: User,
userId: string,
workspaceId: string,
): Promise<void> {
const user = await this.userRepo.findById(userId, workspaceId);
if (!user || user.deletedAt) {
throw new BadRequestException('Workspace member not found');
}
const workspaceOwnerCount = await this.userRepo.roleCountByWorkspaceId(
UserRole.OWNER,
workspaceId,
);
if (user.role === UserRole.OWNER && workspaceOwnerCount === 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
}
if (authUser.id === userId) {
throw new BadRequestException('You cannot delete yourself');
}
if (authUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) {
throw new BadRequestException('You cannot delete a user with owner role');
}
await executeTx(this.db, async (trx) => {
await this.userRepo.updateUser(
{
name: 'Deleted user',
email: v4() + '@deleted.docmost.com',
avatarUrl: null,
settings: null,
deletedAt: new Date(),
},
userId,
workspaceId,
trx,
);
await trx.deleteFrom('groupUsers').where('userId', '=', userId).execute();
await trx
.deleteFrom('spaceMembers')
.where('userId', '=', userId)
.execute();
await trx
.deleteFrom('authAccounts')
.where('userId', '=', userId)
.execute();
});
try {
await this.attachmentQueue.add(QueueJob.DELETE_USER_AVATARS, user);
} catch (err) {
// empty
}
}
}