-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from bakaqc/feature/implement-crud-api-user
backend: implement crud api user
- Loading branch information
Showing
8 changed files
with
358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |