Merge branch 'dev' into staging

This commit is contained in:
Maxime Lalo 2023-11-30 10:47:03 +01:00
commit 886ed4a4cc
15 changed files with 870 additions and 71 deletions

View File

@ -46,6 +46,7 @@
"@pinata/sdk": "^2.1.0",
"@prisma/client": "^4.11.0",
"adm-zip": "^0.5.10",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"classnames": "^2.3.2",
@ -55,13 +56,14 @@
"file-type-checker": "^1.0.8",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.94",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.99",
"module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1",
"next": "^13.1.5",
"node-cache": "^5.1.2",
"node-schedule": "^2.1.1",
"ovh": "^2.0.3",
"prisma-query": "^2.0.0",
"puppeteer": "^21.3.4",
"reflect-metadata": "^0.1.13",
@ -74,6 +76,7 @@
},
"devDependencies": {
"@types/adm-zip": "^0.5.3",
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.13",
"@types/cron": "^2.0.1",
"@types/express": "^4.17.16",

View File

@ -0,0 +1,231 @@
import { Response, Request } from "express";
import { Controller, Post } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
import CustomersService, {
InvalidPasswordError,
InvalidTotpCodeError,
NotRegisteredCustomerError,
PasswordAlreadySetError,
SmsNotExpiredError,
TooSoonForNewCode,
TotpCodeExpiredError,
} from "@Services/customer/CustomersService/CustomersService";
import AuthService from "@Services/common/AuthService/AuthService";
import { Customer } from "le-coffre-resources/dist/SuperAdmin";
@Controller()
@Service()
export default class AuthController extends ApiController {
constructor(private customerService: CustomersService, private authService: AuthService) {
super();
}
@Post("/api/v1/customer/auth/mail/verify-sms")
protected async mailVerifySms(req: Request, response: Response) {
const email = req.body["email"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
try {
const customer = await this.customerService.verifyEmail2FASms(email);
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) });
} catch (error) {
if (error instanceof SmsNotExpiredError) {
this.httpTooEarlyRequest(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
}
}
@Post("/api/v1/customer/auth/ask-new-password")
protected async askNewPassword(req: Request, response: Response) {
const email = req.body["email"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
try {
const customer = await this.customerService.generateCodeForNewPassword(email);
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) });
} catch (error) {
if (error instanceof SmsNotExpiredError) {
this.httpTooEarlyRequest(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
}
}
@Post("/api/v1/customer/auth/login")
protected async login(req: Request, response: Response) {
const email = req.body["email"];
const totpCode = req.body["totpCode"];
const password = req.body["password"];
if (!email) {
this.httpBadRequest(response, "email is required");
return;
}
if (!totpCode) {
this.httpBadRequest(response, "totpCode is required");
return;
}
if (!password) {
this.httpBadRequest(response, "password is required");
return;
}
try {
const customer = await this.customerService.login(email, totpCode, password);
if (!customer) {
this.httpBadRequest(response, "Customer not found");
return;
}
const customerHydrated = Customer.hydrate<Customer>(customer);
const payload = await this.authService.getCustomerJwtPayload([customerHydrated]);
const accessToken = this.authService.generateAccessToken(payload);
const refreshToken = this.authService.generateRefreshToken(payload);
this.httpSuccess(response, { accessToken, refreshToken });
} catch (error) {
if (error instanceof TotpCodeExpiredError || error instanceof NotRegisteredCustomerError) {
this.httpBadRequest(response, error.message);
return;
}
if (error instanceof InvalidTotpCodeError || error instanceof InvalidPasswordError) {
this.httpUnauthorized(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
return;
}
}
@Post("/api/v1/customer/auth/set-password")
protected async setPassword(req: Request, response: Response) {
const email = req.body["email"];
const totpCode = req.body["totpCode"];
const password = req.body["password"];
if (!email) {
this.httpBadRequest(response, "Email is required");
return;
}
if (!totpCode) {
this.httpBadRequest(response, "Sms code is required");
return;
}
if (!password) {
this.httpBadRequest(response, "Password is required");
return;
}
try {
const customer = await this.customerService.setFirstPassword(email, totpCode, password);
if (!customer) {
this.httpBadRequest(response, "Customer not found");
return;
}
const customerHydrated = Customer.hydrate<Customer>(customer);
const payload = await this.authService.getCustomerJwtPayload([customerHydrated]);
const accessToken = this.authService.generateAccessToken(payload);
const refreshToken = this.authService.generateRefreshToken(payload);
this.httpSuccess(response, { accessToken, refreshToken });
} catch (error) {
if (error instanceof TotpCodeExpiredError || error instanceof PasswordAlreadySetError) {
this.httpBadRequest(response, error.message);
return;
}
if (error instanceof InvalidTotpCodeError) {
this.httpUnauthorized(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
return;
}
}
@Post("/api/v1/customer/auth/verify-totp-code")
protected async verifyTotpCode(req: Request, response: Response) {
const totpCode = req.body["totpCode"];
const email = req.body["email"];
if (!totpCode) {
this.httpBadRequest(response, "totpCode is required");
return;
}
if (!email) {
this.httpBadRequest(response, "email is required");
return;
}
try {
const code = await this.customerService.verifyTotpCode(totpCode, email);
if (!code) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, {
validCode: true,
reason: code.reason,
});
} catch (error) {
if (error instanceof InvalidTotpCodeError || error instanceof TotpCodeExpiredError) {
this.httpUnauthorized(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
}
}
@Post("/api/v1/customer/auth/send-another-code")
protected async sendAnotherCode(req: Request, response: Response) {
const email = req.body["email"];
if (!email) {
this.httpBadRequest(response, "email is required");
return;
}
try {
const customer = await this.customerService.askAnotherCode(email);
if (!customer) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) });
} catch (error) {
if (error instanceof TooSoonForNewCode || error instanceof TotpCodeExpiredError) {
this.httpUnauthorized(response, error.message);
return;
}
console.log(error);
this.httpInternalError(response);
}
}
}

View File

@ -45,7 +45,7 @@ import LiveVoteController from "./api/super-admin/LiveVoteController";
import DocumentControllerId360 from "./api/id360/DocumentController";
import CustomerControllerId360 from "./api/id360/CustomerController";
import UserNotificationController from "./api/notary/UserNotificationController";
import AuthController from "./api/customer/AuthController";
/**
* @description This allow to declare all controllers used in the application
@ -99,5 +99,6 @@ export default {
Container.get(UserNotificationController);
Container.get(DocumentControllerId360);
Container.get(CustomerControllerId360);
Container.get(AuthController);
},
};

View File

@ -109,6 +109,18 @@ export class BackendVariables {
@IsNotEmpty()
public readonly DOCAPOST_APP_PASSWORD!: string;
@IsNotEmpty()
public readonly SMS_PROVIDER!: string;
@IsNotEmpty()
public readonly OVH_APP_KEY!: string;
@IsNotEmpty()
public readonly OVH_APP_SECRET!: string;
@IsNotEmpty()
public readonly OVH_CONSUMER_KEY!: string;
public constructor() {
dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
@ -146,6 +158,11 @@ export class BackendVariables {
this.BACK_API_HOST = process.env["BACK_API_HOST"]!;
this.DOCAPOST_APP_ID = process.env["DOCAPOST_APP_ID"]!;
this.DOCAPOST_APP_PASSWORD = process.env["DOCAPOST_APP_PASSWORD"]!;
this.SMS_PROVIDER = process.env["SMS_PROVIDER"]!;
this.OVH_APP_KEY = process.env["OVH_APP_KEY"]!;
this.OVH_APP_SECRET = process.env["OVH_APP_SECRET"]!;
this.OVH_CONSUMER_KEY = process.env["OVH_CONSUMER_KEY"]!;
}
public async validate(groups?: string[]) {

View File

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "customers" ADD COLUMN "password" VARCHAR(255),
ADD COLUMN "totpCode" VARCHAR(255),
ADD COLUMN "totpCodeExpire" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,31 @@
/*
Warnings:
- You are about to drop the column `totpCode` on the `customers` table. All the data in the column will be lost.
- You are about to drop the column `totpCodeExpire` on the `customers` table. All the data in the column will be lost.
*/
-- CreateEnum
CREATE TYPE "TotpCodesReasons" AS ENUM ('LOGIN', 'RESET_PASSWORD', 'FIRST_LOGIN');
-- AlterTable
ALTER TABLE "customers" DROP COLUMN "totpCode",
DROP COLUMN "totpCodeExpire";
-- CreateTable
CREATE TABLE "totp_codes" (
"uid" TEXT NOT NULL,
"customer_uid" VARCHAR(255) NOT NULL,
"code" VARCHAR(255) NOT NULL,
"reason" "TotpCodesReasons" NOT NULL DEFAULT 'LOGIN',
"expire_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "totp_codes_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "totp_codes_uid_key" ON "totp_codes"("uid");
-- AddForeignKey
ALTER TABLE "totp_codes" ADD CONSTRAINT "totp_codes_customer_uid_fkey" FOREIGN KEY ("customer_uid") REFERENCES "customers"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -101,7 +101,8 @@ model Customers {
updated_at DateTime? @updatedAt
office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers")
documents Documents[]
password String? @db.VarChar(255)
totpCodes TotpCodes[]
@@map("customers")
}
@ -341,6 +342,24 @@ model Votes {
@@map("votes")
}
model TotpCodes {
uid String @id @unique @default(uuid())
customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade)
customer_uid String @db.VarChar(255)
code String @db.VarChar(255)
reason TotpCodesReasons @default(LOGIN)
expire_at DateTime? @default(now())
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("totp_codes")
}
enum TotpCodesReasons {
LOGIN
RESET_PASSWORD
FIRST_LOGIN
}
enum ECivility {
MALE
FEMALE

View File

@ -4,6 +4,10 @@ import { Service } from "typedi";
import { Customers, ECivility, ECustomerStatus, Prisma } from "@prisma/client";
import { Customer } from "le-coffre-resources/dist/SuperAdmin";
type IExcludedCustomerVars = {
totpCodeExpire?: Date | null;
password?: string;
};
@Service()
export default class CustomersRepository extends BaseRepository {
constructor(private database: Database) {
@ -25,6 +29,15 @@ export default class CustomersRepository extends BaseRepository {
return this.model.findMany({ ...query, include: { contact: { include: { address: true } } } });
}
/**
* @description : Find one customers
*/
public async findOne(query: Prisma.CustomersFindFirstArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findFirst({ ...query, include: { contact: true } });
return this.model.findFirst({ ...query, include: { contact: { include: { address: true } }, ...query.include } });
}
/**
* @description : Create a customer
*/
@ -61,7 +74,7 @@ export default class CustomersRepository extends BaseRepository {
/**
* @description : Update data from a customer
*/
public async update(uid: string, customer: Customer): Promise<Customers> {
public async update(uid: string, customer: Customer, excludedVars?: IExcludedCustomerVars): Promise<Customers> {
const updateArgs: Prisma.CustomersUpdateArgs = {
where: {
uid: uid,
@ -79,8 +92,10 @@ export default class CustomersRepository extends BaseRepository {
address: {},
},
},
password: excludedVars && excludedVars.password,
},
};
if (customer.contact!.address) {
updateArgs.data.contact!.update!.address!.update = {
address: customer.contact!.address!.address,
@ -88,6 +103,7 @@ export default class CustomersRepository extends BaseRepository {
city: customer.contact!.address!.city,
};
}
return this.model.update({ ...updateArgs, include: { contact: true } });
}

View File

@ -0,0 +1,70 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Prisma, TotpCodes } from "@prisma/client";
import { TotpCodes as TotpCode } from "le-coffre-resources/dist/Customer";
@Service()
export default class TotpCodesRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().totpCodes;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many totp codes
*/
public async findMany(query: Prisma.TotpCodesFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findMany({ ...query });
return this.model.findMany({ ...query });
}
/**
* @description : Find one totp code
*/
public async findOne(query: Prisma.TotpCodesFindFirstArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findFirst({ ...query });
return this.model.findFirst({ ...query });
}
/**
* @description : Create a totp code
*/
public async create(totpCode: TotpCode): Promise<TotpCodes> {
const createArgs: Prisma.TotpCodesCreateArgs = {
data: {
code: totpCode.code!,
reason: totpCode.reason!,
customer: {
connect: {
uid: totpCode.customer_uid!,
},
},
expire_at: totpCode.expire_at!,
},
};
return this.model.create({ ...createArgs });
}
/**
* Disable a totp code
*/
public async disable(totpCode: TotpCode): Promise<TotpCodes> {
return this.model.update({
where: {
uid: totpCode.uid!,
},
data: {
expire_at: new Date(),
},
});
}
}

View File

@ -20,6 +20,10 @@ export default abstract class BaseController {
return this.httpResponse(response, HttpCodes.BAD_REQUEST, responseData);
}
protected httpTooEarlyRequest(response: Response, responseData: IResponseData = "Http Too Early Request") {
return this.httpResponse(response, HttpCodes.TOO_EARLY, responseData);
}
protected httpValidationError(response: Response, responseData: IResponseData = "Http Validation Error") {
return this.httpResponse(response, HttpCodes.VALIDATION_ERROR, responseData);
}

View File

@ -9,5 +9,6 @@ enum HttpCodes {
NOT_FOUND = 404,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
TOO_EARLY = 425,
}
export default HttpCodes;

View File

@ -6,6 +6,7 @@ import UsersService from "@Services/super-admin/UsersService/UsersService";
import CustomersService from "@Services/super-admin/CustomersService/CustomersService";
import { ECustomerStatus } from "@prisma/client";
import { Customer } from "le-coffre-resources/dist/Notary";
import bcrypt from "bcrypt";
enum PROVIDER_OPENID {
idNot = "idNot",
@ -19,9 +20,9 @@ export interface ICustomerJwtPayload {
}
export interface IdNotJwtPayload {
sub: string,
profile_idn: string,
entity_idn: string,
sub: string;
profile_idn: string;
entity_idn: string;
}
export interface IUserJwtPayload {
@ -44,7 +45,7 @@ export default class AuthService extends BaseService {
}
public async getCustomerJwtPayload(customers: Customer[]): Promise<ICustomerJwtPayload | null> {
for (const customer of customers){
for (const customer of customers) {
if (customer.status === ECustomerStatus["PENDING"]) {
customer.status = ECustomerStatus["VALIDATED"];
await this.customerService.update(customer.uid!, customer);
@ -52,7 +53,7 @@ export default class AuthService extends BaseService {
}
return {
customerId: customers[0]!.uid!,
email: customers[0]!.contact!.email,
email: customers[0]!.contact!.email,
};
}
@ -69,7 +70,7 @@ export default class AuthService extends BaseService {
if (user.office_role) {
user.office_role.rules.forEach((rule) => {
if(!rules.includes(rule.name)) {
if (!rules.includes(rule.name)) {
rules.push(rule.name);
}
});
@ -84,11 +85,11 @@ export default class AuthService extends BaseService {
};
}
public generateAccessToken(user: any): string {
return jwt.sign({ ...user}, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
return jwt.sign({ ...user }, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
}
public generateRefreshToken(user: any): string {
return jwt.sign({ ...user}, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" });
return jwt.sign({ ...user }, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" });
}
public verifyAccessToken(token: string, callback?: VerifyCallback) {
@ -98,4 +99,12 @@ export default class AuthService extends BaseService {
public verifyRefreshToken(token: string, callback?: VerifyCallback) {
return jwt.verify(token, this.variables.REFRESH_TOKEN_SECRET, callback);
}
public comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
public hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}

View File

@ -0,0 +1,43 @@
import { BackendVariables } from "@Common/config/variables/Variables";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
Service()
export default class OvhService extends BaseService {
constructor(private variables: BackendVariables) {
super();
}
/**
* @description : Get all Customers
* @throws {Error} If Customers cannot be get
*/
public async sendSms(phoneNumber: string, message: string): Promise<void> {
const ovh = require('ovh')({
appKey: this.variables.OVH_APP_KEY,
appSecret: this.variables.OVH_APP_SECRET,
consumerKey: this.variables.OVH_CONSUMER_KEY,
});
// Get the serviceName (name of your SMS account)
ovh.request('GET', '/sms', function (err: any, serviceName: string) {
if(err) {
console.log(err, serviceName);
}
else {
console.log("My account SMS is " + serviceName);
// Send a simple SMS with a short number using your serviceName
ovh.request('POST', '/sms/' + serviceName + '/jobs', {
message: message,
senderForResponse: true,
receivers: [phoneNumber]
}, function (errsend: any, result: any) {
console.log(errsend, result);
});
}
});
}
}

View File

@ -1,11 +1,64 @@
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) {
constructor(
private customerRepository: CustomersRepository,
private authService: AuthService,
private totpCodesRepository: TotpCodesRepository,
private variables: BackendVariables,
private ovhService: OvhService,
) {
super();
}
@ -16,4 +69,331 @@ export default class CustomersService extends BaseService {
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 | 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) 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<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
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<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.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<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): 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: 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>({
...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<TotpCodes>({
reason,
customer_uid: customer.uid,
customer: Customer.hydrate<Customer>(customer),
created_at: new Date(),
code: totpPin.toString(),
expire_at: expireAt,
}),
);
return await this.customerRepository.update(
customer.uid as string,
Customer.hydrate<Customer>({
...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<TotpCodes | 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;
}
}

View File

@ -4,11 +4,7 @@
// "module": "es2022",
"target": "es2017",
"module": "commonjs",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"sourceMap": true,
"outDir": "./dist",
@ -18,7 +14,7 @@
"resolveJsonModule": true,
/* Strict Type-Checking Options */
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"allowUnusedLabels": true,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true,
"strict": true,
@ -32,6 +28,7 @@
"noPropertyAccessFromIndexSignature": true,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true,
@ -39,64 +36,37 @@
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@App/*": [
"src/app/*"
],
"@Api/*": [
"src/app/api/*"
],
"@Services/*": [
"src/services/*"
],
"@Repositories/*": [
"src/common/repositories/*"
],
"@Entries/*": [
"src/entries/*"
],
"@Common/*": [
"src/common/*"
],
"@Config/*": [
"src/common/config/*"
],
"@Entities/*": [
"src/common/ressources/*"
],
"@System/*": [
"src/common/system/*"
],
"@ControllerPattern/*": [
"src/common/system/controller-pattern/*"
],
"@Test/*": [
"src/test/*"
],
"@App/*": ["src/app/*"],
"@Api/*": ["src/app/api/*"],
"@Services/*": ["src/services/*"],
"@Repositories/*": ["src/common/repositories/*"],
"@Entries/*": ["src/entries/*"],
"@Common/*": ["src/common/*"],
"@Config/*": ["src/common/config/*"],
"@Entities/*": ["src/common/ressources/*"],
"@System/*": ["src/common/system/*"],
"@ControllerPattern/*": ["src/common/system/controller-pattern/*"],
"@Test/*": ["src/test/*"]
},
// "rootDirs": [],
// "typeRoots": [],
// "types": [],
// "allowSyntheticDefaultImports": true,
// "rootDirs": [],
// "typeRoots": [],
// "types": [],
// "allowSyntheticDefaultImports": true,
"esModuleInterop": true,
// "allowUmdGlobalAccess": true,
// "allowUmdGlobalAccess": true,
/* Source Map Options */
//"sourceRoot": "./src",
//"mapRoot": "./dist",
//"inlineSourceMap": false,
//"inlineSources": false,
//"sourceRoot": "./src",
//"mapRoot": "./dist",
//"inlineSourceMap": false,
//"inlineSources": false,
/* Experimental Options */
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"isolatedModules": true,
"isolatedModules": true
},
"include": [
"**/*.ts",
"**/*.tsx", "src/services/common/TestService",
],
"exclude": [
"node_modules"
]
}
"include": ["**/*.ts", "**/*.tsx", "src/services/common/TestService"],
"exclude": ["node_modules"]
}