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"; 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"); } } @Service() export default class CustomersService extends BaseService { constructor( private customerRepository: CustomersRepository, private authService: AuthService, private totpCodesRepository: TotpCodesRepository, ) { 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 : 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 * 5: Hash the password * 6: 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; // 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 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 if (validTotpCode.code !== totpCode) throw new InvalidTotpCodeError(); // 5: Hash the password const hashedPassword = await this.authService.hashPassword(password); // 6: 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: 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(); }); 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(); return await this.customerRepository.update( customer.uid as string, Customer.hydrate({ ...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), }), { 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) { console.log(totpPin); } /** * * 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; } }