Skip to content

Commit 0b88d9c

Browse files
committed
feat(auth): ✨ add update user
1 parent 7670539 commit 0b88d9c

10 files changed

Lines changed: 306 additions & 20 deletions

File tree

libs/common/src/utils/common.util.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ export function convertToObjectId(
88
}
99
return new Types.ObjectId(input);
1010
}
11+
12+
/**
13+
*
14+
* @param stringValue The string value to check if it is true
15+
* @returns true if the string value is true, otherwise false
16+
* @description true value: true
17+
*/
18+
export function isTrueSet(stringValue: string | boolean) {
19+
return !!stringValue && String(stringValue)?.toLowerCase()?.trim() === 'true';
20+
}

src/modules/auth/auth.controller.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
SerializeOptions,
88
Req,
99
Get,
10-
UseGuards,
10+
Patch,
1111
} from '@nestjs/common';
1212
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
1313
import { NullableType } from '~/common/types';
@@ -22,10 +22,10 @@ import {
2222
AuthConfirmEmailDto,
2323
AuthForgotPasswordDto,
2424
AuthResetPasswordDto,
25+
AuthUpdateDto,
2526
} from './dtos';
2627
import { AuthRoles } from './guards';
27-
import { AuthGuard } from '@nestjs/passport';
28-
import { JwtRefreshPayloadType } from './strategies/types';
28+
import { JwtPayloadType, JwtRefreshPayloadType } from './strategies/types';
2929
import { Types } from 'mongoose';
3030

3131
@ApiTags('auth')
@@ -78,19 +78,6 @@ export class AuthController {
7878
@SerializeOptions({
7979
groups: ['me'],
8080
})
81-
@Get('me')
82-
@HttpCode(HttpStatus.OK)
83-
@ApiOkResponse({
84-
type: User,
85-
})
86-
public me(@Req() request: { user: { userId: string } }): Promise<NullableType<User>> {
87-
return this.authService.me(request.user.userId);
88-
}
89-
90-
@UseGuards(AuthGuard('jwt-refresh'))
91-
@SerializeOptions({
92-
groups: ['me'],
93-
})
9481
@Post('refresh')
9582
@HttpCode(HttpStatus.OK)
9683
@ApiOkResponse({ type: RefreshTokenResponseDto })
@@ -111,4 +98,30 @@ export class AuthController {
11198
resetPassword(@Body() resetPasswordDto: AuthResetPasswordDto): Promise<void> {
11299
return this.authService.resetPassword(resetPasswordDto.hash, resetPasswordDto.password);
113100
}
101+
102+
@AuthRoles()
103+
@SerializeOptions({
104+
groups: ['me'],
105+
})
106+
@Get('me')
107+
@HttpCode(HttpStatus.OK)
108+
@ApiOkResponse({
109+
type: User,
110+
})
111+
public me(@Req() request: { user: { userId: string } }): Promise<NullableType<User>> {
112+
return this.authService.me(request.user.userId);
113+
}
114+
115+
@AuthRoles()
116+
@SerializeOptions({
117+
groups: ['me'],
118+
})
119+
@Patch('me')
120+
@HttpCode(HttpStatus.OK)
121+
public update(
122+
@Req() request: { user: JwtPayloadType },
123+
@Body() userDto: AuthUpdateDto,
124+
): Promise<NullableType<User>> {
125+
return this.authService.update(request.user, userDto);
126+
}
114127
}

src/modules/auth/auth.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { JwtStrategy, AnonymousStrategy, JwtRefreshStrategy } from './strategies
88
import { RedisModule } from '~/common/redis';
99
import { SessionModule } from '~/modules/session';
1010
import { MailModule } from '../mail';
11+
import { GhnModule } from '~/third-party';
1112

1213
@Module({
1314
imports: [
@@ -17,6 +18,12 @@ import { MailModule } from '../mail';
1718
SessionModule,
1819
UsersModule,
1920
RedisModule,
21+
GhnModule.forRoot({
22+
host: process.env.GHN_URL ?? '',
23+
token: process.env.GHN_API_TOKEN ?? '',
24+
shopId: +process.env.GHN_SHOP_ID! ?? 0,
25+
testMode: true,
26+
}),
2027
],
2128
controllers: [AuthController],
2229
providers: [AuthService, AnonymousStrategy, JwtStrategy, JwtRefreshStrategy],

src/modules/auth/auth.service.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import { faker } from '@faker-js/faker';
1212
import * as crypto from 'crypto';
1313
import { v4 as uuid } from 'uuid';
1414
import * as bcrypt from 'bcryptjs';
15-
import { AuthEmailLoginDto, LoginResponseDto } from './dtos';
15+
import { AuthEmailLoginDto, AuthUpdateDto, LoginResponseDto } from './dtos';
1616
import { convertTimeString } from 'convert-time-string';
1717
import { RedisService } from '~/common/redis';
1818
import { PREFIX_REVOKE_ACCESS_TOKEN, PREFIX_REVOKE_REFRESH_TOKEN } from './auth.constant';
1919
import { JwtPayloadType, JwtRefreshPayloadType } from './strategies/types';
2020
import { Session, SessionService } from '~/modules/session';
2121
import { MailService } from '~/modules/mail';
22+
import { GhnService } from '~/third-party';
2223

2324
@Injectable()
2425
export class AuthService {
@@ -30,6 +31,7 @@ export class AuthService {
3031
private readonly mailService: MailService,
3132
private readonly redisService: RedisService,
3233
private readonly sessionService: SessionService,
34+
private readonly ghnService: GhnService,
3335
) {}
3436

3537
async register(dto: AuthSignupDto): Promise<void> {
@@ -512,6 +514,120 @@ export class AuthService {
512514
]);
513515
}
514516

517+
async update(
518+
userJwtPayload: JwtPayloadType,
519+
userDto: AuthUpdateDto,
520+
): Promise<NullableType<User>> {
521+
const userData = userDto;
522+
if (userData?.userName) {
523+
const user = await this.usersService.findByUserName(userData.userName);
524+
if (user) {
525+
throw new HttpException(
526+
{
527+
status: HttpStatus.UNPROCESSABLE_ENTITY,
528+
errors: {
529+
userName: 'userNameAlreadyExists',
530+
},
531+
},
532+
HttpStatus.UNPROCESSABLE_ENTITY,
533+
);
534+
}
535+
}
536+
537+
if (userData.password) {
538+
if (!userData.oldPassword) {
539+
throw new HttpException(
540+
{
541+
status: HttpStatus.UNPROCESSABLE_ENTITY,
542+
errors: {
543+
oldPassword: 'missingOldPassword',
544+
},
545+
},
546+
HttpStatus.UNPROCESSABLE_ENTITY,
547+
);
548+
}
549+
550+
const currentUser = await this.usersService.findById(userJwtPayload.userId);
551+
552+
if (!currentUser) {
553+
throw new HttpException(
554+
{
555+
status: HttpStatus.UNPROCESSABLE_ENTITY,
556+
errors: {
557+
user: 'userNotFound',
558+
},
559+
},
560+
HttpStatus.UNPROCESSABLE_ENTITY,
561+
);
562+
}
563+
564+
if (!currentUser.password) {
565+
throw new HttpException(
566+
{
567+
status: HttpStatus.UNPROCESSABLE_ENTITY,
568+
errors: {
569+
oldPassword: 'incorrectOldPassword',
570+
},
571+
},
572+
HttpStatus.UNPROCESSABLE_ENTITY,
573+
);
574+
}
575+
576+
const isValidOldPassword = await bcrypt.compare(
577+
userData.oldPassword,
578+
currentUser.password,
579+
);
580+
581+
if (!isValidOldPassword) {
582+
throw new HttpException(
583+
{
584+
status: HttpStatus.UNPROCESSABLE_ENTITY,
585+
errors: {
586+
oldPassword: 'incorrectOldPassword',
587+
},
588+
},
589+
HttpStatus.UNPROCESSABLE_ENTITY,
590+
);
591+
} else {
592+
await this.sessionService.softDelete({
593+
user: {
594+
_id: currentUser._id,
595+
},
596+
excludeId: userJwtPayload.sessionId,
597+
});
598+
}
599+
}
600+
601+
if (userData?.address !== null || userData?.address !== undefined) {
602+
if (userData.address?.length === 0) {
603+
userData.address = [];
604+
} else {
605+
const addressPromises = userData.address?.map((address) =>
606+
this.ghnService.getSelectedAddress(address),
607+
);
608+
609+
try {
610+
await Promise.all(addressPromises ?? []);
611+
} catch (error) {
612+
throw new HttpException(
613+
{
614+
status: HttpStatus.UNPROCESSABLE_ENTITY,
615+
errors: {
616+
address: 'invalidAddress',
617+
message: error.message,
618+
},
619+
},
620+
HttpStatus.UNPROCESSABLE_ENTITY,
621+
);
622+
}
623+
}
624+
}
625+
626+
await this.usersService.update(userJwtPayload.userId, userData);
627+
628+
return new User(await this.usersService.findById(userJwtPayload.userId));
629+
}
630+
515631
private async getTokensData(data: {
516632
userId: User['_id'];
517633
role: User['role'];
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { Transform, Type } from 'class-transformer';
3+
import {
4+
IsBoolean,
5+
IsEnum,
6+
IsNotEmpty,
7+
IsNumber,
8+
IsObject,
9+
IsOptional,
10+
IsPhoneNumber,
11+
IsString,
12+
MinLength,
13+
ValidateNested,
14+
} from 'class-validator';
15+
import { isTrueSet } from '~/common/utils';
16+
import { UserAddressSchema } from '~/modules/users';
17+
import { AddressType } from '~/modules/users/enums';
18+
19+
class ProvinceSchemaDTO {
20+
@ApiProperty({ description: 'The id of province', example: 201 })
21+
@IsNotEmpty()
22+
@IsNumber()
23+
@Type(() => Number)
24+
provinceId: number;
25+
}
26+
27+
class DistrictSchemaDTO {
28+
@ApiProperty({ description: 'The id of district', example: 1490 })
29+
@IsNotEmpty()
30+
@IsNumber()
31+
@Type(() => Number)
32+
districtId: number;
33+
}
34+
35+
class WardSchemaDTO {
36+
@ApiProperty({ description: 'The code of ward', example: '1A0807' })
37+
@IsNotEmpty()
38+
@IsString()
39+
wardCode: string;
40+
}
41+
42+
export class AddressSchemaDTO implements UserAddressSchema {
43+
constructor(address: AddressSchemaDTO) {
44+
Object.assign(this, address);
45+
}
46+
47+
@ApiProperty({
48+
description: 'The name type of address',
49+
enum: AddressType,
50+
example: AddressType.Home,
51+
})
52+
@IsEnum(AddressType)
53+
@IsNotEmpty()
54+
type: string;
55+
56+
@ApiProperty({ description: 'The name of customer', example: 'John Doe' })
57+
@IsString()
58+
@IsNotEmpty()
59+
customerName: string;
60+
61+
@ApiProperty({ description: 'The phone number of customer', example: '0123456789' })
62+
@IsPhoneNumber('VN')
63+
@IsNotEmpty()
64+
phoneNumbers: string;
65+
66+
@ApiProperty({ description: 'The province level address', type: ProvinceSchemaDTO })
67+
@IsObject()
68+
@ValidateNested()
69+
@Type(() => ProvinceSchemaDTO)
70+
provinceLevel: ProvinceSchemaDTO;
71+
72+
@ApiProperty({ description: 'The district level address', type: DistrictSchemaDTO })
73+
@IsObject()
74+
@ValidateNested()
75+
@Type(() => DistrictSchemaDTO)
76+
districtLevel: DistrictSchemaDTO;
77+
78+
@ApiProperty({ description: 'The ward level address', type: WardSchemaDTO })
79+
@IsObject()
80+
@ValidateNested()
81+
@Type(() => WardSchemaDTO)
82+
wardLevel: WardSchemaDTO;
83+
84+
@ApiProperty({ description: 'The detailed address', example: '18 Tam Trinh' })
85+
@IsString()
86+
@IsNotEmpty()
87+
detail: string;
88+
89+
@ApiPropertyOptional({
90+
description: 'The boolean value to check if this address is default or not',
91+
})
92+
@IsOptional()
93+
@IsBoolean()
94+
@Transform(({ value }) => isTrueSet(value))
95+
isDefault: boolean;
96+
}
97+
98+
export class AuthUpdateDto {
99+
@ApiPropertyOptional({ example: 'JohnDoe' })
100+
@IsOptional()
101+
@IsNotEmpty({ message: 'mustBeNotEmpty' })
102+
userName?: string;
103+
104+
@ApiPropertyOptional({ example: 'John' })
105+
@IsOptional()
106+
@IsNotEmpty({ message: 'mustBeNotEmpty' })
107+
firstName?: string;
108+
109+
@ApiPropertyOptional({ example: 'Doe' })
110+
@IsOptional()
111+
@IsNotEmpty({ message: 'mustBeNotEmpty' })
112+
lastName?: string;
113+
114+
@ApiPropertyOptional()
115+
@IsOptional()
116+
@IsNotEmpty()
117+
@MinLength(6)
118+
password?: string;
119+
120+
@ApiPropertyOptional()
121+
@IsOptional()
122+
@IsNotEmpty({ message: 'mustBeNotEmpty' })
123+
oldPassword?: string;
124+
125+
@ApiPropertyOptional({ type: [AddressSchemaDTO] })
126+
@IsOptional()
127+
@Type(() => AddressSchemaDTO)
128+
address?: AddressSchemaDTO[];
129+
}

src/modules/auth/dtos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './auth-resend-confirm-email.dto';
66
export * from './auth-confirm-email.dto';
77
export * from './auth-forgot-password.dto';
88
export * from './auth-reset-password.dto';
9+
export * from './auth-update.dto';

src/modules/auth/strategies/jwt.strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
2121
// Why we don't check if the user exists in the database:
2222
// https://github.com/brocoders/nestjs-boilerplate/blob/main/docs/auth.md#about-jwt-strategy
2323
public async validate(payload: JwtPayloadType): Promise<OrNeverType<JwtPayloadType>> {
24-
if (!payload.userId || !payload.hash) {
24+
if (!payload.userId || !payload.sessionId) {
2525
throw new HttpException(
2626
{
2727
status: HttpStatus.UNAUTHORIZED,

0 commit comments

Comments
 (0)