2024-11-12 10:16:32 +01:00

407 lines
14 KiB
TypeScript

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("Code déjà envoyé");
}
}
export class TotpCodeExpiredError extends Error {
constructor() {
super("Code non trouvé ou expiré, veuillez raffraîchir la page");
}
}
export class InvalidTotpCodeError extends Error {
constructor() {
super("Code invalide");
}
}
export class NotRegisteredCustomerError extends Error {
constructor() {
super("Ce client n'existe pas");
}
}
export class InvalidPasswordError extends Error {
constructor() {
super("Mot de passe incorrect");
}
}
export class PasswordAlreadySetError extends Error {
constructor() {
super("Le mot de passe a déjà été défini");
}
}
export class TooSoonForNewCode extends Error {
constructor() {
super("Vous devez attendre 30 secondes avant de pouvoir demander un nouveau 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<Customers[]> {
return this.customerRepository.findMany(query);
}
/**
* @description : Get all Customers
* @throws {Error} If Customers cannot be get
*/
public async getOne(query: Prisma.CustomersFindFirstArgs): Promise<Customers | null> {
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>(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<TotpCodesResource>({
...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<Customer | 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>(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<Customer | null> {
// 1: Check if the customer exists
const customer = await this.getByEmail(email);
if (!customer) return null;
const customerHydrated = Customer.hydrate<Customer>(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<Customer | null> {
// 1: Check if the customer exists
const customer = await this.getByEmail(email);
if (!customer) return null;
const customerHydrated = Customer.hydrate<Customer>(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>({
...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>(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>({
...customer,
}),
{
password,
},
);
}
private getByEmail(email: string) {
return this.customerRepository.findOne({
where: {
contact: {
email:{
equals: email,
mode: 'insensitive'
}
},
},
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<TotpCodes> {
// Create the totpCode in table using repository
return await this.totpCodesRepository.create(
TotpCodesResource.hydrate<TotpCodesResource>({
reason,
customer_uid: customer.uid,
customer: Customer.hydrate<Customer>(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 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<TotpCodesResource | null> {
// 1: Check if the customer exists
const customer = await this.getByEmail(email);
if (!customer) return null;
const customerHydrated = Customer.hydrate<Customer>(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;
}
}