Merge branch 'staging' into preprod

This commit is contained in:
Vins 2023-12-05 11:26:06 +01:00
commit 6605c3101b
45 changed files with 1058 additions and 121 deletions

View File

@ -5,7 +5,7 @@ scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4
lecoffreBack: lecoffreBack:
serviceAccountName: lecoffre-back-sa serviceAccountName: lecoffre-back-sa
envSecrets: stg-env envSecrets: stg-env
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run start'" command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run api:start'"
imagePullSecrets: imagePullSecrets:
- name: docker-pull-secret - name: docker-pull-secret
image: image:

View File

@ -46,6 +46,8 @@
"@pinata/sdk": "^2.1.0", "@pinata/sdk": "^2.1.0",
"@prisma/client": "^4.11.0", "@prisma/client": "^4.11.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -55,13 +57,14 @@
"file-type-checker": "^1.0.8", "file-type-checker": "^1.0.8",
"fp-ts": "^2.16.1", "fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0", "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.104",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"monocle-ts": "^2.3.13", "monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"next": "^13.1.5", "next": "^13.1.5",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"ovh": "^2.0.3",
"prisma-query": "^2.0.0", "prisma-query": "^2.0.0",
"puppeteer": "^21.3.4", "puppeteer": "^21.3.4",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -74,6 +77,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.5.3", "@types/adm-zip": "^0.5.3",
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/cron": "^2.0.1", "@types/cron": "^2.0.1",
"@types/express": "^4.17.16", "@types/express": "^4.17.16",

View File

@ -0,0 +1,244 @@
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,
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 res = await this.customerService.verifyEmail2FASms(email);
if (!res) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, {
partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4),
totpCodeUid: res.totpCode.uid,
});
} catch (error) {
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, "totpCode is required");
return;
}
if (!password) {
this.httpBadRequest(response, "password is required");
return;
}
const passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/);
if (!passwordRegex.test(password)) {
this.httpBadRequest(response, "Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number");
return;
}
try {
const customer = await this.customerService.setPassword(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) {
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"];
const totpCodeUid = req.body["totpCodeUid"];
if (!email) {
this.httpBadRequest(response, "email is required");
return;
}
if (!totpCodeUid) {
this.httpBadRequest(response, "totpCodeUid is required");
return;
}
try {
const res = await this.customerService.askAnotherCode(email, totpCodeUid);
if (!res) {
this.httpNotFoundRequest(response, "Customer not found");
return;
}
this.httpSuccess(response, {
partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4),
totpCodeUid: res.totpCode.uid,
});
} 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 DocumentControllerId360 from "./api/id360/DocumentController";
import CustomerControllerId360 from "./api/id360/CustomerController"; import CustomerControllerId360 from "./api/id360/CustomerController";
import UserNotificationController from "./api/notary/UserNotificationController"; import UserNotificationController from "./api/notary/UserNotificationController";
import AuthController from "./api/customer/AuthController";
/** /**
* @description This allow to declare all controllers used in the application * @description This allow to declare all controllers used in the application
@ -99,5 +99,6 @@ export default {
Container.get(UserNotificationController); Container.get(UserNotificationController);
Container.get(DocumentControllerId360); Container.get(DocumentControllerId360);
Container.get(CustomerControllerId360); Container.get(CustomerControllerId360);
Container.get(AuthController);
}, },
}; };

View File

@ -109,6 +109,24 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly DOCAPOST_APP_PASSWORD!: string; 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;
@IsNotEmpty()
public readonly OVH_SMS_SERVICE_NAME!: string;
@IsNotEmpty()
public readonly SMS_FACTOR_TOKEN!: string;
public constructor() { public constructor() {
dotenv.config(); dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!; this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
@ -146,6 +164,13 @@ export class BackendVariables {
this.BACK_API_HOST = process.env["BACK_API_HOST"]!; this.BACK_API_HOST = process.env["BACK_API_HOST"]!;
this.DOCAPOST_APP_ID = process.env["DOCAPOST_APP_ID"]!; this.DOCAPOST_APP_ID = process.env["DOCAPOST_APP_ID"]!;
this.DOCAPOST_APP_PASSWORD = process.env["DOCAPOST_APP_PASSWORD"]!; 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"]!;
this.OVH_SMS_SERVICE_NAME = process.env["OVH_SMS_SERVICE_NAME"]!;
this.SMS_FACTOR_TOKEN = process.env["SMS_FACTOR_TOKEN"]!;
} }
public async validate(groups?: string[]) { 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

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "totp_codes" ADD COLUMN "resent" BOOLEAN NOT NULL DEFAULT false;

View File

@ -101,7 +101,8 @@ model Customers {
updated_at DateTime? @updatedAt updated_at DateTime? @updatedAt
office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers") office_folders OfficeFolders[] @relation("OfficeFolderHasCustomers")
documents Documents[] documents Documents[]
password String? @db.VarChar(255)
totpCodes TotpCodes[]
@@map("customers") @@map("customers")
} }
@ -341,6 +342,25 @@ model Votes {
@@map("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)
resent Boolean @default(false)
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 { enum ECivility {
MALE MALE
FEMALE FEMALE

View File

@ -173,8 +173,8 @@ export default async function main() {
first_name: "Angela", first_name: "Angela",
last_name: "Dubois", last_name: "Dubois",
email: "angela.dubois@gmail.com", email: "angela.dubois@gmail.com",
phone_number: "06 12 34 56 78", phone_number: "+33785186013",
cell_phone_number: "06 12 34 56 78", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -185,8 +185,8 @@ export default async function main() {
first_name: "Maxime", first_name: "Maxime",
last_name: "Lalo", last_name: "Lalo",
email: "maxime.lalo@smart-chain.fr", email: "maxime.lalo@smart-chain.fr",
phone_number: "06 23 45 67 89", phone_number: "+33785186013",
cell_phone_number: "06 23 45 67 89", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -197,8 +197,8 @@ export default async function main() {
first_name: "Vincent", first_name: "Vincent",
last_name: "Alamelle", last_name: "Alamelle",
email: "vincent.alamelle@smart-chain.fr", email: "vincent.alamelle@smart-chain.fr",
phone_number: "06 34 56 78 90", phone_number: "+33785186013",
cell_phone_number: "06 34 56 78 90", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -209,8 +209,8 @@ export default async function main() {
first_name: "Melissa", first_name: "Melissa",
last_name: "Desde", last_name: "Desde",
email: "melissa.desde@smart-chain.fr", email: "melissa.desde@smart-chain.fr",
phone_number: "06 45 67 89 01", phone_number: "+33785186013",
cell_phone_number: "06 45 67 89 01", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -221,8 +221,8 @@ export default async function main() {
first_name: "Maxime", first_name: "Maxime",
last_name: "Leroy", last_name: "Leroy",
email: "maxime.leroy@hotmail.fr", email: "maxime.leroy@hotmail.fr",
phone_number: "06 56 78 90 12", phone_number: "+33785186013",
cell_phone_number: "06 56 78 90 12", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -233,8 +233,8 @@ export default async function main() {
first_name: "Paul", first_name: "Paul",
last_name: "Dupont", last_name: "Dupont",
email: "paul.dupont@outlook.com", email: "paul.dupont@outlook.com",
phone_number: "06 67 89 01 23", phone_number: "+33785186013",
cell_phone_number: "06 67 89 01 23", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -245,8 +245,8 @@ export default async function main() {
first_name: "Jean", first_name: "Jean",
last_name: "Dubignot", last_name: "Dubignot",
email: "jean.dubignot@gmail.com", email: "jean.dubignot@gmail.com",
phone_number: "06 78 90 12 34", phone_number: "+33785186013",
cell_phone_number: "06 78 90 12 34", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -257,8 +257,8 @@ export default async function main() {
first_name: "Vincent", first_name: "Vincent",
last_name: "Martin", last_name: "Martin",
email: "vincent.martin@gmail.com", email: "vincent.martin@gmail.com",
phone_number: "06 89 01 23 45", phone_number: "+33785186013",
cell_phone_number: "06 89 01 23 45", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -269,8 +269,8 @@ export default async function main() {
first_name: "Lucie", first_name: "Lucie",
last_name: "Chevalier", last_name: "Chevalier",
email: "lucie.chevalier@outlook.com", email: "lucie.chevalier@outlook.com",
phone_number: "07 12 34 56 78", phone_number: "+33785186013",
cell_phone_number: "07 12 34 56 78", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -281,8 +281,8 @@ export default async function main() {
first_name: "Sébastien", first_name: "Sébastien",
last_name: "Dubois", last_name: "Dubois",
email: "sebastien.dubois@gmail.com", email: "sebastien.dubois@gmail.com",
phone_number: "07 23 45 67 89", phone_number: "+33785186013",
cell_phone_number: "07 23 45 67 89", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -293,8 +293,8 @@ export default async function main() {
first_name: "Mathilde", first_name: "Mathilde",
last_name: "Durand", last_name: "Durand",
email: "mathilde.durand@gmail.com", email: "mathilde.durand@gmail.com",
phone_number: "07 34 56 78 90", phone_number: "+33785186013",
cell_phone_number: "07 34 56 78 90", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -305,8 +305,8 @@ export default async function main() {
first_name: "Antoine", first_name: "Antoine",
last_name: "Bernard", last_name: "Bernard",
email: "antoine.bernard@outlook.com", email: "antoine.bernard@outlook.com",
phone_number: "07 45 67 89 01", phone_number: "+33785186013",
cell_phone_number: "07 45 67 89 01", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -317,8 +317,8 @@ export default async function main() {
first_name: "Camille", first_name: "Camille",
last_name: "Laurent", last_name: "Laurent",
email: "camille.laurent@gmail.com", email: "camille.laurent@gmail.com",
phone_number: "07 56 78 90 12", phone_number: "+33785186013",
cell_phone_number: "07 56 78 90 12", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -329,8 +329,8 @@ export default async function main() {
first_name: "Julien", first_name: "Julien",
last_name: "Mercier", last_name: "Mercier",
email: "julien.mercier@hotmail.fr", email: "julien.mercier@hotmail.fr",
phone_number: "07 67 89 01 23", phone_number: "+33785186013",
cell_phone_number: "07 67 89 01 23", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -341,8 +341,8 @@ export default async function main() {
first_name: "Charlotte", first_name: "Charlotte",
last_name: "Lefebvre", last_name: "Lefebvre",
email: "charlotte.lefebvre@gmail.com", email: "charlotte.lefebvre@gmail.com",
phone_number: "07 78 90 12 34", phone_number: "+33785186013",
cell_phone_number: "07 78 90 12 34", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -353,8 +353,8 @@ export default async function main() {
first_name: "Caroline", first_name: "Caroline",
last_name: "Pallut", last_name: "Pallut",
email: "caroline.pallut@gmail.com", email: "caroline.pallut@gmail.com",
phone_number: "07 89 01 23 45", phone_number: "+33785186013",
cell_phone_number: "07 89 01 23 45", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -365,8 +365,8 @@ export default async function main() {
first_name: "Nadège", first_name: "Nadège",
last_name: "Gauchet", last_name: "Gauchet",
email: "nedege.gauchet@outlook.com", email: "nedege.gauchet@outlook.com",
phone_number: "06 11 22 33 44", phone_number: "+33785186013",
cell_phone_number: "06 11 22 33 44", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -377,8 +377,8 @@ export default async function main() {
first_name: "Matthieu", first_name: "Matthieu",
last_name: "Bougeard", last_name: "Bougeard",
email: "matthieu.bougeard@gmail.com", email: "matthieu.bougeard@gmail.com",
phone_number: "07 22 33 44 55", phone_number: "+33785186013",
cell_phone_number: "07 22 33 44 55", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -389,8 +389,8 @@ export default async function main() {
first_name: "Cécile", first_name: "Cécile",
last_name: "Celton", last_name: "Celton",
email: "cecile.celton@outlook.com", email: "cecile.celton@outlook.com",
phone_number: "06 55 66 77 88", phone_number: "+33785186013",
cell_phone_number: "06 55 66 77 88", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -401,8 +401,8 @@ export default async function main() {
first_name: "Gwendal", first_name: "Gwendal",
last_name: "Texier", last_name: "Texier",
email: "gwendal.texier@gmail.com", email: "gwendal.texier@gmail.com",
phone_number: "07 88 99 00 11", phone_number: "+33785186013",
cell_phone_number: "07 88 99 00 11", cell_phone_number: "+33785186013",
birthdate: null, birthdate: null,
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
@ -1548,7 +1548,7 @@ export default async function main() {
office: offices[0], office: offices[0],
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
} },
]; ];
const deedTypes: DeedType[] = [ const deedTypes: DeedType[] = [

View File

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

View File

@ -0,0 +1,79 @@
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!,
resent: totpCode.resent!,
},
};
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(),
resent: true,
},
});
}
/**
* Delete many totp codes
*/
public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs) {
return this.model.deleteMany({ ...query });
}
}

View File

@ -20,6 +20,10 @@ export default abstract class BaseController {
return this.httpResponse(response, HttpCodes.BAD_REQUEST, responseData); 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") { protected httpValidationError(response: Response, responseData: IResponseData = "Http Validation Error") {
return this.httpResponse(response, HttpCodes.VALIDATION_ERROR, responseData); return this.httpResponse(response, HttpCodes.VALIDATION_ERROR, responseData);
} }

View File

@ -9,5 +9,6 @@ enum HttpCodes {
NOT_FOUND = 404, NOT_FOUND = 404,
UNAUTHORIZED = 401, UNAUTHORIZED = 401,
FORBIDDEN = 403, FORBIDDEN = 403,
TOO_EARLY = 425,
} }
export default HttpCodes; 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 CustomersService from "@Services/super-admin/CustomersService/CustomersService";
import { ECustomerStatus } from "@prisma/client"; import { ECustomerStatus } from "@prisma/client";
import { Customer } from "le-coffre-resources/dist/Notary"; import { Customer } from "le-coffre-resources/dist/Notary";
import bcrypt from "bcrypt";
enum PROVIDER_OPENID { enum PROVIDER_OPENID {
idNot = "idNot", idNot = "idNot",
@ -19,9 +20,9 @@ export interface ICustomerJwtPayload {
} }
export interface IdNotJwtPayload { export interface IdNotJwtPayload {
sub: string, sub: string;
profile_idn: string, profile_idn: string;
entity_idn: string, entity_idn: string;
} }
export interface IUserJwtPayload { export interface IUserJwtPayload {
@ -44,7 +45,7 @@ export default class AuthService extends BaseService {
} }
public async getCustomerJwtPayload(customers: Customer[]): Promise<ICustomerJwtPayload | null> { public async getCustomerJwtPayload(customers: Customer[]): Promise<ICustomerJwtPayload | null> {
for (const customer of customers){ for (const customer of customers) {
if (customer.status === ECustomerStatus["PENDING"]) { if (customer.status === ECustomerStatus["PENDING"]) {
customer.status = ECustomerStatus["VALIDATED"]; customer.status = ECustomerStatus["VALIDATED"];
await this.customerService.update(customer.uid!, customer); await this.customerService.update(customer.uid!, customer);
@ -52,7 +53,7 @@ export default class AuthService extends BaseService {
} }
return { return {
customerId: customers[0]!.uid!, 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) { if (user.office_role) {
user.office_role.rules.forEach((rule) => { user.office_role.rules.forEach((rule) => {
if(!rules.includes(rule.name)) { if (!rules.includes(rule.name)) {
rules.push(rule.name); rules.push(rule.name);
} }
}); });
@ -84,11 +85,11 @@ export default class AuthService extends BaseService {
}; };
} }
public generateAccessToken(user: any): string { 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 { 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) { public verifyAccessToken(token: string, callback?: VerifyCallback) {
@ -98,4 +99,12 @@ export default class AuthService extends BaseService {
public verifyRefreshToken(token: string, callback?: VerifyCallback) { public verifyRefreshToken(token: string, callback?: VerifyCallback) {
return jwt.verify(token, this.variables.REFRESH_TOKEN_SECRET, callback); 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

@ -6,6 +6,7 @@ import IdNotService from "../IdNotService/IdNotService";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import NotificationBuilder from "@Common/notifications/NotificationBuilder"; import NotificationBuilder from "@Common/notifications/NotificationBuilder";
import EmailBuilder from "@Common/emails/EmailBuilder"; import EmailBuilder from "@Common/emails/EmailBuilder";
import TotpService from "@Services/customer/TotpService/TotpService";
// import { PrismaClient } from "@prisma/client"; // import { PrismaClient } from "@prisma/client";
@Service() @Service()
@ -16,6 +17,7 @@ export default class CronService {
private idNotService: IdNotService, private idNotService: IdNotService,
private notificationBuilder: NotificationBuilder, private notificationBuilder: NotificationBuilder,
private emailBuilder: EmailBuilder, private emailBuilder: EmailBuilder,
private totpService: TotpService,
) {} ) {}
public async sendMails() { public async sendMails() {
@ -86,8 +88,8 @@ export default class CronService {
// Once a day at midnight // Once a day at midnight
try { try {
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const expiringDocuments15Days: [{uid: string, expiration_date: Date}] = await prisma.$queryRaw const expiringDocuments15Days: [{ uid: string; expiration_date: Date }] =
`SELECT distinct o.uid, f.created_at as expiration_date await prisma.$queryRaw`SELECT distinct o.uid, f.created_at as expiration_date
FROM documents d FROM documents d
JOIN files f ON d.uid=f.document_uid JOIN files f ON d.uid=f.document_uid
JOIN office_folders o ON o.uid=d.folder_uid JOIN office_folders o ON o.uid=d.folder_uid
@ -95,11 +97,24 @@ export default class CronService {
AND d.document_status = 'DEPOSITED' AND d.document_status = 'DEPOSITED'
AND current_date = (DATE(f.created_at) + interval '3 months' - interval '2 days');`; AND current_date = (DATE(f.created_at) + interval '3 months' - interval '2 days');`;
expiringDocuments15Days.forEach(expiringFolder => { expiringDocuments15Days.forEach((expiringFolder) => {
this.notificationBuilder.sendDocumentExpiringSoonNotification(expiringFolder.uid, expiringFolder.expiration_date); this.notificationBuilder.sendDocumentExpiringSoonNotification(expiringFolder.uid, expiringFolder.expiration_date);
}); });
} catch (e) {
console.error(e);
}
});
// Start job
if (!cronJob.running) {
cronJob.start();
}
}
public async cleanExpiredTotpCodes() {
const cronJob = new CronJob("0 0 * * *", async () => {
// Once a day at midnight
try {
await this.totpService.cleanExpiredTotpCodes();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@ -117,11 +117,16 @@ export default class IdNotService extends BaseService {
const query = new URLSearchParams({ const query = new URLSearchParams({
client_id: this.variables.IDNOT_CLIENT_ID, client_id: this.variables.IDNOT_CLIENT_ID,
client_secret: this.variables.IDNOT_CLIENT_SECRET, client_secret: this.variables.IDNOT_CLIENT_SECRET,
redirect_uri: `${this.variables.APP_HOST}/authorized-client`, redirect_uri: this.variables.IDNOT_REDIRECT_URL,
code: code, code: code,
grant_type: "authorization_code", grant_type: "authorization_code",
}); });
console.log(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query);
const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" });
if(token.status !== 200) console.error(await token.text());
const decodedToken = (await token.json()) as IIdNotToken; const decodedToken = (await token.json()) as IIdNotToken;
const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload;

View File

@ -0,0 +1,36 @@
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();
}
public async sendSms(phoneNumber: string, message: string): Promise<boolean> {
const ovh = require("ovh")({
appKey: this.variables.OVH_APP_KEY,
appSecret: this.variables.OVH_APP_SECRET,
consumerKey: this.variables.OVH_CONSUMER_KEY,
});
const serviceName = this.variables.OVH_SMS_SERVICE_NAME;
await ovh.request('POST', '/sms/' + serviceName + '/jobs/', {
message: message,
sender: "LeCoffre",
senderForResponse: true,
receivers: [phoneNumber],
}, (error: any, response: any) => {
if (error) {
console.error('Error sending Ovh Sms');
return false;
} else {
console.log('SMS sent successfully via Ovh');
return true;
}
});
return true;
}
}

View File

@ -0,0 +1,32 @@
import { BackendVariables } from "@Common/config/variables/Variables";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
import axios from "axios";
@Service()
export default class SmsFactorService extends BaseService {
constructor(private variables: BackendVariables) {
super();
}
public async sendSms(phoneNumber: string, message: string){
axios
.get(
"https://api.smsfactor.com/send/simulate?to=" +
phoneNumber +
"&sender=LeCoffre&text=" +
message +
"&token=" +
this.variables.SMS_FACTOR_TOKEN,
{},
)
.then((response) => {
console.log("SMS sent successfully via Sms Factor");
return true;
})
.catch((error) => {
console.error("Error sending Sms Factor SMS");
return false;
});
}
}

View File

@ -1,11 +1,66 @@
import { Customers, Prisma } from "@prisma/client"; // import { BackendVariables } from "@Common/config/variables/Variables";
import { Customers, Prisma, TotpCodes } from "@prisma/client";
import CustomersRepository from "@Repositories/CustomersRepository"; import CustomersRepository from "@Repositories/CustomersRepository";
import TotpCodesRepository from "@Repositories/TotpCodesRepository";
import BaseService from "@Services/BaseService"; 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 { 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() @Service()
export default class CustomersService extends BaseService { 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,
private smsFactorService: SmsFactorService,
) {
super(); super();
} }
@ -16,4 +71,337 @@ export default class CustomersService extends BaseService {
public async get(query: Prisma.CustomersFindManyArgs): Promise<Customers[]> { public async get(query: Prisma.CustomersFindManyArgs): Promise<Customers[]> {
return this.customerRepository.findMany(query); 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,
},
},
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.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<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;
}
} }

View File

@ -0,0 +1,51 @@
import { Prisma, TotpCodes } from "@prisma/client";
import TotpCodesRepository from "@Repositories/TotpCodesRepository";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
@Service()
export default class TotpService extends BaseService {
constructor(private totpCodesRepository: TotpCodesRepository) {
super();
}
/**
* @description : Get all totp codes
* @throws {Error} If totp codes cannot be get
*/
public async get(query: Prisma.TotpCodesFindManyArgs): Promise<TotpCodes[]> {
return this.totpCodesRepository.findMany(query);
}
/**
* @description : Get one totp code
* @throws {Error} If totp code cannot be get
*/
public async getOne(query: Prisma.TotpCodesFindFirstArgs): Promise<TotpCodes | null> {
return this.totpCodesRepository.findOne(query);
}
/**
* @description : Delete many totp codes
* @throws {Error} If totp code cannot be deleted
*/
public async deleteMany(query: Prisma.TotpCodesDeleteManyArgs): Promise<Prisma.BatchPayload> {
return this.totpCodesRepository.deleteMany(query);
}
/**
* @description : Delete every totp code expired for more than 30 days
* @throws {Error} If totp codes cannot be deleted
*/
public async cleanExpiredTotpCodes() {
const query: Prisma.TotpCodesDeleteManyArgs = {
where: {
expire_at: {
lte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
},
},
};
return this.totpCodesRepository.deleteMany(query);
}
}

View File

@ -4,11 +4,7 @@
// "module": "es2022", // "module": "es2022",
"target": "es2017", "target": "es2017",
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"jsx": "preserve", "jsx": "preserve",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
@ -18,7 +14,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"allowUnreachableCode": false, "allowUnreachableCode": false,
"allowUnusedLabels": false, "allowUnusedLabels": true,
"exactOptionalPropertyTypes": false, "exactOptionalPropertyTypes": false,
"noImplicitOverride": true, "noImplicitOverride": true,
"strict": true, "strict": true,
@ -32,6 +28,7 @@
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
/* Additional Checks */ /* Additional Checks */
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true, "useUnknownInCatchVariables": true,
@ -39,39 +36,17 @@
"moduleResolution": "node", "moduleResolution": "node",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@App/*": [ "@App/*": ["src/app/*"],
"src/app/*" "@Api/*": ["src/app/api/*"],
], "@Services/*": ["src/services/*"],
"@Api/*": [ "@Repositories/*": ["src/common/repositories/*"],
"src/app/api/*" "@Entries/*": ["src/entries/*"],
], "@Common/*": ["src/common/*"],
"@Services/*": [ "@Config/*": ["src/common/config/*"],
"src/services/*" "@Entities/*": ["src/common/ressources/*"],
], "@System/*": ["src/common/system/*"],
"@Repositories/*": [ "@ControllerPattern/*": ["src/common/system/controller-pattern/*"],
"src/common/repositories/*" "@Test/*": ["src/test/*"]
],
"@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": [], // "rootDirs": [],
// "typeRoots": [], // "typeRoots": [],
@ -90,13 +65,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
"isolatedModules": true, "isolatedModules": true
}, },
"include": [ "include": ["**/*.ts", "**/*.tsx", "src/services/common/TestService"],
"**/*.ts", "exclude": ["node_modules"]
"**/*.tsx", "src/services/common/TestService",
],
"exclude": [
"node_modules"
]
} }