import { BackendVariables } from "@Common/config/variables/Variables"; import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import TotpCodesRepository from "@Repositories/TotpCodesRepository"; import BaseService from "@Services/BaseService"; import AuthService from "@Services/common/AuthService/AuthService"; import TotpCodes, { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; import OvhService from "@Services/common/OvhService/OvhService"; export class SmsNotExpiredError extends Error { constructor() { super("SMS code not expired"); } } export class TotpCodeExpiredError extends Error { constructor() { super("Totp code not found or expired"); } } export class InvalidTotpCodeError extends Error { constructor() { super("Invalid Totp code"); } } export class NotRegisteredCustomerError extends Error { constructor() { super("Customer not registered"); } } export class InvalidPasswordError extends Error { constructor() { super("Invalid password"); } } export class PasswordAlreadySetError extends Error { constructor() { super("Password already set"); } } export class TooSoonForNewCode extends Error { constructor() { super("You need to wait at least 30 seconds before asking for a new code"); } } @Service() export default class CustomersService extends BaseService { constructor( private customerRepository: CustomersRepository, private authService: AuthService, private totpCodesRepository: TotpCodesRepository, private variables: BackendVariables, private ovhService: OvhService, ) { super(); } /** * @description : Get all Customers * @throws {Error} If Customers cannot be get */ public async get(query: Prisma.CustomersFindManyArgs): Promise { return this.customerRepository.findMany(query); } /** * @description : Get all Customers * @throws {Error} If Customers cannot be get */ public async getOne(query: Prisma.CustomersFindFirstArgs): Promise { return this.customerRepository.findOne(query); } /** * @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 verifyEmail2FASms(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(); const reason = customer.password ? TotpCodesReasons.LOGIN : TotpCodesReasons.FIRST_LOGIN; // 4: Save the SMS code in database await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), reason); // 5: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); 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 && totpCode.reason === TotpCodesReasons.RESET_PASSWORD; }); if (validTotpCode) throw new SmsNotExpiredError(); // 3: Archive all active totp codes for this customer const activeTotpCodes = customerHydrated.totpCodes?.filter((totpCode) => { return totpCode.expire_at && totpCode.expire_at.getTime() > now; }); if (activeTotpCodes) { await Promise.all( activeTotpCodes.map(async (totpCode) => { await this.totpCodesRepository.disable(totpCode); }), ); } // 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 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 * @param email * @param totpCode * @param password * @returns */ public async setFirstPassword(email: string, totpCode: string, password: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; const customerHydrated = Customer.hydrate(customer); // 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(); // 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); // 6: Hash the password const hashedPassword = await this.authService.hashPassword(password); // 7: Set the password in database and return the result of the update return await this.setPassword(customer, hashedPassword); } /** * * @description : Login a customer * 1: Check if the customer exists * 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 if the user has a password or it's their first login * 5: Check if the password is valid * 6: Disable the totp code used * 7: Return the customer * @param email * @param totpCode * @param password * @returns Customer | null */ public async login(email: string, totpCode: string, password: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; const customerHydrated = Customer.hydrate(customer); // 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() && totpCode.reason === TotpCodesReasons.LOGIN; }); if (!validTotpCode) throw new TotpCodeExpiredError(); // 3: Check if the SMS code is valid if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 4: Check if the user has a password or it's their first login if (!customer.password) throw new NotRegisteredCustomerError(); // 5: Check if the password is valid const isPasswordValid = await this.authService.comparePassword(password, customer.password); if (!isPasswordValid) throw new InvalidPasswordError(); // 6: Disable the totp code used await this.totpCodesRepository.disable(validTotpCode); // 7: Return the customer return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), ); } public async askAnotherCode(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: Get last code sent const lastCode = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.expire_at && totpCode.expire_at.getTime() > now; }); if (!lastCode) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago if (lastCode.created_at && lastCode.created_at.getTime() > now - 30000) throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); // 5: Disable the old code await this.totpCodesRepository.disable(lastCode); // 6: Save the SMS code in database await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), lastCode.reason!); // 7: Send the SMS code to the customer await this.sendSmsCodeToCustomer(totpPin, customer); return customer; } /** * @description : Set password for a customer * @throws {Error} If customer cannot be updated */ private async setPassword(customer: Customer, password: string) { return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), { password, }, ); } private getByEmail(email: string) { return this.customerRepository.findOne({ where: { contact: { email, }, }, include: { contact: true, totpCodes: true, }, }); } /** * @description : Saves a TotpPin in database */ private async saveTotpPin(customer: Customer, totpPin: number, expireAt: Date, reason: TotpCodesReasons) { // Create the totpCode in table using repository await this.totpCodesRepository.create( TotpCodes.hydrate({ reason, customer_uid: customer.uid, customer: Customer.hydrate(customer), created_at: new Date(), code: totpPin.toString(), expire_at: expireAt, }), ); return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...customer, }), ); } private generateTotp() { return Math.floor(100000 + Math.random() * 900000); } private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { try { // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement const selectedProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.ovhService : this.ovhService; // Envoi du SMS if(!customer.contact?.phone_number) return false; let success = await selectedProvider.sendSms(customer.contact?.phone_number, totpPin.toString()); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur // if (!success) { // const alternateProvider = this.variables.SMS_PROVIDER === 'OVH' ? this.smsService2 : this.ovhService; // success = await alternateProvider.sendSms(customer.contact?.phone_number, totpPin); // } return success; } catch (error) { console.error(`Erreur lors de l'envoi du SMS : ${error}`); return false; } } /** * * 1: Check if the customer exists * 2: Check if a totp code is existing and is not expired in the array * 3: Check if the totp code is valid * 4: Return the customer * @param totpCode * @param email * @returns */ public async verifyTotpCode(totpCode: string, email: string): Promise { // 1: Check if the customer exists const customer = await this.getByEmail(email); if (!customer) return null; const customerHydrated = Customer.hydrate(customer); // 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(); // 3: Check if the SMS code is valid if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 4: Return the customer return validTotpCode; } }