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

@ -1,23 +0,0 @@
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
MinLength,
} from 'class-validator';
export class CreateUserDto {
@IsOptional()
@MinLength(3)
@IsString()
name: string;
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@MinLength(8)
@IsString()
password: string;
}

View File

@ -1,5 +1,5 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { CreateUserDto } from '../../auth/dto/create-user.dto';
import { IsOptional, IsString } from 'class-validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {

View File

@ -3,19 +3,22 @@ import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Workspace } from '../../workspace/entities/workspace.entity';
import { WorkspaceUser } from '../../workspace/entities/workspace-user.entity';
import { Page } from '../../page/entities/page.entity';
import { Comment } from '../../comment/entities/comment.entity';
import { Space } from '../../space/entities/space.entity';
import { SpaceUser } from '../../space/entities/space-user.entity';
import { Group } from '../../group/entities/group.entity';
@Entity('users')
@Unique(['email', 'workspaceId'])
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@ -23,7 +26,7 @@ export class User {
@Column({ length: 255, nullable: true })
name: string;
@Column({ length: 255, unique: true })
@Column({ length: 255 })
email: string;
@Column({ nullable: true })
@ -35,6 +38,15 @@ export class User {
@Column({ nullable: true })
avatarUrl: string;
@Column({ nullable: true, length: 100 })
role: string;
@Column({ nullable: true })
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.users)
workspace: Workspace;
@Column({ length: 100, nullable: true })
locale: string;
@ -56,11 +68,8 @@ export class User {
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Workspace, (workspace) => workspace.creator)
workspaces: Workspace[];
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.user)
workspaceUsers: WorkspaceUser[];
@OneToMany(() => Group, (group) => group.creator)
groups: Group[];
@OneToMany(() => Page, (page) => page.creator)
createdPages: Page[];
@ -69,10 +78,10 @@ export class User {
comments: Comment[];
@OneToMany(() => Space, (space) => space.creator)
spaces: Space[];
createdSpaces: Space[];
@OneToMany(() => SpaceUser, (spaceUser) => spaceUser.user)
spaceUsers: SpaceUser[];
spaces: SpaceUser[];
toJSON() {
delete this.password;
@ -85,8 +94,3 @@ export class User {
this.password = await bcrypt.hash(this.password, saltRounds);
}
}
export type UserRole = {
role: string;
};
export type UserWithRole = User & UserRole;

View File

@ -7,11 +7,29 @@ export class UserRepository extends Repository<User> {
constructor(private dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
async findByEmail(email: string) {
return this.findOneBy({ email: email });
async findByEmail(email: string): Promise<User> {
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
return await queryBuilder.where('user.email = :email', { email }).getOne();
}
async findById(userId: string) {
return this.findOneBy({ id: userId });
async findById(userId: string): Promise<User> {
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
return await queryBuilder.where('user.id = :id', { id: userId }).getOne();
}
async findOneByEmail(email: string, workspaceId: string): Promise<User> {
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
return await queryBuilder
.where('user.email = :email', { email })
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
.getOne();
}
async findOneByIdx(userId: string, workspaceId: string): Promise<User> {
const queryBuilder = this.dataSource.createQueryBuilder(User, 'user');
return await queryBuilder
.where('user.id = :id', { id: userId })
.andWhere('user.workspaceId = :workspaceId', { workspaceId })
.getOne();
}
}

View File

@ -1,20 +1,19 @@
import {
Body,
Controller,
UseGuards,
HttpCode,
HttpStatus,
UnauthorizedException,
Post,
Body,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { User } from './entities/user.entity';
import { Workspace } from '../workspace/entities/workspace.entity';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
@UseGuards(JwtGuard)
@UseGuards(JwtAuthGuard)
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@ -28,16 +27,13 @@ export class UserController {
throw new UnauthorizedException('Invalid user');
}
return { user };
return user;
}
@HttpCode(HttpStatus.OK)
@Post('info')
async getUserInfo(@AuthUser() user: User) {
const data: { workspace: Workspace; user: User } =
await this.userService.getUserInstance(user.id);
return data;
return await this.userService.getUserInstance(user.id);
}
@HttpCode(HttpStatus.OK)

View File

@ -1,15 +1,12 @@
import { Global, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRepository } from './repositories/user.repository';
import { AuthModule } from '../auth/auth.module';
import { WorkspaceModule } from '../workspace/workspace.module';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([User]), AuthModule, WorkspaceModule],
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService, UserRepository],

View File

@ -3,7 +3,7 @@ import { UserService } from './user.service';
import { UserRepository } from './repositories/user.repository';
import { User } from './entities/user.entity';
import { BadRequestException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { CreateUserDto } from '../auth/dto/create-user.dto';
describe('UserService', () => {
let userService: UserService;
@ -63,7 +63,7 @@ describe('UserService', () => {
lastLoginIp: null,
};
userRepository.findByEmail.mockResolvedValue(undefined);
//userRepository.findByEmail.mockResolvedValue(undefined);
userRepository.save.mockResolvedValue(savedUser);
const result = await userService.create(createUserDto);

View File

@ -3,92 +3,31 @@ import {
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';
import { UserRepository } from './repositories/user.repository';
import { plainToInstance } from 'class-transformer';
import * as bcrypt from 'bcrypt';
import { WorkspaceService } from '../workspace/services/workspace.service';
import { DataSource, EntityManager } from 'typeorm';
import { transactionWrapper } from '../../helpers/db.helper';
import { CreateWorkspaceDto } from '../workspace/dto/create-workspace.dto';
import { Workspace } from '../workspace/entities/workspace.entity';
export type UserWithWorkspace = {
user: User;
workspace: Workspace;
};
@Injectable()
export class UserService {
constructor(
private userRepository: UserRepository,
private workspaceService: WorkspaceService,
private dataSource: DataSource,
) {}
async create(
createUserDto: CreateUserDto,
manager?: EntityManager,
): Promise<User> {
let user: User;
const existingUser: User = await this.findByEmail(createUserDto.email);
if (existingUser) {
throw new BadRequestException('A user with this email already exists');
}
await transactionWrapper(
async (manager: EntityManager) => {
user = plainToInstance(User, createUserDto);
user.locale = 'en';
user.lastLoginAt = new Date();
user.name = createUserDto.email.split('@')[0];
user = await manager.save(User, user);
const createWorkspaceDto: CreateWorkspaceDto = {
name: 'My Workspace',
};
await this.workspaceService.createOrJoinWorkspace(
user.id,
createWorkspaceDto,
manager,
);
},
this.dataSource,
manager,
);
return user;
}
async getUserInstance(userId: string): Promise<UserWithWorkspace> {
const user: User = await this.findById(userId);
if (!user) {
throw new NotFoundException('User not found');
}
let workspace;
try {
workspace = await this.workspaceService.getUserCurrentWorkspace(userId);
} catch (error) {
//console.log(error);
}
return { user, workspace };
}
constructor(private userRepository: UserRepository) {}
async findById(userId: string) {
return this.userRepository.findById(userId);
}
async findByEmail(email: string) {
return this.userRepository.findByEmail(email);
async getUserInstance(userId: string): Promise<any> {
const user: User = await this.userRepository.findOne({
relations: ['workspace'],
where: {
id: userId,
},
});
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
async update(userId: string, updateUserDto: UpdateUserDto) {
@ -101,6 +40,7 @@ export class UserService {
user.name = updateUserDto.name;
}
// todo need workspace scoping
if (updateUserDto.email && user.email != updateUserDto.email) {
if (await this.userRepository.findByEmail(updateUserDto.email)) {
throw new BadRequestException('A user with this email already exists');
@ -114,11 +54,4 @@ export class UserService {
return this.userRepository.save(user);
}
async compareHash(
plainPassword: string,
passwordHash: string,
): Promise<boolean> {
return await bcrypt.compare(plainPassword, passwordHash);
}
}