switch to nx monorepo

This commit is contained in:
Philipinho
2024-01-09 18:58:26 +01:00
parent e1bb2632b8
commit 093e634c0b
273 changed files with 11419 additions and 31 deletions

View File

@ -0,0 +1,23 @@
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

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

View File

@ -0,0 +1,79 @@
import {
BeforeInsert,
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
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';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 255, nullable: true })
name: string;
@Column({ length: 255, unique: true })
email: string;
@Column({ nullable: true })
emailVerifiedAt: Date;
@Column()
password: string;
@Column({ nullable: true })
avatarUrl: string;
@Column({ length: 100, nullable: true })
locale: string;
@Column({ length: 300, nullable: true })
timezone: string;
@Column({ type: 'jsonb', nullable: true })
settings: any;
@Column({ nullable: true })
lastLoginAt: Date;
@Column({ length: 100, nullable: true })
lastLoginIp: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Workspace, (workspace) => workspace.creator)
workspaces: Workspace[];
@OneToMany(() => WorkspaceUser, (workspaceUser) => workspaceUser.user)
workspaceUsers: WorkspaceUser[];
@OneToMany(() => Page, (page) => page.creator)
createdPages: Page[];
@OneToMany(() => Comment, (comment) => comment.creator)
comments: Comment[];
toJSON() {
delete this.password;
return this;
}
@BeforeInsert()
async hashPassword() {
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
}
}

View File

@ -0,0 +1,17 @@
import { DataSource, Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository extends Repository<User> {
constructor(private dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
async findByEmail(email: string) {
return this.findOneBy({ email: email });
}
async findById(userId: string) {
return this.findOneBy({ id: userId });
}
}

View File

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,58 @@
import {
Controller,
Get,
UseGuards,
HttpCode,
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')
export class UserController {
constructor(private readonly userService: UserService) {}
@HttpCode(HttpStatus.OK)
@Get('me')
async getUser(@Req() req: FastifyRequest) {
const jwtPayload = req['user'];
const user: User = await this.userService.findById(jwtPayload.sub);
if (!user) {
throw new UnauthorizedException('Invalid user');
}
return { user };
}
@HttpCode(HttpStatus.OK)
@Get('info')
async getUserInfo(@Req() req: FastifyRequest) {
const jwtPayload = req['user'];
const data: { workspace: Workspace; user: User } =
await this.userService.getUserInstance(jwtPayload.sub);
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

@ -0,0 +1,20 @@
import { forwardRef, 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';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
forwardRef(() => AuthModule),
WorkspaceModule,
],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService, UserRepository],
})
export class UserModule {}

View File

@ -0,0 +1,77 @@
import { Test, TestingModule } from '@nestjs/testing';
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';
describe('UserService', () => {
let userService: UserService;
let userRepository: any;
const mockUserRepository = () => ({
findByEmail: jest.fn(),
save: jest.fn(),
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: UserRepository,
useFactory: mockUserRepository,
},
],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get<UserRepository>(UserRepository);
});
it('should be defined', () => {
expect(userService).toBeDefined();
expect(userRepository).toBeDefined();
});
describe('create', () => {
const createUserDto: CreateUserDto = {
name: 'John Doe',
email: 'test@test.com',
password: 'password',
};
it('should throw an error if a user with this email already exists', async () => {
userRepository.findByEmail.mockResolvedValue(new User());
await expect(userService.create(createUserDto)).rejects.toThrow(
BadRequestException,
);
});
it('should create the user if it does not already exist', async () => {
const savedUser = {
...createUserDto,
id: expect.any(String),
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
lastLoginAt: expect.any(Date),
locale: 'en',
emailVerifiedAt: null,
avatar_url: null,
timezone: null,
settings: null,
lastLoginIp: null,
};
userRepository.findByEmail.mockResolvedValue(undefined);
userRepository.save.mockResolvedValue(savedUser);
const result = await userService.create(createUserDto);
expect(result).toMatchObject(savedUser);
expect(userRepository.save).toHaveBeenCalledWith(
expect.objectContaining(createUserDto),
);
});
});
});

View File

@ -0,0 +1,85 @@
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';
import { UserRepository } from './repositories/user.repository';
import { plainToInstance } from 'class-transformer';
import * as bcrypt from 'bcrypt';
import { WorkspaceService } from '../workspace/services/workspace.service';
import { Workspace } from '../workspace/entities/workspace.entity';
@Injectable()
export class UserService {
constructor(
private userRepository: UserRepository,
private workspaceService: WorkspaceService,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const existingUser: User = await this.findByEmail(createUserDto.email);
if (existingUser) {
throw new BadRequestException('A user with this email already exists');
}
let user: User = plainToInstance(User, createUserDto);
user.locale = 'en';
user.lastLoginAt = new Date();
user = await this.userRepository.save(user);
//TODO: only create workspace if it is not a signup to an existing workspace
await this.workspaceService.create(user.id);
return user;
}
async getUserInstance(userId: string) {
const user: User = await this.findById(userId);
const workspace: Workspace =
await this.workspaceService.getUserCurrentWorkspace(userId);
return { user, workspace };
}
async findById(userId: string) {
return this.userRepository.findById(userId);
}
async findByEmail(email: string) {
return this.userRepository.findByEmail(email);
}
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;
}
if (updateUserDto.avatarUrl) {
user.avatarUrl = updateUserDto.avatarUrl;
}
return this.userRepository.save(user);
}
async compareHash(
plainPassword: string,
passwordHash: string,
): Promise<boolean> {
return await bcrypt.compare(plainPassword, passwordHash);
}
}