fix: the email vaerification logic error

This commit is contained in:
mahyargdz
2025-02-26 15:25:18 +03:30
parent f8a1a7b784
commit 93c5ad49d3
320 changed files with 215 additions and 92 deletions
@@ -3,7 +3,7 @@ import { IsEmail, IsNotEmpty } from "class-validator";
import { AuthMessage } from "../../../common/enums/message.enum";
export class UpdateEmailDto {
export class ChangeEmailDto {
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
@IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT })
@ApiProperty({ description: "Email", example: "ma@gmail.com" })
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+6 -9
View File
@@ -1,7 +1,7 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString, IsUUID } from "class-validator";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
import { AuthMessage } from "../../../common/enums/message.enum";
export class EmailVerifyQueryDto {
@IsNotEmpty({ message: AuthMessage.TOKEN_NOT_EMPTY })
@@ -9,11 +9,8 @@ export class EmailVerifyQueryDto {
@ApiProperty({ description: "Token", example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" })
token: string;
@IsNotEmpty({ message: UserMessage.USER_ID_SHOULD_BE_A_UUID })
@IsUUID(4, { message: UserMessage.USER_ID_SHOULD_BE_A_UUID })
@ApiProperty({ description: "User id", example: "f7b1b3b1-4b7b-4b7b-4b7b-4b7b4b7b4b7b" })
userId: string;
@IsNotEmpty({ message: AuthMessage.TIMESTAMP_NOT_EMPTY })
ts: string;
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
@IsEmail(undefined, { message: AuthMessage.INVALID_EMAIL_FORMAT })
@ApiProperty({ description: "email of user", default: "admin-dsc@gmail.com" })
email: string;
}
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+5
View File
@@ -0,0 +1,5 @@
export interface IEmailVerify {
hash: string;
email: string;
nonce: string;
}
+46 -36
View File
@@ -1,4 +1,4 @@
import { createHmac } from "node:crypto";
import { createHmac, randomBytes } from "node:crypto";
import { BadRequestException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@@ -20,12 +20,14 @@ import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
import { RequestOtpDto } from "../../auth/DTO/request-otp.dto";
import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto";
import { UserSettingsService } from "../../settings/providers/user-settings.service";
import { CacheService } from "../../utils/providers/cache.service";
import { EmailService } from "../../utils/providers/email.service";
import { OTPService } from "../../utils/providers/otp.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { PasswordService } from "../../utils/providers/password.service";
import { SmsService } from "../../utils/providers/sms.service";
import { WalletsService } from "../../wallets/providers/wallets.service";
import { ChangeEmailDto } from "../DTO/change-email.dto";
import { CheckValidityDTO } from "../DTO/check-validity.dto";
import { CreateAdminDto } from "../DTO/create-admin.dto";
import { CreateLegalUserDto } from "../DTO/create-legal-user.dto";
@@ -35,7 +37,6 @@ import { CreateCustomerDto } from "../DTO/create-user.dto";
import { SearchAdminQueryDto } from "../DTO/search-admins-query.dto";
import { SearchCustomersDto } from "../DTO/search-customers.dto";
import { SearchRolesQueryDto } from "../DTO/search-roles.dto";
import { UpdateEmailDto } from "../DTO/update-email.dto";
import { UpdateProfileDto } from "../DTO/update-profile.dto";
import { CreateUserGroupDto } from "../DTO/user-group.dto";
import { EmailVerifyQueryDto } from "../DTO/verify-email-query.dto";
@@ -68,6 +69,7 @@ export class UsersService {
private readonly configService: ConfigService,
private readonly otpService: OTPService,
private readonly smsService: SmsService,
private readonly cacheService: CacheService,
) {}
/************************************************************ */
@@ -138,18 +140,23 @@ export class UsersService {
}
/************************************************************ */
async updateEmail(userId: string, updateDto: UpdateEmailDto) {
const { email } = updateDto;
async changeEmail(userId: string, changeDto: ChangeEmailDto) {
const { email } = changeDto;
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
//
const existEmail = await this.userRepository.findOneBy({ email, id: Not(userId) });
if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST);
//
const emailLink = await this.createEmailHashLink(user.id);
const emailLink = await this.createEmailHashLink(email);
await this.emailService.sendVerificationEmail(email, emailLink, `${user.firstName} ${user.lastName}`);
await this.userRepository.save({ ...user, email });
if (user.email !== email) {
user.email = email;
user.emailVerified = false;
}
await this.userRepository.save(user);
return {
message: UserMessage.VERIFY_EMAIL_LINK_SENT,
@@ -157,37 +164,40 @@ export class UsersService {
};
}
/************************************************************ */
//TODO:fix this later
async verifyEmail(queryDto: EmailVerifyQueryDto, rep: FastifyReply) {
const frontUrl = this.configService.getOrThrow<string>("FRONT_URL");
const { token, userId, ts } = queryDto;
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
async verifyEmail(queryDto: EmailVerifyQueryDto, rep: FastifyReply) {
const frontUrl = this.configService.getOrThrow<string>("SITE_URL");
const { token, email } = queryDto;
const user = await this.userRepository.findOneBy({ email, emailVerified: true });
if (user) throw new BadRequestException(UserMessage.EMAIL_EXIST);
const cachedData = await this.cacheService.get(`EMAIL_VERIFY:${email}`);
if (!cachedData) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN);
const { hash, nonce } = JSON.parse(cachedData as string);
const secret = this.configService.getOrThrow<string>("EMAIL_SECRET");
// Check if timestamp is within allowed time window (e.g., 24 hours)
const timestamp = parseInt(ts, 10);
if (Date.now() - timestamp > 24 * 60 * 60 * 1000) {
throw new BadRequestException(UserMessage.EXPIRED_EMAIL_TOKEN);
}
if (hash !== token) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN);
const payload = `${user.id}:${timestamp}`;
const expectedHash = createHmac("sha256", secret).update(payload).digest("hex");
const expectedHash = createHmac("sha256", secret).update(`${email}:${nonce}`).digest("hex");
if (expectedHash !== hash) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN);
if (expectedHash !== token) throw new BadRequestException(UserMessage.INVALID_EMAIL_TOKEN);
const userToUpdate = await this.userRepository.findOneBy({ email });
if (!userToUpdate) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
user.emailVerified = true;
await this.userRepository.save(user);
userToUpdate.emailVerified = true;
await this.userRepository.save(userToUpdate);
// return { message: UserMessage.EMAIL_VERIFIED };
return rep.status(HttpStatus.FOUND).redirect(frontUrl);
await this.cacheService.del(`EMAIL_VERIFY:${email}`);
return rep.status(HttpStatus.FOUND).redirect(`${frontUrl}/profile`);
}
/************************************************************ */
async updatePhone(userId: string, requestOtpDto: RequestOtpDto) {
async changePhone(userId: string, requestOtpDto: RequestOtpDto) {
const { phone } = requestOtpDto;
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
@@ -570,6 +580,7 @@ export class UsersService {
paginate: true,
};
}
/************************************************************ */
async createRealUserData(createDto: CreateRealUserDto, userId: string) {
const userExist = await this.userRepository.findOneBy({
@@ -655,17 +666,16 @@ export class UsersService {
/************************************************************ */
private async createEmailHashLink(userId: string) {
private async createEmailHashLink(email: string) {
const secret = this.configService.getOrThrow<string>("EMAIL_SECRET");
const timestamp = Date.now();
const payload = `${userId}:${timestamp}`;
const nonce = randomBytes(16).toString("hex");
const hash = createHmac("sha256", secret).update(`${email}:${nonce}`).digest("hex");
const hash = createHmac("sha256", secret).update(payload).digest("hex");
const url = new URL(`${this.configService.getOrThrow<string>("SITE_URL")}/users/verify-email`);
const url = new URL(`${this.configService.getOrThrow<string>("API_URL")}/users/verify-email`);
url.searchParams.append("token", hash);
url.searchParams.append("userId", userId);
url.searchParams.append("ts", timestamp.toString());
url.searchParams.append("email", email);
await this.cacheService.set(`EMAIL_VERIFY:${email}`, JSON.stringify({ hash, email, nonce }), 5 * 60 * 1000);
return url.toString();
}
View File
View File
View File
View File
View File
View File
+9 -9
View File
@@ -2,6 +2,7 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query,
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { FastifyReply } from "fastify";
import { ChangeEmailDto } from "./DTO/change-email.dto";
import { CheckValidityDTO } from "./DTO/check-validity.dto";
import { CreateAdminDto } from "./DTO/create-admin.dto";
import { CreateLegalUserDto } from "./DTO/create-legal-user.dto";
@@ -11,7 +12,6 @@ import { CreateCustomerDto } from "./DTO/create-user.dto";
import { SearchAdminQueryDto } from "./DTO/search-admins-query.dto";
import { SearchCustomersDto } from "./DTO/search-customers.dto";
import { SearchRolesQueryDto } from "./DTO/search-roles.dto";
import { UpdateEmailDto } from "./DTO/update-email.dto";
import { UpdateProfileDto } from "./DTO/update-profile.dto";
import { CreateUserGroupDto } from "./DTO/user-group.dto";
import { EmailVerifyQueryDto } from "./DTO/verify-email-query.dto";
@@ -59,10 +59,10 @@ export class UsersController {
/************************************************************ */
@AuthGuards()
@ApiOperation({ summary: "Update user email" })
@Patch("update-email")
updateEmail(@Body() updateEmailDto: UpdateEmailDto, @UserDec("id") userId: string) {
return this.usersService.updateEmail(userId, updateEmailDto);
@ApiOperation({ summary: "change user email" })
@Patch("change-email")
changeEmail(@Body() changeEmailDto: ChangeEmailDto, @UserDec("id") userId: string) {
return this.usersService.changeEmail(userId, changeEmailDto);
}
/************************************************************ */
@@ -76,10 +76,10 @@ export class UsersController {
/************************************************************ */
@AuthGuards()
@ApiOperation({ summary: "Update user phone" })
@Patch("update-phone")
updatePhone(@Body() updateDto: RequestOtpDto, @UserDec("id") userId: string) {
return this.usersService.updatePhone(userId, updateDto);
@ApiOperation({ summary: "change user phone" })
@Patch("change-phone")
changePhone(@Body() changeDto: RequestOtpDto, @UserDec("id") userId: string) {
return this.usersService.changePhone(userId, changeDto);
}
/************************************************************ */
View File