This commit is contained in:
VincentAlamelle 2024-01-04 11:18:33 +01:00 committed by GitHub
commit 0f05ce8c8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 177 additions and 32 deletions

View File

@ -45,6 +45,7 @@
"@mailchimp/mailchimp_transactional": "^1.0.50", "@mailchimp/mailchimp_transactional": "^1.0.50",
"@pinata/sdk": "^2.1.0", "@pinata/sdk": "^2.1.0",
"@prisma/client": "^4.11.0", "@prisma/client": "^4.11.0",
"@sentry/node": "^7.91.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"axios": "^1.6.2", "axios": "^1.6.2",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
@ -57,7 +58,7 @@
"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.104", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.106",
"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",

View File

@ -56,7 +56,7 @@ export default class CustomersController extends ApiController {
/** /**
* @description Create a new customer * @description Create a new customer
*/ */
@Post("/api/v1/notary/customers", [authHandler, ruleHandler]) @Post("/api/v1/admin/customers", [authHandler, ruleHandler])
protected async post(req: Request, response: Response) { protected async post(req: Request, response: Response) {
try { try {
//init IUser resource with request body values //init IUser resource with request body values
@ -69,6 +69,8 @@ export default class CustomersController extends ApiController {
return; return;
} }
if (!customerEntity.contact?.cell_phone_number) return;
const customers = await this.customersService.get({ const customers = await this.customersService.get({
where: { where: {
contact: { email: customerEntity.contact?.email }, contact: { email: customerEntity.contact?.email },

View File

@ -35,7 +35,7 @@ export default class AuthController extends ApiController {
return; return;
} }
this.httpSuccess(response, { this.httpSuccess(response, {
partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), partialPhoneNumber: res.customer.contact?.cell_phone_number?.replace(/\s/g, "").slice(-4),
totpCodeUid: res.totpCode.uid, totpCodeUid: res.totpCode.uid,
}); });
} catch (error) { } catch (error) {
@ -58,7 +58,7 @@ export default class AuthController extends ApiController {
this.httpNotFoundRequest(response, "Customer not found"); this.httpNotFoundRequest(response, "Customer not found");
return; return;
} }
this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4) }); this.httpSuccess(response, { partialPhoneNumber: customer.contact?.cell_phone_number?.replace(/\s/g, "").slice(-4) });
} catch (error) { } catch (error) {
if (error instanceof SmsNotExpiredError) { if (error instanceof SmsNotExpiredError) {
this.httpTooEarlyRequest(response, error.message); this.httpTooEarlyRequest(response, error.message);
@ -229,7 +229,7 @@ export default class AuthController extends ApiController {
return; return;
} }
this.httpSuccess(response, { this.httpSuccess(response, {
partialPhoneNumber: res.customer.contact?.cell_phone_number.replace(/\s/g, "").slice(-4), partialPhoneNumber: res.customer.contact?.cell_phone_number?.replace(/\s/g, "").slice(-4),
totpCodeUid: res.totpCode.uid, totpCodeUid: res.totpCode.uid,
}); });
} catch (error) { } catch (error) {

View File

@ -5,11 +5,14 @@ import { Service } from "typedi";
import AuthService, { IUserJwtPayload } from "@Services/common/AuthService/AuthService"; import AuthService, { IUserJwtPayload } from "@Services/common/AuthService/AuthService";
import IdNotService from "@Services/common/IdNotService/IdNotService"; import IdNotService from "@Services/common/IdNotService/IdNotService";
import WhitelistService from "@Services/common/WhitelistService/WhitelistService";
import User from "le-coffre-resources/dist/SuperAdmin";
import UsersService from "@Services/super-admin/UsersService/UsersService";
@Controller() @Controller()
@Service() @Service()
export default class UserController extends ApiController { export default class UserController extends ApiController {
constructor(private authService: AuthService, private idNotService: IdNotService) { constructor(private authService: AuthService, private idNotService: IdNotService, private whitelistService: WhitelistService, private userService: UsersService) {
super(); super();
} }
@ -25,16 +28,39 @@ export default class UserController extends ApiController {
if (!code) throw new Error("code is required"); if (!code) throw new Error("code is required");
const idNotToken = await this.idNotService.getIdNotToken(code); const idNotToken = await this.idNotService.getIdNotToken(code);
if(!idNotToken) { if(!idNotToken) {
this.httpValidationError(response, "IdNot token undefined"); this.httpValidationError(response, "IdNot token undefined");
return; return;
} }
const user = await this.idNotService.getOrCreateUser(idNotToken); const user = await this.idNotService.getOrCreateUser(idNotToken);
if(!user) { if(!user) {
this.httpUnauthorized(response); this.httpUnauthorized(response, "Email not found");
return; return;
} }
//Whitelist feature
//Get user with contact
const prismaUser = await this.userService.getByUid(user.uid, {contact: true });
if (!prismaUser) {
this.httpNotFoundRequest(response, "user not found");
return;
}
//Hydrate user to be able to use his contact
const userHydrated = User.hydrate<User>(prismaUser, { strategy: "excludeAll" });
//Check if user is whitelisted
const isWhitelisted = await this.whitelistService.getByEmail(userHydrated.contact!.email);
//If not whitelisted, return 409 Not whitelisted
if (!isWhitelisted) {
this.httpNotWhitelisted(response);
return;
}
await this.idNotService.updateUser(user.uid); await this.idNotService.updateUser(user.uid);
await this.idNotService.updateOffice(user.office_uid); await this.idNotService.updateOffice(user.office_uid);

View File

@ -68,6 +68,8 @@ export default class CustomersController extends ApiController {
return; return;
} }
if (!customerEntity.contact?.cell_phone_number) return;
const customers = await this.customersService.get({ const customers = await this.customersService.get({
where: { where: {
contact: { email: customerEntity.contact?.email }, contact: { email: customerEntity.contact?.email },

View File

@ -57,7 +57,7 @@ export default class CustomersController extends ApiController {
/** /**
* @description Create a new customer * @description Create a new customer
*/ */
@Post("/api/v1/notary/customers", [authHandler, ruleHandler]) @Post("/api/v1/super-admin/customers", [authHandler, ruleHandler])
protected async post(req: Request, response: Response) { protected async post(req: Request, response: Response) {
try { try {
//init IUser resource with request body values //init IUser resource with request body values
@ -70,6 +70,8 @@ export default class CustomersController extends ApiController {
return; return;
} }
if (!customerEntity.contact?.cell_phone_number) return;
const customers = await this.customersService.get({ const customers = await this.customersService.get({
where: { where: {
contact: { email: customerEntity.contact?.email }, contact: { email: customerEntity.contact?.email },

View File

@ -1,6 +1,7 @@
import HttpException from "@Common/system/controller-pattern/exceptions/HttpException"; import HttpException from "@Common/system/controller-pattern/exceptions/HttpException";
import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; import HttpCodes from "@Common/system/controller-pattern/HttpCodes";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import * as Sentry from "@sentry/node";
export default function errorHandler(error: any, req: Request, response: Response, next: NextFunction) { export default function errorHandler(error: any, req: Request, response: Response, next: NextFunction) {
const errorStatus: number = error["status"]; const errorStatus: number = error["status"];
@ -15,6 +16,18 @@ export default function errorHandler(error: any, req: Request, response: Respons
return; return;
} }
const transaction = Sentry.startTransaction({
op: "Crashed Application",
name: "Crashed Application",
});
if (error instanceof Error) {
Sentry.captureException(error.stack);
} else {
Sentry.captureException(error);
}
transaction.finish();
if (error instanceof HttpException) { if (error instanceof HttpException) {
response.status(error.httpCode).send(error.message); response.status(error.httpCode).send(error.message);
return; return;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "contacts" ALTER COLUMN "cell_phone_number" DROP NOT NULL;

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "whitelist" (
"uid" TEXT NOT NULL,
"email" VARCHAR(255) NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "whitelist_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "whitelist_uid_key" ON "whitelist"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "whitelist_email_key" ON "whitelist"("email");

View File

@ -37,7 +37,7 @@ model Contacts {
last_name String @db.VarChar(255) last_name String @db.VarChar(255)
email String @db.VarChar(255) email String @db.VarChar(255)
phone_number String? @db.VarChar(50) phone_number String? @db.VarChar(50)
cell_phone_number String @db.VarChar(50) cell_phone_number String? @db.VarChar(50)
civility ECivility @default(MALE) civility ECivility @default(MALE)
address Addresses? @relation(fields: [address_uid], references: [uid], onDelete: Cascade) address Addresses? @relation(fields: [address_uid], references: [uid], onDelete: Cascade)
address_uid String? @unique @db.VarChar(255) address_uid String? @unique @db.VarChar(255)
@ -72,6 +72,15 @@ model Users {
@@map("users") @@map("users")
} }
model Whitelist {
uid String @id @unique @default(uuid())
email String @unique @db.VarChar(255)
active Boolean @default(true)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("whitelist")
}
model Offices { model Offices {
uid String @id @unique @default(uuid()) uid String @id @unique @default(uuid())
idNot String @unique @db.VarChar(255) idNot String @unique @db.VarChar(255)

View File

@ -56,7 +56,7 @@ export default class UsersRepository extends BaseRepository {
last_name: user.contact!.last_name, last_name: user.contact!.last_name,
email: user.contact!.email, email: user.contact!.email,
phone_number: user.contact?.phone_number, phone_number: user.contact?.phone_number,
cell_phone_number: user.contact!.cell_phone_number, cell_phone_number: user.contact!.cell_phone_number || null,
civility: ECivility[user.contact?.civility as keyof typeof ECivility], civility: ECivility[user.contact?.civility as keyof typeof ECivility],
}, },
}, },

View File

@ -0,0 +1,39 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Prisma } from "prisma/prisma-client";
@Service()
export default class WhitelistRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().whitelist;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many whitelist
*/
public async findMany(query: Prisma.WhitelistFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
return this.model.findMany(query);
}
/**
* @description : find unique by email
*/
public async findOneByEmail(email: string) {
return this.model.findUnique({
where: {
email: email,
},
});
}
}

View File

@ -48,6 +48,10 @@ export default abstract class BaseController {
return this.httpResponse(response, HttpCodes.FORBIDDEN, responseData); return this.httpResponse(response, HttpCodes.FORBIDDEN, responseData);
} }
protected httpNotWhitelisted(response: Response, responseData: IResponseData = "Not whitelisted") {
return this.httpResponse(response, HttpCodes.VALIDATION_ERROR, responseData);
}
protected httpResponse(response: Response, httpCode: HttpCodes, responseData: IResponseData = {}) { protected httpResponse(response: Response, httpCode: HttpCodes, responseData: IResponseData = {}) {
if (responseData instanceof Error) { if (responseData instanceof Error) {
throw responseData; throw responseData;

View File

@ -8,6 +8,7 @@ import bodyParser from "body-parser";
import errorHandler from "@App/middlewares/ErrorHandler"; import errorHandler from "@App/middlewares/ErrorHandler";
import { BackendVariables } from "@Common/config/variables/Variables"; import { BackendVariables } from "@Common/config/variables/Variables";
import multer from "multer"; import multer from "multer";
import "../sentry.config";
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();

16
src/sentry.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { BackendVariables } from "@Common/config/variables/Variables";
import * as Sentry from "@sentry/node";
import Container from "typedi";
const variables = Container.get(BackendVariables);
Sentry.init({
dsn: "https://ca6a89e8b480c814e1b5828b8412c681@o4506382103281664.ingest.sentry.io/4506399972130816",
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
environment: variables.ENV,
});
Sentry.setTag("service", "leCoffre-back");

View File

@ -121,9 +121,6 @@ export default class IdNotService extends BaseService {
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()); if(token.status !== 200) console.error(await token.text());
@ -363,6 +360,10 @@ export default class IdNotService extends BaseService {
}, },
}; };
if(!userToAdd.contact.email) {
return null;
}
let userHydrated = User.hydrate<User>(userToAdd); let userHydrated = User.hydrate<User>(userToAdd);
const user = await this.userService.create(userHydrated); const user = await this.userService.create(userHydrated);
const userOffice = await this.officeService.getByUid(user.office_uid); const userOffice = await this.officeService.getByUid(user.office_uid);

View File

@ -27,7 +27,6 @@ export default class OvhService extends BaseService {
console.error('Error sending Ovh Sms'); console.error('Error sending Ovh Sms');
return false; return false;
} else { } else {
console.log('SMS sent successfully via Ovh');
return true; return true;
} }
}); });

View File

@ -21,7 +21,6 @@ export default class SmsFactorService extends BaseService {
{}, {},
) )
.then((response) => { .then((response) => {
console.log("SMS sent successfully via Sms Factor");
return true; return true;
}) })
.catch((error) => { .catch((error) => {

View File

@ -0,0 +1,14 @@
import WhitelistRepository from "@Repositories/WhitelistRepository";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
@Service()
export default class WhitelistService extends BaseService {
constructor(private whitelistRepository: WhitelistRepository) {
super();
}
public async getByEmail(email: string): Promise<any> {
return this.whitelistRepository.findOneByEmail(email);
}
}

View File

@ -12,43 +12,43 @@ import SmsFactorService from "@Services/common/SmsFactorService/SmsFactorService
export class SmsNotExpiredError extends Error { export class SmsNotExpiredError extends Error {
constructor() { constructor() {
super("SMS code not expired"); super("Code déjà envoyé");
} }
} }
export class TotpCodeExpiredError extends Error { export class TotpCodeExpiredError extends Error {
constructor() { constructor() {
super("Totp code not found or expired"); super("Code non trouvé ou expiré, veuillez raffraîchir la page");
} }
} }
export class InvalidTotpCodeError extends Error { export class InvalidTotpCodeError extends Error {
constructor() { constructor() {
super("Invalid Totp code"); super("Code invalide");
} }
} }
export class NotRegisteredCustomerError extends Error { export class NotRegisteredCustomerError extends Error {
constructor() { constructor() {
super("Customer not registered"); super("Ce client n'existe pas");
} }
} }
export class InvalidPasswordError extends Error { export class InvalidPasswordError extends Error {
constructor() { constructor() {
super("Invalid password"); super("Mot de passe incorrect");
} }
} }
export class PasswordAlreadySetError extends Error { export class PasswordAlreadySetError extends Error {
constructor() { constructor() {
super("Password already set"); super("Le mot de passe a déjà été défini");
} }
} }
export class TooSoonForNewCode extends Error { export class TooSoonForNewCode extends Error {
constructor() { constructor() {
super("You need to wait at least 30 seconds before asking for a new code"); super("Vous devez attendre 30 secondes avant de pouvoir demander un nouveau code");
} }
} }
@Service() @Service()
@ -110,7 +110,7 @@ export default class CustomersService extends BaseService {
const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), reason); const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), reason);
if (!totpCode) return null; if (!totpCode) return null;
// 5: Send the SMS code to the customer // 5: Send the SMS code to the customer
if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); if (this.variables.ENV !== "dev") await this.sendSmsCodeToCustomer(totpPin, customer);
return { return {
customer, customer,
totpCode: TotpCodesResource.hydrate<TotpCodesResource>({ totpCode: TotpCodesResource.hydrate<TotpCodesResource>({
@ -162,7 +162,7 @@ export default class CustomersService extends BaseService {
await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD); await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60000), TotpCodesReasons.RESET_PASSWORD);
// 5: Send the SMS code to the customer // 5: Send the SMS code to the customer
if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); if (this.variables.ENV !== "dev") await this.sendSmsCodeToCustomer(totpPin, customer);
return customer; return customer;
} }
@ -292,7 +292,7 @@ export default class CustomersService extends BaseService {
const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), totpCodeToResend.reason!, true); const totpCode = await this.saveTotpPin(customer, totpPin, new Date(now + 5 * 60 * 1000), totpCodeToResend.reason!, true);
// 7: Send the SMS code to the customer // 7: Send the SMS code to the customer
if(this.variables.ENV !== 'dev') await this.sendSmsCodeToCustomer(totpPin, customer); if (this.variables.ENV !== "dev") await this.sendSmsCodeToCustomer(totpPin, customer);
return { customer, totpCode }; return { customer, totpCode };
} }
@ -364,7 +364,6 @@ export default class CustomersService extends BaseService {
if (!customer.contact?.cell_phone_number) return; if (!customer.contact?.cell_phone_number) return;
let success = await this.ovhService.sendSms(customer.contact?.cell_phone_number, message); let success = await this.ovhService.sendSms(customer.contact?.cell_phone_number, message);
// Si l'envoi échoue, basculez automatiquement sur le second fournisseur // Si l'envoi échoue, basculez automatiquement sur le second fournisseur
if (!success) { if (!success) {
//const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService; //const alternateProvider = this.variables.SMS_PROVIDER === "OVH" ? this.smsFactorService : this.ovhService;