407 lines
14 KiB
TypeScript
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;
|
|
}
|
|
}
|