Preprod (#176)
This commit is contained in:
commit
0f05ce8c8e
@ -45,6 +45,7 @@
|
||||
"@mailchimp/mailchimp_transactional": "^1.0.50",
|
||||
"@pinata/sdk": "^2.1.0",
|
||||
"@prisma/client": "^4.11.0",
|
||||
"@sentry/node": "^7.91.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios": "^1.6.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
@ -57,7 +58,7 @@
|
||||
"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.104",
|
||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.106",
|
||||
"module-alias": "^2.2.2",
|
||||
"monocle-ts": "^2.3.13",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
|
@ -56,7 +56,7 @@ export default class CustomersController extends ApiController {
|
||||
/**
|
||||
* @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) {
|
||||
try {
|
||||
//init IUser resource with request body values
|
||||
@ -69,6 +69,8 @@ export default class CustomersController extends ApiController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customerEntity.contact?.cell_phone_number) return;
|
||||
|
||||
const customers = await this.customersService.get({
|
||||
where: {
|
||||
contact: { email: customerEntity.contact?.email },
|
||||
|
@ -35,7 +35,7 @@ export default class AuthController extends ApiController {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
} catch (error) {
|
||||
@ -58,7 +58,7 @@ export default class AuthController extends ApiController {
|
||||
this.httpNotFoundRequest(response, "Customer not found");
|
||||
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) {
|
||||
if (error instanceof SmsNotExpiredError) {
|
||||
this.httpTooEarlyRequest(response, error.message);
|
||||
@ -229,7 +229,7 @@ export default class AuthController extends ApiController {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -5,11 +5,14 @@ import { Service } from "typedi";
|
||||
import AuthService, { IUserJwtPayload } from "@Services/common/AuthService/AuthService";
|
||||
|
||||
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()
|
||||
@Service()
|
||||
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();
|
||||
}
|
||||
|
||||
@ -25,16 +28,39 @@ export default class UserController extends ApiController {
|
||||
if (!code) throw new Error("code is required");
|
||||
|
||||
const idNotToken = await this.idNotService.getIdNotToken(code);
|
||||
|
||||
if(!idNotToken) {
|
||||
this.httpValidationError(response, "IdNot token undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.idNotService.getOrCreateUser(idNotToken);
|
||||
|
||||
if(!user) {
|
||||
this.httpUnauthorized(response);
|
||||
this.httpUnauthorized(response, "Email not found");
|
||||
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.updateOffice(user.office_uid);
|
||||
|
||||
|
@ -68,6 +68,8 @@ export default class CustomersController extends ApiController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customerEntity.contact?.cell_phone_number) return;
|
||||
|
||||
const customers = await this.customersService.get({
|
||||
where: {
|
||||
contact: { email: customerEntity.contact?.email },
|
||||
|
@ -57,7 +57,7 @@ export default class CustomersController extends ApiController {
|
||||
/**
|
||||
* @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) {
|
||||
try {
|
||||
//init IUser resource with request body values
|
||||
@ -70,6 +70,8 @@ export default class CustomersController extends ApiController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customerEntity.contact?.cell_phone_number) return;
|
||||
|
||||
const customers = await this.customersService.get({
|
||||
where: {
|
||||
contact: { email: customerEntity.contact?.email },
|
||||
|
@ -1,6 +1,7 @@
|
||||
import HttpException from "@Common/system/controller-pattern/exceptions/HttpException";
|
||||
import HttpCodes from "@Common/system/controller-pattern/HttpCodes";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
export default function errorHandler(error: any, req: Request, response: Response, next: NextFunction) {
|
||||
const errorStatus: number = error["status"];
|
||||
@ -15,6 +16,18 @@ export default function errorHandler(error: any, req: Request, response: Respons
|
||||
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) {
|
||||
response.status(error.httpCode).send(error.message);
|
||||
return;
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "contacts" ALTER COLUMN "cell_phone_number" DROP NOT NULL;
|
@ -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");
|
@ -37,7 +37,7 @@ model Contacts {
|
||||
last_name String @db.VarChar(255)
|
||||
email String @db.VarChar(255)
|
||||
phone_number String? @db.VarChar(50)
|
||||
cell_phone_number String @db.VarChar(50)
|
||||
cell_phone_number String? @db.VarChar(50)
|
||||
civility ECivility @default(MALE)
|
||||
address Addresses? @relation(fields: [address_uid], references: [uid], onDelete: Cascade)
|
||||
address_uid String? @unique @db.VarChar(255)
|
||||
@ -72,6 +72,15 @@ model 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 {
|
||||
uid String @id @unique @default(uuid())
|
||||
idNot String @unique @db.VarChar(255)
|
||||
|
@ -56,7 +56,7 @@ export default class UsersRepository extends BaseRepository {
|
||||
last_name: user.contact!.last_name,
|
||||
email: user.contact!.email,
|
||||
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],
|
||||
},
|
||||
},
|
||||
|
39
src/common/repositories/WhitelistRepository.ts
Normal file
39
src/common/repositories/WhitelistRepository.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -48,6 +48,10 @@ export default abstract class BaseController {
|
||||
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 = {}) {
|
||||
if (responseData instanceof Error) {
|
||||
throw responseData;
|
||||
|
@ -8,6 +8,7 @@ import bodyParser from "body-parser";
|
||||
import errorHandler from "@App/middlewares/ErrorHandler";
|
||||
import { BackendVariables } from "@Common/config/variables/Variables";
|
||||
import multer from "multer";
|
||||
import "../sentry.config";
|
||||
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
|
16
src/sentry.config.ts
Normal file
16
src/sentry.config.ts
Normal 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");
|
@ -121,9 +121,6 @@ export default class IdNotService extends BaseService {
|
||||
code: 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" });
|
||||
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);
|
||||
const user = await this.userService.create(userHydrated);
|
||||
const userOffice = await this.officeService.getByUid(user.office_uid);
|
||||
|
@ -27,7 +27,6 @@ export default class OvhService extends BaseService {
|
||||
console.error('Error sending Ovh Sms');
|
||||
return false;
|
||||
} else {
|
||||
console.log('SMS sent successfully via Ovh');
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
@ -21,7 +21,6 @@ export default class SmsFactorService extends BaseService {
|
||||
{},
|
||||
)
|
||||
.then((response) => {
|
||||
console.log("SMS sent successfully via Sms Factor");
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
|
14
src/services/common/WhitelistService/WhitelistService.ts
Normal file
14
src/services/common/WhitelistService/WhitelistService.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -12,43 +12,43 @@ import SmsFactorService from "@Services/common/SmsFactorService/SmsFactorService
|
||||
|
||||
export class SmsNotExpiredError extends Error {
|
||||
constructor() {
|
||||
super("SMS code not expired");
|
||||
super("Code déjà envoyé");
|
||||
}
|
||||
}
|
||||
|
||||
export class TotpCodeExpiredError extends Error {
|
||||
constructor() {
|
||||
super("Totp code not found or expired");
|
||||
super("Code non trouvé ou expiré, veuillez raffraîchir la page");
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidTotpCodeError extends Error {
|
||||
constructor() {
|
||||
super("Invalid Totp code");
|
||||
super("Code invalide");
|
||||
}
|
||||
}
|
||||
|
||||
export class NotRegisteredCustomerError extends Error {
|
||||
constructor() {
|
||||
super("Customer not registered");
|
||||
super("Ce client n'existe pas");
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidPasswordError extends Error {
|
||||
constructor() {
|
||||
super("Invalid password");
|
||||
super("Mot de passe incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordAlreadySetError extends Error {
|
||||
constructor() {
|
||||
super("Password already set");
|
||||
super("Le mot de passe a déjà été défini");
|
||||
}
|
||||
}
|
||||
|
||||
export class TooSoonForNewCode extends Error {
|
||||
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()
|
||||
@ -110,7 +110,7 @@ export default class CustomersService extends BaseService {
|
||||
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);
|
||||
if (this.variables.ENV !== "dev") await this.sendSmsCodeToCustomer(totpPin, customer);
|
||||
return {
|
||||
customer,
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
// 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 };
|
||||
}
|
||||
|
||||
@ -364,7 +364,6 @@ export default class CustomersService extends BaseService {
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user