feat: workspace invitation

This commit is contained in:
Philipinho
2024-03-04 17:48:01 +00:00
parent bfebfb27a9
commit 181bd78951
6 changed files with 218 additions and 10 deletions

View File

@ -19,6 +19,13 @@ import { CurrentWorkspace } from '../../../decorators/current-workspace.decorato
import { Workspace } from '../entities/workspace.entity'; import { Workspace } from '../entities/workspace.entity';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { WorkspaceUserService } from '../services/workspace-user.service'; import { WorkspaceUserService } from '../services/workspace-user.service';
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
import { Public } from '../../../decorators/public.decorator';
import {
AcceptInviteDto,
InviteUserDto,
RevokeInviteDto,
} from '../dto/invitation.dto';
@UseGuards(JwtGuard) @UseGuards(JwtGuard)
@Controller('workspaces') @Controller('workspaces')
@ -26,6 +33,7 @@ export class WorkspaceController {
constructor( constructor(
private readonly workspaceService: WorkspaceService, private readonly workspaceService: WorkspaceService,
private readonly workspaceUserService: WorkspaceUserService, private readonly workspaceUserService: WorkspaceUserService,
private readonly workspaceInvitationService: WorkspaceInvitationService,
) {} ) {}
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ -115,4 +123,36 @@ export class WorkspaceController {
workspace.id, workspace.id,
); );
} }
@HttpCode(HttpStatus.OK)
@Post('invite')
async inviteUser(
@Body() inviteUserDto: InviteUserDto,
@AuthUser() authUser: User,
@CurrentWorkspace() workspace: Workspace,
) {
return this.workspaceInvitationService.createInvitation(
authUser,
workspace.id,
inviteUserDto,
);
}
@Public()
@HttpCode(HttpStatus.OK)
@Post('invite/accept')
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
return this.workspaceInvitationService.acceptInvitation(
acceptInviteDto.invitationId,
);
}
// TODO: authorize permission with guards
@HttpCode(HttpStatus.OK)
@Post('invite/revoke')
async revokeInvite(@Body() revokeInviteDto: RevokeInviteDto) {
return this.workspaceInvitationService.revokeInvitation(
revokeInviteDto.invitationId,
);
}
} }

View File

@ -0,0 +1,23 @@
import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { WorkspaceUserRole } from '../entities/workspace-user.entity';
export class InviteUserDto {
@IsString()
@IsOptional()
name: string;
@IsEmail()
email: string;
@IsEnum(WorkspaceUserRole)
role: string;
}
export class InvitationIdDto {
@IsUUID()
invitationId: string;
}
export class AcceptInviteDto extends InvitationIdDto {}
export class RevokeInviteDto extends InvitationIdDto {}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
@Injectable()
export class WorkspaceInvitationRepository extends Repository<WorkspaceInvitation> {
constructor(private dataSource: DataSource) {
super(WorkspaceInvitation, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,121 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { WorkspaceInvitationRepository } from '../repositories/workspace-invitation.repository';
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
import { User } from '../../user/entities/user.entity';
import { WorkspaceService } from './workspace.service';
import { WorkspaceUserService } from './workspace-user.service';
import { WorkspaceUserRole } from '../entities/workspace-user.entity';
import { UserService } from '../../user/user.service';
import { InviteUserDto } from '../dto/invitation.dto';
@Injectable()
export class WorkspaceInvitationService {
constructor(
private workspaceInvitationRepository: WorkspaceInvitationRepository,
private workspaceService: WorkspaceService,
private workspaceUserService: WorkspaceUserService,
private userService: UserService,
) {}
async findInvitedUserByEmail(
email,
workspaceId,
): Promise<WorkspaceInvitation> {
return this.workspaceInvitationRepository.findOneBy({
email: email,
workspaceId: workspaceId,
});
}
async createInvitation(
authUser: User,
workspaceId: string,
inviteUserDto: InviteUserDto,
): Promise<WorkspaceInvitation> {
const authUserMembership =
await this.workspaceUserService.findWorkspaceUser(
authUser.id,
workspaceId,
);
if (!authUserMembership) {
throw new BadRequestException('Inviting user must be a workspace member');
}
if (authUserMembership.role != WorkspaceUserRole.OWNER) {
throw new BadRequestException(
'Only workspace owners can invite new members',
);
}
const invitedUser = await this.userService.findByEmail(inviteUserDto.email);
// check if invited user is already a workspace member
if (invitedUser) {
const invitedUserMembership =
await this.workspaceUserService.findWorkspaceUser(
invitedUser.id,
workspaceId,
);
if (invitedUserMembership) {
throw new BadRequestException(
'This user already a member of this workspace',
);
}
}
// check if user was already invited
const existingInvitation = await this.findInvitedUserByEmail(
inviteUserDto.email,
workspaceId,
);
if (existingInvitation) {
throw new BadRequestException('This user has already been invited');
}
const invitation = new WorkspaceInvitation();
invitation.workspaceId = workspaceId;
invitation.email = inviteUserDto.email;
invitation.role = inviteUserDto.role;
invitation.invitedById = authUser.id;
// TODO: send invitation email
return await this.workspaceInvitationRepository.save(invitation);
}
async acceptInvitation(invitationId: string) {
const invitation = await this.workspaceInvitationRepository.findOneBy({
id: invitationId,
});
if (!invitation) {
throw new BadRequestException('Invalid or expired invitation code');
}
// TODO: to be completed
// check if user is in the system already
const invitedUser = await this.userService.findByEmail(invitation.email);
if (invitedUser) {
// fetch the workspace
// add the user to the workspace
}
return invitation;
}
async revokeInvitation(invitationId: string): Promise<void> {
const invitation = await this.workspaceInvitationRepository.findOneBy({
id: invitationId,
});
if (!invitation) {
throw new BadRequestException('Invitation not found');
}
await this.workspaceInvitationRepository.delete(invitationId);
}
}

View File

@ -10,7 +10,6 @@ import {
} from '../entities/workspace-user.entity'; } from '../entities/workspace-user.entity';
import { Workspace } from '../entities/workspace.entity'; import { Workspace } from '../entities/workspace.entity';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto'; import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { SpaceService } from '../../space/space.service';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options'; import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto'; import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result'; import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
@ -22,7 +21,6 @@ import { transactionWrapper } from '../../../helpers/db.helper';
export class WorkspaceUserService { export class WorkspaceUserService {
constructor( constructor(
private workspaceUserRepository: WorkspaceUserRepository, private workspaceUserRepository: WorkspaceUserRepository,
private spaceService: SpaceService,
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
@ -72,7 +70,7 @@ export class WorkspaceUserService {
workspaceUserRoleDto: UpdateWorkspaceUserRoleDto, workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
workspaceId: string, workspaceId: string,
) { ) {
const workspaceUser = await this.getWorkspaceUser( const workspaceUser = await this.findAndValidateWorkspaceUser(
workspaceUserRoleDto.userId, workspaceUserRoleDto.userId,
workspaceId, workspaceId,
); );
@ -105,9 +103,10 @@ export class WorkspaceUserService {
userId: string, userId: string,
workspaceId: string, workspaceId: string,
): Promise<void> { ): Promise<void> {
await this.getWorkspaceUser(userId, workspaceId); const workspaceUser = await this.findAndValidateWorkspaceUser(
userId,
const workspaceUser = await this.getWorkspaceUser(userId, workspaceId); workspaceId,
);
const workspaceOwnerCount = await this.workspaceUserRepository.count({ const workspaceOwnerCount = await this.workspaceUserRepository.count({
where: { where: {
@ -183,17 +182,28 @@ export class WorkspaceUserService {
userId: string, userId: string,
workspaceId: string, workspaceId: string,
): Promise<string> { ): Promise<string> {
const workspaceUser = await this.getWorkspaceUser(userId, workspaceId); const workspaceUser = await this.findAndValidateWorkspaceUser(
userId,
workspaceId,
);
return workspaceUser.role ? workspaceUser.role : null; return workspaceUser.role ? workspaceUser.role : null;
} }
async getWorkspaceUser( async findWorkspaceUser(
userId: string, userId: string,
workspaceId: string, workspaceId: string,
): Promise<WorkspaceUser> { ): Promise<WorkspaceUser> {
const workspaceUser = await this.workspaceUserRepository.findOne({ return await this.workspaceUserRepository.findOneBy({
where: { userId, workspaceId }, userId,
workspaceId,
}); });
}
async findAndValidateWorkspaceUser(
userId: string,
workspaceId: string,
): Promise<WorkspaceUser> {
const workspaceUser = await this.findWorkspaceUser(userId, workspaceId);
if (!workspaceUser) { if (!workspaceUser) {
throw new BadRequestException('Workspace member not found'); throw new BadRequestException('Workspace member not found');

View File

@ -10,6 +10,8 @@ import { WorkspaceUserRepository } from './repositories/workspace-user.repositor
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { SpaceModule } from '../space/space.module'; import { SpaceModule } from '../space/space.module';
import { WorkspaceUserService } from './services/workspace-user.service'; import { WorkspaceUserService } from './services/workspace-user.service';
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
@Module({ @Module({
imports: [ imports: [
@ -21,8 +23,10 @@ import { WorkspaceUserService } from './services/workspace-user.service';
providers: [ providers: [
WorkspaceService, WorkspaceService,
WorkspaceUserService, WorkspaceUserService,
WorkspaceInvitationService,
WorkspaceRepository, WorkspaceRepository,
WorkspaceUserRepository, WorkspaceUserRepository,
WorkspaceInvitationRepository,
], ],
exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository], exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
}) })