mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-13 08:12:32 +10:00
feat: workspace invitation
This commit is contained in:
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
apps/server/src/core/workspace/dto/invitation.dto.ts
Normal file
23
apps/server/src/core/workspace/dto/invitation.dto.ts
Normal 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 {}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
|||||||
@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user