From 7186af8e7faca52ec394bde7bcfd6a5861567ee8 Mon Sep 17 00:00:00 2001 From: radmehr Date: Sun, 10 May 2026 09:53:43 +0330 Subject: [PATCH] feat: add update profile service --- .../validators/match-fields.validator.ts | 11 +- src/i18n/en/users.json | 6 +- src/i18n/en/validation.json | 3 +- src/i18n/fa/users.json | 6 +- src/i18n/fa/validation.json | 3 +- .../auth/http/login.post.endpoints.http | 3 +- src/modules/users/dto/create-user.dto.ts | 114 ------------------ .../users/http/users.get.endpoints.http | 2 +- .../users/http/users.patch.endpoints.http | 16 ++- .../users/providers/user-profile.provider.ts | 71 +++++++++++ src/modules/users/providers/users.service.ts | 20 +-- src/modules/users/users.controller.ts | 6 +- src/modules/users/users.module.ts | 6 +- 13 files changed, 129 insertions(+), 138 deletions(-) delete mode 100644 src/modules/users/dto/create-user.dto.ts create mode 100644 src/modules/users/providers/user-profile.provider.ts diff --git a/src/common/validators/match-fields.validator.ts b/src/common/validators/match-fields.validator.ts index 17dfe8f..15e130a 100644 --- a/src/common/validators/match-fields.validator.ts +++ b/src/common/validators/match-fields.validator.ts @@ -3,6 +3,7 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import { I18nContext } from 'nestjs-i18n'; @ValidatorConstraint({ name: 'MatchFields', async: false }) export class MatchFieldsConstraint implements ValidatorConstraintInterface { @@ -18,6 +19,14 @@ export class MatchFieldsConstraint implements ValidatorConstraintInterface { defaultMessage(args: ValidationArguments): string { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [property] = args.constraints; - return `${args.property} must match ${property}`; + const i18n = I18nContext.current(); + + const field1 = i18n?.t(`users.fields.${args.property}`) ?? args.property; + const field2 = i18n?.t(`users.fields.${property}`) ?? (property as string); + + return ( + i18n?.t('validation.fieldsMustMatch', { args: { field1, field2 } }) ?? + `${args.property} must match ${property}` + ); } } diff --git a/src/i18n/en/users.json b/src/i18n/en/users.json index 5496e7b..55585ba 100644 --- a/src/i18n/en/users.json +++ b/src/i18n/en/users.json @@ -3,10 +3,14 @@ "firstName": "Firstname", "lastName": "Lastname", "phone": "Phone", - "password": "Password" + "password": "Password", + "confirmPassword": "Confirm Password" }, "errors": { "passwordPattern": "Password should contain atleast 8 characters, including letters, numbers and symbols.", "userNotExist": "User does not exist." + }, + "messages": { + "updateSuccess": "Updated profile info schedule." } } diff --git a/src/i18n/en/validation.json b/src/i18n/en/validation.json index e9f12aa..9542d93 100644 --- a/src/i18n/en/validation.json +++ b/src/i18n/en/validation.json @@ -2,5 +2,6 @@ "requiredField": "{field} is required.", "minLengthField": "{field} should contain atleast {min} characters.", "maxLengthField": "{field} should contain maximum {max} characters.", - "wrongField": "{field} is not correct." + "wrongField": "{field} is not correct.", + "fieldsMustMatch": "{field1} must match {field2}" } diff --git a/src/i18n/fa/users.json b/src/i18n/fa/users.json index 4a2f91f..385bc40 100644 --- a/src/i18n/fa/users.json +++ b/src/i18n/fa/users.json @@ -3,10 +3,14 @@ "firstName": "نام", "lastName": "نام خانوادگی", "phone": "شماره موبایل", - "password": "رمز عبور" + "password": "رمز عبور", + "confirmPassword": "تکرار رمز عبور" }, "errors": { "passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد.", "userNotExist": "کاربر مورد نظر وجود ندارد." + }, + "messages": { + "updateSuccess": "اطلاعات حساب کاربری با موفقیت ثبت شد." } } diff --git a/src/i18n/fa/validation.json b/src/i18n/fa/validation.json index 2ad4eb4..4588e13 100644 --- a/src/i18n/fa/validation.json +++ b/src/i18n/fa/validation.json @@ -3,5 +3,6 @@ "minLengthField": "{field} باید حداقل شامل {min} کاراکتر باشد.", "maxLengthField": "{field} باید حداکثر شامل {max} کاراکتر باشد.", "wrongField": "{field} معتبر نیست.", - "wrongFieldFormat": "فرمت {field} معتبر نیست." + "wrongFieldFormat": "فرمت {field} معتبر نیست.", + "fieldsMustMatch": "مقادیر {field1} و {field2} باید با هم برابر باشند." } diff --git a/src/modules/auth/http/login.post.endpoints.http b/src/modules/auth/http/login.post.endpoints.http index b1fa493..6f9cf1a 100644 --- a/src/modules/auth/http/login.post.endpoints.http +++ b/src/modules/auth/http/login.post.endpoints.http @@ -3,5 +3,6 @@ Content-Type: application/json lang: fa { - "phone": "09121111114" + "phone": "09333026363", + "password": "rad1.3.5" } \ No newline at end of file diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts deleted file mode 100644 index bd582f3..0000000 --- a/src/modules/users/dto/create-user.dto.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Transform } from 'class-transformer'; -import { - IsNotEmpty, - IsString, - Matches, - MaxLength, - MinLength, -} from 'class-validator'; -import Utils from '@/common/utils'; -import { i18nValidationMessage as t } from 'nestjs-i18n'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateUserDTO { - @ApiProperty({ - type: 'string', - description: `User's Firstname`, - example: 'Radmehr', - required: true, - }) - @IsString() - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(users.fields.firstName)', - }), - }) - @MinLength(3, { - message: t('validation.minLengthField', { - field: '$t(users.fields.firstName)', - min: 3, - }), - }) - @MaxLength(96, { - message: t('validation.maxLengthField', { - field: '$t(users.fields.firstName)', - max: 96, - }), - }) - firstName: string; - - @ApiProperty({ - type: 'string', - description: `User's Lastname`, - example: 'Tarnas', - required: true, - }) - @IsString() - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(users.fields.lastName)', - }), - }) - @MinLength(3, { - message: t('validation.minLengthField', { - field: '$t(users.fields.lastName)', - min: 3, - }), - }) - @MaxLength(96, { - message: t('validation.maxLengthField', { - field: '$t(users.fields.lastName)', - min: 3, - }), - }) - lastName: string; - - @ApiProperty({ - type: 'string', - description: `User's Phone Number`, - example: '09121111111', - required: true, - }) - @IsString() - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(users.fields.phone)', - }), - }) - @Matches(/^(?:09|9)[0-9]{9}$/, { - message: t('validation.wrongField', { - field: '$t(users.fields.phone)', - }), - }) - @Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string)) - phone: string; - - @ApiProperty({ - type: 'string', - description: 'Password for User Account', - example: 'Password1@', - required: true, - }) - @IsString() - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(users.fields.password)', - }), - }) - @MinLength(8, { - message: t('validation.minLengthField', { - field: '$t(users.fields.password)', - min: 8, - }), - }) - @MaxLength(96, { - message: t('validation.maxLengthField', { - field: '$t(users.fields.password)', - max: 96, - }), - }) - @Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z\d]).{8,}$/, { - message: t('users.errors.passwordPattern'), - }) - password: string; -} diff --git a/src/modules/users/http/users.get.endpoints.http b/src/modules/users/http/users.get.endpoints.http index 24c8592..0ddb983 100644 --- a/src/modules/users/http/users.get.endpoints.http +++ b/src/modules/users/http/users.get.endpoints.http @@ -1,2 +1,2 @@ GET http://localhost:3000/users/profile -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjYsInBob25lIjoiMDkxMjExMTExMTEiLCJpYXQiOjE3Nzc5NzU3NjQsImV4cCI6MTc3Nzk3OTM2NCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.m3sIOApct0GYBNQ7TuWjf7wo2ZCvu-3vPYeWEHTuHMc +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c \ No newline at end of file diff --git a/src/modules/users/http/users.patch.endpoints.http b/src/modules/users/http/users.patch.endpoints.http index 4f115c4..0246eae 100644 --- a/src/modules/users/http/users.patch.endpoints.http +++ b/src/modules/users/http/users.patch.endpoints.http @@ -1,10 +1,16 @@ PATCH http://localhost:3000/users/profile Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjUsInBob25lIjoiMDkxMjExMTExMTEiLCJpYXQiOjE3NzIxNzQwMzUsImV4cCI6MTc3MjE3NzYzNSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.bux4kC-J9AV8HJpo41t8-iRGDJwVOVb0yMK9cAs4ELA +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c +lang: fa + +// { +// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInBob25lIjoiMDkzMzMwMjYzNjMiLCJpYXQiOjE3NzgzOTE0MTQsImV4cCI6MTc3ODM5NTAxNCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.ckp0DoNxarVkraiel02daRttbmESuN9vKQe3JNjBL4c", +// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsImp0aSI6IjI3ZTk1NGI2LWRjZDktNDVkNy04OWRhLTIwYWM0NjcwZmZmNCIsImlhdCI6MTc3ODM5MTQxNCwiZXhwIjoxNzc5MjU1NDE0LCJhdWQiOiJsb2NhbGhvc3QiLCJpc3MiOiJsb2NhbGhvc3QifQ.mX3KideTda658EEi03cQGxgi4j6qwPeoCg2wr9dj_u0" +// } { - "firstName": "Radmehr", - "lastName": "Tarnas", - "password": "Password1@", - "confirmPassword": "ldsfjlsdf" + "firstName": "Radi", + "lastName": "Tar" + // "password": "Password1@", + // "confirmPassword": "Password1@" } \ No newline at end of file diff --git a/src/modules/users/providers/user-profile.provider.ts b/src/modules/users/providers/user-profile.provider.ts new file mode 100644 index 0000000..1f42eb4 --- /dev/null +++ b/src/modules/users/providers/user-profile.provider.ts @@ -0,0 +1,71 @@ +import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider'; +import { Injectable, RequestTimeoutException } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; +import { User } from '../user.entity'; +import { UpdateProfileDTO } from '../dto/update-profile.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UserProfileProvider { + constructor( + /** + * Inject User Repository + */ + @InjectRepository(User) + private readonly userRepository: Repository, + + /** + * Inject Hashing Provider + */ + private readonly hasingProvider: HashingProvider, + + /** + * Inject I18n Service + */ + private readonly i18nService: I18nService, + ) {} + + public getProfile(user: User) { + return { + phone: user.phone, + firstName: user.firstName, + lastName: user.lastName, + dateJoined: user.createdAt, + }; + } + + public async updateProfile(user: User, updateProfileDto: UpdateProfileDTO) { + const { firstName, lastName, password, confirmPassword } = updateProfileDto; + const updateData: Partial = {}; + + if (firstName !== undefined) { + updateData.firstName = firstName; + } + + if (lastName !== undefined) { + updateData.lastName = lastName; + } + + if ( + password !== undefined && + confirmPassword !== undefined && + password === confirmPassword + ) { + updateData.password = await this.hasingProvider.hash(password); + } + + try { + await this.userRepository.update(user.id, updateData); + } catch (err) { + throw new RequestTimeoutException('common.errors.requestTimeout', { + cause: err, + description: 'Error connecting to the database', + }); + } + + return { + message: this.i18nService.translate('users.messages.updateSuccess'), + }; + } +} diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index b71bad7..0799a6a 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -6,15 +6,22 @@ import { import { Repository } from 'typeorm'; import { User } from '../user.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { UpdateProfileDTO } from '../dto/update-profile.dto'; +import { UserProfileProvider } from './user-profile.provider'; @Injectable() export class UsersService { constructor( /** - * Injecting User Repository + * Inject User Repository */ @InjectRepository(User) private readonly userRepository: Repository, + + /** + * Inject UpdateProfile Provider + */ + private readonly userProfileProvider: UserProfileProvider, ) {} public async findOneById(id: number) { @@ -62,11 +69,10 @@ export class UsersService { } public getProfile(user: User) { - return { - phone: user.phone, - firstName: user.firstName, - lastName: user.lastName, - dateJoined: user.createdAt, - }; + return this.userProfileProvider.getProfile(user); + } + + public async updateProfile(user: User, updateProfileDto: UpdateProfileDTO) { + return await this.userProfileProvider.updateProfile(user, updateProfileDto); } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index a09c43c..0c2bafb 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -8,7 +8,7 @@ import { UpdateProfileDTO } from './dto/update-profile.dto'; export class UsersController { constructor( /** - * Injecting Users Service + * Inject Users Service */ private readonly usersService: UsersService, ) {} @@ -19,10 +19,10 @@ export class UsersController { } @Patch('profile') - public updateUserProfile( + public async updateUserProfile( @ActiveUser() user: User, @Body() updateProfileDto: UpdateProfileDTO, ) { - return updateProfileDto; + return await this.usersService.updateProfile(user, updateProfileDto); } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 59f571e..f21a8a8 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -3,11 +3,13 @@ import { UsersController } from './users.controller'; import { UsersService } from './providers/users.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './user.entity'; +import { HashingModule } from '@/common/modules/hashing/hashing.module'; +import { UserProfileProvider } from './providers/user-profile.provider'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([User]), HashingModule], controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, UserProfileProvider], exports: [UsersService], }) export class UsersModule {}