Refactoring

* Refactor workspace membership system
* Create setup endpoint
* Use Passport.js
* Several updates and fixes
This commit is contained in:
Philipinho
2024-03-16 22:58:12 +00:00
parent b42fe48e9b
commit a821e37028
87 changed files with 2703 additions and 2307 deletions

View File

@ -7,18 +7,14 @@ import {
UseGuards,
} from '@nestjs/common';
import { WorkspaceService } from '../services/workspace.service';
import { JwtGuard } from '../../auth/guards/jwt.guard';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { RemoveWorkspaceUserDto } from '../dto/remove-workspace-user.dto';
import { AddWorkspaceUserDto } from '../dto/add-workspace-user.dto';
import { AuthUser } from '../../../decorators/auth-user.decorator';
import { User } from '../../user/entities/user.entity';
import { CurrentWorkspace } from '../../../decorators/current-workspace.decorator';
import { AuthWorkspace } from '../../../decorators/auth-workspace.decorator';
import { Workspace } from '../entities/workspace.entity';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { WorkspaceUserService } from '../services/workspace-user.service';
import { WorkspaceInvitationService } from '../services/workspace-invitation.service';
import { Public } from '../../../decorators/public.decorator';
import {
@ -27,14 +23,15 @@ import {
RevokeInviteDto,
} from '../dto/invitation.dto';
import { Action } from '../../casl/ability.action';
import { WorkspaceUser } from '../entities/workspace-user.entity';
import { WorkspaceInvitation } from '../entities/workspace-invitation.entity';
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';
@UseGuards(JwtGuard)
@Controller('workspaces')
@UseGuards(JwtAuthGuard)
@Controller('workspace')
export class WorkspaceController {
constructor(
private readonly workspaceService: WorkspaceService,
@ -43,33 +40,21 @@ export class WorkspaceController {
) {}
@HttpCode(HttpStatus.OK)
@Post('/')
async getUserWorkspaces(
@Body()
pagination: PaginationOptions,
@Post('/info')
async getWorkspace(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceService.getUserWorkspaces(user.id, pagination);
return this.workspaceService.getWorkspaceInfo(workspace.id);
}
/*
@HttpCode(HttpStatus.OK)
@Post('create')
async createWorkspace(
@Body() createWorkspaceDto: CreateWorkspaceDto,
@AuthUser() user: User,
) {
return this.workspaceService.create(user.id, createWorkspaceDto);
}
*/
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, Workspace))
@HttpCode(HttpStatus.OK)
@Post('update')
async updateWorkspace(
@Body() updateWorkspaceDto: UpdateWorkspaceDto,
@CurrentWorkspace() workspace: Workspace,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceService.update(workspace.id, updateWorkspaceDto);
}
@ -79,19 +64,19 @@ export class WorkspaceController {
@HttpCode(HttpStatus.OK)
@Post('delete')
async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto) {
return this.workspaceService.delete(deleteWorkspaceDto);
// return this.workspaceService.delete(deleteWorkspaceDto);
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, WorkspaceUser),
ability.can(Action.Read, 'workspaceUser'),
)
@HttpCode(HttpStatus.OK)
@Post('members')
async getWorkspaceMembers(
@Body()
pagination: PaginationOptions,
@CurrentWorkspace() workspace: Workspace,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceUserService.getWorkspaceUsers(
workspace.id,
@ -100,48 +85,25 @@ export class WorkspaceController {
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Manage, WorkspaceUser),
)
// @CheckPolicies((ability: AppAbility) =>
// ability.can(Action.Manage, 'WorkspaceUser'),
// )
@HttpCode(HttpStatus.OK)
@Post('members/add')
async addWorkspaceMember(
@Body() addWorkspaceUserDto: AddWorkspaceUserDto,
@CurrentWorkspace() workspace: Workspace,
) {
return this.workspaceUserService.addUserToWorkspace(
addWorkspaceUserDto.userId,
workspace.id,
addWorkspaceUserDto.role,
);
@Post('members/deactivate')
async deactivateWorkspaceMember() {
return this.workspaceUserService.deactivateUser();
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Manage, WorkspaceUser),
)
@HttpCode(HttpStatus.OK)
@Post('members/remove')
async removeWorkspaceMember(
@Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto,
@CurrentWorkspace() workspace: Workspace,
) {
return this.workspaceUserService.removeUserFromWorkspace(
removeWorkspaceUserDto.userId,
workspace.id,
);
}
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Manage, WorkspaceUser),
ability.can(Action.Manage, 'workspaceUser'),
)
@HttpCode(HttpStatus.OK)
@Post('members/role')
async updateWorkspaceMemberRole(
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
@AuthUser() authUser: User,
@CurrentWorkspace() workspace: Workspace,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceUserService.updateWorkspaceUserRole(
authUser,
@ -159,7 +121,7 @@ export class WorkspaceController {
async inviteUser(
@Body() inviteUserDto: InviteUserDto,
@AuthUser() authUser: User,
@CurrentWorkspace() workspace: Workspace,
@AuthWorkspace() workspace: Workspace,
) {
return this.workspaceInvitationService.createInvitation(
authUser,
@ -172,9 +134,9 @@ export class WorkspaceController {
@HttpCode(HttpStatus.OK)
@Post('invite/accept')
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
return this.workspaceInvitationService.acceptInvitation(
acceptInviteDto.invitationId,
);
// return this.workspaceInvitationService.acceptInvitation(
// acceptInviteDto.invitationId,
//);
}
// TODO: authorize permission with guards

View File

@ -1,5 +1,5 @@
import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { WorkspaceUserRole } from '../entities/workspace-user.entity';
import { UserRole } from '../../../helpers/types/permission';
export class InviteUserDto {
@IsString()
@ -9,7 +9,7 @@ export class InviteUserDto {
@IsEmail()
email: string;
@IsEnum(WorkspaceUserRole)
@IsEnum(UserRole)
role: string;
}

View File

@ -1,52 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Unique,
} from 'typeorm';
import { Workspace } from './workspace.entity';
import { User } from '../../user/entities/user.entity';
@Entity('workspace_users')
@Unique(['workspaceId', 'userId'])
export class WorkspaceUser {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@ManyToOne(() => User, (user) => user.workspaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: User;
@Column()
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column({ length: 100, nullable: true })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
export enum WorkspaceUserRole {
OWNER = 'owner',
ADMIN = 'admin',
MEMBER = 'member',
}

View File

@ -4,18 +4,18 @@ import {
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
OneToOne,
DeleteDateColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { WorkspaceUser } from './workspace-user.entity';
import { Page } from '../../page/entities/page.entity';
import { WorkspaceInvitation } from './workspace-invitation.entity';
import { Comment } from '../../comment/entities/comment.entity';
import { Space } from '../../space/entities/space.entity';
import { Group } from '../../group/entities/group.entity';
import { UserRole } from '../../../helpers/types/permission';
@Entity('workspaces')
export class Workspace {
@ -46,12 +46,15 @@ export class Workspace {
@Column({ type: 'jsonb', nullable: true })
settings: any;
@Column()
@Column({ default: UserRole.MEMBER })
defaultRole: string;
@Column({ nullable: true, type: 'uuid' })
creatorId: string;
@ManyToOne(() => User, (user) => user.workspaces)
@JoinColumn({ name: 'creatorId' })
creator: User;
//@ManyToOne(() => User, (user) => user.workspaces)
// @JoinColumn({ name: 'creatorId' })
// creator: User;
@Column({ nullable: true })
defaultSpaceId: string;
@ -66,8 +69,11 @@ export class Workspace {
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.workspace)
workspaceUsers: WorkspaceUser[];
@DeleteDateColumn()
deletedAt: Date;
@OneToMany(() => User, (user) => user.workspace)
users: [];
@OneToMany(
() => WorkspaceInvitation,
@ -87,5 +93,5 @@ export class Workspace {
@OneToMany(() => Group, (group) => group.workspace)
groups: [];
workspaceUser?: WorkspaceUser;
// workspaceUser?: WorkspaceUser;
}

View File

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

View File

@ -8,7 +8,24 @@ export class WorkspaceRepository extends Repository<Workspace> {
super(Workspace, dataSource.createEntityManager());
}
async findById(workspaceId: string) {
return this.findOneBy({ id: workspaceId });
async findById(workspaceId: string): Promise<Workspace> {
// see: https://github.com/typeorm/typeorm/issues/9316
const queryBuilder = this.dataSource.createQueryBuilder(
Workspace,
'workspace',
);
return await queryBuilder
.where('workspace.id = :id', { id: workspaceId })
.getOne();
}
async findFirst(): Promise<Workspace> {
const createdWorkspace = await this.find({
order: {
createdAt: 'ASC',
},
take: 1,
});
return createdWorkspace[0];
}
}

View File

@ -3,10 +3,11 @@ import { WorkspaceInvitationRepository } from '../repositories/workspace-invitat
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';
import { WorkspaceUserService } from './workspace-user.service';
import { UserRole } from '../../../helpers/types/permission';
import { UserRepository } from '../../user/repositories/user.repository';
@Injectable()
export class WorkspaceInvitationService {
@ -15,6 +16,7 @@ export class WorkspaceInvitationService {
private workspaceService: WorkspaceService,
private workspaceUserService: WorkspaceUserService,
private userService: UserService,
private userRepository: UserRepository,
) {}
async findInvitedUserByEmail(
@ -32,37 +34,17 @@ export class WorkspaceInvitationService {
workspaceId: string,
inviteUserDto: InviteUserDto,
): Promise<WorkspaceInvitation> {
const authUserMembership =
await this.workspaceUserService.findWorkspaceUser(
authUser.id,
// check if invited user is already a workspace member
const invitedUser =
await this.workspaceUserService.findWorkspaceUserByEmail(
inviteUserDto.email,
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',
);
}
throw new BadRequestException(
'User is already a member of this workspace',
);
}
// check if user was already invited
@ -72,7 +54,7 @@ export class WorkspaceInvitationService {
);
if (existingInvitation) {
throw new BadRequestException('This user has already been invited');
throw new BadRequestException('User has already been invited');
}
const invitation = new WorkspaceInvitation();
@ -97,14 +79,22 @@ export class WorkspaceInvitationService {
// TODO: to be completed
// check if user is in the system already
const invitedUser = await this.userService.findByEmail(invitation.email);
// check if user is already a member
const invitedUser =
await this.workspaceUserService.findWorkspaceUserByEmail(
invitation.email,
invitation.workspaceId,
);
if (invitedUser) {
// fetch the workspace
// add the user to the workspace
throw new BadRequestException(
'User is already a member of this workspace',
);
}
return invitation;
// add create account for user
// add the user to the workspace
return null;
}
async revokeInvitation(invitationId: string): Promise<void> {

View File

@ -1,69 +1,34 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
import {
WorkspaceUser,
WorkspaceUserRole,
} from '../entities/workspace-user.entity';
import { Workspace } from '../entities/workspace.entity';
import { BadRequestException, Injectable } from '@nestjs/common';
import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { User } from '../../user/entities/user.entity';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../../helpers/db.helper';
import { WorkspaceRepository } from '../repositories/workspace.repository';
import { UserRepository } from '../../user/repositories/user.repository';
import { UserRole } from '../../../helpers/types/permission';
@Injectable()
export class WorkspaceUserService {
constructor(
private workspaceUserRepository: WorkspaceUserRepository,
private dataSource: DataSource,
private workspaceRepository: WorkspaceRepository,
private userRepository: UserRepository,
) {}
async addUserToWorkspace(
userId: string,
async getWorkspaceUsers(
workspaceId: string,
role: string,
manager?: EntityManager,
): Promise<WorkspaceUser> {
let addedUser;
await transactionWrapper(
async (manager) => {
const userExists = await manager.exists(User, {
where: { id: userId },
});
if (!userExists) {
throw new NotFoundException('User not found');
}
const existingWorkspaceUser = await manager.findOneBy(WorkspaceUser, {
userId: userId,
workspaceId: workspaceId,
});
if (existingWorkspaceUser) {
throw new BadRequestException(
'User is already a member of this workspace',
);
}
const workspaceUser = new WorkspaceUser();
workspaceUser.userId = userId;
workspaceUser.workspaceId = workspaceId;
workspaceUser.role = role;
addedUser = await manager.save(workspaceUser);
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<User>> {
const [workspaceUsers, count] = await this.userRepository.findAndCount({
where: {
workspaceId,
},
this.dataSource,
manager,
);
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
return addedUser;
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(workspaceUsers, paginationMeta);
}
async updateWorkspaceUserRole(
@ -80,16 +45,14 @@ export class WorkspaceUserService {
return workspaceUser;
}
const workspaceOwnerCount = await this.workspaceUserRepository.count({
const workspaceOwnerCount = await this.userRepository.count({
where: {
role: WorkspaceUserRole.OWNER,
role: UserRole.OWNER,
workspaceId,
},
});
if (
workspaceUser.role === WorkspaceUserRole.OWNER &&
workspaceOwnerCount === 1
) {
if (workspaceUser.role === UserRole.OWNER && workspaceOwnerCount === 1) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
@ -97,105 +60,26 @@ export class WorkspaceUserService {
workspaceUser.role = workspaceUserRoleDto.role;
return this.workspaceUserRepository.save(workspaceUser);
return this.userRepository.save(workspaceUser);
}
async removeUserFromWorkspace(
userId: string,
workspaceId: string,
): Promise<void> {
const workspaceUser = await this.findAndValidateWorkspaceUser(
userId,
workspaceId,
);
async deactivateUser(): Promise<any> {
return 'todo';
}
const workspaceOwnerCount = await this.workspaceUserRepository.count({
where: {
role: WorkspaceUserRole.OWNER,
},
});
if (
workspaceUser.role === WorkspaceUserRole.OWNER &&
workspaceOwnerCount === 1
) {
throw new BadRequestException(
'There must be at least one workspace owner',
);
}
await this.workspaceUserRepository.delete({
userId,
async findWorkspaceUser(userId: string, workspaceId: string): Promise<User> {
return await this.userRepository.findOneBy({
id: userId,
workspaceId,
});
}
async getUserWorkspaces(
userId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Workspace>> {
const [workspaces, count] = await this.workspaceUserRepository.findAndCount(
{
where: { userId: userId },
relations: ['workspace'],
take: paginationOptions.limit,
skip: paginationOptions.skip,
},
);
const userWorkspaces = workspaces.map(
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(userWorkspaces, paginationMeta);
}
async getWorkspaceUsers(
async findWorkspaceUserByEmail(
email: string,
workspaceId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<any>> {
const [workspaceUsers, count] =
await this.workspaceUserRepository.findAndCount({
relations: ['user'],
where: {
workspace: {
id: workspaceId,
},
},
take: paginationOptions.limit,
skip: paginationOptions.skip,
});
const users = workspaceUsers.map((workspaceUser) => {
workspaceUser.user.password = '';
return {
...workspaceUser.user,
role: workspaceUser.role,
};
});
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(users, paginationMeta);
}
async getUserRoleInWorkspace(
userId: string,
workspaceId: string,
): Promise<string> {
const workspaceUser = await this.findAndValidateWorkspaceUser(
userId,
workspaceId,
);
return workspaceUser.role ? workspaceUser.role : null;
}
async findWorkspaceUser(
userId: string,
workspaceId: string,
): Promise<WorkspaceUser> {
return await this.workspaceUserRepository.findOneBy({
userId,
): Promise<User> {
return await this.userRepository.findOneBy({
email: email,
workspaceId,
});
}
@ -203,13 +87,13 @@ export class WorkspaceUserService {
async findAndValidateWorkspaceUser(
userId: string,
workspaceId: string,
): Promise<WorkspaceUser> {
const workspaceUser = await this.findWorkspaceUser(userId, workspaceId);
): Promise<User> {
const user = await this.findWorkspaceUser(userId, workspaceId);
if (!workspaceUser) {
if (!user) {
throw new BadRequestException('Workspace member not found');
}
return workspaceUser;
return user;
}
}

View File

@ -1,32 +1,31 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateWorkspaceDto } from '../dto/create-workspace.dto';
import { WorkspaceRepository } from '../repositories/workspace.repository';
import { WorkspaceUserRepository } from '../repositories/workspace-user.repository';
import {
WorkspaceUser,
WorkspaceUserRole,
} from '../entities/workspace-user.entity';
import { Workspace } from '../entities/workspace.entity';
import { plainToInstance } from 'class-transformer';
import { v4 as uuid } from 'uuid';
import { v4 as uuidv4 } from 'uuid';
import { UpdateWorkspaceDto } from '../dto/update-workspace.dto';
import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto';
import { SpaceService } from '../../space/space.service';
import { PaginationOptions } from '../../../helpers/pagination/pagination-options';
import { PaginationMetaDto } from '../../../helpers/pagination/pagination-meta-dto';
import { PaginatedResult } from '../../../helpers/pagination/paginated-result';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../../helpers/db.helper';
import { CreateSpaceDto } from '../../space/dto/create-space.dto';
import { WorkspaceUserService } from './workspace-user.service';
import { UserRepository } from '../../user/repositories/user.repository';
import { SpaceRole, UserRole } from '../../../helpers/types/permission';
import { User } from '../../user/entities/user.entity';
import { EnvironmentService } from '../../../environment/environment.service';
import { Space } from '../../space/entities/space.entity';
@Injectable()
export class WorkspaceService {
constructor(
private workspaceRepository: WorkspaceRepository,
private workspaceUserRepository: WorkspaceUserRepository,
private userRepository: UserRepository,
private spaceService: SpaceService,
private workspaceUserService: WorkspaceUserService,
private environmentService: EnvironmentService,
private dataSource: DataSource,
) {}
@ -35,115 +34,118 @@ export class WorkspaceService {
return this.workspaceRepository.findById(workspaceId);
}
async save(workspace: Workspace) {
return this.workspaceRepository.save(workspace);
}
async getWorkspaceInfo(workspaceId: string): Promise<Workspace> {
const space = await this.workspaceRepository
.createQueryBuilder('workspace')
.where('workspace.id = :workspaceId', { workspaceId })
.loadRelationCountAndMap(
'workspace.userCount',
'workspace.users',
'workspaceUsers',
)
.getOne();
async createOrJoinWorkspace(
userId,
createWorkspaceDto?: CreateWorkspaceDto,
manager?: EntityManager,
) {
await transactionWrapper(
async (manager: EntityManager) => {
const workspaceCount = await manager
.createQueryBuilder(Workspace, 'workspace')
.getCount();
if (!space) {
throw new NotFoundException('Workspace not found');
}
if (workspaceCount === 0) {
// create first workspace and add user to workspace as owner
const createdWorkspace = await this.create(
userId,
createWorkspaceDto ?? null,
manager,
);
await this.workspaceUserService.addUserToWorkspace(
userId,
createdWorkspace.id,
WorkspaceUserRole.OWNER,
manager,
);
// create default space and add user to it too.
const createdSpace = await this.spaceService.create(
userId,
createdWorkspace.id,
{ name: 'General' } as CreateSpaceDto,
manager,
);
await this.spaceService.addUserToSpace(
userId,
createdSpace.id,
WorkspaceUserRole.OWNER,
createdWorkspace.id,
manager,
);
createdWorkspace.defaultSpaceId = createdSpace.id;
await manager.save(createdWorkspace);
} else {
// limited to single workspace
// fetch the oldest workspace and add user to it
const firstWorkspace = await manager.find(Workspace, {
order: {
createdAt: 'ASC',
},
take: 1,
});
// add user to workspace and default space
await this.workspaceUserService.addUserToWorkspace(
userId,
firstWorkspace[0].id,
WorkspaceUserRole.MEMBER,
manager,
);
await this.spaceService.addUserToSpace(
userId,
firstWorkspace[0].defaultSpaceId,
WorkspaceUserRole.MEMBER,
firstWorkspace[0].id,
manager,
);
}
},
this.dataSource,
manager,
);
return space;
}
async create(
userId: string,
createWorkspaceDto?: CreateWorkspaceDto,
user: User,
createWorkspaceDto: CreateWorkspaceDto,
manager?: EntityManager,
): Promise<Workspace> {
let workspace: Workspace;
await transactionWrapper(
return await transactionWrapper(
async (manager) => {
if (createWorkspaceDto) {
workspace = plainToInstance(Workspace, createWorkspaceDto);
} else {
workspace = new Workspace();
}
workspace.inviteCode = uuid();
workspace.creatorId = userId;
//if (workspace.name && !workspace.hostname?.trim()) {
// workspace.hostname = generateHostname(createWorkspaceDto.name);
// }
let workspace = new Workspace();
workspace.name = createWorkspaceDto.name;
workspace.hostname = createWorkspaceDto?.hostname;
workspace.description = createWorkspaceDto.description;
workspace.inviteCode = uuidv4();
workspace.creatorId = user.id;
workspace = await manager.save(workspace);
user.workspaceId = workspace.id;
user.role = UserRole.OWNER;
await manager.save(user);
// create default space
const spaceData: CreateSpaceDto = {
name: 'General',
};
// create default space
const createdSpace = await this.spaceService.create(
user.id,
workspace.id,
spaceData,
manager,
);
// and add user to it too.
await this.spaceService.addUserToSpace(
user.id,
createdSpace.id,
SpaceRole.OWNER,
workspace.id,
manager,
);
workspace.defaultSpaceId = createdSpace.id;
await manager.save(workspace);
return workspace;
},
this.dataSource,
manager,
);
}
return workspace;
async addUserToWorkspace(
user: User,
workspaceId,
assignedRole?: UserRole,
manager?: EntityManager,
): Promise<Workspace> {
return await transactionWrapper(
async (manager: EntityManager) => {
const workspace = await manager.findOneBy(Workspace, {
id: workspaceId,
});
if (!workspace) {
throw new BadRequestException('Workspace does not exist');
}
user.role = assignedRole ?? workspace.defaultRole;
user.workspaceId = workspace.id;
await manager.save(user);
const space = await manager.findOneBy(Space, {
id: workspace.defaultSpaceId,
workspaceId,
});
if (!space) {
throw new NotFoundException('Space not found');
}
// add user to default space
await this.spaceService.addUserToSpace(
user.id,
space.id,
space.defaultRole,
workspace.id,
manager,
);
return workspace;
},
this.dataSource,
manager,
);
}
async update(
@ -178,42 +180,4 @@ export class WorkspaceService {
// remove all existing users from workspace
// delete workspace
}
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
const userWorkspace = await this.workspaceUserRepository.findOne({
relations: ['workspace'],
where: { userId: userId },
order: {
createdAt: 'ASC',
},
});
if (!userWorkspace) {
throw new NotFoundException('No workspace found for this user');
}
const { workspace, ...workspaceUser } = userWorkspace;
return { ...workspace, workspaceUser } as Workspace;
}
async getUserWorkspaces(
userId: string,
paginationOptions: PaginationOptions,
): Promise<PaginatedResult<Workspace>> {
const [workspaces, count] = await this.workspaceUserRepository.findAndCount(
{
where: { userId: userId },
relations: ['workspace'],
take: paginationOptions.limit,
skip: paginationOptions.skip,
},
);
const userWorkspaces = workspaces.map(
(userWorkspace: WorkspaceUser) => userWorkspace.workspace,
);
const paginationMeta = new PaginationMetaDto({ count, paginationOptions });
return new PaginatedResult(userWorkspaces, paginationMeta);
}
}

View File

@ -4,20 +4,17 @@ import { WorkspaceController } from './controllers/workspace.controller';
import { WorkspaceRepository } from './repositories/workspace.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from './entities/workspace.entity';
import { WorkspaceUser } from './entities/workspace-user.entity';
import { WorkspaceInvitation } from './entities/workspace-invitation.entity';
import { WorkspaceUserRepository } from './repositories/workspace-user.repository';
import { AuthModule } from '../auth/auth.module';
import { SpaceModule } from '../space/space.module';
import { WorkspaceUserService } from './services/workspace-user.service';
import { WorkspaceInvitationService } from './services/workspace-invitation.service';
import { WorkspaceInvitationRepository } from './repositories/workspace-invitation.repository';
import { WorkspaceUserService } from './services/workspace-user.service';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]),
AuthModule,
SpaceModule,
TypeOrmModule.forFeature([Workspace, WorkspaceInvitation]),
SpaceModule, UserModule
],
controllers: [WorkspaceController],
providers: [
@ -25,9 +22,8 @@ import { WorkspaceInvitationRepository } from './repositories/workspace-invitati
WorkspaceUserService,
WorkspaceInvitationService,
WorkspaceRepository,
WorkspaceUserRepository,
WorkspaceInvitationRepository,
],
exports: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],
exports: [WorkspaceService, WorkspaceRepository],
})
export class WorkspaceModule {}

View File

@ -1,5 +0,0 @@
export function generateHostname(name: string): string {
let hostname = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
hostname = hostname.substring(0, 30);
return hostname;
}