diff --git a/Dockerfile b/Dockerfile index d33dbefb..e98bde33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,5 +36,5 @@ COPY --from=deps --chown=lecoffreuser leCoffre/src/common/databases ./src/common RUN apk update && apk add chromium USER lecoffreuser -CMD ["npm", "run", "start"] +CMD ["npm", "run", "api:start"] EXPOSE 3001 \ No newline at end of file diff --git a/package.json b/package.json index 688f4c6c..ba8a4fdc 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.139", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/customer/NotesController.ts b/src/app/api/customer/NotesController.ts new file mode 100644 index 00000000..11d59973 --- /dev/null +++ b/src/app/api/customer/NotesController.ts @@ -0,0 +1,202 @@ +import { Response, Request } from "express"; +import { Controller, Get, Post, Put } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; +import { Service } from "typedi"; +import { Prisma } from "@prisma/client"; +import authHandler from "@App/middlewares/AuthHandler"; +import Note from "le-coffre-resources/dist/Customer/Note"; +import NotesService from "@Services/customer/NotesService/NotesService"; + +@Controller() +@Service() +export default class NotesController extends ApiController { + constructor(private notesService: NotesService) { + super(); + } + + /** + * @description Get all Notes + * @returns Note[] list of Notes + */ + @Get("/api/v1/customer/notes", [authHandler]) + protected async get(req: Request, response: Response) { + try { + //get query + let query: Prisma.NotesFindManyArgs = {}; + if (req.query["q"]) { + query = JSON.parse(req.query["q"] as string); + if (query.where?.uid) { + this.httpBadRequest(response, "You can't filter by uid"); + return; + } + } + + //call service to get prisma entity + const noteEntities = await this.notesService.get(query); + + //Hydrate ressource with prisma entity + const notes = Note.hydrateArray(noteEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, notes); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Get a specific customer note + */ + @Get("/api/v1/customer/notes/customer/:customer-uid/folders/:folder-uid/", [authHandler]) + protected async getCustomerNoteByFolderUid(req: Request, response: Response) { + try { + const customerUid = req.params["customer-uid"]; + const folderUid = req.params["folder-uid"]; + if (!customerUid || !folderUid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + //get query + const query: Prisma.NotesFindManyArgs = { + where: { + customer: { + uid: customerUid + }, + folder: { + uid: folderUid + } + } + }; + + //call service to get prisma entity + const noteEntities = await this.notesService.get(query); + + if(noteEntities.length === 0) { + this.httpNotFoundRequest(response, "No notes found"); + return; + } + + //Hydrate ressource with prisma entity + const notes = Note.hydrateArray(noteEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, notes); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Get a specific note by uid + */ + @Get("/api/v1/customer/notes/:uid", [authHandler]) + protected async getOneByUid(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + //get query + let query; + if (req.query["q"]) { + query = JSON.parse(req.query["q"] as string); + } + + const noteEntity = await this.notesService.getByUid(uid, query); + + if (!noteEntity) { + this.httpNotFoundRequest(response, "note not found"); + return; + } + + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntity); + + //success + this.httpSuccess(response, note); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Create a new note + */ + @Post("/api/v1/customer/notes", [authHandler]) + protected async post(req: Request, response: Response) { + try { + //init OfficeFolder resource with request body values + const noteRessource = Note.hydrate(req.body); + // noteRessource.validateOrReject?.({ groups: ["createFolder"], forbidUnknownValues: false }); + + //call service to get prisma entity + try { + const noteEntity = await this.notesService.create(noteRessource); + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntity, { + strategy: "excludeAll", + }); + //success + this.httpCreated(response, note); + } catch (error) { + this.httpValidationError(response, error); + return; + } + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Modify a specific note by uid + */ + @Put("/api/v1/customer/notes/:uid", [authHandler]) + protected async put(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const noteFound = await this.notesService.getByUid(uid); + + if (!noteFound) { + this.httpNotFoundRequest(response, "office folder not found"); + return; + } + + //init OfficeFolder resource with request body values + const noteEntity = Note.hydrate(req.body); + + //validate folder + //await validateOrReject(noteEntity, { groups: ["updateFolder"], forbidUnknownValues: false }); + + //call service to get prisma entity + try { + const noteEntityUpdated = await this.notesService.update(uid, noteEntity); + + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpSuccess(response, note); + } catch (error) { + this.httpValidationError(response, error); + return; + } + } catch (error) { + this.httpInternalError(response, error); + return; + } + } +} diff --git a/src/app/api/customer/OfficeFoldersController.ts b/src/app/api/customer/OfficeFoldersController.ts index a90a5876..f8bea23d 100644 --- a/src/app/api/customer/OfficeFoldersController.ts +++ b/src/app/api/customer/OfficeFoldersController.ts @@ -95,7 +95,7 @@ export default class OfficeFoldersController extends ApiController { } //Hydrate ressource with prisma entity - const officeFolder = OfficeFolderNotary.hydrate(officeFolderEntity, { strategy: "excludeAll" }); + const officeFolder = OfficeFolderNotary.hydrate(officeFolderEntity); if(officeFolder.customers) { officeFolder.customers = officeFolder.customers!.filter((customer) => customer.contact?.email === email); diff --git a/src/app/api/notary/OfficeFolderAnchorsController.ts b/src/app/api/notary/OfficeFolderAnchorsController.ts index cb16501b..96bba906 100644 --- a/src/app/api/notary/OfficeFolderAnchorsController.ts +++ b/src/app/api/notary/OfficeFolderAnchorsController.ts @@ -68,6 +68,7 @@ export default class OfficeFoldersController extends ApiController { files: true, }, }, + office: true, }; const officeFolderFound = await this.officeFoldersService.getByUid(uid, query); @@ -87,7 +88,7 @@ export default class OfficeFoldersController extends ApiController { } const sortedHashes = [...folderHashes].sort(); - const anchoringProof = await this.secureService.download(sortedHashes); + const anchoringProof = await this.secureService.download(sortedHashes, officeFolder.office!.name); const addFileToZip = (zip: Zip) => (uid: string): Promise => (async () => { diff --git a/src/app/api/notary/OfficeFoldersController.ts b/src/app/api/notary/OfficeFoldersController.ts index 45941476..2d8f7792 100644 --- a/src/app/api/notary/OfficeFoldersController.ts +++ b/src/app/api/notary/OfficeFoldersController.ts @@ -261,9 +261,9 @@ export default class OfficeFoldersController extends ApiController { let query; if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); - } + } - const officeFolderEntity = await this.officeFoldersService.getByUid(uid, query); + const officeFolderEntity = await this.officeFoldersService.getByUid(uid, query); if (!officeFolderEntity) { this.httpNotFoundRequest(response, "folder not found"); @@ -271,7 +271,8 @@ export default class OfficeFoldersController extends ApiController { } //Hydrate ressource with prisma entity - const officeFolder = OfficeFolder.hydrate(officeFolderEntity, { strategy: "excludeAll" }); + const officeFolder = OfficeFolder.hydrate(officeFolderEntity); + //success this.httpSuccess(response, officeFolder); diff --git a/src/app/index.ts b/src/app/index.ts index 1590b6f1..c5767457 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -53,6 +53,7 @@ import SubscriptionsController from "./api/admin/SubscriptionsController"; import StripeController from "./api/admin/StripeController"; import StripeWebhooks from "@Common/webhooks/stripeWebhooks"; import RulesGroupsController from "./api/admin/RulesGroupsController"; +import NotesController from "./api/customer/NotesController"; /** * @description This allow to declare all controllers used in the application @@ -114,5 +115,6 @@ export default { Container.get(StripeController); Container.get(StripeWebhooks); Container.get(RulesGroupsController); + Container.get(NotesController); }, }; diff --git a/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql b/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql new file mode 100644 index 00000000..848a2ab9 --- /dev/null +++ b/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "notes" ( + "uid" TEXT NOT NULL, + "content" VARCHAR(1000) NOT NULL, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + "customer_uid" VARCHAR(255) NOT NULL, + "folder_uid" VARCHAR(255) NOT NULL, + + CONSTRAINT "notes_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "notes_uid_key" ON "notes"("uid"); + +-- AddForeignKey +ALTER TABLE "notes" ADD CONSTRAINT "notes_customer_uid_fkey" FOREIGN KEY ("customer_uid") REFERENCES "customers"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notes" ADD CONSTRAINT "notes_folder_uid_fkey" FOREIGN KEY ("folder_uid") REFERENCES "office_folders"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index e42133a9..259c59f1 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -128,6 +128,7 @@ model Customers { totpCodes TotpCodes[] office Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade) office_uid String @db.VarChar(255) + notes Notes[] @@map("customers") } @@ -172,6 +173,7 @@ model OfficeFolders { folder_anchor OfficeFolderAnchors? @relation(fields: [folder_anchor_uid], references: [uid]) folder_anchor_uid String? @unique @db.VarChar(255) + notes Notes[] @@unique([folder_number, office_uid]) @@map("office_folders") @@ -417,6 +419,18 @@ model Seats { @@map("seats") } +model Notes { + uid String @id @unique @default(uuid()) + content String @db.VarChar(1000) + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) + customer_uid String @db.VarChar(255) + folder OfficeFolders @relation(fields: [folder_uid], references: [uid], onDelete: Cascade) + folder_uid String @db.VarChar(255) + @@map("notes") +} + enum ESubscriptionStatus { ACTIVE INACTIVE diff --git a/src/common/repositories/NotesRepository.ts b/src/common/repositories/NotesRepository.ts new file mode 100644 index 00000000..8300ab96 --- /dev/null +++ b/src/common/repositories/NotesRepository.ts @@ -0,0 +1,74 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { Notes, Prisma } from "@prisma/client"; +import Note from "le-coffre-resources/dist/Customer/Note"; + +@Service() +export default class NotesRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().notes; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many notes + */ + public async findMany(query: Prisma.NotesFindManyArgs) { + return this.model.findMany(query); + } + + /** + * @description : Find one note + */ + public async findOneByUid(uid: string, query?: Prisma.NotesInclude) { + return this.model.findUnique({ + where: { uid }, + include: query, + }); + } + + /** + * @description : Create new note + */ + public async create(note: Note): Promise { + const createArgs: Prisma.NotesCreateArgs = { + data: { + content: note.content || "", + customer: { + connect: { + uid: note.customer!.uid, + }, + }, + folder: { + connect: { + uid: note.folder!.uid, + }, + }, + }, + }; + + return this.model.create(createArgs); + } + + /** + * @description : Update data of an office folder + */ + public async update(noteUid: string, note: Note): Promise { + const updateArgs: Prisma.NotesUpdateArgs = { + where: { + uid: noteUid, + }, + data: { + content: note.content || "", + }, + }; + + return this.model.update(updateArgs); + } +} diff --git a/src/services/common/AnchoringProofService/AnchoringProofService.ts b/src/services/common/AnchoringProofService/AnchoringProofService.ts index 8d43a5b0..4487f75c 100644 --- a/src/services/common/AnchoringProofService/AnchoringProofService.ts +++ b/src/services/common/AnchoringProofService/AnchoringProofService.ts @@ -6,6 +6,7 @@ export interface AnchoringProofData { rootHash: string; anchoringTime: string; txLink: string; + office_name: string; } @Service() @@ -14,57 +15,105 @@ export default class AnchoringProofService extends BaseService { super(); } - private static svgTemplateDocument: string = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[ANCHORING_TIME]] - Certificat de dépôt international - Hash : - [[ROOT_HASH]] - - Déposant(s) - Auteur : - Not.IT - Partenaire technique : - Smart-Chain - - Explorateur blockchain - [[TX_LINK]] - - + // private static svgTemplateDocument: string = ` + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // [[ANCHORING_TIME]] + // Certificat de dépôt international + // Hash : + // [[ROOT_HASH]] + // + // Déposant(s) + // Auteur : + // Not.IT + // Partenaire technique : + // Smart-Chain + // + // Explorateur blockchain + // [[TX_LINK]] + // + // + // `; + + private static svgTemplateDocumentBis: string = ` + + + + + + + + + + + + + + + + + +Certificat de dépôt international +[[ANCHORING_TIME]] +Nom de l'office +[[OFFICE_NAME]] + +Déposant(s) +LEcoffre.io + +Hash +[[ROOT_HASH]] + +Explorateur blockchain +[[TX_LINK]] + + +À quoi ça sert ? +Un certificat d'ancrage sur la blockchain permet d'établir de manière irréfutable l'enregistrement de données spécifiques à une date et une heure déterminées. Il constitue une preuve immuable et juridiquement opposable de l'existence et de l'intégrité des informations. En cas de litige, ce certificat peut démontrer de manière incontestable que les données n'ont pas été altérées depuis leur enregistrement. + +Qu'est-ce qu'un explorateur blockchain ? +Un explorateur blockchain est un outil numérique permettant d'accéder et de consulter les transactions et les données enregistrées sur la blockchain. Il assure une transparence totale, permettant à toute partie prenante de vérifier les opérations et les enregistrements effectués sur la chaîne de blocs. + +Qu'est-ce qu'un hash ? +Un hash est une empreinte cryptographique unique générée par un algorithme à partir de données spécifiques. Il agit comme une signature numérique permettant de vérifier l'intégrité des données : toute modification, même infime, des données d'origine entraîne la production d'un hash distinct, garantissant ainsi l'authenticité et l'intégrité des informations. + + + + + `; /** @@ -81,7 +130,7 @@ export default class AnchoringProofService extends BaseService { var htmlContent = ` - ${AnchoringProofService.svgTemplateDocument} + ${AnchoringProofService.svgTemplateDocumentBis} `; @@ -89,6 +138,7 @@ export default class AnchoringProofService extends BaseService { htmlContent = htmlContent .replace("[[ROOT_HASH]]", data.rootHash) .replace("[[ANCHORING_TIME]]", data.anchoringTime) + .replace("[[OFFICE_NAME]]", data.office_name) .replace(/\[\[TX_LINK\]\]/g, data.txLink); await page.setContent(htmlContent); @@ -126,4 +176,4 @@ export default class AnchoringProofService extends BaseService { return buffer; } -} +} \ No newline at end of file diff --git a/src/services/common/SecureService/SecureService.ts b/src/services/common/SecureService/SecureService.ts index 37dd961b..b69d1d90 100644 --- a/src/services/common/SecureService/SecureService.ts +++ b/src/services/common/SecureService/SecureService.ts @@ -63,7 +63,7 @@ export default class SecureService extends BaseService { * @description : Download the anchoring proof document * @throws {Error} If transaction is not verified on chain */ - public async download(hash_sources: string[]) { + public async download(hash_sources: string[], office_name: string) { const anchor = await this.verify(hash_sources); if (anchor.transactions.length === 0) { @@ -71,6 +71,8 @@ export default class SecureService extends BaseService { } const transaction = anchor.transactions[0]; + console.log(transaction); + if (transaction.status !== EAnchoringStatus.VERIFIED_ON_CHAIN) { throw new Error(`Transaction not verified on chain: ${transaction.status}`); @@ -83,6 +85,7 @@ export default class SecureService extends BaseService { timeZone: "Europe/Paris", timeZoneName: "short", }), + office_name: office_name, }); } } diff --git a/src/services/customer/NotesService/NotesService.ts b/src/services/customer/NotesService/NotesService.ts new file mode 100644 index 00000000..81b120be --- /dev/null +++ b/src/services/customer/NotesService/NotesService.ts @@ -0,0 +1,48 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import { Notes, Prisma } from "@prisma/client"; +import NotesRepository from "@Repositories/NotesRepository"; +import Note from "le-coffre-resources/dist/Customer/Note"; + +@Service() +export default class NotesService extends BaseService { + constructor( + private notesRepository: NotesRepository, + ) { + super(); + } + + /** + * @description : Get all notes + * @throws {Error} If notes cannot be get + */ + public async get(query: Prisma.NotesFindManyArgs) { + return this.notesRepository.findMany(query); + } + + /** + * @description : Get a note by uid + * @throws {Error} If note cannot be get + */ + public async getByUid(uid: string, query?: Prisma.NotesInclude) { + return this.notesRepository.findOneByUid(uid, query); + } + + /** + * @description : Create a new note + * @throws {Error} If note cannot be created + */ + public async create(noteEntity: Note): Promise { + return this.notesRepository.create(noteEntity); + } + + /** + * @description : Modify a note + * @throws {Error} If note cannot be modified + */ + public async update(noteUid: string, noteEntity: Note): Promise { + return this.notesRepository.update(noteUid, noteEntity); + } + + +}