Merge branch 'feature/anchoring-api' of github.com:smart-chain-fr/leCoffre into feature/anchoring-api

This commit is contained in:
OxSaitama 2023-09-22 10:42:35 +02:00
commit 4a38b29ea3
7 changed files with 227 additions and 3 deletions

15
package-lock.json generated
View File

@ -18,9 +18,11 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^2.3.1", "cron": "^2.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.77", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.77",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"next": "^13.1.5", "next": "^13.1.5",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
@ -2474,6 +2476,11 @@
"node": ">= 0.6" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"license": "MIT", "license": "MIT",
@ -3901,6 +3908,14 @@
"version": "2.2.3", "version": "2.2.3",
"license": "MIT" "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": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"license": "MIT" "license": "MIT"

View File

@ -51,9 +51,11 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^2.3.1", "cron": "^2.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.77", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.77",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"next": "^13.1.5", "next": "^13.1.5",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",

View File

@ -0,0 +1,111 @@
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";
import authHandler from "@App/middlewares/AuthHandler";
import ruleHandler from "@App/middlewares/RulesHandler";
import folderHandler from "@App/middlewares/OfficeMembershipHandlers/FolderHandler";
@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", [authHandler, ruleHandler, folderHandler])
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<OfficeFolder>(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", [authHandler, ruleHandler, folderHandler])
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<OfficeFolder>(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;
}
}
}

View File

@ -41,6 +41,7 @@ import OfficeRolesControllerNotary from "./api/notary/OfficeRolesController";
import FilesControllerCustomer from "./api/customer/FilesController"; import FilesControllerCustomer from "./api/customer/FilesController";
import DocumentsControllerCustomer from "./api/customer/DocumentsController"; import DocumentsControllerCustomer from "./api/customer/DocumentsController";
import OfficeFoldersController from "./api/customer/OfficeFoldersController"; import OfficeFoldersController from "./api/customer/OfficeFoldersController";
import OfficeFolderAnchorsController from "./api/notary/OfficeFolderAnchorsController";
import CustomersController from "./api/customer/CustomersController"; import CustomersController from "./api/customer/CustomersController";
import AppointmentsController from "./api/super-admin/AppointmentsController"; import AppointmentsController from "./api/super-admin/AppointmentsController";
import VotesController from "./api/super-admin/VotesController"; import VotesController from "./api/super-admin/VotesController";
@ -100,6 +101,7 @@ export default {
Container.get(FilesControllerCustomer); Container.get(FilesControllerCustomer);
Container.get(DocumentsControllerCustomer); Container.get(DocumentsControllerCustomer);
Container.get(OfficeFoldersController); Container.get(OfficeFoldersController);
Container.get(OfficeFolderAnchorsController);
Container.get(CustomersController) Container.get(CustomersController)
}, },
}; };

View File

@ -76,11 +76,16 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly MAILCHIMP_API_KEY!: string; public readonly MAILCHIMP_API_KEY!: string;
@IsNotEmpty()
public readonly SECURE_API_KEY!: string;
@IsNotEmpty()
public readonly SECURE_API_BASE_URL!: string;
@IsNotEmpty() @IsNotEmpty()
public readonly ENV!: string; public readonly ENV!: string;
public constructor() { public constructor() {
dotenv.config(); dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!; this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
this.DATABASE_HOST = process.env["DATABASE_HOST"]!; this.DATABASE_HOST = process.env["DATABASE_HOST"]!;
@ -106,10 +111,11 @@ export class BackendVariables {
this.ACCESS_TOKEN_SECRET = process.env["ACCESS_TOKEN_SECRET"]!; this.ACCESS_TOKEN_SECRET = process.env["ACCESS_TOKEN_SECRET"]!;
this.REFRESH_TOKEN_SECRET = process.env["REFRESH_TOKEN_SECRET"]!; this.REFRESH_TOKEN_SECRET = process.env["REFRESH_TOKEN_SECRET"]!;
this.MAILCHIMP_API_KEY = process.env["MAILCHIMP_API_KEY"]!; 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"]!; this.ENV = process.env["ENV"]!;
} }
public async validate(groups?: string[]) { public async validate(groups?: string[]) {
const validationOptions = groups ? { groups } : undefined; const validationOptions = groups ? { groups } : undefined;
try { try {

View File

@ -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<OfficeFolder>()("documents", []);
export const documentFilesLens = Optics.Lens.fromNullableProp<Document>()("files", []);
export const fileHashLens = Optics.Lens.fromProp<File>()("hash");
/**
* Traversals
*/
export const documentsTraversal = Optics.fromTraversable(Array.Traversable)<Document>();
export const filesTraversal = Optics.fromTraversable(Array.Traversable)<File>();
export const folderHashesTraversal = folderDocumentsLens
.composeTraversal(documentsTraversal)
.composeLens(documentFilesLens)
.composeTraversal(filesTraversal)
.composeLens(fileHashLens);
/**
* Getters
*/
export const getFolderHashes = (folder: OfficeFolder) => Traversal.getAll(folder)(folderHashesTraversal);

View File

@ -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();
}
}