From 6af5c9a9ca5105d8f0a7ae60db6de09f2c675b7f Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:10:48 +0100 Subject: [PATCH] Add new user and workspace endpoints * add account update endpoint to user module * add membership management endpoints to workspace module --- server/src/core/user/entities/user.entity.ts | 6 +- server/src/core/user/user.controller.ts | 14 +++ server/src/core/user/user.service.ts | 29 ++++- .../controllers/workspace.controller.ts | 116 +++++++++++++++++- .../workspace/dto/add-workspace-user.dto.ts | 11 ++ .../workspace/dto/delete-workspace.dto.ts | 6 + .../dto/remove-workspace-user.dto.ts | 7 ++ .../dto/update-workspace-user-role.dto.ts | 11 ++ .../entities/workspace-user.entity.ts | 2 +- .../workspace/entities/workspace.entity.ts | 1 + .../workspace/services/workspace.service.ts | 104 +++++++++++++++- server/src/core/workspace/workspace.module.ts | 2 + 12 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 server/src/core/workspace/dto/add-workspace-user.dto.ts create mode 100644 server/src/core/workspace/dto/delete-workspace.dto.ts create mode 100644 server/src/core/workspace/dto/remove-workspace-user.dto.ts create mode 100644 server/src/core/workspace/dto/update-workspace-user-role.dto.ts diff --git a/server/src/core/user/entities/user.entity.ts b/server/src/core/user/entities/user.entity.ts index c480a3ac..6c400497 100644 --- a/server/src/core/user/entities/user.entity.ts +++ b/server/src/core/user/entities/user.entity.ts @@ -3,6 +3,8 @@ import { Column, CreateDateColumn, Entity, + JoinTable, + ManyToMany, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, @@ -57,10 +59,10 @@ export class User { workspaces: Workspace[]; @OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.user) - workspaceUser: WorkspaceUser[]; + workspaceUsers: WorkspaceUser[]; @OneToMany(() => Page, (page) => page.creator) - createdPages; + createdPages: Page[]; toJSON() { delete this.password; diff --git a/server/src/core/user/user.controller.ts b/server/src/core/user/user.controller.ts index 1fa6f22d..fc434cb8 100644 --- a/server/src/core/user/user.controller.ts +++ b/server/src/core/user/user.controller.ts @@ -6,12 +6,15 @@ import { HttpStatus, Req, UnauthorizedException, + Post, + Body, } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtGuard } from '../auth/guards/JwtGuard'; import { FastifyRequest } from 'fastify'; import { User } from './entities/user.entity'; import { Workspace } from '../workspace/entities/workspace.entity'; +import { UpdateUserDto } from './dto/update-user.dto'; @UseGuards(JwtGuard) @Controller('user') @@ -41,4 +44,15 @@ export class UserController { return data; } + + @HttpCode(HttpStatus.OK) + @Post('update') + async updateUser( + @Req() req: FastifyRequest, + @Body() updateUserDto: UpdateUserDto, + ) { + const jwtPayload = req['user']; + + return this.userService.update(jwtPayload.sub, updateUserDto); + } } diff --git a/server/src/core/user/user.service.ts b/server/src/core/user/user.service.ts index 3690253d..0c2bb06e 100644 --- a/server/src/core/user/user.service.ts +++ b/server/src/core/user/user.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @@ -6,8 +10,7 @@ import { UserRepository } from './repositories/user.repository'; import { plainToInstance } from 'class-transformer'; import * as bcrypt from 'bcrypt'; import { WorkspaceService } from '../workspace/services/workspace.service'; -import { CreateWorkspaceDto } from '../workspace/dto/create-workspace.dto'; -import { Workspace } from "../workspace/entities/workspace.entity"; +import { Workspace } from '../workspace/entities/workspace.entity'; @Injectable() export class UserService { @@ -49,8 +52,24 @@ export class UserService { return this.userRepository.findByEmail(email); } - async update(id: number, updateUserDto: UpdateUserDto) { - return `This action updates a #${id} user`; + async update(userId: string, updateUserDto: UpdateUserDto) { + const user = await this.userRepository.findById(userId); + if (!user) { + throw new NotFoundException('User not found'); + } + + if (updateUserDto.name) { + user.name = updateUserDto.name; + } + + if (updateUserDto.email && user.email != updateUserDto.email) { + if (await this.userRepository.findByEmail(updateUserDto.email)) { + throw new BadRequestException('A user with this email already exists'); + } + user.email = updateUserDto.email; + } + + return this.userRepository.save(user); } async compareHash( diff --git a/server/src/core/workspace/controllers/workspace.controller.ts b/server/src/core/workspace/controllers/workspace.controller.ts index c935d18e..b53655dc 100644 --- a/server/src/core/workspace/controllers/workspace.controller.ts +++ b/server/src/core/workspace/controllers/workspace.controller.ts @@ -1,7 +1,121 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { WorkspaceService } from '../services/workspace.service'; +import { FastifyRequest } from 'fastify'; +import { JwtGuard } from '../../auth/guards/JwtGuard'; +import { UpdateWorkspaceDto } from '../dto/update-workspace.dto'; +import { CreateWorkspaceDto } from '../dto/create-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'; +@UseGuards(JwtGuard) @Controller('workspace') export class WorkspaceController { constructor(private readonly workspaceService: WorkspaceService) {} + + @HttpCode(HttpStatus.OK) + @Post('create') + async createWorkspace( + @Req() req: FastifyRequest, + @Body() createWorkspaceDto: CreateWorkspaceDto, + ) { + const jwtPayload = req['user']; + const userId = jwtPayload.sub; + return this.workspaceService.create(userId, createWorkspaceDto); + } + + @HttpCode(HttpStatus.OK) + @Post('update') + async updateWorkspace( + @Req() req: FastifyRequest, + @Body() updateWorkspaceDto: UpdateWorkspaceDto, + ) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + + return this.workspaceService.update(workspaceId, updateWorkspaceDto); + } + + @HttpCode(HttpStatus.OK) + @Post('delete') + async deleteWorkspace(@Body() deleteWorkspaceDto: DeleteWorkspaceDto, + ) { + return this.workspaceService.delete(deleteWorkspaceDto); + } + + @HttpCode(HttpStatus.OK) + @Get('members') + async getWorkspaceMembers(@Req() req: FastifyRequest) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + + return this.workspaceService.getWorkspaceUsers(workspaceId); + } + + @HttpCode(HttpStatus.OK) + @Post('member') + async addWorkspaceMember( + @Req() req: FastifyRequest, + @Body() addWorkspaceUserDto: AddWorkspaceUserDto, + ) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + + return this.workspaceService.addUserToWorkspace( + addWorkspaceUserDto.userId, + workspaceId, + addWorkspaceUserDto.role, + ); + } + + @HttpCode(HttpStatus.OK) + @Delete('member') + async removeWorkspaceMember( + @Req() req: FastifyRequest, + @Body() removeWorkspaceUserDto: RemoveWorkspaceUserDto, + ) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + + return this.workspaceService.removeUserFromWorkspace( + removeWorkspaceUserDto.userId, + workspaceId, + ); + } + + @HttpCode(HttpStatus.OK) + @Post('member/role') + async updateWorkspaceMemberRole( + @Req() req: FastifyRequest, + @Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto, + ) { + const jwtPayload = req['user']; + const workspaceId = ( + await this.workspaceService.getUserCurrentWorkspace(jwtPayload.sub) + ).id; + + return this.workspaceService.updateWorkspaceUserRole( + workspaceUserRoleDto, + workspaceId, + ); + } } diff --git a/server/src/core/workspace/dto/add-workspace-user.dto.ts b/server/src/core/workspace/dto/add-workspace-user.dto.ts new file mode 100644 index 00000000..4fdeb282 --- /dev/null +++ b/server/src/core/workspace/dto/add-workspace-user.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class AddWorkspaceUserDto { + @IsNotEmpty() + @IsUUID() + userId: string; + + @IsNotEmpty() + @IsString() + role: string; +} diff --git a/server/src/core/workspace/dto/delete-workspace.dto.ts b/server/src/core/workspace/dto/delete-workspace.dto.ts new file mode 100644 index 00000000..0696f508 --- /dev/null +++ b/server/src/core/workspace/dto/delete-workspace.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class DeleteWorkspaceDto { + @IsString() + workspaceId: string; +} diff --git a/server/src/core/workspace/dto/remove-workspace-user.dto.ts b/server/src/core/workspace/dto/remove-workspace-user.dto.ts new file mode 100644 index 00000000..38ef4504 --- /dev/null +++ b/server/src/core/workspace/dto/remove-workspace-user.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class RemoveWorkspaceUserDto { + @IsNotEmpty() + @IsUUID() + userId: string; +} diff --git a/server/src/core/workspace/dto/update-workspace-user-role.dto.ts b/server/src/core/workspace/dto/update-workspace-user-role.dto.ts new file mode 100644 index 00000000..2dc52a3b --- /dev/null +++ b/server/src/core/workspace/dto/update-workspace-user-role.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class UpdateWorkspaceUserRoleDto { + @IsNotEmpty() + @IsUUID() + userId: string; + + @IsNotEmpty() + @IsString() + role: string; +} diff --git a/server/src/core/workspace/entities/workspace-user.entity.ts b/server/src/core/workspace/entities/workspace-user.entity.ts index b14c3b89..bdcf7e41 100644 --- a/server/src/core/workspace/entities/workspace-user.entity.ts +++ b/server/src/core/workspace/entities/workspace-user.entity.ts @@ -20,7 +20,7 @@ export class WorkspaceUser { @Column() userId: string; - @ManyToOne(() => User, (user) => user.workspaceUser, { + @ManyToOne(() => User, (user) => user.workspaceUsers, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'userId' }) diff --git a/server/src/core/workspace/entities/workspace.entity.ts b/server/src/core/workspace/entities/workspace.entity.ts index 10d8def3..7726b2f3 100644 --- a/server/src/core/workspace/entities/workspace.entity.ts +++ b/server/src/core/workspace/entities/workspace.entity.ts @@ -7,6 +7,7 @@ import { ManyToOne, OneToMany, JoinColumn, + ManyToMany, } from 'typeorm'; import { User } from '../../user/entities/user.entity'; import { WorkspaceUser } from './workspace-user.entity'; diff --git a/server/src/core/workspace/services/workspace.service.ts b/server/src/core/workspace/services/workspace.service.ts index 0be59bec..ce9a9950 100644 --- a/server/src/core/workspace/services/workspace.service.ts +++ b/server/src/core/workspace/services/workspace.service.ts @@ -1,4 +1,8 @@ -import { Injectable } 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'; @@ -7,6 +11,9 @@ import { Workspace } from '../entities/workspace.entity'; import { plainToInstance } from 'class-transformer'; import { v4 as uuid } from 'uuid'; import { generateHostname } from '../workspace.util'; +import { UpdateWorkspaceDto } from '../dto/update-workspace.dto'; +import { DeleteWorkspaceDto } from '../dto/delete-workspace.dto'; +import { UpdateWorkspaceUserRoleDto } from '../dto/update-workspace-user-role.dto'; @Injectable() export class WorkspaceService { @@ -40,6 +47,33 @@ export class WorkspaceService { return workspace; } + async update( + workspaceId: string, + updateWorkspaceDto: UpdateWorkspaceDto, + ): Promise { + const workspace = await this.workspaceRepository.findById(workspaceId); + if (!workspace) { + throw new NotFoundException('Workspace not found'); + } + + if (updateWorkspaceDto.name) { + workspace.name = updateWorkspaceDto.name; + } + + return this.workspaceRepository.save(workspace); + } + + async delete(deleteWorkspaceDto: DeleteWorkspaceDto) { + const workspace = await this.workspaceRepository.findById( + deleteWorkspaceDto.workspaceId, + ); + if (!workspace) { + throw new NotFoundException('Workspace not found'); + } + + return 0; + } + async addUserToWorkspace( userId: string, workspaceId: string, @@ -53,14 +87,51 @@ export class WorkspaceService { return this.workspaceUserRepository.save(workspaceUser); } + async updateWorkspaceUserRole( + workspaceUserRoleDto: UpdateWorkspaceUserRoleDto, + workspaceId: string, + ) { + const workspaceUser = await this.workspaceUserRepository.findOne({ + where: { userId: workspaceUserRoleDto.userId, workspaceId: workspaceId }, + }); + + if (!workspaceUser) { + throw new BadRequestException('user is not a member of this workspace'); + } + + if (workspaceUser.role === workspaceUserRoleDto.role) { + return workspaceUser; + } + + workspaceUser.role = workspaceUserRoleDto.role; + // if there is only one workspace owner, prevent the role change + + return this.workspaceUserRepository.save(workspaceUser); + } + + async removeUserFromWorkspace( + userId: string, + workspaceId: string, + ): Promise { + const workspaceUser = await this.workspaceUserRepository.findOne({ + where: { userId, workspaceId }, + }); + + if (!workspaceUser) { + throw new BadRequestException('User is not a member of this workspace'); + } + + await this.workspaceUserRepository.delete({ + userId, + workspaceId, + }); + } + async findById(workspaceId: string): Promise { return await this.workspaceRepository.findById(workspaceId); } - async getUserCurrentWorkspace( - userId: string, - workspaceId?: string, - ): Promise { + async getUserCurrentWorkspace(userId: string): Promise { // TODO: use workspaceId and fetch workspace based on the id // we currently assume the user belongs to one workspace const userWorkspace = await this.workspaceUserRepository.findOne({ @@ -71,7 +142,7 @@ export class WorkspaceService { return userWorkspace.workspace; } - async userWorkspaces(userId: string): Promise { + async getUserWorkspaces(userId: string): Promise { const workspaces = await this.workspaceUserRepository.find({ where: { userId: userId }, relations: ['workspace'], @@ -81,4 +152,25 @@ export class WorkspaceService { (userWorkspace: WorkspaceUser) => userWorkspace.workspace, ); } + + async getWorkspaceUsers(workspaceId: string) { + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + relations: ['workspaceUsers', 'workspaceUsers.user'], + }); + + if (!workspace) { + throw new BadRequestException('Invalid workspace'); + } + + const users = workspace.workspaceUsers.map((workspaceUser) => { + workspaceUser.user.password = ''; + return { + ...workspaceUser.user, + workspaceRole: workspaceUser.role, + }; + }); + + return { users }; + } } diff --git a/server/src/core/workspace/workspace.module.ts b/server/src/core/workspace/workspace.module.ts index 9f45dee0..87ff54ff 100644 --- a/server/src/core/workspace/workspace.module.ts +++ b/server/src/core/workspace/workspace.module.ts @@ -7,10 +7,12 @@ 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'; @Module({ imports: [ TypeOrmModule.forFeature([Workspace, WorkspaceUser, WorkspaceInvitation]), + AuthModule, ], controllers: [WorkspaceController], providers: [WorkspaceService, WorkspaceRepository, WorkspaceUserRepository],