mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-18 18:51:05 +10:00
implement new invitation system
* fix comments on the frontend * move jwt token service to its own module * other fixes and updates
This commit is contained in:
@ -1,32 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||
import { TokenService } from './services/token.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { SignupService } from './services/signup.service';
|
||||
import { GroupModule } from '../group/group.module';
|
||||
import { TokenModule } from './token.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
useFactory: async (environmentService: EnvironmentService) => {
|
||||
return {
|
||||
secret: environmentService.getJwtSecret(),
|
||||
signOptions: {
|
||||
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
WorkspaceModule,
|
||||
GroupModule,
|
||||
],
|
||||
imports: [TokenModule, WorkspaceModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, SignupService, TokenService, JwtStrategy],
|
||||
exports: [TokenService],
|
||||
providers: [AuthService, SignupService, JwtStrategy],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@ -9,8 +9,8 @@ import {
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsOptional()
|
||||
@MinLength(3)
|
||||
@MaxLength(35)
|
||||
@MinLength(2)
|
||||
@MaxLength(60)
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
|
||||
@ -3,19 +3,19 @@ import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { WorkspaceService } from '../../workspace/services/workspace.service';
|
||||
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
import { GroupUserService } from '../../group/services/group-user.service';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
constructor(
|
||||
private userRepo: UserRepo,
|
||||
private workspaceService: WorkspaceService,
|
||||
private groupUserService: GroupUserService,
|
||||
private groupUserRepo: GroupUserRepo,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
) {}
|
||||
|
||||
@ -56,7 +56,7 @@ export class SignupService {
|
||||
);
|
||||
|
||||
// add user to default group
|
||||
await this.groupUserService.addUserToDefaultGroup(
|
||||
await this.groupUserRepo.addUserToDefaultGroup(
|
||||
user.id,
|
||||
workspaceId,
|
||||
trx,
|
||||
|
||||
24
apps/server/src/core/auth/token.module.ts
Normal file
24
apps/server/src/core/auth/token.module.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||
import { TokenService } from './services/token.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
useFactory: async (environmentService: EnvironmentService) => {
|
||||
return {
|
||||
secret: environmentService.getAppSecret(),
|
||||
signOptions: {
|
||||
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
||||
issuer: 'Docmost',
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
],
|
||||
providers: [TokenService],
|
||||
exports: [TokenService],
|
||||
})
|
||||
export class TokenModule {}
|
||||
@ -11,7 +11,6 @@ import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
export type Subjects =
|
||||
| 'Workspace'
|
||||
| 'WorkspaceInvitation'
|
||||
| 'Space'
|
||||
| 'SpaceMember'
|
||||
| 'Group'
|
||||
@ -36,8 +35,6 @@ export default class CaslAbilityFactory {
|
||||
can([Action.Manage], 'Workspace');
|
||||
can([Action.Manage], 'WorkspaceUser');
|
||||
|
||||
can([Action.Manage], 'WorkspaceInvitation');
|
||||
|
||||
// Groups
|
||||
can([Action.Manage], 'Group');
|
||||
can([Action.Manage], 'GroupUser');
|
||||
|
||||
@ -66,8 +66,7 @@ export class CommentService {
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
// return created comment and creator relation
|
||||
return this.findById(createdComment.id);
|
||||
return createdComment;
|
||||
}
|
||||
|
||||
async findByPageId(
|
||||
@ -114,7 +113,12 @@ export class CommentService {
|
||||
return comment;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.commentRepo.deleteComment(id);
|
||||
async remove(commentId: string): Promise<void> {
|
||||
const comment = await this.commentRepo.findById(commentId);
|
||||
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
await this.commentRepo.deleteComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,9 @@ import { ArrayMaxSize, ArrayMinSize, IsArray, IsUUID } from 'class-validator';
|
||||
import { GroupIdDto } from './group-id.dto';
|
||||
|
||||
export class AddGroupUserDto extends GroupIdDto {
|
||||
// @IsOptional()
|
||||
// @IsUUID()
|
||||
// userId: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMaxSize(50, {
|
||||
message: 'userIds must an array with no more than 50 elements',
|
||||
message: 'you cannot add more than 50 users at a time',
|
||||
})
|
||||
@ArrayMinSize(1)
|
||||
@IsUUID(4, { each: true })
|
||||
|
||||
@ -7,17 +7,14 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { GroupService } from './group.service';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
|
||||
@Injectable()
|
||||
export class GroupUserService {
|
||||
constructor(
|
||||
private groupRepo: GroupRepo,
|
||||
private groupUserRepo: GroupUserRepo,
|
||||
private userRepo: UserRepo,
|
||||
@Inject(forwardRef(() => GroupService))
|
||||
@ -40,24 +37,6 @@ export class GroupUserService {
|
||||
return groupUsers;
|
||||
}
|
||||
|
||||
async addUserToDefaultGroup(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
const defaultGroup = await this.groupRepo.getDefaultGroup(
|
||||
workspaceId,
|
||||
trx,
|
||||
);
|
||||
await this.addUserToGroup(userId, defaultGroup.id, workspaceId, trx);
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
async addUsersToGroupBatch(
|
||||
userIds: string[],
|
||||
groupId: string,
|
||||
@ -90,48 +69,6 @@ export class GroupUserService {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async addUserToGroup(
|
||||
userId: string,
|
||||
groupId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<void> {
|
||||
await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||
const user = await this.userRepo.findById(userId, workspaceId, {
|
||||
trx: trx,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const groupUserExists = await this.groupUserRepo.getGroupUserById(
|
||||
userId,
|
||||
groupId,
|
||||
trx,
|
||||
);
|
||||
|
||||
if (groupUserExists) {
|
||||
throw new BadRequestException(
|
||||
'User is already a member of this group',
|
||||
);
|
||||
}
|
||||
|
||||
await this.groupUserRepo.insertGroupUser(
|
||||
{
|
||||
userId,
|
||||
groupId,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
async removeUserFromGroup(
|
||||
userId: string,
|
||||
groupId: string,
|
||||
|
||||
@ -4,42 +4,22 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private userRepo: UserRepo,
|
||||
) {}
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('me')
|
||||
async getUser(
|
||||
@AuthUser() authUser: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const user = await this.userRepo.findById(authUser.id, workspace.id);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid user');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('info')
|
||||
async getUserIno(
|
||||
@AuthUser() authUser: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
|
||||
@ -4,19 +4,20 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { WorkspaceService } from '../services/workspace.service';
|
||||
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
|
||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||
import { AuthUser } from '../../../decorators/auth-user.decorator';
|
||||
import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator';
|
||||
import { PaginationOptions } from '../../../kysely/pagination/pagination-options';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
|
||||
import { Public } from '../../../decorators/public.decorator';
|
||||
import {
|
||||
AcceptInviteDto,
|
||||
InvitationIdDto,
|
||||
InviteUserDto,
|
||||
RevokeInviteDto,
|
||||
} from '../dto/invitation.dto';
|
||||
@ -24,7 +25,6 @@ import { Action } from '../../casl/ability.action';
|
||||
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
||||
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
||||
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
||||
import { WorkspaceUserService } from '../services/workspace-user.service';
|
||||
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
@ -33,7 +33,6 @@ import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
export class WorkspaceController {
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceUserService: WorkspaceUserService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
) {}
|
||||
|
||||
@ -59,16 +58,6 @@ export class WorkspaceController {
|
||||
return this.workspaceService.update(workspace.id, updateWorkspaceDto);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'Workspace'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('delete')
|
||||
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
|
||||
// return this.workspaceService.delete(deleteWorkspaceDto);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Read, 'WorkspaceUser'),
|
||||
@ -80,10 +69,7 @@ export class WorkspaceController {
|
||||
pagination: PaginationOptions,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.workspaceUserService.getWorkspaceUsers(
|
||||
workspace.id,
|
||||
pagination,
|
||||
);
|
||||
return this.workspaceService.getWorkspaceUsers(workspace.id, pagination);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@ -93,7 +79,7 @@ export class WorkspaceController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members/deactivate')
|
||||
async deactivateWorkspaceMember() {
|
||||
return this.workspaceUserService.deactivateUser();
|
||||
return this.workspaceService.deactivateUser();
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@ -107,7 +93,7 @@ export class WorkspaceController {
|
||||
@AuthUser() authUser: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.workspaceUserService.updateWorkspaceUserRole(
|
||||
return this.workspaceService.updateWorkspaceUserRole(
|
||||
authUser,
|
||||
workspaceUserRoleDto,
|
||||
workspace.id,
|
||||
@ -116,37 +102,91 @@ export class WorkspaceController {
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceInvitation'),
|
||||
ability.can(Action.Read, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invite')
|
||||
async inviteUser(
|
||||
@Body() inviteUserDto: InviteUserDto,
|
||||
@AuthUser() authUser: User,
|
||||
@Post('invites')
|
||||
async getInvitations(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
/* return this.workspaceInvitationService.createInvitation(
|
||||
authUser,
|
||||
return this.workspaceInvitationService.getInvitations(
|
||||
workspace.id,
|
||||
inviteUserDto,
|
||||
);*/
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invite/accept')
|
||||
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
|
||||
// return this.workspaceInvitationService.acceptInvitation(
|
||||
// acceptInviteDto.invitationId,
|
||||
//);
|
||||
@Post('invites/info')
|
||||
async getInvitationById(@Body() dto: InvitationIdDto, @Req() req: any) {
|
||||
return this.workspaceInvitationService.getInvitationById(
|
||||
dto.invitationId,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: authorize permission with guards
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invite/revoke')
|
||||
async revokeInvite(@Body() revokeInviteDto: RevokeInviteDto) {
|
||||
// return this.workspaceInvitationService.revokeInvitation(
|
||||
// revokeInviteDto.invitationId,
|
||||
// );
|
||||
@Post('invites/create')
|
||||
async inviteUser(
|
||||
@Body() inviteUserDto: InviteUserDto,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@AuthUser() authUser: User,
|
||||
) {
|
||||
return this.workspaceInvitationService.createInvitation(
|
||||
inviteUserDto,
|
||||
workspace.id,
|
||||
authUser,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/resend')
|
||||
async resendInvite(
|
||||
@Body() revokeInviteDto: RevokeInviteDto,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.workspaceInvitationService.resendInvitation(
|
||||
revokeInviteDto.invitationId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/revoke')
|
||||
async revokeInvite(
|
||||
@Body() revokeInviteDto: RevokeInviteDto,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.workspaceInvitationService.revokeInvitation(
|
||||
revokeInviteDto.invitationId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/accept')
|
||||
async acceptInvite(
|
||||
@Body() acceptInviteDto: AcceptInviteDto,
|
||||
@Req() req: any,
|
||||
) {
|
||||
return this.workspaceInvitationService.acceptInvitation(
|
||||
acceptInviteDto,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export class AddWorkspaceUserDto {
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
userId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
role: string;
|
||||
}
|
||||
@ -1,13 +1,35 @@
|
||||
import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEmail,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { UserRole } from '../../../helpers/types/permission';
|
||||
|
||||
export class InviteUserDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name: string;
|
||||
@IsArray()
|
||||
@ArrayMaxSize(50, {
|
||||
message: 'you cannot invite more than 50 users at a time',
|
||||
})
|
||||
@ArrayMinSize(1)
|
||||
@IsEmail({}, { each: true })
|
||||
emails: string[];
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(25, {
|
||||
message: 'you cannot add invited users to more than 25 groups at a time',
|
||||
})
|
||||
@ArrayMinSize(0)
|
||||
@IsUUID(4, { each: true })
|
||||
groupIds: string[];
|
||||
|
||||
@IsEnum(UserRole)
|
||||
role: string;
|
||||
@ -18,6 +40,19 @@ export class InvitationIdDto {
|
||||
invitationId: string;
|
||||
}
|
||||
|
||||
export class AcceptInviteDto extends InvitationIdDto {}
|
||||
export class AcceptInviteDto extends InvitationIdDto {
|
||||
@MinLength(2)
|
||||
@MaxLength(60)
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@MinLength(8)
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class RevokeInviteDto extends InvitationIdDto {}
|
||||
|
||||
@ -1,106 +1,318 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WorkspaceService } from './workspace.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { WorkspaceUserService } from './workspace-user.service';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AcceptInviteDto, InviteUserDto } from '../dto/invitation.dto';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import {
|
||||
Group,
|
||||
User,
|
||||
WorkspaceInvitation,
|
||||
} from '@docmost/db/types/entity.types';
|
||||
import { MailService } from '../../../integrations/mail/mail.service';
|
||||
import InvitationEmail from '@docmost/transactional/emails/invitation-email';
|
||||
import { hashPassword } from '../../../helpers';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import InvitationAcceptedEmail from '@docmost/transactional/emails/invitation-accepted-email';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { TokenService } from '../../auth/services/token.service';
|
||||
import { nanoIdGen } from '../../../helpers/nanoid.utils';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { executeWithPagination } from '@docmost/db/pagination/pagination';
|
||||
import { TokensDto } from '../../auth/dto/tokens.dto';
|
||||
|
||||
// need reworking
|
||||
@Injectable()
|
||||
export class WorkspaceInvitationService {
|
||||
private readonly logger = new Logger(WorkspaceInvitationService.name);
|
||||
constructor(
|
||||
private workspaceService: WorkspaceService,
|
||||
private workspaceUserService: WorkspaceUserService,
|
||||
private userService: UserService,
|
||||
private userRepo: UserRepo,
|
||||
private groupUserRepo: GroupUserRepo,
|
||||
private mailService: MailService,
|
||||
private environmentService: EnvironmentService,
|
||||
private tokenService: TokenService,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
) {}
|
||||
/*
|
||||
async findInvitedUserByEmail(
|
||||
email,
|
||||
workspaceId,
|
||||
): Promise<WorkspaceInvitation> {
|
||||
return this.workspaceInvitationRepository.findOneBy({
|
||||
email: email,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
async getInvitations(workspaceId: string, pagination: PaginationOptions) {
|
||||
let query = this.db
|
||||
.selectFrom('workspaceInvitations')
|
||||
.select(['id', 'email', 'role', 'workspaceId', 'createdAt'])
|
||||
.where('workspaceId', '=', workspaceId);
|
||||
|
||||
if (pagination.query) {
|
||||
query = query.where((eb) =>
|
||||
eb('email', 'ilike', `%${pagination.query}%`),
|
||||
);
|
||||
}
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getInvitationById(invitationId: string, workspaceId: string) {
|
||||
const invitation = await this.db
|
||||
.selectFrom('workspaceInvitations')
|
||||
.select(['id', 'email', 'createdAt'])
|
||||
.where('id', '=', invitationId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!invitation) {
|
||||
throw new NotFoundException('Invitation not found');
|
||||
}
|
||||
|
||||
return invitation;
|
||||
}
|
||||
|
||||
async createInvitation(
|
||||
authUser: User,
|
||||
workspaceId: string,
|
||||
inviteUserDto: InviteUserDto,
|
||||
): Promise<WorkspaceInvitation> {
|
||||
// check if invited user is already a workspace member
|
||||
const invitedUser =
|
||||
await this.workspaceUserService.findWorkspaceUserByEmail(
|
||||
inviteUserDto.email,
|
||||
workspaceId,
|
||||
);
|
||||
workspaceId: string,
|
||||
authUser: User,
|
||||
): Promise<void> {
|
||||
const { emails, role, groupIds } = inviteUserDto;
|
||||
|
||||
if (invitedUser) {
|
||||
let invites: WorkspaceInvitation[] = [];
|
||||
|
||||
try {
|
||||
await executeTx(this.db, async (trx) => {
|
||||
// we do not want to invite existing members
|
||||
const findExistingUsers = await this.db
|
||||
.selectFrom('users')
|
||||
.select(['email'])
|
||||
.where('users.email', 'in', emails)
|
||||
.where('users.workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
|
||||
let existingUserEmails = [];
|
||||
if (findExistingUsers) {
|
||||
existingUserEmails = findExistingUsers.map((user) => user.email);
|
||||
}
|
||||
|
||||
// filter out existing users
|
||||
const inviteEmails = emails.filter(
|
||||
(email) => !existingUserEmails.includes(email),
|
||||
);
|
||||
|
||||
let validGroups = [];
|
||||
if (groupIds && groupIds.length > 0) {
|
||||
validGroups = await trx
|
||||
.selectFrom('groups')
|
||||
.select(['id', 'name'])
|
||||
.where('groups.id', 'in', groupIds)
|
||||
.where('groups.workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
const invitesToInsert = inviteEmails.map((email) => ({
|
||||
email: email,
|
||||
role: role,
|
||||
token: nanoIdGen(16),
|
||||
workspaceId: workspaceId,
|
||||
invitedById: authUser.id,
|
||||
groupIds: validGroups?.map((group: Partial<Group>) => group.id),
|
||||
}));
|
||||
|
||||
invites = await trx
|
||||
.insertInto('workspaceInvitations')
|
||||
.values(invitesToInsert)
|
||||
.onConflict((oc) => oc.columns(['email', 'workspaceId']).doNothing())
|
||||
.returningAll()
|
||||
.execute();
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(`createInvitation - ${err}`);
|
||||
throw new BadRequestException(
|
||||
'User is already a member of this workspace',
|
||||
'An error occurred while processing the invitations.',
|
||||
);
|
||||
}
|
||||
|
||||
// check if user was already invited
|
||||
const existingInvitation = await this.findInvitedUserByEmail(
|
||||
inviteUserDto.email,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (existingInvitation) {
|
||||
throw new BadRequestException('User has already been invited');
|
||||
// do not send code to do nothing users
|
||||
if (invites) {
|
||||
invites.forEach((invitation: WorkspaceInvitation) => {
|
||||
this.sendInvitationMail(
|
||||
invitation.id,
|
||||
invitation.email,
|
||||
invitation.token,
|
||||
authUser.name,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 already a member
|
||||
const invitedUser =
|
||||
await this.workspaceUserService.findWorkspaceUserByEmail(
|
||||
invitation.email,
|
||||
invitation.workspaceId,
|
||||
);
|
||||
|
||||
if (invitedUser) {
|
||||
throw new BadRequestException(
|
||||
'User is already a member of this workspace',
|
||||
);
|
||||
}
|
||||
// add create account for user
|
||||
// add the user to the workspace
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async revokeInvitation(invitationId: string): Promise<void> {
|
||||
const invitation = await this.workspaceInvitationRepository.findOneBy({
|
||||
id: invitationId,
|
||||
});
|
||||
async acceptInvitation(dto: AcceptInviteDto, workspaceId: string) {
|
||||
const invitation = await this.db
|
||||
.selectFrom('workspaceInvitations')
|
||||
.selectAll()
|
||||
.where('id', '=', dto.invitationId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!invitation) {
|
||||
throw new BadRequestException('Invitation not found');
|
||||
}
|
||||
|
||||
await this.workspaceInvitationRepository.delete(invitationId);
|
||||
if (dto.token !== invitation.token) {
|
||||
throw new BadRequestException('Invalid invitation token');
|
||||
}
|
||||
|
||||
const password = await hashPassword(dto.password);
|
||||
let newUser: User;
|
||||
|
||||
try {
|
||||
await executeTx(this.db, async (trx) => {
|
||||
newUser = await trx
|
||||
.insertInto('users')
|
||||
.values({
|
||||
name: dto.name,
|
||||
email: invitation.email,
|
||||
password: password,
|
||||
workspaceId: workspaceId,
|
||||
role: invitation.role,
|
||||
lastLoginAt: new Date(),
|
||||
invitedById: invitation.invitedById,
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
// add user to default group
|
||||
await this.groupUserRepo.addUserToDefaultGroup(
|
||||
newUser.id,
|
||||
workspaceId,
|
||||
trx,
|
||||
);
|
||||
|
||||
if (invitation.groupIds && invitation.groupIds.length > 0) {
|
||||
// Ensure the groups are valid
|
||||
const validGroups = await trx
|
||||
.selectFrom('groups')
|
||||
.select(['id', 'name'])
|
||||
.where('groups.id', 'in', invitation.groupIds)
|
||||
.where('groups.workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
|
||||
if (validGroups && validGroups.length > 0) {
|
||||
const groupUsersToInsert = validGroups.map((group) => ({
|
||||
userId: newUser.id,
|
||||
groupId: group.id,
|
||||
}));
|
||||
|
||||
// add user to groups specified during invite
|
||||
await trx
|
||||
.insertInto('groupUsers')
|
||||
.values(groupUsersToInsert)
|
||||
.onConflict((oc) => oc.columns(['userId', 'groupId']).doNothing())
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
// delete invitation record
|
||||
await trx
|
||||
.deleteFrom('workspaceInvitations')
|
||||
.where('id', '=', invitation.id)
|
||||
.execute();
|
||||
});
|
||||
} catch (err: any) {
|
||||
this.logger.error(`acceptInvitation - ${err}`);
|
||||
if (err.message.includes('unique constraint')) {
|
||||
throw new BadRequestException('Invitation already accepted');
|
||||
}
|
||||
throw new BadRequestException(
|
||||
'Failed to accept invitation. An error occurred.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!newUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the inviter
|
||||
const invitedByUser = await this.userRepo.findById(
|
||||
invitation.invitedById,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (invitedByUser) {
|
||||
const emailTemplate = InvitationAcceptedEmail({
|
||||
invitedUserName: newUser.name,
|
||||
invitedUserEmail: newUser.email,
|
||||
});
|
||||
|
||||
await this.mailService.sendToQueue({
|
||||
to: invitation.email,
|
||||
subject: `${newUser.name} has accepted your Docmost invite`,
|
||||
template: emailTemplate,
|
||||
});
|
||||
}
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(newUser);
|
||||
return { tokens };
|
||||
}
|
||||
|
||||
*/
|
||||
async resendInvitation(
|
||||
invitationId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
//
|
||||
const invitation = await this.db
|
||||
.selectFrom('workspaceInvitations')
|
||||
.selectAll()
|
||||
.where('id', '=', invitationId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!invitation) {
|
||||
throw new BadRequestException('Invitation not found');
|
||||
}
|
||||
|
||||
const invitedByUser = await this.userRepo.findById(
|
||||
invitation.invitedById,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.sendInvitationMail(
|
||||
invitation.id,
|
||||
invitation.email,
|
||||
invitation.token,
|
||||
invitedByUser.name,
|
||||
);
|
||||
}
|
||||
|
||||
async revokeInvitation(
|
||||
invitationId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('workspaceInvitations')
|
||||
.where('id', '=', invitationId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async sendInvitationMail(
|
||||
invitationId: string,
|
||||
inviteeEmail: string,
|
||||
inviteToken: string,
|
||||
invitedByName: string,
|
||||
): Promise<void> {
|
||||
const inviteLink = `${this.environmentService.getAppUrl()}/invites/${invitationId}?token=${inviteToken}`;
|
||||
|
||||
const emailTemplate = InvitationEmail({
|
||||
inviteLink,
|
||||
});
|
||||
|
||||
await this.mailService.sendToQueue({
|
||||
to: inviteeEmail,
|
||||
subject: `${invitedByName} invited you to Docmost`,
|
||||
template: emailTemplate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||
import { PaginationOptions } from '../../../kysely/pagination/pagination-options';
|
||||
import { UserRole } from '../../../helpers/types/permission';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceUserService {
|
||||
constructor(private userRepo: UserRepo) {}
|
||||
|
||||
async getWorkspaceUsers(
|
||||
workspaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
): Promise<PaginationResult<User>> {
|
||||
const users = await this.userRepo.getUsersPaginated(
|
||||
workspaceId,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
async updateWorkspaceUserRole(
|
||||
authUser: User,
|
||||
userRoleDto: UpdateWorkspaceUserRoleDto,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('Workspace member not found');
|
||||
}
|
||||
|
||||
if (user.role === userRoleDto.role) {
|
||||
return user;
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRepo.updateUser(
|
||||
{
|
||||
role: userRoleDto.role,
|
||||
},
|
||||
user.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
async deactivateUser(): Promise<any> {
|
||||
return 'todo';
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@ import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
|
||||
import { SpaceService } from '../../space/services/space.service';
|
||||
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
|
||||
import { SpaceRole, UserRole } from '../../../helpers/types/permission';
|
||||
import { GroupService } from '../../group/services/group.service';
|
||||
import { SpaceMemberService } from '../../space/services/space-member.service';
|
||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
@ -16,6 +15,11 @@ import { executeTx } from '@docmost/db/utils';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceService {
|
||||
@ -23,8 +27,9 @@ export class WorkspaceService {
|
||||
private workspaceRepo: WorkspaceRepo,
|
||||
private spaceService: SpaceService,
|
||||
private spaceMemberService: SpaceMemberService,
|
||||
private groupService: GroupService,
|
||||
private groupRepo: GroupRepo,
|
||||
private groupUserRepo: GroupUserRepo,
|
||||
private userRepo: UserRepo,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
) {}
|
||||
|
||||
@ -33,7 +38,6 @@ export class WorkspaceService {
|
||||
}
|
||||
|
||||
async getWorkspaceInfo(workspaceId: string) {
|
||||
// todo: add member count
|
||||
const workspace = this.workspaceRepo.findById(workspaceId);
|
||||
if (!workspace) {
|
||||
throw new NotFoundException('Workspace not found');
|
||||
@ -61,11 +65,10 @@ export class WorkspaceService {
|
||||
);
|
||||
|
||||
// create default group
|
||||
const group = await this.groupService.createDefaultGroup(
|
||||
workspace.id,
|
||||
user.id,
|
||||
trx,
|
||||
);
|
||||
const group = await this.groupRepo.createDefaultGroup(workspace.id, {
|
||||
userId: user.id,
|
||||
trx: trx,
|
||||
});
|
||||
|
||||
// add user to workspace
|
||||
await trx
|
||||
@ -181,11 +184,54 @@ export class WorkspaceService {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async delete(workspaceId: string): Promise<void> {
|
||||
const workspace = await this.workspaceRepo.findById(workspaceId);
|
||||
if (!workspace) {
|
||||
throw new NotFoundException('Workspace not found');
|
||||
async getWorkspaceUsers(
|
||||
workspaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
): Promise<PaginationResult<User>> {
|
||||
const users = await this.userRepo.getUsersPaginated(
|
||||
workspaceId,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
async updateWorkspaceUserRole(
|
||||
authUser: User,
|
||||
userRoleDto: UpdateWorkspaceUserRoleDto,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const user = await this.userRepo.findById(userRoleDto.userId, workspaceId);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('Workspace member not found');
|
||||
}
|
||||
//delete
|
||||
|
||||
if (user.role === userRoleDto.role) {
|
||||
return user;
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRepo.updateUser(
|
||||
{
|
||||
role: userRoleDto.role,
|
||||
},
|
||||
user.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
async deactivateUser(): Promise<any> {
|
||||
return 'todo';
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,18 +3,12 @@ import { WorkspaceService } from './services/workspace.service';
|
||||
import { WorkspaceController } from './controllers/workspace.controller';
|
||||
import { SpaceModule } from '../space/space.module';
|
||||
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
|
||||
import { WorkspaceUserService } from './services/workspace-user.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { GroupModule } from '../group/group.module';
|
||||
import { TokenModule } from '../auth/token.module';
|
||||
|
||||
@Module({
|
||||
imports: [SpaceModule, UserModule, GroupModule],
|
||||
imports: [SpaceModule, TokenModule],
|
||||
controllers: [WorkspaceController],
|
||||
providers: [
|
||||
WorkspaceService,
|
||||
WorkspaceUserService,
|
||||
WorkspaceInvitationService,
|
||||
],
|
||||
providers: [WorkspaceService, WorkspaceInvitationService],
|
||||
exports: [WorkspaceService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
|
||||
Reference in New Issue
Block a user