From 82d24a71a447b92b5169be0f3123b420ad67d30c Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 14:44:41 +0100 Subject: [PATCH] :sparkles: Verify codes reasons and add a route for password forgotten --- src/app/api/customer/AuthController.ts | 25 ++++++++ .../CustomersService/CustomersService.ts | 58 ++++++++++++++++--- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/app/api/customer/AuthController.ts b/src/app/api/customer/AuthController.ts index f125cd2f..218748d7 100644 --- a/src/app/api/customer/AuthController.ts +++ b/src/app/api/customer/AuthController.ts @@ -45,6 +45,31 @@ export default class AuthController extends ApiController { } } + @Post("/api/v1/customer/auth/ask-new-password") + protected async askNewPassword(req: Request, response: Response) { + const email = req.body["email"]; + if (!email) { + this.httpBadRequest(response, "Email is required"); + return; + } + + try { + const customer = await this.customerService.generateCodeForNewPassword(email); + if (!customer) { + this.httpNotFoundRequest(response, "Customer not found"); + return; + } + this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); + } catch (error) { + if (error instanceof SmsNotExpiredError) { + this.httpTooEarlyRequest(response, error.message); + return; + } + console.log(error); + this.httpInternalError(response); + } + } + @Post("/api/v1/customer/auth/login") protected async login(req: Request, response: Response) { const email = req.body["email"]; diff --git a/src/services/customer/CustomersService/CustomersService.ts b/src/services/customer/CustomersService/CustomersService.ts index 062687f6..65bd52f8 100644 --- a/src/services/customer/CustomersService/CustomersService.ts +++ b/src/services/customer/CustomersService/CustomersService.ts @@ -102,12 +102,45 @@ export default class CustomersService extends BaseService { return customer; } + /** + * @description : Send SMS to verify the email of a customer (2FA) + * 1: Check if the customer exists + * 2: Check in the array of totpCodes if one is still valid + * 3: Generate a new SMS code + * 4: Save the SMS code in database + * 5: Send the SMS code to the customer + */ + public async generateCodeForNewPassword(email: string): Promise { + // 1: Check if the customer exists + const customer = await this.getByEmail(email); + if (!customer) return null; + const now = new Date().getTime(); + + const customerHydrated = Customer.hydrate(customer); + + // 2: Check in the array of totpCodes if one is still valid + const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { + return totpCode.expire_at && totpCode.expire_at.getTime() > now; + }); + if (validTotpCode) throw new SmsNotExpiredError(); + + // 3: Generate a new SMS code + const totpPin = this.generateTotp(); + + // 4: Save the SMS code in database + await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD); + + // 5: Send the SMS code to the customer + await this.sendSmsCodeToCustomer(totpPin, customer); + return customer; + } + /** * @description : Set the password of a customer when it's the first time they connect * 1: Check if the customer exists - * 2: Check if the password is already set - * 3: Check if a totp code is existing and is not expired in the array - * 4: Check if the SMS code is valid + * 2: Check if a totp code is existing and is not expired in the array + * 3: Check if the SMS code is valid + * 4: Check the totpcode reason is valid * 5: Disable the totp code used * 6: Hash the password * 7: Set the password in database and return the result of the update @@ -121,19 +154,26 @@ export default class CustomersService extends BaseService { const customer = await this.getByEmail(email); if (!customer) return null; - // 2: Check if the password is already set - if (customer.password) throw new PasswordAlreadySetError(); - const customerHydrated = Customer.hydrate(customer); - // 3: Check if a totp code is existing and is not expired in the array + // 2: Check if a totp code is existing and is not expired in the array const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); }); + if (!validTotpCode) throw new TotpCodeExpiredError(); - // 4: Check if the SMS code is valid + // 3: Check if the SMS code is valid if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); + // 4: Check the totpcode reason is valid + // If the customer already has a password, the reason must be RESET_PASSWORD + // If the customer doesn't have a password, the reason must be FIRST_LOGIN + if ( + (customer.password && validTotpCode.reason !== TotpCodesReasons.RESET_PASSWORD) || + (!customer.password && validTotpCode.reason !== TotpCodesReasons.FIRST_LOGIN) + ) + throw new InvalidTotpCodeError(); + // 5: Disable the totp code used await this.totpCodesRepository.disable(validTotpCode); @@ -167,7 +207,7 @@ export default class CustomersService extends BaseService { // 2: Check if a totp code is existing and is not expired in the array const validTotpCode = customerHydrated.totpCodes?.find((totpCode) => { - return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime(); + return totpCode.expire_at && new Date().getTime() < totpCode.expire_at.getTime() && totpCode.reason === TotpCodesReasons.LOGIN; }); if (!validTotpCode) throw new TotpCodeExpiredError();