From 15b51a56a18fc59799257da41f5eedde00a93fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:48:59 +0200 Subject: [PATCH 1/3] add optics for folder files path retrieval --- src/common/optics/notary/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/optics/notary/index.ts b/src/common/optics/notary/index.ts index 4e9441e2..1ee49646 100644 --- a/src/common/optics/notary/index.ts +++ b/src/common/optics/notary/index.ts @@ -10,6 +10,7 @@ import { Document, File, OfficeFolder } from "le-coffre-resources/dist/Notary"; export const folderDocumentsLens = Optics.Lens.fromNullableProp()("documents", []); export const documentFilesLens = Optics.Lens.fromNullableProp()("files", []); export const fileHashLens = Optics.Lens.fromProp()("hash"); +export const filePathLens = Optics.Lens.fromProp()("file_path"); /** * Traversals @@ -17,13 +18,16 @@ export const fileHashLens = Optics.Lens.fromProp()("hash"); export const documentsTraversal = Optics.fromTraversable(Array.Traversable)(); export const filesTraversal = Optics.fromTraversable(Array.Traversable)(); -export const folderHashesTraversal = folderDocumentsLens +export const folderFilesTraversal = folderDocumentsLens .composeTraversal(documentsTraversal) .composeLens(documentFilesLens) - .composeTraversal(filesTraversal) - .composeLens(fileHashLens); + .composeTraversal(filesTraversal); + +export const folderHashesTraversal = folderFilesTraversal.composeLens(fileHashLens); +export const folderFilesPathTraversal = folderFilesTraversal.composeLens(filePathLens); /** * Getters */ export const getFolderHashes = (folder: OfficeFolder) => Traversal.getAll(folder)(folderHashesTraversal); +export const getFolderFilesPath = (folder: OfficeFolder) => Traversal.getAll(folder)(folderFilesPathTraversal); From 0728559e0ec16563309a482b29b3a64df532f3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:47:12 +0200 Subject: [PATCH 2/3] update `anchors/download` route to get a complete zip archive The zip will contain: - the anchoring proof PDF ; - plus all accessible files stored on IPFS. If the key is invalid, a broken file will be added to the archive. If the key is missing, the file will not be added to the archive, because there is no way it could be deciphered. For now, all network errors from IPFS are silently ignored, resulting in an archive containing only the anchoring proof. --- package.json | 2 ++ .../notary/OfficeFolderAnchorsController.ts | 29 +++++++++++++++---- src/common/optics/notary/index.ts | 7 +++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 93cd586e..0c161e19 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@mailchimp/mailchimp_transactional": "^1.0.50", "@pinata/sdk": "^2.1.0", "@prisma/client": "^4.11.0", + "adm-zip": "^0.5.10", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "classnames": "^2.3.2", @@ -71,6 +72,7 @@ "uuidv4": "^6.2.13" }, "devDependencies": { + "@types/adm-zip": "^0.5.3", "@types/cors": "^2.8.13", "@types/cron": "^2.0.1", "@types/express": "^4.17.16", diff --git a/src/app/api/notary/OfficeFolderAnchorsController.ts b/src/app/api/notary/OfficeFolderAnchorsController.ts index 3a43a703..c5a8a65b 100644 --- a/src/app/api/notary/OfficeFolderAnchorsController.ts +++ b/src/app/api/notary/OfficeFolderAnchorsController.ts @@ -3,9 +3,10 @@ import { Controller, Get, Post } from "@ControllerPattern/index"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; import { Document, OfficeFolder } from "le-coffre-resources/dist/Notary"; -import { getFolderHashes } from "@Common/optics/notary"; +import { getFolderHashes, getFolderFilesUid } from "@Common/optics/notary"; import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; import OfficeFolderAnchorsRepository from "@Repositories/OfficeFolderAnchorsRepository"; +import FilesService from "@Services/common/FilesService/FilesService"; import SecureService from "@Services/common/SecureService/SecureService"; import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; @@ -14,6 +15,7 @@ import OfficeFolderAnchor from "le-coffre-resources/dist/Notary/OfficeFolderAnch import NotificationBuilder from "@Common/notifications/NotificationBuilder"; import { Prisma } from "@prisma/client"; import OfficeFolderAnchorsService from "@Services/notary/OfficeFolderAnchorsService/OfficeFolderAnchorsService"; +import Zip from "adm-zip"; const hydrateOfficeFolderAnchor = (data: any): OfficeFolderAnchor => OfficeFolderAnchor.hydrate( @@ -41,13 +43,14 @@ export default class OfficeFoldersController extends ApiController { private officeFolderAnchorsRepository: OfficeFolderAnchorsRepository, private officeFolderAnchorsService: OfficeFolderAnchorsService, private officeFoldersService: OfficeFoldersService, + private filesService: FilesService, private notificationBuilder: NotificationBuilder, ) { super(); } /** - * @description Download a folder anchoring proof document + * @description Download a folder anchoring proof document along with all accessible files */ @Get("/api/v1/notary/anchors/download/:uid", [authHandler, ruleHandler, folderHandler]) protected async download(req: Request, response: Response) { @@ -76,6 +79,7 @@ export default class OfficeFoldersController extends ApiController { const officeFolder = OfficeFolder.hydrate(officeFolderFound, { strategy: "excludeAll" }); const folderHashes = getFolderHashes(officeFolder); + const folderFilesUid = getFolderFilesUid(officeFolder); if (folderHashes.length === 0) { this.httpNotFoundRequest(response, "No file hash to anchor"); @@ -83,10 +87,25 @@ export default class OfficeFoldersController extends ApiController { } const sortedHashes = [...folderHashes].sort(); - const buffer = await this.secureService.download(sortedHashes); + const anchoringProof = await this.secureService.download(sortedHashes); - response.setHeader("Content-Type", "application/pdf"); - this.httpSuccess(response, buffer); + const addFileToZip = (zip: Zip) => (uid: string): Promise => + (async () => { + const data = await this.filesService.download(uid); + if (!data?.buffer) return; + zip.addFile(`${uid}-${data.file.file_name}`, data.buffer); + })() + + const uids: string[] = folderFilesUid.filter((uid): uid is string => uid !== undefined); + + const zip = new Zip(); + zip.addFile(`${uid}-certificat-de-dépôt.pdf`, anchoringProof); + await Promise.allSettled( + uids.map(addFileToZip(zip)) + ) + + response.setHeader("Content-Type", "application/zip"); + this.httpSuccess(response, zip.toBuffer()); } catch (error) { this.httpInternalError(response, error); return; diff --git a/src/common/optics/notary/index.ts b/src/common/optics/notary/index.ts index 1ee49646..ae7a4587 100644 --- a/src/common/optics/notary/index.ts +++ b/src/common/optics/notary/index.ts @@ -10,7 +10,7 @@ import { Document, File, OfficeFolder } from "le-coffre-resources/dist/Notary"; export const folderDocumentsLens = Optics.Lens.fromNullableProp()("documents", []); export const documentFilesLens = Optics.Lens.fromNullableProp()("files", []); export const fileHashLens = Optics.Lens.fromProp()("hash"); -export const filePathLens = Optics.Lens.fromProp()("file_path"); +export const fileUidLens = Optics.Lens.fromProp()("uid"); /** * Traversals @@ -24,10 +24,11 @@ export const folderFilesTraversal = folderDocumentsLens .composeTraversal(filesTraversal); export const folderHashesTraversal = folderFilesTraversal.composeLens(fileHashLens); -export const folderFilesPathTraversal = folderFilesTraversal.composeLens(filePathLens); +export const folderFilesUidTraversal = folderFilesTraversal.composeLens(fileUidLens); /** * Getters */ +export const getFolderFiles = (folder: OfficeFolder) => Traversal.getAll(folder)(folderFilesTraversal); export const getFolderHashes = (folder: OfficeFolder) => Traversal.getAll(folder)(folderHashesTraversal); -export const getFolderFilesPath = (folder: OfficeFolder) => Traversal.getAll(folder)(folderFilesPathTraversal); +export const getFolderFilesUid = (folder: OfficeFolder) => Traversal.getAll(folder)(folderFilesUidTraversal); From 40734f70fd027c3d9501b6a46ef7b9a9b8f6add4 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Wed, 18 Oct 2023 18:55:29 +0200 Subject: [PATCH 3/3] refacto user handling --- src/app/api/customer/DocumentsController.ts | 59 +++++++++------ src/app/api/customer/FilesController.ts | 3 + .../api/customer/OfficeFoldersController.ts | 35 ++++++--- src/app/api/id360/CustomerController.ts | 2 +- .../CustomerHandler/DocumentHandler.ts | 71 +++++++++++++------ .../CustomerHandler/FileHandler.ts | 18 +++-- .../CustomerHandler/FolderHandler.ts | 10 ++- .../migration.sql | 5 ++ src/common/databases/schema.prisma | 4 +- src/common/repositories/ContactRepository.ts | 8 +-- .../CustomersService/CustomersService.ts | 21 +----- .../common/AuthService/AuthService.ts | 14 ++-- .../common/ContactService/ContactService.ts | 8 +-- .../CustomersService/CustomersService.ts | 21 +----- .../CustomersService/CustomersService.ts | 21 +----- .../super-admin/CustomersService.test.ts | 3 +- 16 files changed, 165 insertions(+), 138 deletions(-) create mode 100644 src/common/databases/migrations/20231017095322_email_phone_number_not_unique/migration.sql diff --git a/src/app/api/customer/DocumentsController.ts b/src/app/api/customer/DocumentsController.ts index ab286bd1..7e33f229 100644 --- a/src/app/api/customer/DocumentsController.ts +++ b/src/app/api/customer/DocumentsController.ts @@ -4,12 +4,11 @@ import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; import DocumentsService from "@Services/customer/DocumentsService/DocumentsService"; import { Documents, Prisma } from "@prisma/client"; -import { Document } from "le-coffre-resources/dist/Customer"; +import { Document, OfficeFolder } from "le-coffre-resources/dist/Customer"; import authHandler from "@App/middlewares/AuthHandler"; import documentHandler from "@App/middlewares/CustomerHandler/DocumentHandler"; import { validateOrReject } from "class-validator"; import OfficeFoldersService from "@Services/super-admin/OfficeFoldersService/OfficeFoldersService"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; @Controller() @Service() @@ -30,16 +29,19 @@ export default class DocumentsController extends ApiController { if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); } - const customerId: string = req.body.user.customerId; - if(query.where?.depositor) delete query.where.depositor; - if(query.where?.depositor_uid) delete query.where.depositor_uid; - const customerWhereInput: Prisma.DocumentsWhereInput = { ...query.where, depositor: { uid: customerId } }; + const email: string = req.body.user.email; + if (!email) { + this.httpBadRequest(response, "Missing customer email"); + return; + } + if (query.where?.depositor) delete query.where.depositor; + if (query.where?.depositor_uid) delete query.where.depositor_uid; + const customerWhereInput: Prisma.DocumentsWhereInput = { ...query.where, depositor: { contact: { email: email } } }; query.where = customerWhereInput; - + if (query.include?.folder) delete query.include.folder; //call service to get prisma entity const documentEntities: Documents[] = await this.documentsService.get(query); - //Hydrate ressource with prisma entity const documents = Document.hydrateArray(documentEntities, { strategy: "excludeAll" }); @@ -55,7 +57,7 @@ export default class DocumentsController extends ApiController { /** * @description Get a specific document by uid */ - @Get("/api/v1/customer/documents/:uid",[authHandler,documentHandler]) + @Get("/api/v1/customer/documents/:uid", [authHandler, documentHandler]) protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -64,9 +66,10 @@ export default class DocumentsController extends ApiController { return; } //get query - let query; + let query: Prisma.DocumentsInclude = {}; if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); + if (query.folder) delete query.folder; } const documentEntity = await this.documentsService.getByUid(uid, query); @@ -91,29 +94,43 @@ export default class DocumentsController extends ApiController { * @description Create a new File * @returns File created */ - @Post("/api/v1/customer/documents", [authHandler]) + @Post("/api/v1/customer/documents", [authHandler, documentHandler]) protected async post(req: Request, response: Response) { try { //init Document resource with request body values const documentEntity = Document.hydrate(req.body); - if(!documentEntity.folder?.uid) { + const email = req.body.user.email; + + if (!documentEntity.folder?.uid) { this.httpBadRequest(response, "No folder uid provided"); return; - } + } - const folder = await this.officeFoldersService.getByUid(documentEntity.folder.uid, {folder_anchor: true}); - if(!folder) { + const folder = await this.officeFoldersService.getByUid(documentEntity.folder.uid, { + folder_anchor: true, + customers: { include: { contact: true } }, + }); + if (!folder) { this.httpBadRequest(response, "Folder not found"); return; } - - const folderEntity = OfficeFolder.hydrate(folder); - if (folderEntity.folder_anchor?.status === "VERIFIED_ON_CHAIN") { - this.httpBadRequest(response, "Cannot update a verified folder"); + const folderEntity = OfficeFolder.hydrate(folder, { strategy: "excludeAll" }); + if (!folderEntity.customers) { + this.httpBadRequest(response, "No customers found in folder"); + return; + } + const depositor = folderEntity.customers.find((customer) => customer.contact?.email === email); + + delete documentEntity.depositor; + documentEntity.depositor = depositor; + + try { + //validate document + await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); + } catch (error) { + this.httpValidationError(response, error); return; } - //validate document - await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); //call service to get prisma entity const documentEntityCreated = await this.documentsService.create(documentEntity); diff --git a/src/app/api/customer/FilesController.ts b/src/app/api/customer/FilesController.ts index 8fedd66d..d5db8206 100644 --- a/src/app/api/customer/FilesController.ts +++ b/src/app/api/customer/FilesController.ts @@ -34,6 +34,8 @@ export default class FilesController extends ApiController { const customerId: string = req.body.user.customerId; const customerWhereInput: Prisma.FilesWhereInput = { document: { depositor: { uid: customerId } } }; query.where = customerWhereInput; + if(query.include?.document) delete query.include.document; + //call service to get prisma entity const fileEntities = await this.filesService.get(query); @@ -210,6 +212,7 @@ export default class FilesController extends ApiController { let query; if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); + if(query.document) delete query.document; } const fileEntity = await this.filesService.getByUid(uid, query); diff --git a/src/app/api/customer/OfficeFoldersController.ts b/src/app/api/customer/OfficeFoldersController.ts index 7e44ef33..899e346a 100644 --- a/src/app/api/customer/OfficeFoldersController.ts +++ b/src/app/api/customer/OfficeFoldersController.ts @@ -22,7 +22,7 @@ export default class OfficeFoldersController extends ApiController { * @description Get all folders */ @Get("/api/v1/customer/folders", [authHandler]) - protected async get(req: Request, response: Response) { + protected async get(req: Request, response: Response) { try { //get query let query: Prisma.OfficeFoldersFindManyArgs = {}; @@ -30,14 +30,19 @@ export default class OfficeFoldersController extends ApiController { query = JSON.parse(req.query["q"] as string); } - const customerId: string = req.body.user.customerId; - if(!customerId) { - this.httpBadRequest(response, "No customerId provided"); + const email: string = req.body.user.email; + if (!email) { + this.httpBadRequest(response, "Missing customer email"); return; } - if(query.where?.customers) delete query.where.customers; - const officeFolderWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, customers: { some: { uid: customerId } }}; + if (query.where?.customers) delete query.where.customers; + const officeFolderWhereInput: Prisma.OfficeFoldersWhereInput = { + ...query.where, + customers: { some: { contact: { email: email } } }, + }; query.where = officeFolderWhereInput; + if (query.include) delete query.include; + query.include = { customers: { include: { contact: true } } }; //call service to get prisma entity const officeFolderEntities: OfficeFolders[] = await this.officeFoldersService.get(query); @@ -46,6 +51,11 @@ export default class OfficeFoldersController extends ApiController { const officeFolders = OfficeFolder.hydrateArray(officeFolderEntities, { strategy: "excludeAll", }); + + officeFolders.forEach((officeFolder) => { + officeFolder.customers = officeFolder.customers!.filter((customer) => customer.contact?.email === email); + }); + //success this.httpSuccess(response, officeFolders); } catch (error) { @@ -53,7 +63,7 @@ export default class OfficeFoldersController extends ApiController { return; } } - + /** * @description Get a specific folder by uid * @returns IFolder @@ -67,12 +77,13 @@ export default class OfficeFoldersController extends ApiController { return; } - let query; + const email: string = req.body.user.email; + + let query: Prisma.OfficeFoldersInclude = {}; if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); - if(query?.customers) { - query.customers = true; - } + if (query?.customers) delete query.customers; + query.customers = { include: { contact: true } }; } const officeFolderEntity = await this.officeFoldersService.getByUid(uid, query); @@ -84,6 +95,8 @@ export default class OfficeFoldersController extends ApiController { //Hydrate ressource with prisma entity const officeFolder = OfficeFolder.hydrate(officeFolderEntity, { strategy: "excludeAll" }); + officeFolder.customers = officeFolder.customers!.filter((customer) => customer.contact?.email === email); + //success this.httpSuccess(response, officeFolder); } catch (error) { diff --git a/src/app/api/id360/CustomerController.ts b/src/app/api/id360/CustomerController.ts index 58d3fec6..685d3700 100644 --- a/src/app/api/id360/CustomerController.ts +++ b/src/app/api/id360/CustomerController.ts @@ -72,7 +72,7 @@ export default class CustomerController extends ApiController { } const customersHydrated = Customer.hydrateArray(customer); - const payload = await this.authService.getCustomerJwtPayload(customersHydrated[0]!); + const payload = await this.authService.getCustomerJwtPayload(customersHydrated); const accessToken = this.authService.generateAccessToken(payload); const refreshToken = this.authService.generateRefreshToken(payload); this.httpSuccess(response, { accessToken, refreshToken }); diff --git a/src/app/middlewares/CustomerHandler/DocumentHandler.ts b/src/app/middlewares/CustomerHandler/DocumentHandler.ts index 71c0cded..5f390ba7 100644 --- a/src/app/middlewares/CustomerHandler/DocumentHandler.ts +++ b/src/app/middlewares/CustomerHandler/DocumentHandler.ts @@ -3,36 +3,65 @@ import DocumentsService from "@Services/customer/DocumentsService/DocumentsServi import Document from "le-coffre-resources/dist/SuperAdmin/Document"; import { NextFunction, Request, Response } from "express"; import Container from "typedi"; +import ContactsService from "@Services/common/ContactService/ContactService"; +import OfficeFoldersService from "@Services/super-admin/OfficeFoldersService/OfficeFoldersService"; +import { OfficeFolder } from "le-coffre-resources/dist/SuperAdmin"; export default async function documentHandler(req: Request, response: Response, next: NextFunction) { try { const customerId = req.body.user.customerId; + const customerEmail = req.body.user.email; const uid = req.path && req.path.split("/")[5]; - if (!uid) { - response.status(HttpCodes.BAD_REQUEST).send("Missing document uid"); - return; - } + if (uid) { + const documentService = Container.get(DocumentsService); + const document = await documentService.getByUid(uid, { folder: { include: { folder_anchor: true } } }); - const documentService = Container.get(DocumentsService); - const document = await documentService.getByUid(uid, { folder: { include: { folder_anchor: true } } }); - - if (!document) { - response.status(HttpCodes.NOT_FOUND).send("Document not found"); - return; - } - - if (document?.depositor_uid != customerId) { - response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); - return; - } - - if (req.method === "POST" || req.method === "PUT") { - const documentEntity = Document.hydrate(document); - if (documentEntity.folder?.folder_anchor?.status === "VERIFIED_ON_CHAIN") { - response.status(HttpCodes.BAD_REQUEST).send("Cannot update a verified folder"); + if (!document) { + response.status(HttpCodes.NOT_FOUND).send("Document not found"); return; } + + if (document?.depositor_uid != customerId) { + const contactService = Container.get(ContactsService); + const customers = await contactService.getByEmail(customerEmail); + if (customers && !customers.find((customer) => customer.uid === document?.depositor_uid)) { + response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); + return; + } + } + + if (req.method === "PUT" || req.method === "DELETE") { + const documentEntity = Document.hydrate(document); + if (documentEntity.folder!.folder_anchor?.status === "VERIFIED_ON_CHAIN") { + response.status(HttpCodes.BAD_REQUEST).send("Cannot update a verified folder"); + return; + } + } + } + + if (req.method === "POST") { + const documentEntity = Document.hydrate(req.body); + const officeFolderService = Container.get(OfficeFoldersService); + if (documentEntity.folder?.uid) { + const folder = await officeFolderService.getByUid(documentEntity.folder.uid, { + folder_anchor: true, + customers: { include: { contact: true } }, + }); + if (!folder) { + response.status(HttpCodes.NOT_FOUND).send("Folder not found"); + return; + } + const folderEntity = OfficeFolder.hydrate(folder); + if (folderEntity.folder_anchor?.status === "VERIFIED_ON_CHAIN") { + response.status(HttpCodes.BAD_REQUEST).send("Cannot update a verified folder"); + return; + } + if (!folderEntity.customers?.find((customer) => customer.contact?.email === customerEmail)) { + response.status(HttpCodes.BAD_REQUEST).send("Cannot post a document in this folder"); + return; + } + } } next(); diff --git a/src/app/middlewares/CustomerHandler/FileHandler.ts b/src/app/middlewares/CustomerHandler/FileHandler.ts index 212ea3f7..6c3acfc2 100644 --- a/src/app/middlewares/CustomerHandler/FileHandler.ts +++ b/src/app/middlewares/CustomerHandler/FileHandler.ts @@ -5,9 +5,11 @@ import File from "le-coffre-resources/dist/SuperAdmin/File"; import { NextFunction, Request, Response } from "express"; import Container from "typedi"; import { EDocumentStatus } from "@prisma/client"; +import CustomersService from "@Services/super-admin/CustomersService/CustomersService"; export default async function fileHandler(req: Request, response: Response, next: NextFunction) { const customerId = req.body.user.customerId; + const customerEmail = req.body.user.email; const uid = req.path && req.path.split("/")[5]; const file: string | undefined = req.body["q"]; @@ -24,8 +26,12 @@ export default async function fileHandler(req: Request, response: Response, next return; } if (file.document.depositor_uid != customerId) { - response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); - return; + const customerService = Container.get(CustomersService); + const customers = await customerService.get({where: {contact: { email: customerEmail}}}); + if (customers && !customers.find((customer) => customer.uid === file.document.depositor_uid)) { + response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); + return; + } } if (req.method === "PUT") { if (file.document.document_status === EDocumentStatus.VALIDATED) { @@ -43,8 +49,12 @@ export default async function fileHandler(req: Request, response: Response, next return; } if (documentFound.depositor_uid != customerId) { - response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); - return; + const customerService = Container.get(CustomersService); + const customers = await customerService.get({where: {contact: { email: customerEmail}}}); + if (customers && !customers.find((customer) => customer.uid === documentFound.depositor_uid)) { + response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); + return; + } } if (documentFound.document_status === EDocumentStatus.VALIDATED) { response.status(HttpCodes.BAD_REQUEST).send("Cannot update a validated document"); diff --git a/src/app/middlewares/CustomerHandler/FolderHandler.ts b/src/app/middlewares/CustomerHandler/FolderHandler.ts index aaf6ad54..5e2ac111 100644 --- a/src/app/middlewares/CustomerHandler/FolderHandler.ts +++ b/src/app/middlewares/CustomerHandler/FolderHandler.ts @@ -1,10 +1,12 @@ import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; import OfficeFoldersService from "@Services/customer/OfficeFoldersService/OfficeFoldersService"; +import CustomersService from "@Services/super-admin/CustomersService/CustomersService"; import { NextFunction, Request, Response } from "express"; import Container from "typedi"; export default async function officeFolderHandler(req: Request, response: Response, next: NextFunction) { const customerId = req.body.user.customerId; + const customerEmail = req.body.user.email; const uid = req.path && req.path.split("/")[5]; if (uid) { @@ -15,8 +17,12 @@ export default async function officeFolderHandler(req: Request, response: Respon return; } if (!officeFolder.customers.find((customer) => customer.uid == customerId)) { - response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); - return; + const customerService = Container.get(CustomersService); + const customers = await customerService.get({where: {contact: { email: customerEmail}}}); + if (customers && !customers.filter((customer) => officeFolder.customers.includes(customer))) { + response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); + return; + } } } diff --git a/src/common/databases/migrations/20231017095322_email_phone_number_not_unique/migration.sql b/src/common/databases/migrations/20231017095322_email_phone_number_not_unique/migration.sql new file mode 100644 index 00000000..7bc0a3d4 --- /dev/null +++ b/src/common/databases/migrations/20231017095322_email_phone_number_not_unique/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "contacts_cell_phone_number_key"; + +-- DropIndex +DROP INDEX "contacts_email_key"; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 113f741d..dcd5a5b0 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -35,9 +35,9 @@ model Contacts { uid String @id @unique @default(uuid()) first_name String @db.VarChar(255) last_name String @db.VarChar(255) - email String @unique @db.VarChar(255) + email String @db.VarChar(255) phone_number String? @db.VarChar(50) - cell_phone_number String @unique @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) diff --git a/src/common/repositories/ContactRepository.ts b/src/common/repositories/ContactRepository.ts index da12d0aa..b1784291 100644 --- a/src/common/repositories/ContactRepository.ts +++ b/src/common/repositories/ContactRepository.ts @@ -18,8 +18,8 @@ export default class ContactRepository extends BaseRepository { /** * @description : Find unique customer by email */ - public async findOneByEmail(email: string): Promise<(Contacts & {customers: Customers | null}) | null> { - return this.model.findUnique({ + public async findSomeByEmail(email: string): Promise<(Contacts & {customers: Customers | null})[] | null> { + return this.model.findMany({ where: { email: email, }, @@ -30,8 +30,8 @@ export default class ContactRepository extends BaseRepository { /** * @description : Find unique customer by email */ - public async findOneByPhoneNumber(cell_phone_number: string): Promise<(Contacts & {customers: Customers | null}) | null> { - return this.model.findUnique({ + public async findSomeByPhoneNumber(cell_phone_number: string): Promise<(Contacts & {customers: Customers | null})[] | null> { + return this.model.findMany({ where: { cell_phone_number: cell_phone_number, }, diff --git a/src/services/admin/CustomersService/CustomersService.ts b/src/services/admin/CustomersService/CustomersService.ts index b4878d7d..7656bed2 100644 --- a/src/services/admin/CustomersService/CustomersService.ts +++ b/src/services/admin/CustomersService/CustomersService.ts @@ -1,13 +1,12 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; -import ContactsService from "@Services/common/ContactService/ContactService"; import { Customer } from "le-coffre-resources/dist/Admin"; import { Service } from "typedi"; @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository, private contactService: ContactsService) { + constructor(private customerRepository: CustomersRepository) { super(); } @@ -24,14 +23,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be created */ public async create(customerEntity: Customer): Promise { - const customers = await this.get({ - where: { - contact: { - OR: [{ email: customerEntity.contact?.email }, { cell_phone_number: customerEntity.contact?.cell_phone_number }], - }, - }, - }); - if(customers[0]) return customers[0]; return this.customerRepository.create(customerEntity); } @@ -40,16 +31,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be modified */ public async update(uid: string, customerEntity: Customer): Promise { - let errors = []; - if(customerEntity.contact?.email) { - const contactWithSameEmail = await this.contactService.getByEmail(customerEntity.contact.email); - if(contactWithSameEmail && contactWithSameEmail.uid != customerEntity.contact.uid) errors.push({property: "email", constraints: {email: "Email déjà utilisé"}}); - } - if(customerEntity.contact?.cell_phone_number) { - const contactWithSamePhoneNumber = await this.contactService.getByPhone(customerEntity.contact.cell_phone_number); - if(contactWithSamePhoneNumber && contactWithSamePhoneNumber.uid != customerEntity.contact.uid) errors.push({property: "cell_phone_number", constraints: {phone: "numéro de téléphone déjà utilisé"}}); - } - if(errors.length != 0) throw errors; return this.customerRepository.update(uid, customerEntity); } diff --git a/src/services/common/AuthService/AuthService.ts b/src/services/common/AuthService/AuthService.ts index ca272d65..f4f35b53 100644 --- a/src/services/common/AuthService/AuthService.ts +++ b/src/services/common/AuthService/AuthService.ts @@ -43,14 +43,16 @@ export default class AuthService extends BaseService { super(); } - public async getCustomerJwtPayload(customer: Customer): Promise { - if(customer.status === ECustomerStatus["PENDING"]) { - customer.status = ECustomerStatus["VALIDATED"]; - this.customerService.update(customer.uid!, customer); + public async getCustomerJwtPayload(customers: Customer[]): Promise { + for (const customer of customers){ + if (customer.status === ECustomerStatus["PENDING"]) { + customer.status = ECustomerStatus["VALIDATED"]; + await this.customerService.update(customer.uid!, customer); + } } return { - customerId: customer.uid!, - email: customer.contact!.email, + customerId: customers[0]!.uid!, + email: customers[0]!.contact!.email, }; } diff --git a/src/services/common/ContactService/ContactService.ts b/src/services/common/ContactService/ContactService.ts index f56f1b83..c6c81431 100644 --- a/src/services/common/ContactService/ContactService.ts +++ b/src/services/common/ContactService/ContactService.ts @@ -13,15 +13,15 @@ export default class ContactsService extends BaseService { * @description : Get all Contacts * @throws {Error} If Contacts cannot be get */ - public async getByEmail(email: string): Promise<(Contacts & {customers: Customers | null}) | null> { - return this.customerRepository.findOneByEmail(email); + public async getByEmail(email: string): Promise<(Contacts & {customers: Customers | null})[] | null> { + return this.customerRepository.findSomeByEmail(email); } /** * @description : Create a new customer * @throws {Error} If customer cannot be created */ - public async getByPhone(cell_phone_number: string): Promise<(Contacts & {customers: Customers | null}) | null> { - return this.customerRepository.findOneByPhoneNumber(cell_phone_number); + public async getByPhone(cell_phone_number: string): Promise<(Contacts & {customers: Customers | null})[] | null> { + return this.customerRepository.findSomeByPhoneNumber(cell_phone_number); } } diff --git a/src/services/notary/CustomersService/CustomersService.ts b/src/services/notary/CustomersService/CustomersService.ts index 721affd9..72e6923d 100644 --- a/src/services/notary/CustomersService/CustomersService.ts +++ b/src/services/notary/CustomersService/CustomersService.ts @@ -1,13 +1,12 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; -import ContactsService from "@Services/common/ContactService/ContactService"; import { Customer } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository, private contactService: ContactsService) { + constructor(private customerRepository: CustomersRepository) { super(); } @@ -24,14 +23,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be created */ public async create(customerEntity: Customer): Promise { - const customers = await this.get({ - where: { - contact: { - OR: [{ email: customerEntity.contact?.email }, { cell_phone_number: customerEntity.contact?.cell_phone_number }], - }, - }, - }); - if(customers[0]) return customers[0]; return this.customerRepository.create(customerEntity); } @@ -40,16 +31,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be modified */ public async update(uid: string, customerEntity: Customer): Promise { - let errors = []; - if(customerEntity.contact?.email) { - const contactWithSameEmail = await this.contactService.getByEmail(customerEntity.contact.email); - if(contactWithSameEmail && contactWithSameEmail.uid != customerEntity.contact.uid) errors.push({property: "email", constraints: {email: "mail déjà utilisé"}}); - } - if(customerEntity.contact?.cell_phone_number) { - const contactWithSamePhoneNumber = await this.contactService.getByPhone(customerEntity.contact.cell_phone_number); - if(contactWithSamePhoneNumber && contactWithSamePhoneNumber.uid != customerEntity.contact.uid) errors.push({property: "cell_phone_number", constraints: {phone: "numéro de téléphone déjà utilisé"}}); - } - if(errors.length != 0) throw errors; return this.customerRepository.update(uid, customerEntity); } diff --git a/src/services/super-admin/CustomersService/CustomersService.ts b/src/services/super-admin/CustomersService/CustomersService.ts index 5b8165ee..5501e1cb 100644 --- a/src/services/super-admin/CustomersService/CustomersService.ts +++ b/src/services/super-admin/CustomersService/CustomersService.ts @@ -1,13 +1,12 @@ import { Customers, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; -import ContactsService from "@Services/common/ContactService/ContactService"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; import { Service } from "typedi"; @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository, private contactService: ContactsService) { + constructor(private customerRepository: CustomersRepository) { super(); } @@ -24,14 +23,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be created */ public async create(customerEntity: Customer): Promise { - const customers = await this.get({ - where: { - contact: { - OR: [{ email: customerEntity.contact?.email }, { cell_phone_number: customerEntity.contact?.cell_phone_number }], - }, - }, - }); - if(customers[0]) return customers[0]; return this.customerRepository.create(customerEntity); } @@ -40,16 +31,6 @@ export default class CustomersService extends BaseService { * @throws {Error} If customer cannot be modified */ public async update(uid: string, customerEntity: Customer): Promise { - let errors = []; - if(customerEntity.contact?.email) { - const contactWithSameEmail = await this.contactService.getByEmail(customerEntity.contact.email); - if(contactWithSameEmail && contactWithSameEmail.uid != customerEntity.contact.uid) errors.push({property: "email", constraints: {email: "mail déjà utilisé"}}); - } - if(customerEntity.contact?.cell_phone_number) { - const contactWithSamePhoneNumber = await this.contactService.getByPhone(customerEntity.contact.cell_phone_number); - if(contactWithSamePhoneNumber && contactWithSamePhoneNumber.uid != customerEntity.contact.uid) errors.push({property: "cell_phone_number", constraints: {phone: "numéro de téléphone déjà utilisé"}}); - } - if(errors.length != 0) throw errors; return this.customerRepository.update(uid, customerEntity); } diff --git a/src/test/services/super-admin/CustomersService.test.ts b/src/test/services/super-admin/CustomersService.test.ts index 01a540cd..4b3f383e 100644 --- a/src/test/services/super-admin/CustomersService.test.ts +++ b/src/test/services/super-admin/CustomersService.test.ts @@ -6,11 +6,10 @@ import { PrismaClient } from "@prisma/client"; import { customer, customerContact, customerContact_, customer_ } from "@Test/config/MockedData"; import Container from "typedi"; import CustomersRepository from "@Repositories/CustomersRepository"; -import ContactService from "@Services/common/ContactService/ContactService"; const prisma = new PrismaClient(); -const CustomersServiceTest = new CustomersService(Container.get(CustomersRepository), Container.get(ContactService)); +const CustomersServiceTest = new CustomersService(Container.get(CustomersRepository)); afterAll(async () => { /*