Skip to content

Commit

Permalink
Merge pull request #20 from bakaqc/feature/implement-crud-api-user
Browse files Browse the repository at this point in the history
backend: implement crud api user
  • Loading branch information
bakaqc authored Dec 21, 2024
2 parents d51c415 + 47d7323 commit 03da857
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 2 deletions.
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "evora-backend",
"version": "0.0.1",
"version": "0.0.2",
"description": "Evora connects customers with event organizers quickly through detailed online booking.",
"repository": "https://github.com/bakaqc/evora-17c",
"author": "Quốc Chương",
Expand Down
4 changes: 3 additions & 1 deletion backend/src/domains/domains.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';

import { UsersModule } from '@/domains/users/users.module';

@Module({
imports: [],
imports: [UsersModule],
})
export class DomainsModule {}
14 changes: 14 additions & 0 deletions backend/src/domains/users/dto/changePassword.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class ChangePasswordDto {
@ApiProperty({ description: 'User current password' })
@IsString()
@IsNotEmpty()
currentPassword: string;

@ApiProperty({ description: 'User new password' })
@IsString()
@IsNotEmpty()
newPassword: string;
}
66 changes: 66 additions & 0 deletions backend/src/domains/users/dto/createUser.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmail,
IsIn,
IsNotEmpty,
IsPhoneNumber,
IsString,
} from 'class-validator';

export class CreateUserDto {
@ApiProperty({ description: 'User full name' })
@IsString()
@IsNotEmpty()
fullName: string;

@ApiProperty({ description: 'User email' })
@IsEmail()
@IsNotEmpty()
email: string;

@ApiProperty({ description: 'User password' })
@IsString()
@IsNotEmpty()
password: string;

@ApiProperty({ description: 'User phone number' })
@IsPhoneNumber('VN')
@IsNotEmpty()
phoneNumber: string;

@ApiProperty({ description: 'User address' })
@IsString()
@IsNotEmpty()
address: string;

@ApiProperty({ description: 'User date of birth' })
@IsString()
@IsNotEmpty()
dateOfBirth: Date;

@ApiProperty({ description: 'User gender' })
@IsIn(['male', 'female'])
@IsNotEmpty()
gender: string;

@ApiProperty({ description: 'User avatar' })
@IsString()
@IsNotEmpty()
avatar: string;

@ApiProperty({ description: 'User role' })
@IsIn(['user', 'admin', 'super-admin'])
@IsNotEmpty()
role: string;

@ApiProperty({ description: 'User verification code' })
@IsString()
verificationCode: string;

@ApiProperty({ description: 'User verification code expires' })
@IsString()
verificationCodeExpires: Date;

@ApiProperty({ description: 'User is verified' })
isVerified: boolean;
}
53 changes: 53 additions & 0 deletions backend/src/domains/users/dto/updateUser.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsPhoneNumber, ValidateIf } from 'class-validator';

export class UpdateUserDto {
@ApiPropertyOptional({ description: 'User full name' })
@ValidateIf((o) => o.fullName !== undefined)
fullName?: string;

@ApiPropertyOptional({ description: 'User email' })
@ValidateIf((o) => o.email !== undefined)
email?: string;

@ApiPropertyOptional({ description: 'User phone number' })
@ValidateIf((o) => o.phoneNumber !== undefined)
@IsPhoneNumber('VN', { message: 'Invalid phone number' })
phoneNumber?: string;

@ApiPropertyOptional({ description: 'User address' })
@ValidateIf((o) => o.address !== undefined)
address?: string;

@ApiPropertyOptional({ description: 'User date of birth' })
@ValidateIf((o) => o.dateOfBirth !== undefined)
dateOfBirth?: Date;

@ApiPropertyOptional({ description: 'User gender' })
@ValidateIf((o) => o.gender !== undefined)
@IsIn(['male', 'female'], { message: 'Gender must be male or female' })
gender?: string;

@ApiPropertyOptional({ description: 'User avatar' })
@ValidateIf((o) => o.avatar !== undefined)
avatar?: string;

@ApiPropertyOptional({ description: 'User role' })
@ValidateIf((o) => o.role !== undefined)
@IsIn(['user', 'admin', 'super-admin'], {
message: 'Role must be user, admin or super-admin',
})
role?: string;

@ApiPropertyOptional({ description: 'User verification code' })
@ValidateIf((o) => o.verificationCode !== undefined)
verificationCode?: string;

@ApiPropertyOptional({ description: 'User verification code expires' })
@ValidateIf((o) => o.verificationCodeExpires !== undefined)
verificationCodeExpires?: Date;

@ApiPropertyOptional({ description: 'User is verified' })
@ValidateIf((o) => o.isVerified !== undefined)
isVerified?: boolean;
}
51 changes: 51 additions & 0 deletions backend/src/domains/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { CreateUserDto } from '@/domains/users/dto/createUser.dto';
import { UpdateUserDto } from '@/domains/users/dto/updateUser.dto';
import { UsersService } from '@/domains/users/users.service';

@ApiTags('Users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
async create(@Body() CreateUserDto: CreateUserDto) {
return await this.usersService.create(CreateUserDto);
}

@Get()
async getAll() {
return await this.usersService.getAll();
}

@Get(':identifier')
async getOne(@Param('identifier') identifier: string) {
const isEmail = identifier.includes('@');
return this.usersService.getOne(identifier, isEmail);
}

@Put(':identifier')
async update(
@Param('identifier') identifier: string,
@Body() updateUserDto: UpdateUserDto,
) {
const isEmail = identifier.includes('@');
return this.usersService.update(identifier, updateUserDto, isEmail);
}

@Delete(':identifier')
async delete(@Param('identifier') identifier: string) {
const isEmail = identifier.includes('@');
return this.usersService.delete(identifier, isEmail);
}
}
14 changes: 14 additions & 0 deletions backend/src/domains/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UsersController } from '@/domains/users/users.controller';
import { UsersService } from '@/domains/users/users.service';
import { UserSchema } from '@/schemas/user.schema';

@Module({
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
156 changes: 156 additions & 0 deletions backend/src/domains/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { CreateUserDto } from '@/domains/users/dto/createUser.dto';
import { UpdateUserDto } from '@/domains/users/dto/updateUser.dto';
import { User } from '@/schemas/user.schema';
import { hash } from '@/utils/hash.util';

@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);

constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
) {}

async create(createUserDto: CreateUserDto) {
const existingUser = await this.userModel.findOne({
email: createUserDto.email,
});

if (existingUser) {
this.logger.error(
`User with email ${createUserDto.email} already exists!`,
);

throw new ConflictException('User already exists');
}

const createdUser = new this.userModel({
...createUserDto,
hashedPassword: await hash(createUserDto.password),
});

this.logger.debug(
`Creating user with email ${createdUser.email}`,
createdUser,
);

await createdUser.save();

this.logger.log(`User with email ${createdUser.email} created`);

return {
success: true,
message: 'User created successfully.',
data: this.cleanUser(createdUser),
};
}

async getAll() {
const users = await this.userModel.find().select('-hashedPassword -__v');

if (!users) {
this.logger.error('No users found');

throw new NotFoundException('No users found');
}

this.logger.debug(`Found ${users.length} users`, users);

this.logger.log('Users fetched successfully');

return {
success: true,
message: 'Users fetched successfully.',
data: users,
};
}

async getOne(identifier: string, isEmail = false) {
const user = await this.findUser(identifier, isEmail);

this.logger.debug(
`Found user with ${isEmail ? 'email' : 'ID'} ${identifier}`,
user,
);
this.logger.log('User fetched successfully');

return {
success: true,
message: 'User fetched successfully.',
data: this.cleanUser(user),
};
}

async update(
identifier: string,
updateUserDto: UpdateUserDto,
isEmail = false,
) {
const user = await this.findUser(identifier, isEmail);

const updatedUser = await this.userModel.findOneAndUpdate(
{ _id: user._id },
updateUserDto,
{ new: true },
);

this.logger.debug(
`Updated user with ${isEmail ? 'email' : 'ID'} ${identifier}`,
updatedUser,
);
this.logger.log('User updated successfully');

return {
success: true,
message: 'User updated successfully.',
data: this.cleanUser(updatedUser),
};
}

async delete(identifier: string, isEmail = false) {
const user = await this.findUser(identifier, isEmail);

await this.userModel.deleteOne({ _id: user._id });

this.logger.debug(
`Deleted user with ${isEmail ? 'email' : 'ID'} ${identifier}`,
);
this.logger.log('User deleted successfully');

return {
success: true,
message: 'User deleted successfully.',
};
}

async findUser(identifier: string, isEmail = false) {
const query = isEmail ? { email: identifier } : { _id: identifier };

const user = await this.userModel
.findOne(query)
.select('-hashedPassword -__v');

if (!user) {
const identifierType = isEmail ? 'email' : 'ID';
this.logger.error(`User with ${identifierType} ${identifier} not found`);
throw new NotFoundException('User not found');
}

return user;
}

cleanUser(user: any) {
const userObject = user.toObject();
delete userObject.hashedPassword;
return userObject;
}
}

0 comments on commit 03da857

Please sign in to comment.