Add new user and workspace endpoints

* add account update endpoint to user module
* add membership management endpoints to workspace module
This commit is contained in:
Philipinho
2023-09-04 19:10:48 +01:00
parent f2cbd0c19b
commit 6af5c9a9ca
12 changed files with 294 additions and 15 deletions

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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,
);
}
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class AddWorkspaceUserDto {
@IsNotEmpty()
@IsUUID()
userId: string;
@IsNotEmpty()
@IsString()
role: string;
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class DeleteWorkspaceDto {
@IsString()
workspaceId: string;
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsUUID } from 'class-validator';
export class RemoveWorkspaceUserDto {
@IsNotEmpty()
@IsUUID()
userId: string;
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class UpdateWorkspaceUserRoleDto {
@IsNotEmpty()
@IsUUID()
userId: string;
@IsNotEmpty()
@IsString()
role: string;
}

View File

@ -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' })

View File

@ -7,6 +7,7 @@ import {
ManyToOne,
OneToMany,
JoinColumn,
ManyToMany,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { WorkspaceUser } from './workspace-user.entity';

View File

@ -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<Workspace> {
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<void> {
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<Workspace> {
return await this.workspaceRepository.findById(workspaceId);
}
async getUserCurrentWorkspace(
userId: string,
workspaceId?: string,
): Promise<Workspace> {
async getUserCurrentWorkspace(userId: string): Promise<Workspace> {
// 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<Workspace[]> {
async getUserWorkspaces(userId: string): Promise<Workspace[]> {
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 };
}
}

View File

@ -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],