Merge Dev in staging (#81)

This commit is contained in:
Arnaud D. Natali 2023-09-22 17:00:11 +02:00 committed by GitHub
commit 7e60ae5f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 845 additions and 413 deletions

813
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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.73",
"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",

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 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";
@ -100,6 +101,7 @@ export default {
Container.get(FilesControllerCustomer);
Container.get(DocumentsControllerCustomer);
Container.get(OfficeFoldersController);
Container.get(OfficeFolderAnchorsController);
Container.get(CustomersController)
},
};

View File

@ -76,11 +76,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"]!;
@ -106,8 +111,9 @@ 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[]) {
const validationOptions = groups ? { groups } : undefined;

View File

@ -0,0 +1,71 @@
/*
Warnings:
- The values [ANCHORED] on the enum `EDocumentStatus` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `blockchain_anchor_uid` on the `documents` table. All the data in the column will be lost.
- You are about to drop the `blockchain_anchors` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[folder_anchor_uid]` on the table `office_folders` will be added. If there are existing duplicate values, this will fail.
- Added the required column `hash` to the `files` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "EBlockchainName" AS ENUM ('TEZOS');
-- CreateEnum
CREATE TYPE "EAnchoringStatus" AS ENUM ('QUEUED', 'ATTEMPTING', 'VERIFIED_ON_CHAIN', 'VERIFYING_ON_CHAIN', 'ABANDONED');
-- AlterEnum
BEGIN;
CREATE TYPE "EDocumentStatus_new" AS ENUM ('ASKED', 'DEPOSITED', 'VALIDATED', 'REFUSED');
ALTER TABLE "documents" ALTER COLUMN "document_status" DROP DEFAULT;
ALTER TABLE "document_history" ALTER COLUMN "document_status" DROP DEFAULT;
ALTER TABLE "documents" ALTER COLUMN "document_status" TYPE "EDocumentStatus_new" USING ("document_status"::text::"EDocumentStatus_new");
ALTER TABLE "document_history" ALTER COLUMN "document_status" TYPE "EDocumentStatus_new" USING ("document_status"::text::"EDocumentStatus_new");
ALTER TYPE "EDocumentStatus" RENAME TO "EDocumentStatus_old";
ALTER TYPE "EDocumentStatus_new" RENAME TO "EDocumentStatus";
DROP TYPE "EDocumentStatus_old";
ALTER TABLE "documents" ALTER COLUMN "document_status" SET DEFAULT 'ASKED';
ALTER TABLE "document_history" ALTER COLUMN "document_status" SET DEFAULT 'ASKED';
COMMIT;
-- DropForeignKey
ALTER TABLE "documents" DROP CONSTRAINT "documents_blockchain_anchor_uid_fkey";
-- AlterTable
ALTER TABLE "documents" DROP COLUMN "blockchain_anchor_uid";
-- AlterTable
ALTER TABLE "files" ADD COLUMN "hash" VARCHAR(255) NOT NULL;
-- AlterTable
ALTER TABLE "office_folders" ADD COLUMN "folder_anchor_uid" VARCHAR(255);
-- DropTable
DROP TABLE "blockchain_anchors";
-- CreateTable
CREATE TABLE "office_folder_anchors" (
"uid" TEXT NOT NULL,
"hash_sources" TEXT[],
"root_hash" VARCHAR(255) NOT NULL,
"blockchain" "EBlockchainName" NOT NULL DEFAULT 'TEZOS',
"status" "EAnchoringStatus" NOT NULL DEFAULT 'QUEUED',
"anchor_nb_try" INTEGER NOT NULL DEFAULT 0,
"anchored_at" TIMESTAMP(3),
"tx_id" VARCHAR(255),
"tx_link" VARCHAR(255),
"tx_hash" VARCHAR(255),
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "office_folder_anchors_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "office_folder_anchors_uid_key" ON "office_folder_anchors"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "office_folders_folder_anchor_uid_key" ON "office_folders"("folder_anchor_uid");
-- AddForeignKey
ALTER TABLE "office_folders" ADD CONSTRAINT "office_folders_folder_anchor_uid_fkey" FOREIGN KEY ("folder_anchor_uid") REFERENCES "office_folder_anchors"("uid") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -133,17 +133,41 @@ model OfficeFolders {
customers Customers[] @relation("OfficeFolderHasCustomers")
documents Documents[]
folder_anchor OfficeFolderAnchors? @relation(fields: [folder_anchor_uid], references: [uid])
folder_anchor_uid String? @unique @db.VarChar(255)
@@unique([folder_number, office_uid])
@@map("office_folders")
}
model OfficeFolderAnchors {
uid String @id @unique @default(uuid())
hash_sources String[]
root_hash String @db.VarChar(255)
blockchain EBlockchainName @default(TEZOS)
status EAnchoringStatus @default(QUEUED)
anchor_nb_try Int @default(0)
anchored_at DateTime?
tx_id String? @db.VarChar(255)
tx_link String? @db.VarChar(255)
tx_hash String? @db.VarChar(255)
folder OfficeFolders?
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("office_folder_anchors")
}
model Documents {
uid String @id @unique @default(uuid())
document_status EDocumentStatus @default(ASKED)
document_type DocumentTypes @relation(fields: [document_type_uid], references: [uid])
document_type_uid String @db.VarChar(255)
blockchain_anchor BlockchainAnchors? @relation(fields: [blockchain_anchor_uid], references: [uid])
blockchain_anchor_uid String? @db.VarChar(255)
folder OfficeFolders @relation(fields: [folder_uid], references: [uid])
folder_uid String @db.VarChar(255)
depositor Customers @relation(fields: [depositor_uid], references: [uid], onDelete: Cascade)
@ -175,6 +199,7 @@ model Files {
file_path String @unique @db.VarChar(255)
file_name String @db.VarChar(255)
mimetype String @db.VarChar(255)
hash String @db.VarChar(255)
size Int
archived_at DateTime?
key String? @db.VarChar(255)
@ -184,16 +209,6 @@ model Files {
@@map("files")
}
model BlockchainAnchors {
uid String @id @unique @default(uuid())
smartSigJobId String @unique @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
documents Documents[]
@@map("blockchain_anchors")
}
model DocumentTypes {
uid String @id @unique @default(uuid())
name String @db.VarChar(255)
@ -346,7 +361,6 @@ enum EDocumentStatus {
ASKED
DEPOSITED
VALIDATED
ANCHORED
REFUSED
}
@ -359,3 +373,15 @@ enum EAppointmentStatus {
OPEN
CLOSED
}
enum EBlockchainName {
TEZOS
}
enum EAnchoringStatus {
QUEUED
ATTEMPTING
VERIFIED_ON_CHAIN
VERIFYING_ON_CHAIN
ABANDONED
}

View File

@ -669,6 +669,18 @@ export default async function main() {
created_at: new Date(),
updated_at: new Date(),
},
{
name: "POST anchors",
label: "Ancrer un dossier",
created_at: new Date(),
updated_at: new Date(),
},
{
name: "GET anchors",
label: "Vérifier l'ancrage un dossier",
created_at: new Date(),
updated_at: new Date(),
},
{
name: "POST deed-types",
label: "Création des types d'actes",
@ -756,14 +768,14 @@ export default async function main() {
label: "Administrateur",
created_at: new Date(),
updated_at: new Date(),
rules: rules.slice(0, 33),
rules: rules.slice(0, 35),
},
{
name: "notary",
label: "Notaire",
created_at: new Date(),
updated_at: new Date(),
rules: rules.slice(0, 23),
rules: rules.slice(0, 25),
},
{
name: "default",
@ -780,7 +792,7 @@ export default async function main() {
created_at: new Date(),
updated_at: new Date(),
office: offices[0]!,
rules: rules.slice(0, 33),
rules: rules.slice(0, 35),
},
{
name: "Collaborateur",

View File

@ -45,6 +45,34 @@ export default class EmailBuilder {
nbTrySend: null,
lastTrySendDate: null,
});
}
public async sendRecapEmails(usersToEmail: [{email: string, civility: string, last_name: string}]){
usersToEmail.forEach(user => {
const to = user.email;
const templateVariables = {
civility: user.civility,
last_name: user.last_name,
link: "http://localhost:3000"
};
const templateName = ETemplates.DOCUMENT_RECAP;
const subject = "Récapitulatif hebdromadaire";
this.mailchimpService.create({
templateName,
to,
subject,
templateVariables,
uid: "",
from: null,
cc: [],
cci: [],
sentAt: null,
nbTrySend: null,
lastTrySendDate: null,
});
});
}
}

View File

@ -1,4 +1,5 @@
export const ETemplates = {
DOCUMENT_ASKED: "DOCUMENT_ASKED",
DOCUMENT_REFUSED: "DOCUMENT_REFUSED",
DOCUMENT_RECAP: "DOCUMENT_RECAP",
};

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

@ -21,6 +21,7 @@ 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);
}

View File

@ -38,6 +38,7 @@ export default class FilesRepository extends BaseRepository {
file_name: file.file_name,
file_path: file.file_path,
mimetype: file.mimetype,
hash: file.hash,
size: file.size,
key: key,
},
@ -57,6 +58,7 @@ export default class FilesRepository extends BaseRepository {
file_name: file.file_name,
file_path: file.file_path,
mimetype: file.mimetype,
hash: file.hash,
size: file.size,
key: key,
},

View File

@ -1,17 +1,22 @@
import "module-alias/register";
import "reflect-metadata";
import { Container } from "typedi";
import { BackendVariables } from "@Common/config/variables/Variables";
import CronService from "@Services/common/CronService/CronService";
(async () => {
console.log("Cron started");
try {
const variables = await Container.get(BackendVariables).validate();
Container.get(CronService).archiveFiles();
await Container.get(CronService).updateUsers();
if(variables.ENV !== "dev"){
Container.get(CronService).sendMails();
Container.get(CronService).sendRecapMails();
}
} catch (e) {
console.error(e);
}

View File

@ -84,11 +84,11 @@ export default class AuthService extends BaseService {
};
}
public generateAccessToken(user: any): string {
return jwt.sign({ ...user }, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
return jwt.sign({ ...user, iat: Math.floor(Date.now() / 1000)}, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
}
public generateRefreshToken(user: any): string {
return jwt.sign({ ...user }, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" });
return jwt.sign({ ...user, iat: Math.floor(Date.now() / 1000)}, this.variables.REFRESH_TOKEN_SECRET, { expiresIn: "1h" });
}
public verifyAccessToken(token: string, callback?: VerifyCallback) {

View File

@ -24,6 +24,21 @@ export default class CronService {
}
}
public async sendRecapMails() {
const cronJob = new CronJob("0 20 * * FRI", async () => { // Every friday at 20:00
try {
await this.mailchimpService.sendRecapEmails();
} catch (e) {
console.error(e);
}
});
// Start job
if (!cronJob.running) {
cronJob.start();
}
}
public async archiveFiles() {
const cronJob = new CronJob("0 0 * * MON", async () => { // Every monday at midnight
try {

View File

@ -15,6 +15,10 @@ export default class CryptoService extends BaseService {
return crypto.createHash("sha256").update(String(key)).digest("base64").slice(0, 32);
}
public async getHash(buffer: Buffer): Promise<string> {
return crypto.createHash("sha256").update(buffer).digest("hex");
}
/**
* @description : encrypt data
* @throws {Error} If data cannot be encrypted

View File

@ -71,12 +71,15 @@ export default class FilesService extends BaseService {
public async create(file: File, fileData: Express.Multer.File) {
const key = v4();
const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key);
const hash = await this.cryptoService.getHash(fileData.buffer);
const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname);
let fileToCreate: File = file;
fileToCreate.file_name = fileData.originalname;
fileToCreate.file_path = this.variables.PINATA_GATEWAY.concat(upload.IpfsHash);
fileToCreate.mimetype = fileData.mimetype;
fileToCreate.size = fileData.size;
fileToCreate.hash = hash;
fileToCreate.archived_at = null;
return this.filesRepository.create(fileToCreate, key);

View File

@ -114,9 +114,11 @@ export default class IdNotService extends BaseService {
code: code,
grant_type: "authorization_code",
});
console.log(query)
const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" });
console.log(token)
const decodedToken = (await token.json()) as IIdNotToken;
console.log(decodedToken)
const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload;
return decodedIdToken;

View File

@ -1,9 +1,10 @@
import EmailRepository from "@Repositories/EmailRepository";
import BaseService from "@Services/BaseService";
import { Emails } from "@prisma/client";
import { Emails, PrismaClient } from "@prisma/client";
import { Service } from "typedi";
import MailchimpClient from "@mailchimp/mailchimp_transactional";
import { BackendVariables } from "@Common/config/variables/Variables";
// import DocumentsService from "@Services/super-admin/DocumentsService/DocumentsService";
@Service()
export default class MailchimpService extends BaseService {
@ -113,4 +114,17 @@ export default class MailchimpService extends BaseService {
};
});
}
public async sendRecapEmails() {
const prisma = new PrismaClient();
const usersToEmail = await prisma.$queryRaw
`SELECT DISTINCT c.email, c.civility, c.last_name
FROM Contacts c
JOIN Users u ON c.uid = u.contact_uid
JOIN office_folders of ON u.office_uid = of.office_uid
JOIN Documents d ON of.uid = d.folder_uid
WHERE d.document_status = 'DEPOSITED';`
console.log(usersToEmail);
}
}

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