fix: the email vaerification logic error
This commit is contained in:
Regular → Executable
+1
-1
@@ -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" })
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+6
-9
@@ -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;
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+5
@@ -0,0 +1,5 @@
|
||||
export interface IEmailVerify {
|
||||
hash: string;
|
||||
email: string;
|
||||
nonce: string;
|
||||
}
|
||||
Regular → Executable
+46
-36
@@ -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();
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+9
-9
@@ -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);
|
||||
}
|
||||
|
||||
/************************************************************ */
|
||||
|
||||
Regular → Executable
Reference in New Issue
Block a user