diff --git a/.puppeteerrc.cjs b/.puppeteerrc.cjs deleted file mode 100644 index 5f1a1827..00000000 --- a/.puppeteerrc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -const {join} = require('path'); - -/** - * @type {import("puppeteer").Configuration} - */ -module.exports = { - // Changes the cache location for Puppeteer. - cacheDirectory: join(__dirname, '.cache', 'puppeteer'), -}; diff --git a/Dockerfile b/Dockerfile index a00cf919..9cde09c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,19 @@ # Install dependencies only when needed -FROM node:19-alpine AS deps +FROM node:18-bullseye-slim AS deps WORKDIR leCoffre RUN npm install -D prisma@4.11.0 COPY package.json ./ -RUN apk update && apk add openssh-client git -# ENV PUPPETEER_CACHE_DIR=~/leCoffre/.cache/puppeteer -# ENV PUPPETEER_SKIP_DOWNLOAD=false +RUN apt-get update \ + && apt-get install -y wget gnupg \ + && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \ + && sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y google-chrome-stable chromium fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 openssh-client git \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* COPY id_rsa /root/.ssh/id_rsa RUN chmod 600 ~/.ssh/id_rsa @@ -18,13 +23,12 @@ RUN ssh-keyscan github.com smart-chain-fr/leCoffre-resources.git >> /root/.ssh/k RUN npm install --frozen-lockfile # Rebuild the source code only when needed -FROM node:19-alpine AS builder +FROM node:18-bullseye-slim AS builder WORKDIR leCoffre COPY --from=deps leCoffre/node_modules ./node_modules COPY --from=deps leCoffre/package.json package.json -#COPY --from=deps leCoffre/.cache ./.cache COPY tsconfig.json tsconfig.json COPY src src @@ -33,19 +37,24 @@ RUN npx prisma generate RUN npm run build # Production image, copy all the files and run next -FROM node:19-alpine AS production +FROM node:18-bullseye-slim AS production WORKDIR leCoffre -RUN adduser -D lecoffreuser --uid 10000 && chown -R lecoffreuser . +RUN adduser --disabled-password lecoffreuser --uid 10000 && chown -R lecoffreuser . + +RUN mkdir -p /home/lecoffreuser/Downloads \ + && mkdir -p /home/lecoffreuser/Downloads \ + && chown -R lecoffreuser:lecoffreuser /home/lecoffreuser COPY --from=builder --chown=lecoffreuser leCoffre/node_modules ./node_modules COPY --from=builder --chown=lecoffreuser leCoffre/dist dist COPY --from=builder --chown=lecoffreuser leCoffre/package.json ./package.json -COPY --from=builder --chown=lecoffreuser leCoffre/src/common/databases ./src/common/databases -#COPY --from=builder --chown=lecoffreuser leCoffre/.cache ./.cache +COPY --from=builder --chown=lecoffreuser leCoffre/src/common/databases ./src/common/databases USER lecoffreuser CMD ["npm", "run", "start"] -EXPOSE 3001 \ No newline at end of file +EXPOSE 3001 + + diff --git a/devops/ppd.values.yaml b/devops/ppd.values.yaml index 45b3ccce..855ab153 100644 --- a/devops/ppd.values.yaml +++ b/devops/ppd.values.yaml @@ -28,6 +28,9 @@ lecoffreBack: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/from-to-www-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/client_max_body_size: 200m + nginx.ingress.kubernetes.io/client-body-buffer-size: 2M + nginx.ingress.kubernetes.io/proxy-body-size: 200m env: - key: .env scwID: "id:430001f8-68ab-47b2-92e8-38024c35a80d" diff --git a/devops/prd.values.yaml b/devops/prd.values.yaml index a8d77cca..24ac73a7 100644 --- a/devops/prd.values.yaml +++ b/devops/prd.values.yaml @@ -28,6 +28,9 @@ lecoffreBack: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/from-to-www-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/client_max_body_size: 200m + nginx.ingress.kubernetes.io/client-body-buffer-size: 2M + nginx.ingress.kubernetes.io/proxy-body-size: 200m env: - key: .env scwID: "id:8f66af26-2481-4ef2-b4f0-7f076f9ee18b" diff --git a/devops/stg.values.yaml b/devops/stg.values.yaml index 5ae4ae13..e2ce7a4b 100644 --- a/devops/stg.values.yaml +++ b/devops/stg.values.yaml @@ -28,6 +28,9 @@ lecoffreBack: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/from-to-www-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/client_max_body_size: 200m + nginx.ingress.kubernetes.io/client-body-buffer-size: 2M + nginx.ingress.kubernetes.io/proxy-body-size: 200m # key is name of the environment variable, scwID is the secret ID in SCW with "id:" in front env: - key: .env diff --git a/package.json b/package.json index d64f06cf..c0ec46b8 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "express": "^4.18.2", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.89", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.90", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/admin/CustomersController.ts b/src/app/api/admin/CustomersController.ts index 0094a678..e1badb0d 100644 --- a/src/app/api/admin/CustomersController.ts +++ b/src/app/api/admin/CustomersController.ts @@ -9,6 +9,7 @@ import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; import roleHandler from "@App/middlewares/RolesHandler"; import { Prisma } from "@prisma/client"; +import customerHandler from "@App/middlewares/OfficeMembershipHandlers/CustomerHandler"; @Controller() @Service() @@ -78,7 +79,7 @@ export default class CustomersController extends ApiController { /** * @description Modify a specific customer by uid */ - @Put("/api/v1/admin/customers/:uid", [authHandler, roleHandler, ruleHandler]) + @Put("/api/v1/admin/customers/:uid", [authHandler, roleHandler, ruleHandler, customerHandler]) protected async put(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -119,7 +120,7 @@ export default class CustomersController extends ApiController { /** * @description Get a specific customer by uid */ - @Get("/api/v1/admin/customers/:uid", [authHandler, roleHandler, ruleHandler]) + @Get("/api/v1/admin/customers/:uid", [authHandler, roleHandler, ruleHandler, customerHandler]) protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; diff --git a/src/app/api/admin/DocumentsController.ts b/src/app/api/admin/DocumentsController.ts index 05a21c1e..9f9e322b 100644 --- a/src/app/api/admin/DocumentsController.ts +++ b/src/app/api/admin/DocumentsController.ts @@ -3,7 +3,7 @@ import { Controller, Delete, Get, Post, Put } from "@ControllerPattern/index"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; import DocumentsService from "@Services/admin/DocumentsService/DocumentsService"; -import { Documents, Prisma } from "@prisma/client"; +import { Documents, EDocumentStatus, Prisma } from "@prisma/client"; import { Document } from "le-coffre-resources/dist/Admin"; import { validateOrReject } from "class-validator"; import authHandler from "@App/middlewares/AuthHandler"; @@ -31,8 +31,8 @@ export default class DocumentsController extends ApiController { query = JSON.parse(req.query["q"] as string); } const officeId: string = req.body.user.office_Id; - const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId } ; - if(!query.where) query.where = { document_type : {office: officeWhereInput}}; + const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId }; + if (!query.where) query.where = { document_type: { office: officeWhereInput } }; query.where.document_type!.office = officeWhereInput; //call service to get prisma entity @@ -104,7 +104,55 @@ export default class DocumentsController extends ApiController { await validateOrReject(documentEntity, { groups: ["updateDocument"] }); //call service to get prisma entity - const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity, req.body.refused_reason); + const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity); + + //Hydrate ressource with prisma entity + const document = Document.hydrate(documentEntityUpdated, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, document); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Update a specific document + */ + @Put("/api/v1/notary/documents/:uid/refuse", [authHandler, ruleHandler, documentHandler]) + protected async refuseDocument(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const documentFound = await this.documentsService.getByUid(uid, { + files: true, + }); + + if (!documentFound) { + this.httpNotFoundRequest(response, "document not found"); + return; + } + + //init Document resource with request body values + const documentEntity = Document.hydrate(documentFound); + + // Status to refuse + documentEntity.document_status = EDocumentStatus.REFUSED; + + //validate document + await validateOrReject(documentEntity, { groups: ["updateDocument"] }); + + //call service to get prisma entity + const documentEntityUpdated: Documents = await this.documentsService.refuse(uid, documentEntity, req.body.refused_reason); + + //create email for asked document + // this.emailBuilder.sendDocumentEmails(documentEntityUpdated); + // this.notificationBuilder.sendDocumentAnchoredNotificatiom(documentEntityUpdated); //Hydrate ressource with prisma entity const document = Document.hydrate(documentEntityUpdated, { strategy: "excludeAll" }); diff --git a/src/app/api/admin/OfficeFoldersController.ts b/src/app/api/admin/OfficeFoldersController.ts index 42fbdc58..bf1c8732 100644 --- a/src/app/api/admin/OfficeFoldersController.ts +++ b/src/app/api/admin/OfficeFoldersController.ts @@ -58,10 +58,10 @@ export default class OfficeFoldersController extends ApiController { }; } - const officeId: string = req.body.user.office_Id; - const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId } ; - if(!query.where) query.where = { office: officeWhereInput}; - query.where.office = officeWhereInput; + const userId: string = req.body.user.userId; + if (query.where?.stakeholders) delete query.where.stakeholders; + const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } }; + query.where = officeFoldersWhereInput; //call service to get prisma entity const officeFolderEntities: OfficeFolders[] = await this.officeFoldersService.get(query); diff --git a/src/app/api/customer/DocumentsController.ts b/src/app/api/customer/DocumentsController.ts index 966b613a..ab286bd1 100644 --- a/src/app/api/customer/DocumentsController.ts +++ b/src/app/api/customer/DocumentsController.ts @@ -8,11 +8,13 @@ import { Document } 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() export default class DocumentsController extends ApiController { - constructor(private documentsService: DocumentsService) { + constructor(private documentsService: DocumentsService, private officeFoldersService: OfficeFoldersService) { super(); } @@ -93,8 +95,23 @@ export default class DocumentsController extends ApiController { protected async post(req: Request, response: Response) { try { //init Document resource with request body values - const documentEntity = Document.hydrate(req.body); + const documentEntity = Document.hydrate(req.body); + 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) { + 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"); + return; + } //validate document await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); diff --git a/src/app/api/customer/FilesController.ts b/src/app/api/customer/FilesController.ts index 925b30c7..8fedd66d 100644 --- a/src/app/api/customer/FilesController.ts +++ b/src/app/api/customer/FilesController.ts @@ -6,19 +6,16 @@ import FilesService from "@Services/common/FilesService/FilesService"; import { Files, Prisma } from "@prisma/client"; import { File } from "le-coffre-resources/dist/Customer"; import { Document } from "le-coffre-resources/dist/Customer"; -import { validateOrReject } from "class-validator"; import DocumentsService from "@Services/customer/DocumentsService/DocumentsService"; import authHandler from "@App/middlewares/AuthHandler"; import fileHandler from "@App/middlewares/CustomerHandler/FileHandler"; -import DocumentTypesService from "@Services/super-admin/DocumentTypesService/DocumentTypesService"; -import { DocumentType } from "le-coffre-resources/dist/SuperAdmin"; -import ObjectHydrate from "@Common/helpers/ObjectHydrate"; import NotificationBuilder from "@Common/notifications/NotificationBuilder"; +import { validateOrReject } from "class-validator"; @Controller() @Service() export default class FilesController extends ApiController { - constructor(private filesService: FilesService, private documentService: DocumentsService, private documentTypesService : DocumentTypesService, private notificationBuilder : NotificationBuilder) { + constructor(private filesService: FilesService, private documentService: DocumentsService, private notificationBuilder : NotificationBuilder) { super(); } @@ -93,7 +90,12 @@ export default class FilesController extends ApiController { const fileEntity = File.hydrate(JSON.parse(req.body["q"])); //validate File - // await validateOrReject(fileEntity, { groups: ["createFile"] }); + try { + await validateOrReject(fileEntity, { groups: ["createFile"] }); + } catch (error) { + this.httpBadRequest(response, error); + return; + } //call service to get prisma entity const fileEntityCreated = await this.filesService.create(fileEntity, req.file); @@ -111,6 +113,7 @@ export default class FilesController extends ApiController { strategy: "excludeAll", }); + //success this.httpCreated(response, fileEntityHydrated); } catch (error) { @@ -226,52 +229,4 @@ export default class FilesController extends ApiController { return; } } - - /** - * @description Create a new File - * @returns File created - */ - @Post("/api/v1/customer/addPersonalFile", [authHandler, fileHandler]) - protected async addPersonalFile(req: Request, response: Response) { - try { - //get file - if (!req.file) throw new Error("No file provided"); - - //init File resource with request body values - const fileEntity = File.hydrate(JSON.parse(req.body["q"])); - - const documentTypeEntities = await this.documentTypesService.get({ where: { name: "Other"} }); - const documentTypeEntity = documentTypeEntities[0]; - const documentType = ObjectHydrate.hydrate(new DocumentType(), documentTypeEntity!, { strategy: "excludeAll" }); - - const documentEntity = Document.hydrate({document_type: documentType}); - await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); - const documentEntityCreated = await this.documentService.create(documentEntity); - - const document = Document.hydrate(documentEntityCreated, { - strategy: "excludeAll", - }); - - fileEntity.document = document; - - const fileEntityCreated = await this.filesService.create(fileEntity, req.file); - - const documentToUpdate = Document.hydrate(document!); - - documentToUpdate!.document_status = "DEPOSITED"; - await this.documentService.update(document!.uid!, documentToUpdate); - - //Hydrate ressource with prisma entity - const fileEntityHydrated = File.hydrate(fileEntityCreated, { - strategy: "excludeAll", - }); - - //success - this.httpCreated(response, fileEntityHydrated); - - } catch (error) { - this.httpBadRequest(response, error); - return; - } - } } diff --git a/src/app/api/id360/CustomerController.ts b/src/app/api/id360/CustomerController.ts index fc234983..475b5bb6 100644 --- a/src/app/api/id360/CustomerController.ts +++ b/src/app/api/id360/CustomerController.ts @@ -2,7 +2,7 @@ import { Response, Request } from "express"; import { Controller, Post } from "@ControllerPattern/index"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; -import Id360Service from "@Services/common/Id360Service/Id360Service"; +import Id360Service, { EnrollmentResponse } from "@Services/common/Id360Service/Id360Service"; import CustomersService from "@Services/customer/CustomersService/CustomersService"; import AuthService from "@Services/common/AuthService/AuthService"; import { Customer } from "le-coffre-resources/dist/SuperAdmin"; @@ -34,7 +34,11 @@ export default class CustomerController extends ApiController { return; } try { - const enrollment = await this.id360Service.getEnrollment(callbackToken); + const res = await this.id360Service.getEnrollment(callbackToken); + const enrollment = await res.json() as EnrollmentResponse; + if(enrollment.status === "STARTED") { + this.loginCallback(req, response); + } if (enrollment.status !== "OK") { this.httpUnauthorized(response, "Enrollment status is not OK"); return; diff --git a/src/app/api/id360/DocumentController.ts b/src/app/api/id360/DocumentController.ts index 1b8d43ca..b0285212 100644 --- a/src/app/api/id360/DocumentController.ts +++ b/src/app/api/id360/DocumentController.ts @@ -20,7 +20,6 @@ export default class DocumentController extends ApiController { protected async getDocumentVerificationFromId360(req: Request, response: Response) { try { - console.log("document callback", req, response) this.httpSuccess(response); } catch (error) { console.log(error); @@ -33,7 +32,6 @@ export default class DocumentController extends ApiController { protected async getCustomerVerificationFromId360(req: Request, response: Response) { try { - console.log("customer callback", req, response) this.httpSuccess(response); } catch (error) { console.log(error); diff --git a/src/app/api/idnot/UserController.ts b/src/app/api/idnot/UserController.ts index 102af93a..9952e029 100644 --- a/src/app/api/idnot/UserController.ts +++ b/src/app/api/idnot/UserController.ts @@ -25,6 +25,10 @@ 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) { diff --git a/src/app/api/notary/CustomersController.ts b/src/app/api/notary/CustomersController.ts index 1362bb98..4415fd34 100644 --- a/src/app/api/notary/CustomersController.ts +++ b/src/app/api/notary/CustomersController.ts @@ -8,6 +8,7 @@ import { validateOrReject } from "class-validator"; import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; import { Prisma } from "@prisma/client"; +import customerHandler from "@App/middlewares/OfficeMembershipHandlers/CustomerHandler"; @Controller() @Service() @@ -76,7 +77,7 @@ export default class CustomersController extends ApiController { /** * @description Modify a specific customer by uid */ - @Put("/api/v1/notary/customers/:uid", [authHandler, ruleHandler]) + @Put("/api/v1/notary/customers/:uid", [authHandler, ruleHandler, customerHandler]) protected async put(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -117,7 +118,7 @@ export default class CustomersController extends ApiController { /** * @description Get a specific customer by uid */ - @Get("/api/v1/notary/customers/:uid", [authHandler, ruleHandler]) + @Get("/api/v1/notary/customers/:uid", [authHandler, ruleHandler, customerHandler]) protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; diff --git a/src/app/api/notary/DocumentsController.ts b/src/app/api/notary/DocumentsController.ts index 0be80d12..b6510943 100644 --- a/src/app/api/notary/DocumentsController.ts +++ b/src/app/api/notary/DocumentsController.ts @@ -3,19 +3,24 @@ import { Controller, Delete, Get, Post, Put } from "@ControllerPattern/index"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; import DocumentsService from "@Services/notary/DocumentsService/DocumentsService"; -import { Documents, Prisma } from "@prisma/client"; -import { Document } from "le-coffre-resources/dist/Notary"; +import { Documents, EDocumentStatus, Prisma } from "@prisma/client"; +import { Document, OfficeFolder } from "le-coffre-resources/dist/Notary"; import { validateOrReject } from "class-validator"; import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; import documentHandler from "@App/middlewares/OfficeMembershipHandlers/DocumentHandler"; import EmailBuilder from "@Common/emails/EmailBuilder"; +import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; // import NotificationBuilder from "@Common/notifications/NotificationBuilder"; @Controller() @Service() export default class DocumentsController extends ApiController { - constructor(private documentsService: DocumentsService, private emailBuilder: EmailBuilder) { + constructor( + private documentsService: DocumentsService, + private emailBuilder: EmailBuilder, + private officeFoldersService: OfficeFoldersService, + ) { super(); } @@ -32,8 +37,8 @@ export default class DocumentsController extends ApiController { query = JSON.parse(req.query["q"] as string); } const officeId: string = req.body.user.office_Id; - const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId } ; - if(!query.where) query.where = { document_type : {office: officeWhereInput}}; + const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId }; + if (!query.where) query.where = { document_type: { office: officeWhereInput } }; query.where.document_type!.office = officeWhereInput; //call service to get prisma entity @@ -60,6 +65,20 @@ export default class DocumentsController extends ApiController { //init Document resource with request body values const documentEntity = Document.hydrate(req.body); + const folder = await this.officeFoldersService.getByUid(documentEntity.folder?.uid!, { + folder_anchor: true, + }); + if (!folder) { + this.httpBadRequest(response, "Folder not found"); + return; + } + + const folderRessource = OfficeFolder.hydrate(folder); + if (folderRessource.folder_anchor) { + this.httpBadRequest(response, "Cannot ask document on an anchored or anchoring folder"); + return; + } + //validate document await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); @@ -104,13 +123,59 @@ export default class DocumentsController extends ApiController { //init Document resource with request body values const documentEntity = Document.hydrate(req.body); - + //validate document + await validateOrReject(documentEntity, { groups: ["updateDocument"] }); + + //call service to get prisma entity + const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity); + + //create email for asked document + // this.emailBuilder.sendDocumentEmails(documentEntityUpdated); + // this.notificationBuilder.sendDocumentAnchoredNotificatiom(documentEntityUpdated); + + //Hydrate ressource with prisma entity + const document = Document.hydrate(documentEntityUpdated, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, document); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Update a specific document + */ + @Put("/api/v1/notary/documents/:uid/refuse", [authHandler, ruleHandler, documentHandler]) + protected async refuseDocument(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const documentFound = await this.documentsService.getByUid(uid, { + files: true, + }); + + if (!documentFound) { + this.httpNotFoundRequest(response, "document not found"); + return; + } + + //init Document resource with request body values + const documentEntity = Document.hydrate(documentFound); + + // Status to refuse + documentEntity.document_status = EDocumentStatus.REFUSED; //validate document await validateOrReject(documentEntity, { groups: ["updateDocument"] }); //call service to get prisma entity - const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity, req.body.refused_reason); + const documentEntityUpdated: Documents = await this.documentsService.refuse(uid, documentEntity, req.body.refused_reason); //create email for asked document // this.emailBuilder.sendDocumentEmails(documentEntityUpdated); diff --git a/src/app/api/notary/OfficeFolderAnchorsController.ts b/src/app/api/notary/OfficeFolderAnchorsController.ts index 62c9d3d7..3a43a703 100644 --- a/src/app/api/notary/OfficeFolderAnchorsController.ts +++ b/src/app/api/notary/OfficeFolderAnchorsController.ts @@ -2,7 +2,7 @@ import { Response, Request } from "express"; import { Controller, Get, Post } from "@ControllerPattern/index"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; -import { OfficeFolder } from "le-coffre-resources/dist/Notary"; +import { Document, OfficeFolder } from "le-coffre-resources/dist/Notary"; import { getFolderHashes } from "@Common/optics/notary"; import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; import OfficeFolderAnchorsRepository from "@Repositories/OfficeFolderAnchorsRepository"; @@ -136,6 +136,18 @@ export default class OfficeFoldersController extends ApiController { const officeFolder = OfficeFolder.hydrate(officeFolderFound, { strategy: "excludeAll" }); + // Check if every document is validated in a folder + const documents = officeFolder.documents ?? []; + const documentsValidated = documents.filter((document) => { + let documentHydrated = Document.hydrate(document, { strategy: "excludeAll" }); + return documentHydrated.document_status === "VALIDATED"; + }); + + if (documentsValidated.length !== documents.length && documents.length !== 0) { + this.httpBadRequest(response, "Cannot anchor a folder with non validated documents"); + return; + } + const folderHashes = getFolderHashes(officeFolder); if (folderHashes.length === 0) { diff --git a/src/app/api/notary/OfficeFoldersController.ts b/src/app/api/notary/OfficeFoldersController.ts index f0b19ead..d5a5b115 100644 --- a/src/app/api/notary/OfficeFoldersController.ts +++ b/src/app/api/notary/OfficeFoldersController.ts @@ -44,9 +44,9 @@ export default class OfficeFoldersController extends ApiController { customers: { some: { OR: [ - {contact: { first_name: { contains: filter, mode: "insensitive" } }}, - {contact: { last_name: { contains: filter, mode: "insensitive" } }}, - ] + { contact: { first_name: { contains: filter, mode: "insensitive" } } }, + { contact: { last_name: { contains: filter, mode: "insensitive" } } }, + ], }, }, }, @@ -56,8 +56,8 @@ export default class OfficeFoldersController extends ApiController { } const userId: string = req.body.user.userId; - if(query.where?.stakeholders) delete query.where.stakeholders; - const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: {some: {uid: userId }}}; + if (query.where?.stakeholders) delete query.where.stakeholders; + const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } }; query.where = officeFoldersWhereInput; //call service to get prisma entity @@ -111,7 +111,9 @@ export default class OfficeFoldersController extends ApiController { return; } - const officeFolderFound = await this.officeFoldersService.getByUid(uid); + const officeFolderFound = await this.officeFoldersService.getByUid(uid, { + folder_anchor: true, + }); if (!officeFolderFound) { this.httpNotFoundRequest(response, "office folder not found"); @@ -119,13 +121,17 @@ export default class OfficeFoldersController extends ApiController { } //init OfficeFolder resource with request body values - const officeFolderEntity = OfficeFolder.hydrate(req.body); - + const officefolderToUpdate = OfficeFolder.hydrate(req.body); + const officeFolderFoundEntity = OfficeFolder.hydrate(officeFolderFound); //validate folder - await validateOrReject(officeFolderEntity, { groups: ["updateFolder"], forbidUnknownValues: false }); + await validateOrReject(officefolderToUpdate, { groups: ["updateFolder"], forbidUnknownValues: false }); + if (officeFolderFoundEntity.folder_anchor?.status === "VERIFIED_ON_CHAIN") { + this.httpBadRequest(response, "Cannot update a verified folder"); + return; + } //call service to get prisma entity - const officeFolderEntityUpdated = await this.officeFoldersService.update(uid, officeFolderEntity); + const officeFolderEntityUpdated = await this.officeFoldersService.update(uid, officefolderToUpdate); //Hydrate ressource with prisma entity const officeFolders = OfficeFolder.hydrate(officeFolderEntityUpdated, { @@ -140,6 +146,102 @@ export default class OfficeFoldersController extends ApiController { } } + /** + * @description Modify a specific folder by uid + */ + @Put("/api/v1/notary/folders/:uid/archive", [authHandler, ruleHandler, folderHandler]) + protected async archive(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const officeFolderFound = await this.officeFoldersService.getByUid(uid, { + folder_anchor: true, + }); + + if (!officeFolderFound) { + this.httpNotFoundRequest(response, "office folder not found"); + return; + } + + //init OfficeFolder resource with request body values + const officefolderToUpdate = OfficeFolder.hydrate(officeFolderFound); + + officefolderToUpdate.status = "ARCHIVED"; + officefolderToUpdate.archived_description = req.body.archived_description ?? ""; + + //validate folder + await validateOrReject(officefolderToUpdate, { groups: ["updateFolder"], forbidUnknownValues: false }); + + if ((officeFolderFound as any).folder_anchor?.status !== "VERIFIED_ON_CHAIN") { + this.httpBadRequest(response, "Cannot archive a not anchored folder"); + return; + } + + //call service to get prisma entity + const officeFolderEntityUpdated = await this.officeFoldersService.update(uid, officefolderToUpdate); + + //Hydrate ressource with prisma entity + const officeFolders = OfficeFolder.hydrate(officeFolderEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpSuccess(response, officeFolders); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Modify a specific folder by uid + */ + @Put("/api/v1/notary/folders/:uid/restore", [authHandler, ruleHandler, folderHandler]) + protected async restore(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const officeFolderFound = await this.officeFoldersService.getByUid(uid, { + folder_anchor: true, + }); + + if (!officeFolderFound) { + this.httpNotFoundRequest(response, "office folder not found"); + return; + } + + //init OfficeFolder resource with request body values + const officefolderToUpdate = OfficeFolder.hydrate(officeFolderFound); + + officefolderToUpdate.status = "LIVE"; + officefolderToUpdate.archived_description = ""; + + //validate folder + await validateOrReject(officefolderToUpdate, { groups: ["updateFolder"], forbidUnknownValues: false }); + + //call service to get prisma entity + const officeFolderEntityUpdated = await this.officeFoldersService.update(uid, officefolderToUpdate); + + //Hydrate ressource with prisma entity + const officeFolders = OfficeFolder.hydrate(officeFolderEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpSuccess(response, officeFolders); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } /** * @description Get a specific folder by uid * @returns IFolder diff --git a/src/app/api/super-admin/CustomersController.ts b/src/app/api/super-admin/CustomersController.ts index 62a82548..628fa4ed 100644 --- a/src/app/api/super-admin/CustomersController.ts +++ b/src/app/api/super-admin/CustomersController.ts @@ -9,6 +9,7 @@ import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; import roleHandler from "@App/middlewares/RolesHandler"; import { Prisma } from "@prisma/client"; +import customerHandler from "@App/middlewares/OfficeMembershipHandlers/CustomerHandler"; @Controller() @Service() @@ -78,7 +79,7 @@ export default class CustomersController extends ApiController { /** * @description Modify a specific customer by uid */ - @Put("/api/v1/super-admin/customers/:uid", [authHandler, roleHandler, ruleHandler]) + @Put("/api/v1/super-admin/customers/:uid", [authHandler, roleHandler, ruleHandler, customerHandler]) protected async put(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -119,7 +120,7 @@ export default class CustomersController extends ApiController { /** * @description Get a specific customer by uid */ - @Get("/api/v1/super-admin/customers/:uid", [authHandler, roleHandler, ruleHandler]) + @Get("/api/v1/super-admin/customers/:uid", [authHandler, roleHandler, ruleHandler, customerHandler]) protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; diff --git a/src/app/api/super-admin/DocumentsController.ts b/src/app/api/super-admin/DocumentsController.ts index f589ce4c..f33075d1 100644 --- a/src/app/api/super-admin/DocumentsController.ts +++ b/src/app/api/super-admin/DocumentsController.ts @@ -4,7 +4,7 @@ import roleHandler from "@App/middlewares/RolesHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Controller, Delete, Get, Post, Put } from "@ControllerPattern/index"; -import { Documents, Prisma } from "@prisma/client"; +import { Documents, EDocumentStatus, Prisma } from "@prisma/client"; import DocumentsService from "@Services/super-admin/DocumentsService/DocumentsService"; import { validateOrReject } from "class-validator"; import { Request, Response } from "express"; @@ -31,16 +31,16 @@ export default class DocumentsController extends ApiController { query = JSON.parse(req.query["q"] as string); } const officeId: string = req.body.user.office_Id; - - const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId } ; - - if(!query.where) query.where = { document_type : {office: officeWhereInput}}; - - // query.where.document_type!.office = officeWhereInput; + + const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId }; + + if (!query.where) query.where = { document_type: { office: officeWhereInput } }; + + // query.where.document_type!.office = officeWhereInput; //call service to get prisma entity - - const documentEntities = await this.documentsService.get(query); + + const documentEntities = await this.documentsService.get(query); //Hydrate ressource with prisma entity const documents = Document.hydrateArray(documentEntities, { strategy: "excludeAll" }); @@ -108,7 +108,55 @@ export default class DocumentsController extends ApiController { await validateOrReject(documentEntity, { groups: ["updateDocument"] }); //call service to get prisma entity - const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity, req.body.refused_reason); + const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity); + + //Hydrate ressource with prisma entity + const document = Document.hydrate(documentEntityUpdated, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, document); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Update a specific document + */ + @Put("/api/v1/notary/documents/:uid/refuse", [authHandler, ruleHandler, documentHandler]) + protected async refuseDocument(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const documentFound = await this.documentsService.getByUid(uid, { + files: true, + }); + + if (!documentFound) { + this.httpNotFoundRequest(response, "document not found"); + return; + } + + //init Document resource with request body values + const documentEntity = Document.hydrate(documentFound); + + // Status to refuse + documentEntity.document_status = EDocumentStatus.REFUSED; + + //validate document + await validateOrReject(documentEntity, { groups: ["updateDocument"] }); + + //call service to get prisma entity + const documentEntityUpdated: Documents = await this.documentsService.refuse(uid, documentEntity, req.body.refused_reason); + + //create email for asked document + // this.emailBuilder.sendDocumentEmails(documentEntityUpdated); + // this.notificationBuilder.sendDocumentAnchoredNotificatiom(documentEntityUpdated); //Hydrate ressource with prisma entity const document = Document.hydrate(documentEntityUpdated, { strategy: "excludeAll" }); @@ -170,7 +218,7 @@ export default class DocumentsController extends ApiController { if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); } - + const documentEntity = await this.documentsService.getByUid(uid, query); if (!documentEntity) { diff --git a/src/app/api/super-admin/LiveVoteController.ts b/src/app/api/super-admin/LiveVoteController.ts index 65956b50..30494cf2 100644 --- a/src/app/api/super-admin/LiveVoteController.ts +++ b/src/app/api/super-admin/LiveVoteController.ts @@ -1,6 +1,5 @@ import authHandler from "@App/middlewares/AuthHandler"; import roleHandler from "@App/middlewares/RolesHandler"; -import NotificationBuilder from "@Common/notifications/NotificationBuilder"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Controller, Delete, Post } from "@ControllerPattern/index"; import { EAppointmentStatus, Votes } from "@prisma/client"; @@ -17,7 +16,6 @@ export default class LiveVoteController extends ApiController { constructor( private liveVoteService: LiveVoteService, private usersService: UsersService, - private notificationBuilder: NotificationBuilder, ) { super(); } @@ -30,7 +28,7 @@ export default class LiveVoteController extends ApiController { try { const userId = req.body.user.userId; //init IUser resource with request body values - const voteEntity = Vote.hydrate(req.body); + const voteEntity = Vote.hydrate(req.body, { strategy: "excludeAll" }); //validate user await validateOrReject(voteEntity, { groups: ["createVote"] }); @@ -54,7 +52,7 @@ export default class LiveVoteController extends ApiController { AND: [ { appointment: { - AND: [{ user_uid: voteEntity.appointment.targeted_user.uid }, { status: EAppointmentStatus.OPEN }], + AND: [{ user_uid: voteEntity.appointment.user.uid }, { status: EAppointmentStatus.OPEN }], }, }, { voter: { uid: userId } }, @@ -79,11 +77,7 @@ export default class LiveVoteController extends ApiController { return; } //Hydrate ressource with prisma entity - const vote = Vote.hydrate(voteEntityCreated, { - strategy: "excludeAll", - }); - - await this.notificationBuilder.sendVoteNotification(vote); + const vote = Vote.hydrate(voteEntityCreated, { strategy: "excludeAll" }); //success this.httpCreated(response, vote); diff --git a/src/app/api/super-admin/OfficeFoldersController.ts b/src/app/api/super-admin/OfficeFoldersController.ts index e0b01345..f31d2807 100644 --- a/src/app/api/super-admin/OfficeFoldersController.ts +++ b/src/app/api/super-admin/OfficeFoldersController.ts @@ -44,12 +44,10 @@ export default class OfficeFoldersController extends ApiController { { customers: { some: { - contact: { - OR: [ - { first_name: { contains: filter, mode: "insensitive" } }, - { last_name: { contains: filter, mode: "insensitive" } }, - ], - }, + OR: [ + { contact: { first_name: { contains: filter, mode: "insensitive" } } }, + { contact: { last_name: { contains: filter, mode: "insensitive" } } }, + ], }, }, }, @@ -57,10 +55,11 @@ export default class OfficeFoldersController extends ApiController { }, }; } - const officeId: string = req.body.user.office_Id; - const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId }; - if (!query.where) query.where = { office: officeWhereInput }; - query.where.office = officeWhereInput; + + const userId: string = req.body.user.userId; + if (query.where?.stakeholders) delete query.where.stakeholders; + const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } }; + query.where = officeFoldersWhereInput; //call service to get prisma entity const officeFolderEntities: OfficeFolders[] = await this.officeFoldersService.get(query); diff --git a/src/app/api/super-admin/UsersController.ts b/src/app/api/super-admin/UsersController.ts index fd135312..a4a63b51 100644 --- a/src/app/api/super-admin/UsersController.ts +++ b/src/app/api/super-admin/UsersController.ts @@ -109,30 +109,31 @@ export default class UsersController extends ApiController { //init IUser resource with request body values const userEntity = User.hydrate(req.body); - if(userEntity.role) { + if (userEntity.role) { const role = await this.roleService.getByUid(userEntity.role.uid!); - if(!role) { + if (!role) { this.httpBadRequest(response, "Role not found"); return; } - if (role.name === "super-admin" || userFound.role.name === "super-admin" ) { + if (role.name === "super-admin" || userFound.role.name === "super-admin") { this.httpBadRequest(response, "Cannot assign or remove super-admin role"); return; } } - if(userEntity.office_role) { + if (userEntity.office_role) { const officeRole = await this.officeRoleService.getByUid(userEntity.office_role.uid!); - if(!officeRole) { + if (!officeRole) { this.httpBadRequest(response, "Office role not found"); return; } + if (officeRole.office_uid != userFound.office_uid) { this.httpBadRequest(response, "Cannot assign an office role from another office"); return; } } - + //call service to get prisma entity const userEntityUpdated = await this.usersService.update(uid, userEntity); diff --git a/src/app/middlewares/CustomerHandler/DocumentHandler.ts b/src/app/middlewares/CustomerHandler/DocumentHandler.ts index f263f7dd..71c0cded 100644 --- a/src/app/middlewares/CustomerHandler/DocumentHandler.ts +++ b/src/app/middlewares/CustomerHandler/DocumentHandler.ts @@ -1,5 +1,6 @@ import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; import DocumentsService from "@Services/customer/DocumentsService/DocumentsService"; +import Document from "le-coffre-resources/dist/SuperAdmin/Document"; import { NextFunction, Request, Response } from "express"; import Container from "typedi"; @@ -14,15 +15,27 @@ export default async function documentHandler(req: Request, response: Response, } const documentService = Container.get(DocumentsService); - const document = await documentService.getByUid(uid); + 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"); + return; + } + } + next(); - } catch (error) { console.log(error); response.status(HttpCodes.INTERNAL_ERROR).send("Internal server error"); diff --git a/src/app/middlewares/CustomerHandler/FileHandler.ts b/src/app/middlewares/CustomerHandler/FileHandler.ts index 0c9d82a1..212ea3f7 100644 --- a/src/app/middlewares/CustomerHandler/FileHandler.ts +++ b/src/app/middlewares/CustomerHandler/FileHandler.ts @@ -1,13 +1,20 @@ import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; import FilesService from "@Services/common/FilesService/FilesService"; import DocumentsService from "@Services/customer/DocumentsService/DocumentsService"; +import File from "le-coffre-resources/dist/SuperAdmin/File"; import { NextFunction, Request, Response } from "express"; import Container from "typedi"; +import { EDocumentStatus } from "@prisma/client"; export default async function fileHandler(req: Request, response: Response, next: NextFunction) { const customerId = req.body.user.customerId; const uid = req.path && req.path.split("/")[5]; - const document = req.body.document; + const file: string | undefined = req.body["q"]; + + if (req.file && req.file.mimetype !== "application/pdf" && req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpeg") { + response.status(HttpCodes.BAD_REQUEST).send("File type not supported"); + return; + } if (uid) { const fileService = Container.get(FilesService); @@ -20,12 +27,18 @@ export default async function fileHandler(req: Request, response: Response, next response.status(HttpCodes.UNAUTHORIZED).send("Not authorized with this depositor"); return; } + if (req.method === "PUT") { + if (file.document.document_status === EDocumentStatus.VALIDATED) { + response.status(HttpCodes.BAD_REQUEST).send("Cannot update a validated document"); + return; + } + } } - - if (document) { + if (file) { + const fileEntity = File.hydrate(JSON.parse(file)); const documentService = Container.get(DocumentsService); - const documentFound = await documentService.getByUid(document.uid!); - if(!documentFound) { + const documentFound = await documentService.getByUid(fileEntity.document?.uid!, { folder: { include: { folder_anchor: true } } }); + if (!documentFound) { response.status(HttpCodes.NOT_FOUND).send("Document not found"); return; } @@ -33,6 +46,10 @@ export default async function fileHandler(req: Request, response: Response, next 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"); + return; + } } next(); diff --git a/src/app/middlewares/OfficeMembershipHandlers/CustomerHandler.ts b/src/app/middlewares/OfficeMembershipHandlers/CustomerHandler.ts new file mode 100644 index 00000000..f0b8b532 --- /dev/null +++ b/src/app/middlewares/OfficeMembershipHandlers/CustomerHandler.ts @@ -0,0 +1,28 @@ +import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; +import { NextFunction, Request, Response } from "express"; +import Container from "typedi"; +import CustomersService from "@Services/super-admin/CustomersService/CustomersService"; + +export default async function customerHandler(req: Request, response: Response, next: NextFunction) { + try { + const officeId = req.body.user.office_Id; + const uid = req.path && req.path.split("/")[5]; + + if (uid) { + const customerService = Container.get(CustomersService); + const customer = await customerService.get({where:{AND: [{uid: uid}, {office_folders: {some: {office_uid: officeId}}}]}}); + + if (!customer[0]) { + response.status(HttpCodes.NOT_FOUND).send("Customer not found"); + return; + } + } + + next(); + + } catch (error) { + console.log(error); + response.status(HttpCodes.INTERNAL_ERROR).send("Internal server error"); + return; + } +} diff --git a/src/common/emails/EmailBuilder.ts b/src/common/emails/EmailBuilder.ts index bb41d029..9aec7bc5 100644 --- a/src/common/emails/EmailBuilder.ts +++ b/src/common/emails/EmailBuilder.ts @@ -17,6 +17,11 @@ export default class EmailBuilder { const documentPrisma = await this.documentsService.getByUid(documentEntity.uid, { depositor: {include: {contact: true}}, folder:{include:{ office: true}} }); if(!documentPrisma) throw new Error("Document not found"); const document = Document.hydrate(documentPrisma); + + //Use mailchimpService.get get if an email was sent to the user in the lst hour + const lastEmail = await this.mailchimpService.get({ where: { to: document.depositor!.contact!.email, sentAt: { gte: new Date(Date.now() - 3600000) } } }); + if(lastEmail.length > 0) return; + const to = document.depositor!.contact!.email; const civility = this.getCivility(document.depositor!.contact!.civility); const templateVariables = { diff --git a/src/common/notifications/NotificationBuilder.ts b/src/common/notifications/NotificationBuilder.ts index 3db177a2..e51abb68 100644 --- a/src/common/notifications/NotificationBuilder.ts +++ b/src/common/notifications/NotificationBuilder.ts @@ -14,7 +14,7 @@ export default class NotificationBuilder { private documentsService: DocumentsService, private usersService: UsersService, private foldersService: OfficeFoldersService, - private backendVariables: BackendVariables + private backendVariables: BackendVariables, ) {} public async sendDocumentDepositedNotification(documentEntity: Documents) { @@ -42,10 +42,12 @@ export default class NotificationBuilder { } public async sendFolderAnchoredNotification(folderEntity: OfficeFolders) { - if(!folderEntity.uid) return; - const officeFolderPrisma = await this.foldersService.getByUid(folderEntity.uid, - { folder_anchor: true, office: true, stakeholders: true } - ); + if (!folderEntity.uid) return; + const officeFolderPrisma = await this.foldersService.getByUid(folderEntity.uid, { + folder_anchor: true, + office: true, + stakeholders: true, + }); if (!officeFolderPrisma) throw new Error("Folder not found"); const folder = OfficeFolder.hydrate(officeFolderPrisma); if (folder.folder_anchor?.status !== "VERIFIED_ON_CHAIN") return; @@ -66,26 +68,24 @@ export default class NotificationBuilder { public async sendVoteNotification(vote: Vote) { if (vote.appointment.status !== "OPEN") return; - const superAdminList = await this.usersService.get({ where: { role: { label: "super-admin" } } }); + const superAdminList = await this.usersService.get({ where: { role: { name: "super-admin" } } }); + const voterIndex = superAdminList.findIndex((user) => user.uid === vote.voter.uid); + superAdminList.splice(voterIndex, 1); + const userTargeted = await this.usersService.getByUid(vote.appointment.user.uid!, { contact: true }); + const userTargetedEntity = User.hydrate(userTargeted!, { strategy: "excludeAll" }); let message = ""; if (vote.appointment.choice === "NOMINATE") { - message = - "Un collaborateur souhaite attribuer le titre de Super Administrateur à " + - vote.appointment.targeted_user + - ". Cliquez ici pour voter."; + message = `Un collaborateur souhaite attribuer le titre de Super Administrateur à ${userTargetedEntity.contact?.first_name} ${userTargetedEntity.contact?.last_name}. Cliquez ici pour voter.`; } else if (vote.appointment.choice === "DISMISS") { - message = - "Un collaborateur souhaite retirer le titre de Super Administrateur à " + - vote.appointment.targeted_user + - ". Cliquez ici pour voter."; + message = `Un collaborateur souhaite retirer le titre de Super Administrateur à ${userTargetedEntity.contact?.first_name} ${userTargetedEntity.contact?.last_name}. Cliquez ici pour voter.`; } this.notificationsService.create({ message: message, - redirection_url: `${this.backendVariables.APP_HOST}/users/${vote.appointment.targeted_user.uid}`, + redirection_url: `${this.backendVariables.APP_HOST}/users/${vote.appointment.user.uid}`, created_at: new Date(), updated_at: new Date(), - user: superAdminList || [], + user: superAdminList, }); } diff --git a/src/common/repositories/AppointmentsRepository.ts b/src/common/repositories/AppointmentsRepository.ts index 62b74d34..dcb1e660 100644 --- a/src/common/repositories/AppointmentsRepository.ts +++ b/src/common/repositories/AppointmentsRepository.ts @@ -1,7 +1,7 @@ import Database from "@Common/databases/database"; import BaseRepository from "@Repositories/BaseRepository"; import { Service } from "typedi"; -import { Appointments, EAppointmentStatus, Prisma } from "@prisma/client"; +import { Appointments, EAppointmentStatus, EVote, Prisma, Users, Votes } from "@prisma/client"; @Service() export default class AppointmentsRepository extends BaseRepository { @@ -39,6 +39,18 @@ export default class AppointmentsRepository extends BaseRepository { return this.model.update(updateArgs); } + public async findOneByStatusUserAndChoice(userUid: string, choice: EVote, status: EAppointmentStatus): Promise { + return this.model.findUnique({ + where: { + user_uid_choice_status: { + user_uid: userUid, + choice: choice, + status: status, + }, + }, + }); + } + /** * @description : Find one appointment */ @@ -54,12 +66,12 @@ export default class AppointmentsRepository extends BaseRepository { /** * @description : Find one appointment with votes */ - public async findOneByUidWithVotes(uid: string) { + public async findOneByUidWithVotes(uid: string): Promise<(Appointments & {votes: Votes[], user: Users}) | null> { return this.model.findUnique({ where: { uid: uid, }, - include: {votes: true}, + include: {votes: true, user: true}, }); } diff --git a/src/common/repositories/DocumentsRepository.ts b/src/common/repositories/DocumentsRepository.ts index 4c15dc52..5fcbf2f0 100644 --- a/src/common/repositories/DocumentsRepository.ts +++ b/src/common/repositories/DocumentsRepository.ts @@ -21,7 +21,6 @@ export default class DocumentsRepository extends BaseRepository { * @description : Find many documents */ public async findMany(query: Prisma.DocumentsFindManyArgs) { - query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); return this.model.findMany(query); } @@ -96,7 +95,26 @@ export default class DocumentsRepository extends BaseRepository { /** * @description : Update data of a document */ - public async update(uid: string, document: Partial, refusedReason?: string): Promise { + public async update(uid: string, document: Partial): Promise { + return this.model.update({ + where: { + uid: uid, + }, + data: { + document_status: EDocumentStatus[document.document_status as keyof typeof EDocumentStatus], + document_history: { + create: { + document_status: EDocumentStatus[document.document_status as keyof typeof EDocumentStatus], + }, + }, + }, + }); + } + + /** + * @description : Update data of a document + */ + public async refuse(uid: string, document: Partial, refusedReason?: string): Promise { return this.model.update({ where: { uid: uid, diff --git a/src/common/repositories/VotesRepository.ts b/src/common/repositories/VotesRepository.ts index a9bf7746..a48243df 100644 --- a/src/common/repositories/VotesRepository.ts +++ b/src/common/repositories/VotesRepository.ts @@ -29,10 +29,10 @@ export default class VotesRepository extends BaseRepository { */ public async create(vote: Vote): Promise { let whereArg: Prisma.AppointmentsWhereUniqueInput; - if(vote.appointment.targeted_user.uid) { + if(vote.appointment.user.uid) { whereArg = { user_uid_choice_status: { - user_uid: vote.appointment.targeted_user.uid, + user_uid: vote.appointment.user.uid, choice: EVote[vote.appointment.choice as keyof typeof EVote], status: EAppointmentStatus.OPEN, } @@ -49,7 +49,7 @@ export default class VotesRepository extends BaseRepository { where: whereArg, create: { choice: EVote[vote.appointment.choice as keyof typeof EVote], - user_uid: vote.appointment.targeted_user.uid!, + user_uid: vote.appointment.user.uid!, } }, }, @@ -61,7 +61,7 @@ export default class VotesRepository extends BaseRepository { } }; - return this.model.create({...createArgs, include: {appointment: {include: {votes: true}}}}); + return this.model.create({...createArgs, include: {voter: true, appointment: {include: {votes: true, user: true}}}}); } /** diff --git a/src/entries/App.ts b/src/entries/App.ts index 06ae8e52..c69734e0 100644 --- a/src/entries/App.ts +++ b/src/entries/App.ts @@ -25,8 +25,8 @@ const storage = multer.memoryStorage(); middlwares: [ cors({ origin: "*" }), multer({ storage: storage, limits: { fileSize: 32000000 } }).single("file"), //32 MB maximum - bodyParser.urlencoded({ extended: true }), - bodyParser.json(), + bodyParser.json({ limit: "35mb"}), + bodyParser.urlencoded({ extended: true, limit: "35mb", parameterLimit: 50000 }), ], errorHandler, }); diff --git a/src/services/admin/DocumentsService/DocumentsService.ts b/src/services/admin/DocumentsService/DocumentsService.ts index c8e7aa0b..5bdbead9 100644 --- a/src/services/admin/DocumentsService/DocumentsService.ts +++ b/src/services/admin/DocumentsService/DocumentsService.ts @@ -3,10 +3,11 @@ import { Document } from "le-coffre-resources/dist/Admin"; import DocumentsRepository from "@Repositories/DocumentsRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; +import FilesRepository from "@Repositories/FilesRepository"; @Service() export default class DocumentsService extends BaseService { - constructor(private documentsRepository: DocumentsRepository) { + constructor(private documentsRepository: DocumentsRepository, private filesRepository: FilesRepository) { super(); } @@ -38,8 +39,19 @@ export default class DocumentsService extends BaseService { * @description : Modify a document * @throws {Error} If document cannot be modified */ - public async update(uid: string, document: Partial, refused_reason?: string): Promise { - return this.documentsRepository.update(uid, document, refused_reason); + public async update(uid: string, document: Partial): Promise { + return this.documentsRepository.update(uid, document); + } + + public async refuse(uid: string, document: Partial, refused_reason: string): Promise { + if (document.files) { + for (let i = 0; i < document.files.length; i++) { + console.log("archiving file", document.files[i]?.uid); + await this.filesRepository.deleteKeyAndArchive(document.files[i]?.uid as string); + } + } + + return this.documentsRepository.refuse(uid, document, refused_reason); } /** diff --git a/src/services/common/AnchoringProofService/AnchoringProofService.ts b/src/services/common/AnchoringProofService/AnchoringProofService.ts index 45964213..ea9c2c18 100644 --- a/src/services/common/AnchoringProofService/AnchoringProofService.ts +++ b/src/services/common/AnchoringProofService/AnchoringProofService.ts @@ -72,8 +72,7 @@ export default class AnchoringProofService extends BaseService { */ public async generate(data: AnchoringProofData): Promise { const browser = await puppeteer.launch({ - headless: "new", - executablePath: process.env['PUPPETEER_EXECUTABLE_PATH'], + headless: 'new', args: ["--no-sandbox", "--disable-setuid-sandbox"], }); const page = await browser.newPage(); diff --git a/src/services/common/Id360Service/Id360Service.ts b/src/services/common/Id360Service/Id360Service.ts index 0271396d..9a98fe39 100644 --- a/src/services/common/Id360Service/Id360Service.ts +++ b/src/services/common/Id360Service/Id360Service.ts @@ -4,7 +4,7 @@ import { BackendVariables } from "@Common/config/variables/Variables"; import DocumentsService from "@Services/super-admin/DocumentsService/DocumentsService"; import FilesService from "../FilesService/FilesService"; -type EnrollmentResponse = { +export type EnrollmentResponse = { url: string; id: number; api_key: string; @@ -196,13 +196,12 @@ export default class Id360Service extends BaseService { } public async getEnrollment(token: string) { - const res = await fetch( + return await fetch( `${ this.variables.DOCAPOST_BASE_URL + this.variables.DOCAPOST_ROOT + this.variables.DOCAPOST_VERSION }/enrollment/status/${token}`, { method: "GET" }, ); - return (await res.json()) as EnrollmentResponse; } public async finalizeEnrollment(apiKey: string) { diff --git a/src/services/common/MailchimpService/MailchimpService.ts b/src/services/common/MailchimpService/MailchimpService.ts index 8f9f5f5e..8eb603bf 100644 --- a/src/services/common/MailchimpService/MailchimpService.ts +++ b/src/services/common/MailchimpService/MailchimpService.ts @@ -1,6 +1,6 @@ import EmailRepository from "@Repositories/EmailRepository"; import BaseService from "@Services/BaseService"; -import { Emails } from "@prisma/client"; +import { Emails, Prisma } from "@prisma/client"; import { Service } from "typedi"; import MailchimpClient from "@mailchimp/mailchimp_transactional"; import { BackendVariables } from "@Common/config/variables/Variables"; @@ -24,7 +24,7 @@ export default class MailchimpService extends BaseService { * @description : Get all emails * @throws {Error} If emails cannot be get */ - public async get(query: any): Promise { + public async get(query: Prisma.EmailsFindManyArgs): Promise { return this.emailRepository.findMany(query); } diff --git a/src/services/notary/DocumentsService/DocumentsService.ts b/src/services/notary/DocumentsService/DocumentsService.ts index 7ddb9c6a..c2260763 100644 --- a/src/services/notary/DocumentsService/DocumentsService.ts +++ b/src/services/notary/DocumentsService/DocumentsService.ts @@ -3,10 +3,11 @@ import { Document } from "le-coffre-resources/dist/Notary"; import DocumentsRepository from "@Repositories/DocumentsRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; +import FilesRepository from "@Repositories/FilesRepository"; @Service() export default class DocumentsService extends BaseService { - constructor(private documentsRepository: DocumentsRepository) { + constructor(private documentsRepository: DocumentsRepository, private filesRepository: FilesRepository) { super(); } @@ -38,8 +39,19 @@ export default class DocumentsService extends BaseService { * @description : Modify a document * @throws {Error} If document cannot be modified */ - public async update(uid: string, document: Partial, refused_reason?: string): Promise { - return this.documentsRepository.update(uid, document, refused_reason); + public async update(uid: string, document: Partial): Promise { + return this.documentsRepository.update(uid, document); + } + + public async refuse(uid: string, document: Partial, refused_reason: string): Promise { + if (document.files) { + for (let i = 0; i < document.files.length; i++) { + console.log("archiving file", document.files[i]?.uid); + await this.filesRepository.deleteKeyAndArchive(document.files[i]?.uid as string); + } + } + + return this.documentsRepository.refuse(uid, document, refused_reason); } /** diff --git a/src/services/super-admin/DocumentsService/DocumentsService.ts b/src/services/super-admin/DocumentsService/DocumentsService.ts index cf2bf617..41041d7e 100644 --- a/src/services/super-admin/DocumentsService/DocumentsService.ts +++ b/src/services/super-admin/DocumentsService/DocumentsService.ts @@ -3,10 +3,11 @@ import { Document } from "le-coffre-resources/dist/SuperAdmin"; import DocumentsRepository from "@Repositories/DocumentsRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; +import FilesRepository from "@Repositories/FilesRepository"; @Service() export default class DocumentsService extends BaseService { - constructor(private documentsRepository: DocumentsRepository) { + constructor(private documentsRepository: DocumentsRepository, private filesRepository: FilesRepository) { super(); } @@ -38,8 +39,19 @@ export default class DocumentsService extends BaseService { * @description : Modify a document * @throws {Error} If document cannot be modified */ - public async update(uid: string, document: Partial, refused_reason?: string): Promise { - return this.documentsRepository.update(uid, document, refused_reason); + public async update(uid: string, document: Partial): Promise { + return this.documentsRepository.update(uid, document); + } + + public async refuse(uid: string, document: Partial, refused_reason: string): Promise { + if (document.files) { + for (let i = 0; i < document.files.length; i++) { + console.log("archiving file", document.files[i]?.uid); + await this.filesRepository.deleteKeyAndArchive(document.files[i]?.uid as string); + } + } + + return this.documentsRepository.refuse(uid, document, refused_reason); } /** @@ -69,7 +81,7 @@ export default class DocumentsService extends BaseService { * @description : Get a document by uid * @throws {Error} If document cannot be get by uid */ - public async getByUidWithFiles(uid: string): Promise { + public async getByUidWithFiles(uid: string): Promise<(Documents & { files: Files[] | null }) | null> { return this.documentsRepository.findOneByUidWithFiles(uid); } diff --git a/src/services/super-admin/LiveVoteService/LiveVoteService.ts b/src/services/super-admin/LiveVoteService/LiveVoteService.ts index a034f976..804a888c 100644 --- a/src/services/super-admin/LiveVoteService/LiveVoteService.ts +++ b/src/services/super-admin/LiveVoteService/LiveVoteService.ts @@ -2,10 +2,11 @@ import BaseService from "@Services/BaseService"; import { Service } from "typedi"; import User, { Appointment, Role, Vote } from "le-coffre-resources/dist/SuperAdmin"; import VotesRepository from "@Repositories/VotesRepository"; -import { Appointments, EAppointmentStatus, EVote, Prisma, Votes } from "@prisma/client"; +import { Appointments, EAppointmentStatus, EVote, Prisma, Users, Votes } from "@prisma/client"; import UsersService from "../UsersService/UsersService"; import RolesService from "../RolesService/RolesService"; import AppointmentsRepository from "@Repositories/AppointmentsRepository"; +import NotificationBuilder from "@Common/notifications/NotificationBuilder"; @Service() export default class LiveVoteService extends BaseService { @@ -14,12 +15,13 @@ export default class LiveVoteService extends BaseService { private appointmentRepository: AppointmentsRepository, private userService: UsersService, private roleService: RolesService, + private notificationBuilder: NotificationBuilder, ) { super(); } public async verifyVoterChoice(vote: Vote): Promise { - const userWithRole = await this.userService.getByUidWithRole(vote.appointment.targeted_user.uid!); + const userWithRole = await this.userService.getByUidWithRole(vote.appointment.user.uid!); if (userWithRole!.role.name === "super-admin" && vote.appointment.choice === EVote.DISMISS) { return true; } @@ -65,27 +67,35 @@ export default class LiveVoteService extends BaseService { return this.appointmentRepository.findOneByUid(uid, query); } - public async getAppointmentWithVotes(vote: Vote): Promise { + public async getAppointmentWithVotes(vote: Vote): Promise<(Appointments & { votes: Votes[]; user: Users }) | null> { if (vote.appointment.uid) { - return this.appointmentRepository.findOneByUid(vote.appointment.uid); + return this.appointmentRepository.findOneByUidWithVotes(vote.appointment.uid); } const appointmentByUser = await this.appointmentRepository.findMany({ where: { AND: [ - { user_uid: vote.appointment.targeted_user.uid }, + { user_uid: vote.appointment.user.uid }, { status: EAppointmentStatus.OPEN }, { choice: EVote[vote.appointment.choice as keyof typeof EVote] }, ], }, - include: { votes: true }, + include: { votes: true, user: true }, }); - if (appointmentByUser.length >= 1) { + if (appointmentByUser.length != 0) { return this.appointmentRepository.findOneByUidWithVotes(appointmentByUser[0]!.uid); } return null; } private async closeVote(appointment: Appointments, vote: Votes) { + const apointmentFound = await this.appointmentRepository.findOneByStatusUserAndChoice( + appointment.user_uid, + EVote[appointment.choice as keyof typeof EVote], + EAppointmentStatus.CLOSED, + ); + if (apointmentFound) { + await this.appointmentRepository.delete(apointmentFound.uid); + } await this.appointmentRepository.update(vote.appointment_uid, EAppointmentStatus.CLOSED); const user = await this.userService.getByUid(appointment.user_uid, { role: true }); const userEntity = User.hydrate(user!, { strategy: "excludeAll" }); @@ -98,12 +108,14 @@ export default class LiveVoteService extends BaseService { const roles = await this.roleService.get({ where: { name: "default" } }); const roleEntity = Role.hydrate(roles[0]!, { strategy: "excludeAll" }); userEntity.role = roleEntity; + await this.notificationBuilder.sendDismissNotification(userEntity); await this.userService.update(appointment!.user_uid, userEntity); return vote; } else if (appointment.choice === EVote.NOMINATE) { const roles = await this.roleService.get({ where: { name: "super-admin" } }); const roleEntity = Role.hydrate(roles[0]!, { strategy: "excludeAll" }); userEntity!.role = roleEntity; + await this.notificationBuilder.sendNominateNotification(userEntity); await this.userService.update(appointment!.user_uid, userEntity); return vote; } @@ -118,8 +130,10 @@ export default class LiveVoteService extends BaseService { const appointment = await this.getAppointmentWithVotes(vote); if (appointment) { - const appointmentEntity = Appointment.hydrate(appointment, { strategy: "excludeAll" }); - if (appointmentEntity?.votes && appointmentEntity.votes.length >= 2) { + const voteEntity = Vote.hydrateArray(appointment.votes, { strategy: "excludeAll" }); + const appointementWithVotesHydrated = { ...appointment, votes: voteEntity }; + const appointmentEntity = Appointment.hydrate(appointementWithVotesHydrated, { strategy: "excludeAll" }); + if (appointmentEntity.votes && appointmentEntity.votes.length >= 2) { const voteCreated = await this.voteRepository.create(vote); return this.closeVote(appointment, voteCreated); } @@ -127,7 +141,9 @@ export default class LiveVoteService extends BaseService { const approvedChoice = await this.verifyVoterChoice(vote); if (!approvedChoice) return null; - - return this.voteRepository.create(vote); + const voteCreated = await this.voteRepository.create(vote); + const voteEntity = Vote.hydrate(voteCreated, { strategy: "excludeAll" }); + await this.notificationBuilder.sendVoteNotification(voteEntity); + return voteCreated; } }