From c4dcc1602705a21356a6a16db4d34202f94f9c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:09:06 +0200 Subject: [PATCH 1/4] add `SecureService` --- src/common/config/variables/Variables.ts | 12 +++- .../common/SecureService/SecureService.ts | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/services/common/SecureService/SecureService.ts diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index d865df6e..14e5e0a6 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -73,11 +73,16 @@ export class BackendVariables { @IsNotEmpty() public readonly MAILCHIMP_API_KEY!: string; + @IsNotEmpty() + public readonly SECURE_API_KEY!: string; + + @IsNotEmpty() + public readonly SECURE_API_BASE_URL!: string; + @IsNotEmpty() public readonly ENV!: string; public constructor() { - dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; this.DATABASE_HOST = process.env["DATABASE_HOST"]!; @@ -102,10 +107,11 @@ export class BackendVariables { this.ACCESS_TOKEN_SECRET = process.env["ACCESS_TOKEN_SECRET"]!; this.REFRESH_TOKEN_SECRET = process.env["REFRESH_TOKEN_SECRET"]!; this.MAILCHIMP_API_KEY = process.env["MAILCHIMP_API_KEY"]!; + this.SECURE_API_KEY = process.env["SECURE_API_KEY"]!; + this.SECURE_API_BASE_URL = process.env["SECURE_API_BASE_URL"]!; this.ENV = process.env["ENV"]!; - } - public async validate(groups?: string[]) { + public async validate(groups?: string[]) { const validationOptions = groups ? { groups } : undefined; try { diff --git a/src/services/common/SecureService/SecureService.ts b/src/services/common/SecureService/SecureService.ts new file mode 100644 index 00000000..6ff422df --- /dev/null +++ b/src/services/common/SecureService/SecureService.ts @@ -0,0 +1,59 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import { BackendVariables } from "@Common/config/variables/Variables"; + +@Service() +export default class SecureService extends BaseService { + constructor(protected variables: BackendVariables) { + super(); + } + + /** + * @description : anchor a sequence of hashes + * @throws {Error} If secure job cannot be created + */ + public async anchor(hash_sources: string[]) { + const url = new URL(this.variables.SECURE_API_BASE_URL.concat("/flows/v2/anchor")); + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + apiKey: this.variables.SECURE_API_KEY, + }, + body: JSON.stringify({ + hash_sources, + callback_url: "", + callback_config: {}, + }), + }); + + return await response.json(); + } + + /** + * @description : verify if a sequence of hashes is anchored + * @throws {Error} If secure job cannot be found + */ + public async verify(hash_sources: string[]) { + const params = new URLSearchParams(); + + hash_sources.forEach((hash) => { + params.append("hash_sources", hash); + }); + + const url = new URL(this.variables.SECURE_API_BASE_URL.concat("/flows/v2/verify?").concat(params.toString())); + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + apiKey: this.variables.SECURE_API_KEY, + }, + }); + + return await response.json(); + } +} From b448aea01e4674c55cc22e0a4e172f5fc4f4207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:40:25 +0200 Subject: [PATCH 2/4] add optics for accessing nested hashes --- package-lock.json | 15 +++++++++++++++ package.json | 2 ++ src/common/optics/notary/index.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/common/optics/notary/index.ts diff --git a/package-lock.json b/package-lock.json index 7b2cd07d..7f74a256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,11 @@ "cors": "^2.8.5", "cron": "^2.3.1", "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.77", "module-alias": "^2.2.2", + "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", "next": "^13.1.5", "node-cache": "^5.1.2", @@ -2474,6 +2476,11 @@ "node": ">= 0.6" } }, + "node_modules/fp-ts": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", + "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==" + }, "node_modules/fresh": { "version": "0.5.2", "license": "MIT", @@ -3901,6 +3908,14 @@ "version": "2.2.3", "license": "MIT" }, + "node_modules/monocle-ts": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", + "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==", + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" diff --git a/package.json b/package.json index 9487dca3..57955a72 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,11 @@ "cors": "^2.8.5", "cron": "^2.3.1", "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.77", "module-alias": "^2.2.2", + "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", "next": "^13.1.5", "node-cache": "^5.1.2", diff --git a/src/common/optics/notary/index.ts b/src/common/optics/notary/index.ts new file mode 100644 index 00000000..4e9441e2 --- /dev/null +++ b/src/common/optics/notary/index.ts @@ -0,0 +1,29 @@ +import * as Optics from "monocle-ts"; +import * as Traversal from "monocle-ts/Traversal"; +import * as Array from "fp-ts/Array"; + +import { Document, File, OfficeFolder } from "le-coffre-resources/dist/Notary"; + +/** + * Lenses + */ +export const folderDocumentsLens = Optics.Lens.fromNullableProp()("documents", []); +export const documentFilesLens = Optics.Lens.fromNullableProp()("files", []); +export const fileHashLens = Optics.Lens.fromProp()("hash"); + +/** + * Traversals + */ +export const documentsTraversal = Optics.fromTraversable(Array.Traversable)(); +export const filesTraversal = Optics.fromTraversable(Array.Traversable)(); + +export const folderHashesTraversal = folderDocumentsLens + .composeTraversal(documentsTraversal) + .composeLens(documentFilesLens) + .composeTraversal(filesTraversal) + .composeLens(fileHashLens); + +/** + * Getters + */ +export const getFolderHashes = (folder: OfficeFolder) => Traversal.getAll(folder)(folderHashesTraversal); From dc5eb4cb526089d63bd603418efd2fa5598e2d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:33:30 +0200 Subject: [PATCH 3/4] add `OfficeFolderAnchorsController` - create a job for anchoring all hashes in a folder - monitor the status of an anchoring --- .../notary/OfficeFolderAnchorsController.ts | 108 ++++++++++++++++++ src/app/index.ts | 2 + 2 files changed, 110 insertions(+) create mode 100644 src/app/api/notary/OfficeFolderAnchorsController.ts diff --git a/src/app/api/notary/OfficeFolderAnchorsController.ts b/src/app/api/notary/OfficeFolderAnchorsController.ts new file mode 100644 index 00000000..086162dc --- /dev/null +++ b/src/app/api/notary/OfficeFolderAnchorsController.ts @@ -0,0 +1,108 @@ +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 { getFolderHashes } from "@Common/optics/notary"; +import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; +import SecureService from "@Services/common/SecureService/SecureService"; + +@Controller() +@Service() +export default class OfficeFoldersController extends ApiController { + constructor(private secureService: SecureService, private officeFoldersService: OfficeFoldersService) { + super(); + } + + /** + * @description Create a new folder anchor + */ + @Post("/api/v1/notary/anchors/:uid") + protected async post(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const query = { + documents: { + include: { + files: true, + }, + }, + }; + + const officeFolderFound = await this.officeFoldersService.getByUid(uid, query); + + if (!officeFolderFound) { + this.httpNotFoundRequest(response, "Office folder not found"); + return; + } + + const officeFolder = OfficeFolder.hydrate(officeFolderFound, { strategy: "excludeAll" }); + const folderHashes = getFolderHashes(officeFolder); + + if (folderHashes.length === 0) { + this.httpNotFoundRequest(response, "No file hash to anchor"); + return; + } + + const sortedHashes = [...folderHashes].sort(); + const anchor = await this.secureService.anchor(sortedHashes); + + this.httpSuccess(response, anchor); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Verify a folder anchor status + */ + @Get("/api/v1/notary/anchors/:uid") + protected async get(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const query = { + documents: { + include: { + files: true, + }, + }, + }; + + const officeFolderFound = await this.officeFoldersService.getByUid(uid, query); + + if (!officeFolderFound) { + this.httpNotFoundRequest(response, "Office folder not found"); + return; + } + + const officeFolder = OfficeFolder.hydrate(officeFolderFound, { strategy: "excludeAll" }); + const folderHashes = getFolderHashes(officeFolder); + + if (folderHashes.length === 0) { + this.httpNotFoundRequest(response, "No file hash to anchor"); + return; + } + + const sortedHashes = [...folderHashes].sort(); + const anchor = await this.secureService.verify(sortedHashes); + + this.httpSuccess(response, anchor); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } +} diff --git a/src/app/index.ts b/src/app/index.ts index dc98b2e4..fa011b2c 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -41,6 +41,7 @@ import OfficeRolesControllerNotary from "./api/notary/OfficeRolesController"; import FilesControllerCustomer from "./api/customer/FilesController"; import DocumentsControllerCustomer from "./api/customer/DocumentsController"; import OfficeFoldersController from "./api/customer/OfficeFoldersController"; +import OfficeFolderAnchorsController from "./api/notary/OfficeFolderAnchorsController"; import CustomersController from "./api/customer/CustomersController"; import AppointmentsController from "./api/super-admin/AppointmentsController"; import VotesController from "./api/super-admin/VotesController"; @@ -98,6 +99,7 @@ export default { Container.get(FilesControllerCustomer); Container.get(DocumentsControllerCustomer); Container.get(OfficeFoldersController); + Container.get(OfficeFolderAnchorsController); Container.get(CustomersController) }, }; From 9590adff2e5c8bdfd9db8d95c98207e268574f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Mansot?= <26844641+devfull@users.noreply.github.com> Date: Fri, 22 Sep 2023 10:06:00 +0200 Subject: [PATCH 4/4] add middlewares in `OfficeFolderAnchorsController` --- src/app/api/notary/OfficeFolderAnchorsController.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/api/notary/OfficeFolderAnchorsController.ts b/src/app/api/notary/OfficeFolderAnchorsController.ts index 086162dc..423f521b 100644 --- a/src/app/api/notary/OfficeFolderAnchorsController.ts +++ b/src/app/api/notary/OfficeFolderAnchorsController.ts @@ -6,6 +6,9 @@ import { OfficeFolder } from "le-coffre-resources/dist/Notary"; import { getFolderHashes } from "@Common/optics/notary"; import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; import SecureService from "@Services/common/SecureService/SecureService"; +import authHandler from "@App/middlewares/AuthHandler"; +import ruleHandler from "@App/middlewares/RulesHandler"; +import folderHandler from "@App/middlewares/OfficeMembershipHandlers/FolderHandler"; @Controller() @Service() @@ -17,7 +20,7 @@ export default class OfficeFoldersController extends ApiController { /** * @description Create a new folder anchor */ - @Post("/api/v1/notary/anchors/:uid") + @Post("/api/v1/notary/anchors/:uid", [authHandler, ruleHandler, folderHandler]) protected async post(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -63,7 +66,7 @@ export default class OfficeFoldersController extends ApiController { /** * @description Verify a folder anchor status */ - @Get("/api/v1/notary/anchors/:uid") + @Get("/api/v1/notary/anchors/:uid", [authHandler, ruleHandler, folderHandler]) protected async get(req: Request, response: Response) { try { const uid = req.params["uid"];