// import { BackendVariables } from "@Common/config/variables/Variables"; import { Customers, Prisma, TotpCodes } 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 TotpCodesResource, { 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"; import SmsFactorService from "@Services/common/SmsFactorService/SmsFactorService"; 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, private smsFactorService: SmsFactorService, ) { 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<{ customer: Customer; totpCode: TotpCodesResource } | null> { // 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) return { customer, totpCode: validTotpCode }; // 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 const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), reason); if (!totpCode) return null; // 5: Send the SMS code to the customer // if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode: TotpCodesResource.hydrate({ ...totpCode, reason: totpCode.reason as TotpCodesReasons, }), }; } /** * @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 // if(this.variables.ENV !== 'dev') 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 setPassword(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.setPasswordInDatabase(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, totpCodeUid: string): Promise<{ customer: Customer; totpCode: TotpCodes } | null> { // 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 and check if it's still valid const totpCodeToResend = customerHydrated.totpCodes?.find((totpCode) => { return totpCode.uid === totpCodeUid && totpCode.expire_at && totpCode.expire_at.getTime() > now; }); if (!totpCodeToResend) throw new TotpCodeExpiredError(); // 3: Check if it was created more than 30 seconds ago and hasn't been resent yet if (totpCodeToResend.created_at && totpCodeToResend.created_at.getTime() > now - 30000 && totpCodeToResend.resent) throw new TooSoonForNewCode(); // 4: Generate a new SMS code const totpPin = this.generateTotp(); // 5: Disable the old code await this.totpCodesRepository.disable(totpCodeToResend); // 6: Save the SMS code in database with the same reason as the old one const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), totpCodeToResend.reason!, true); // 7: Send the SMS code to the customer // if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); return { customer, totpCode }; } /** * @description : Set password for a customer * @throws {Error} If customer cannot be updated */ private async setPasswordInDatabase(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, resent?: boolean, ): Promise { // Create the totpCode in table using repository return await this.totpCodesRepository.create( TotpCodesResource.hydrate({ reason, customer_uid: customer.uid, customer: Customer.hydrate(customer), created_at: new Date(), code: totpPin.toString(), expire_at: expireAt, resent: resent || false, }), ); } private generateTotp() { return Math.floor(100000 + Math.random() * 900000); } private async sendSmsCodeToCustomer(totpPin: number, customer: Customer) { const message = "Votre code de vérification LEcoffre.io est : " + totpPin.toString(); // Sélectionnez le fournisseur de SMS en fonction de la variable d'environnement //const selectedProvider = this.variables.SMS_PROVIDER === "OVH" ? this.ovhService : this.smsFactorService; // Envoi du SMS if (!customer.contact?.cell_phone_number) return; let success = await this.ovhService.sendSms(customer.contact?.cell_phone_number, message); // Si l'envoi échoue, basculez automatiquement sur le second fournisseur if (!success) { //const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; await this.smsFactorService.sendSms(customer.contact?.cell_phone_number, message); } } /** * * 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; } }