Merge branch 'cicd' of https://git.4nkweb.com/4nk/lecoffre-front into cicd
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 3m52s
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 3m52s
This commit is contained in:
commit
1c3afac679
5805
package-lock.json
generated
Normal file
5805
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"le-coffre-resources": "git+ssh://git@git.4nkweb.com/4nk/lecoffre-ressources.git#v2.167",
|
"le-coffre-resources": "git+ssh://git@git.4nkweb.com/4nk/lecoffre-ressources.git#v2.167",
|
||||||
"next": "^14.2.3",
|
"next": "^14.2.3",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
|
|
||||||
import MessageBus from 'src/sdk/MessageBus';
|
import MessageBus from 'src/sdk/MessageBus';
|
||||||
import User from 'src/sdk/User';
|
import User from 'src/sdk/User';
|
||||||
|
import { FileData } from '../../../../front/Api/Entities/types';
|
||||||
|
|
||||||
export default class FileService {
|
export default class FileService {
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export default class FileService {
|
|||||||
|
|
||||||
private constructor() { }
|
private constructor() { }
|
||||||
|
|
||||||
public static createFile(fileData: any, validatorId: string): Promise<any> {
|
public static createFile(fileData: FileData, validatorId: string): Promise<any> {
|
||||||
const ownerId = User.getInstance().getPairingId()!;
|
const ownerId = User.getInstance().getPairingId()!;
|
||||||
|
|
||||||
const processData: any = {
|
const processData: any = {
|
||||||
@ -81,7 +82,7 @@ export default class FileService {
|
|||||||
return this.messageBus.getFileByUid(uid);
|
return this.messageBus.getFileByUid(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static updateFile(process: any, newData: any): Promise<void> {
|
public static updateFile(process: any, newData: Partial<FileData> & { isDeleted?: string }): Promise<void> {
|
||||||
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
|
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
|
||||||
this.messageBus.updateProcess(process.processId, { updated_at: new Date().toISOString(), ...newData }, [], null).then((processUpdated: any) => {
|
this.messageBus.updateProcess(process.processId, { updated_at: new Date().toISOString(), ...newData }, [], null).then((processUpdated: any) => {
|
||||||
const newStateId: string = processUpdated.diffs[0]?.state_id;
|
const newStateId: string = processUpdated.diffs[0]?.state_id;
|
||||||
|
@ -41,7 +41,7 @@ export default class Auth extends BaseApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getIdnotJwt(autorizationCode: string | string[]): Promise<{ accessToken: string; refreshToken: string }> {
|
public async getIdnotJwt(autorizationCode: string | string[]): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
const variables = FrontendVariables.getInstance();
|
// const variables = FrontendVariables.getInstance();
|
||||||
|
|
||||||
// TODO: review
|
// TODO: review
|
||||||
const baseBackUrl = 'http://local.lecoffreio.4nkweb:3001'//variables.BACK_API_PROTOCOL + variables.BACK_API_HOST;
|
const baseBackUrl = 'http://local.lecoffreio.4nkweb:3001'//variables.BACK_API_PROTOCOL + variables.BACK_API_HOST;
|
||||||
|
2
src/front/Api/Entities/index.ts
Normal file
2
src/front/Api/Entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './rule';
|
21
src/front/Api/Entities/types.ts
Normal file
21
src/front/Api/Entities/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* FileBlob interface representing a file's binary data and metadata
|
||||||
|
* Used for file transmission and storage in the application
|
||||||
|
*/
|
||||||
|
export interface FileBlob {
|
||||||
|
/** MIME type of the file (e.g., "application/pdf", "image/jpeg") */
|
||||||
|
type: string;
|
||||||
|
/** Binary data of the file as Uint8Array */
|
||||||
|
data: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileData interface representing a complete file object with blob and metadata
|
||||||
|
* Used when creating or updating files in the system
|
||||||
|
*/
|
||||||
|
export interface FileData {
|
||||||
|
/** The file blob containing type and binary data */
|
||||||
|
file_blob: FileBlob;
|
||||||
|
/** The name of the file */
|
||||||
|
file_name: string;
|
||||||
|
}
|
@ -213,9 +213,9 @@ export default function DragAndDrop(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
{documentFiles.length > 0 && (
|
{documentFiles.length > 0 && (
|
||||||
<div className={classes["documents"]}>
|
<div className={classes["documents"]}>
|
||||||
{documentFiles.map((documentFile) => (
|
{documentFiles.map((documentFile, index) => (
|
||||||
<DocumentFileElement
|
<DocumentFileElement
|
||||||
key={documentFile.id}
|
key={documentFile.uid || `${documentFile.id}-${index}`}
|
||||||
isLoading={documentFile.isLoading}
|
isLoading={documentFile.isLoading}
|
||||||
file={documentFile.file}
|
file={documentFile.file}
|
||||||
onRemove={() => handleRemove(documentFile)}
|
onRemove={() => handleRemove(documentFile)}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
|
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
|
||||||
import Notifications from "@Front/Api/LeCoffreApi/Notary/Notifications/Notifications";
|
|
||||||
import OfficeFolderAnchors from "@Front/Api/LeCoffreApi/Notary/OfficeFolderAnchors/OfficeFolderAnchors";
|
|
||||||
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
|
||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import Toasts from "@Front/Stores/Toasts";
|
import Toasts from "@Front/Stores/Toasts";
|
||||||
@ -29,7 +27,7 @@ export default function Navigation() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
const anchors = [] as any[];
|
// const anchors = [] as any[];
|
||||||
|
|
||||||
/* TODO: review
|
/* TODO: review
|
||||||
try {
|
try {
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import LogoIcon from "@Assets/logo_standard_neutral.svg";
|
import LogoIcon from "@Assets/logo_standard_neutral.svg";
|
||||||
import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
// import Stripe from "@Front/Api/LeCoffreApi/Admin/Stripe/Stripe";
|
||||||
import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
// import Subscriptions from "@Front/Api/LeCoffreApi/Admin/Subscriptions/Subscriptions";
|
||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
// import JwtService from "@Front/Services/JwtService/JwtService";
|
||||||
import { InformationCircleIcon, LifebuoyIcon } from "@heroicons/react/24/outline";
|
import { LifebuoyIcon } from "@heroicons/react/24/outline";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import IconButton from "../IconButton";
|
import IconButton from "../IconButton";
|
||||||
import Typography, { ETypo, ETypoColor } from "../Typography";
|
|
||||||
import BurgerMenu from "./BurgerMenu";
|
import BurgerMenu from "./BurgerMenu";
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
import LogoCielNatureIcon from "./logo-ciel-notaires.jpeg";
|
import LogoCielNatureIcon from "./logo-ciel-notaires.jpeg";
|
||||||
@ -32,7 +31,7 @@ export default function Header(props: IProps) {
|
|||||||
const { pathname } = router;
|
const { pathname } = router;
|
||||||
const isOnCustomerLoginPage = Module.getInstance().get().modules.pages.CustomersLogin.props.path === pathname;
|
const isOnCustomerLoginPage = Module.getInstance().get().modules.pages.CustomersLogin.props.path === pathname;
|
||||||
|
|
||||||
const [cancelAt, setCancelAt] = useState<Date | null>(null);
|
// const [cancelAt, setCancelAt] = useState<Date | null>(null);
|
||||||
|
|
||||||
const loadSubscription = useCallback(async () => {
|
const loadSubscription = useCallback(async () => {
|
||||||
/* TODO: review
|
/* TODO: review
|
||||||
@ -86,7 +85,7 @@ export default function Header(props: IProps) {
|
|||||||
)}
|
)}
|
||||||
{isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>}
|
{isOnCustomerLoginPage && <Image width={70} height={70} alt="ciel-nature" src={LogoCielNatureIcon}></Image>}
|
||||||
</div>
|
</div>
|
||||||
{cancelAt && (
|
{/* {cancelAt && (
|
||||||
<div className={classes["subscription-line"]}>
|
<div className={classes["subscription-line"]}>
|
||||||
<InformationCircleIcon height="24" />
|
<InformationCircleIcon height="24" />
|
||||||
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}>
|
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_GENERIC_BLACK}>
|
||||||
@ -94,7 +93,7 @@ export default function Header(props: IProps) {
|
|||||||
{cancelAt.toLocaleDateString()}.
|
{cancelAt.toLocaleDateString()}.
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export default function Rules(props: IProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isShowing, setIsShowing] = React.useState(false);
|
const [isShowing, setIsShowing] = React.useState(false);
|
||||||
const [hasJwt, setHasJwt] = React.useState(false);
|
// const [hasJwt, setHasJwt] = React.useState(false);
|
||||||
|
|
||||||
const getShowValue = useCallback(() => {
|
const getShowValue = useCallback(() => {
|
||||||
if (props.mode === RulesMode.NECESSARY) {
|
if (props.mode === RulesMode.NECESSARY) {
|
||||||
@ -32,7 +32,7 @@ export default function Rules(props: IProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: review
|
// TODO: review
|
||||||
//if (!JwtService.getInstance().decodeJwt()) return;
|
//if (!JwtService.getInstance().decodeJwt()) return;
|
||||||
setHasJwt(true);
|
// setHasJwt(true);
|
||||||
setIsShowing(getShowValue());
|
setIsShowing(getShowValue());
|
||||||
}, [getShowValue, isShowing]);
|
}, [getShowValue, isShowing]);
|
||||||
|
|
||||||
|
@ -5,8 +5,8 @@ import Module from "@Front/Config/Module";
|
|||||||
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
||||||
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
||||||
import User from "le-coffre-resources/dist/Notary";
|
import User from "le-coffre-resources/dist/Notary";
|
||||||
import JwtService from "@Front/Services/JwtService/JwtService";
|
// import JwtService from "@Front/Services/JwtService/JwtService";
|
||||||
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/Admin/Users/Users";
|
// import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/Admin/Users/Users";
|
||||||
|
|
||||||
type IProps = IPropsDashboardWithList;
|
type IProps = IPropsDashboardWithList;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
||||||
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
||||||
import Offices from "@Front/Api/LeCoffreApi/SuperAdmin/Offices/Offices";
|
// import Offices from "@Front/Api/LeCoffreApi/SuperAdmin/Offices/Offices";
|
||||||
|
|
||||||
type IProps = IPropsDashboardWithList;
|
type IProps = IPropsDashboardWithList;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import Module from "@Front/Config/Module";
|
|||||||
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
import { IBlock } from "@Front/Components/DesignSystem/SearchBlockList/BlockList/Block";
|
||||||
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
import DefaultDashboardWithList, { IPropsDashboardWithList } from "../DefaultDashboardWithList";
|
||||||
import { OfficeRole } from "le-coffre-resources/dist/Notary";
|
import { OfficeRole } from "le-coffre-resources/dist/Notary";
|
||||||
import OfficeRoles, { IGetRolesParams } from "@Front/Api/LeCoffreApi/Admin/OfficeRoles/OfficeRoles";
|
// import OfficeRoles, { IGetRolesParams } from "@Front/Api/LeCoffreApi/Admin/OfficeRoles/OfficeRoles";
|
||||||
|
|
||||||
import RoleService from "src/common/Api/LeCoffreApi/sdk/RoleService";
|
import RoleService from "src/common/Api/LeCoffreApi/sdk/RoleService";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
|
// import Users, { IGetUsersparams } from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
|
||||||
import User from "le-coffre-resources/dist/SuperAdmin";
|
import User from "le-coffre-resources/dist/SuperAdmin";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty
|
|||||||
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
|
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
|
||||||
import Customer from "le-coffre-resources/dist/Customer";
|
import Customer from "le-coffre-resources/dist/Customer";
|
||||||
import { OfficeFolder as OfficeFolderNotary } from "le-coffre-resources/dist/Notary";
|
import { OfficeFolder as OfficeFolderNotary } from "le-coffre-resources/dist/Notary";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
import OfficeRib from "@Front/Api/LeCoffreApi/Customer/OfficeRib/OfficeRib";
|
// import OfficeRib from "@Front/Api/LeCoffreApi/Customer/OfficeRib/OfficeRib";
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
folder: OfficeFolderNotary;
|
folder: OfficeFolderNotary;
|
||||||
@ -16,7 +16,8 @@ type IProps = {
|
|||||||
export default function ContactBox(props: IProps) {
|
export default function ContactBox(props: IProps) {
|
||||||
const { folder, customer } = props;
|
const { folder, customer } = props;
|
||||||
|
|
||||||
const [ribUrl, setRibUrl] = useState<string | null>(null);
|
// const [ribUrl, setRibUrl] = useState<string | null>(null);
|
||||||
|
const ribUrl = null;
|
||||||
|
|
||||||
// TODO: review
|
// TODO: review
|
||||||
const stakeholder = {
|
const stakeholder = {
|
||||||
|
@ -9,6 +9,8 @@ import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm";
|
|||||||
|
|
||||||
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
||||||
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
||||||
|
import { FileBlob, FileData } from "@Front/Api/Entities/types";
|
||||||
|
import WatermarkService from "@Front/Services/WatermarkService";
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
document: any;
|
document: any;
|
||||||
@ -30,7 +32,11 @@ export default function DepositDocumentComponent(props: IProps) {
|
|||||||
}, [document.files]);
|
}, [document.files]);
|
||||||
|
|
||||||
const addFile = useCallback(
|
const addFile = useCallback(
|
||||||
(file: File) => {
|
async (file: File) => {
|
||||||
|
try {
|
||||||
|
// Add watermark to the file before processing
|
||||||
|
const watermarkedFile = await WatermarkService.getInstance().addWatermark(file);
|
||||||
|
|
||||||
return new Promise<void>(
|
return new Promise<void>(
|
||||||
(resolve: () => void) => {
|
(resolve: () => void) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@ -39,12 +45,58 @@ export default function DepositDocumentComponent(props: IProps) {
|
|||||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||||
const uint8Array = new Uint8Array(arrayBuffer);
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
const fileBlob: any = {
|
const fileBlob: FileBlob = {
|
||||||
|
type: watermarkedFile.type,
|
||||||
|
data: uint8Array
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileData: FileData = {
|
||||||
|
file_blob: fileBlob,
|
||||||
|
file_name: watermarkedFile.name
|
||||||
|
};
|
||||||
|
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
|
||||||
|
|
||||||
|
FileService.createFile(fileData, validatorId).then((processCreated: any) => {
|
||||||
|
const fileUid: string = processCreated.processData.uid;
|
||||||
|
|
||||||
|
DocumentService.getDocumentByUid(document.uid!).then((process: any) => {
|
||||||
|
if (process) {
|
||||||
|
const document: any = process.processData;
|
||||||
|
|
||||||
|
let files: any[] = document.files;
|
||||||
|
if (!files) {
|
||||||
|
files = [];
|
||||||
|
}
|
||||||
|
files.push({ uid: fileUid });
|
||||||
|
|
||||||
|
DocumentService.updateDocument(process, { files: files, document_status: EDocumentStatus.DEPOSITED }).then(() => resolve());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(watermarkedFile);
|
||||||
|
})
|
||||||
|
.then(onChange)
|
||||||
|
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Fichier uploadé avec succès!" }))
|
||||||
|
.catch((error) => ToasterService.getInstance().error({ title: "Erreur !", description: error.message }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing file with watermark:', error);
|
||||||
|
// If watermarking fails, proceed with original file
|
||||||
|
return new Promise<void>(
|
||||||
|
(resolve: () => void) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (event.target?.result) {
|
||||||
|
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
const fileBlob: FileBlob = {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
data: uint8Array
|
data: uint8Array
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileData: any = {
|
const fileData: FileData = {
|
||||||
file_blob: fileBlob,
|
file_blob: fileBlob,
|
||||||
file_name: file.name
|
file_name: file.name
|
||||||
};
|
};
|
||||||
@ -74,6 +126,7 @@ export default function DepositDocumentComponent(props: IProps) {
|
|||||||
.then(onChange)
|
.then(onChange)
|
||||||
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Fichier uploadé avec succès!" }))
|
.then(() => ToasterService.getInstance().success({ title: "Succès !", description: "Fichier uploadé avec succès!" }))
|
||||||
.catch((error) => ToasterService.getInstance().error({ title: "Erreur !", description: error.message }));
|
.catch((error) => ToasterService.getInstance().error({ title: "Erreur !", description: error.message }));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[document.uid, onChange],
|
[document.uid, onChange],
|
||||||
);
|
);
|
||||||
@ -82,9 +135,9 @@ export default function DepositDocumentComponent(props: IProps) {
|
|||||||
(fileUid: string) => {
|
(fileUid: string) => {
|
||||||
return new Promise<void>(
|
return new Promise<void>(
|
||||||
(resolve: () => void) => {
|
(resolve: () => void) => {
|
||||||
FileService.getFileByUid(fileUid).then((process: any) => {
|
FileService.getFileByUid(fileUid).then((res: any) => {
|
||||||
if (process) {
|
if (res) {
|
||||||
FileService.updateFile(process, { isDeleted: 'true' }).then(() => {
|
FileService.updateFile(res.processId, { isDeleted: 'true' }).then(() => {
|
||||||
DocumentService.getDocumentByUid(document.uid!).then((process: any) => {
|
DocumentService.getDocumentByUid(document.uid!).then((process: any) => {
|
||||||
if (process) {
|
if (process) {
|
||||||
const document: any = process.processData;
|
const document: any = process.processData;
|
||||||
|
@ -6,7 +6,7 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty
|
|||||||
import BackArrow from "@Front/Components/Elements/BackArrow";
|
import BackArrow from "@Front/Components/Elements/BackArrow";
|
||||||
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
|
||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import JwtService, { ICustomerJwtPayload } from "@Front/Services/JwtService/JwtService";
|
// import JwtService, { ICustomerJwtPayload } from "@Front/Services/JwtService/JwtService";
|
||||||
import { ArrowDownTrayIcon, EyeIcon } from "@heroicons/react/24/outline";
|
import { ArrowDownTrayIcon, EyeIcon } from "@heroicons/react/24/outline";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
@ -44,10 +44,10 @@ export default function ReceivedDocuments() {
|
|||||||
const [customer, setCustomer] = useState<Customer | null>(null);
|
const [customer, setCustomer] = useState<Customer | null>(null);
|
||||||
|
|
||||||
const fetchFolderAndCustomer = useCallback(async () => {
|
const fetchFolderAndCustomer = useCallback(async () => {
|
||||||
let jwt: ICustomerJwtPayload | undefined;
|
// let jwt: ICustomerJwtPayload | undefined;
|
||||||
if (typeof document !== "undefined") {
|
// if (typeof document !== "undefined") {
|
||||||
jwt = JwtService.getInstance().decodeCustomerJwt();
|
// jwt = JwtService.getInstance().decodeCustomerJwt();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO: review
|
// TODO: review
|
||||||
LoaderService.getInstance().show();
|
LoaderService.getInstance().show();
|
||||||
|
@ -7,6 +7,7 @@ import { DocumentNotary } from "le-coffre-resources/dist/Notary";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { NextRouter, useRouter } from "next/router";
|
import { NextRouter, useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { FileBlob } from "@Front/Api/Entities/types";
|
||||||
|
|
||||||
import BasePage from "../../Base";
|
import BasePage from "../../Base";
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
@ -28,7 +29,7 @@ type IState = {
|
|||||||
isValidateModalVisible: boolean;
|
isValidateModalVisible: boolean;
|
||||||
refuseText: string;
|
refuseText: string;
|
||||||
selectedFileIndex: number;
|
selectedFileIndex: number;
|
||||||
selectedFile: any;
|
selectedFile: { uid: string; file_name: string; file_blob: FileBlob } | null;
|
||||||
documentNotary: DocumentNotary | null;
|
documentNotary: DocumentNotary | null;
|
||||||
fileBlob: Blob | null;
|
fileBlob: Blob | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -150,7 +151,7 @@ class ViewDocumentsNotaryClass extends BasePage<IPropsClass, IState> {
|
|||||||
{
|
{
|
||||||
documentNotary,
|
documentNotary,
|
||||||
selectedFileIndex: 0,
|
selectedFileIndex: 0,
|
||||||
selectedFile: documentNotary.files![0]!,
|
selectedFile: documentNotary.files![0] as any,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
|
@ -18,6 +18,7 @@ import backgroundImage from "@Assets/images/background_refonte.svg";
|
|||||||
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
|
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
|
||||||
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
||||||
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
|
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
|
||||||
|
import { DocumentData } from "../FolderInformation/ClientView/DocumentTables/types";
|
||||||
|
|
||||||
export default function AskDocuments() {
|
export default function AskDocuments() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -62,17 +63,21 @@ export default function AskDocuments() {
|
|||||||
LoaderService.getInstance().show();
|
LoaderService.getInstance().show();
|
||||||
const documentAsked: [] = values["document_types"] as [];
|
const documentAsked: [] = values["document_types"] as [];
|
||||||
for (let i = 0; i < documentAsked.length; i++) {
|
for (let i = 0; i < documentAsked.length; i++) {
|
||||||
|
const documentTypeUid = documentAsked[i];
|
||||||
|
if (!documentTypeUid) continue;
|
||||||
|
|
||||||
const documentData: any = {
|
const documentData: any = {
|
||||||
folder: {
|
folder: {
|
||||||
uid: folderUid,
|
uid: folderUid as string,
|
||||||
},
|
},
|
||||||
depositor: {
|
depositor: {
|
||||||
uid: customerUid,
|
uid: customerUid as string,
|
||||||
},
|
},
|
||||||
document_type: {
|
document_type: {
|
||||||
uid: documentAsked[i]
|
uid: documentTypeUid
|
||||||
},
|
},
|
||||||
document_status: EDocumentStatus.ASKED
|
document_status: EDocumentStatus.ASKED,
|
||||||
|
file_uid: null,
|
||||||
};
|
};
|
||||||
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
|
const validatorId: string = '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0';
|
||||||
|
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
@import "@Themes/constants.scss";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2xl, 40px);
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--spacing-xl, 32px);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md, 16px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2xl, 40px);
|
||||||
|
|
||||||
|
.drag-drop-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl, 32px);
|
||||||
|
|
||||||
|
@media (min-width: $screen-m) {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-lg, 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-box {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
|
||||||
|
// Force the DragAndDrop component to take full width and height
|
||||||
|
> div {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
|
||||||
|
// Override the fit-content width from DragAndDrop
|
||||||
|
&.root {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
padding: var(--spacing-sm, 8px) var(--spacing-md, 16px);
|
||||||
|
background-color: var(--color-success-50, #f0f9ff);
|
||||||
|
border: 1px solid var(--color-success-200, #bae6fd);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
margin-top: var(--spacing-sm, 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-md, 16px);
|
||||||
|
background-color: var(--color-warning-50, #fffbeb);
|
||||||
|
border: 1px solid var(--color-warning-200, #fde68a);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-result {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg, 24px);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
border: 2px solid;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md, 16px);
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: var(--color-success-50, #f0fdf4);
|
||||||
|
border-color: var(--color-success-200, #bbf7d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: var(--color-error-50, #fef2f2);
|
||||||
|
border-color: var(--color-error-200, #fecaca);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-details {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: var(--spacing-md, 16px);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-line;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merkle-proof-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md, 16px);
|
||||||
|
padding: var(--spacing-lg, 24px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm, 8px);
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px solid var(--color-neutral-200, #e5e7eb);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
padding: var(--spacing-sm, 8px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-description {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px dashed var(--color-neutral-300, #d1d5db);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
background-color: var(--color-neutral-50, #f9fafb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border: 2px solid var(--color-error-200, #fecaca);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
background-color: var(--color-error-50, #fef2f2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-lg, 24px);
|
||||||
|
|
||||||
|
@media (max-width: $screen-s) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md, 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
import Button, { EButtonSize, EButtonstyletype, EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||||
|
import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop";
|
||||||
|
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||||
|
import Module from "@Front/Config/Module";
|
||||||
|
import PdfService from "@Front/Services/PdfService";
|
||||||
|
import { FileBlob } from "@Front/Api/Entities/types";
|
||||||
|
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import MessageBus from "src/sdk/MessageBus";
|
||||||
|
|
||||||
|
import classes from "./classes.module.scss";
|
||||||
|
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
||||||
|
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
folderUid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a File object to FileBlob
|
||||||
|
* @param file - The File object to convert
|
||||||
|
* @returns Promise<FileBlob> - The converted FileBlob
|
||||||
|
*/
|
||||||
|
const convertFileToFileBlob = async (file: File): Promise<FileBlob> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const arrayBuffer = reader.result as ArrayBuffer;
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
resolve({
|
||||||
|
type: file.type,
|
||||||
|
data: uint8Array
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function DocumentVerification(props: IProps) {
|
||||||
|
const { folderUid } = props;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [documentToVerify, setDocumentToVerify] = useState<File | null>(null);
|
||||||
|
const [validationCertificate, setValidationCertificate] = useState<File | null>(null);
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false);
|
||||||
|
const [verificationResult, setVerificationResult] = useState<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
details?: string;
|
||||||
|
merkleProof?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleDocumentToVerifyChange = (files: File[]) => {
|
||||||
|
if (files.length > 0 && files[0]) {
|
||||||
|
setDocumentToVerify(files[0]);
|
||||||
|
} else {
|
||||||
|
setDocumentToVerify(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidationCertificateChange = (files: File[]) => {
|
||||||
|
if (files.length > 0 && files[0]) {
|
||||||
|
setValidationCertificate(files[0]);
|
||||||
|
} else {
|
||||||
|
setValidationCertificate(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyDocuments = async () => {
|
||||||
|
if (!documentToVerify || !validationCertificate) {
|
||||||
|
console.error("Both documents are required for verification");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsVerifying(true);
|
||||||
|
setVerificationResult(null);
|
||||||
|
|
||||||
|
const messageBus = MessageBus.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Here the things we need to verify:
|
||||||
|
// - we can produce the same hash from the document provided than what is in the validation certificate
|
||||||
|
// - the merkle proof is valid with that hash
|
||||||
|
// - the root of the merkle tree is a state id from a commited state in the process
|
||||||
|
// - that process is a file process linked to the right folder
|
||||||
|
// Step 1: Parse the validation certificate
|
||||||
|
const validationData = await PdfService.getInstance().parseCertificate(validationCertificate);
|
||||||
|
|
||||||
|
// Step 2: Convert File to FileBlob and hash the document using MessageBus
|
||||||
|
const fileBlob = await convertFileToFileBlob(documentToVerify);
|
||||||
|
|
||||||
|
await messageBus.isReady();
|
||||||
|
const documentHash = await messageBus.hashDocument(fileBlob, validationData.commitmentId);
|
||||||
|
|
||||||
|
// Step 3: Compare hashes
|
||||||
|
const hashesMatch = documentHash.toLowerCase() === validationData.documentHash.toLowerCase();
|
||||||
|
|
||||||
|
if (!hashesMatch) {
|
||||||
|
throw new Error('Hash du document invalide, le document fourni n\'est pas celui qui a été certifié');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Verify the merkle proof
|
||||||
|
const merkleProof = validationData.merkleProof;
|
||||||
|
const merkleProofValid = await messageBus.verifyMerkleProof(merkleProof, documentHash);
|
||||||
|
|
||||||
|
if (!merkleProofValid) {
|
||||||
|
throw new Error('Preuve de Merkle invalide, le document n\'a pas été certifié là où le certificat le prétend');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Verify that this file process depends on the right folder process
|
||||||
|
// First pin all the validated documents related to the folder
|
||||||
|
const documentProcesses = await DocumentService.getDocuments();
|
||||||
|
|
||||||
|
const documents = documentProcesses.filter((process: any) =>
|
||||||
|
process.processData.document_status === "VALIDATED" &&
|
||||||
|
process.processData.folder.uid === folderUid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!documents || documents.length === 0) {
|
||||||
|
throw new Error(`Aucune demande de document trouvé pour le dossier ${folderUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: verify that the merkle proof match the last commited state for the file process
|
||||||
|
const stateId = JSON.parse(validationData.merkleProof)['root'];
|
||||||
|
|
||||||
|
let stateIdExists = false;
|
||||||
|
for (const doc of documents) {
|
||||||
|
const processData = doc.processData;
|
||||||
|
|
||||||
|
for (const file of processData.files) {
|
||||||
|
const fileUid = file.uid;
|
||||||
|
const fileProcess = await FileService.getFileByUid(fileUid);
|
||||||
|
const lastUpdatedStateId = fileProcess.lastUpdatedFileState.state_id;
|
||||||
|
|
||||||
|
stateIdExists = lastUpdatedStateId === stateId; // we assume that last state is the validated document, that seems reasonable
|
||||||
|
if (stateIdExists) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateIdExists) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stateIdExists) {
|
||||||
|
throw new Error('La preuve fournie ne correspond à aucun document demandé pour ce dossier.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerificationResult({
|
||||||
|
success: true,
|
||||||
|
message: "✅ Vérification réussie ! Le document est authentique et intègre.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Verification failed:", error);
|
||||||
|
setVerificationResult({
|
||||||
|
success: false,
|
||||||
|
message: `❌ Erreur lors de la vérification: ${error}`,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsVerifying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToFolder = () => {
|
||||||
|
const folderPath = Module.getInstance()
|
||||||
|
.get()
|
||||||
|
.modules.pages.Folder.pages.FolderInformation.props.path.replace("[folderUid]", folderUid);
|
||||||
|
router.push(folderPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bothDocumentsPresent = documentToVerify && validationCertificate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes["root"]}>
|
||||||
|
<div className={classes["header"]}>
|
||||||
|
<Typography typo={ETypo.TITLE_H2} color={ETypoColor.TEXT_PRIMARY}>
|
||||||
|
Vérification de Documents
|
||||||
|
</Typography>
|
||||||
|
<Typography typo={ETypo.TEXT_LG_REGULAR} color={ETypoColor.TEXT_SECONDARY}>
|
||||||
|
Vérifiez l'intégrité et l'authenticité de vos documents
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes["content"]}>
|
||||||
|
<div className={classes["drag-drop-container"]}>
|
||||||
|
<div className={classes["drag-drop-box"]}>
|
||||||
|
<DragAndDrop
|
||||||
|
title="Document à valider"
|
||||||
|
description="Glissez-déposez ou cliquez pour sélectionner le document que vous souhaitez vérifier"
|
||||||
|
onChange={handleDocumentToVerifyChange}
|
||||||
|
/>
|
||||||
|
{documentToVerify && (
|
||||||
|
<div className={classes["file-info"]}>
|
||||||
|
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_SUCCESS_500}>
|
||||||
|
✓ {documentToVerify.name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes["drag-drop-box"]}>
|
||||||
|
<DragAndDrop
|
||||||
|
title="Certificat de validation"
|
||||||
|
description="Glissez-déposez ou cliquez pour sélectionner le certificat de validation correspondant"
|
||||||
|
onChange={handleValidationCertificateChange}
|
||||||
|
/>
|
||||||
|
{validationCertificate && (
|
||||||
|
<div className={classes["file-info"]}>
|
||||||
|
<Typography typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_SUCCESS_500}>
|
||||||
|
✓ {validationCertificate.name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!bothDocumentsPresent && (
|
||||||
|
<div className={classes["warning"]}>
|
||||||
|
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_WARNING_500}>
|
||||||
|
⚠️ Veuillez sélectionner les deux documents pour procéder à la vérification
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verificationResult && (
|
||||||
|
<div className={`${classes["verification-result"]} ${classes[verificationResult.success ? "success" : "error"]}`}>
|
||||||
|
<Typography typo={ETypo.TEXT_LG_REGULAR} color={verificationResult.success ? ETypoColor.COLOR_SUCCESS_500 : ETypoColor.COLOR_ERROR_500}>
|
||||||
|
{verificationResult.message}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={classes["actions"]}>
|
||||||
|
<Button
|
||||||
|
variant={EButtonVariant.SECONDARY}
|
||||||
|
styletype={EButtonstyletype.TEXT}
|
||||||
|
size={EButtonSize.LG}
|
||||||
|
onClick={handleBackToFolder}
|
||||||
|
disabled={isVerifying}
|
||||||
|
>
|
||||||
|
Retour au dossier
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={EButtonVariant.PRIMARY}
|
||||||
|
styletype={EButtonstyletype.CONTAINED}
|
||||||
|
size={EButtonSize.LG}
|
||||||
|
onClick={handleVerifyDocuments}
|
||||||
|
rightIcon={<ShieldCheckIcon />}
|
||||||
|
disabled={!bothDocumentsPresent || isVerifying}
|
||||||
|
isLoading={isVerifying}
|
||||||
|
>
|
||||||
|
{isVerifying ? "Vérification en cours..." : "Vérifier les documents"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import Modal from "@Front/Components/DesignSystem/Modal";
|
import Modal from "@Front/Components/DesignSystem/Modal";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { FileBlob } from "@Front/Api/Entities/types";
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
file: any;
|
file: {
|
||||||
|
uid: string;
|
||||||
|
file_blob: FileBlob;
|
||||||
|
};
|
||||||
url: string;
|
url: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
@ -6,7 +6,7 @@ import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag"
|
|||||||
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import useOpenable from "@Front/Hooks/useOpenable";
|
import useOpenable from "@Front/Hooks/useOpenable";
|
||||||
import { ArrowDownTrayIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { ArrowDownTrayIcon, EyeIcon, TrashIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||||
import { useMediaQuery } from "@mui/material";
|
import { useMediaQuery } from "@mui/material";
|
||||||
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
|
import { EDocumentStatus } from "le-coffre-resources/dist/Customer/Document";
|
||||||
import DocumentNotary, { EDocumentNotaryStatus } from "le-coffre-resources/dist/Notary/DocumentNotary";
|
import DocumentNotary, { EDocumentNotaryStatus } from "le-coffre-resources/dist/Notary/DocumentNotary";
|
||||||
@ -22,6 +22,9 @@ import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
|||||||
import DocumentTypeService from "src/common/Api/LeCoffreApi/sdk/DocumentTypeService";
|
import DocumentTypeService from "src/common/Api/LeCoffreApi/sdk/DocumentTypeService";
|
||||||
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
||||||
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
|
import LoaderService from "src/common/Api/LeCoffreApi/sdk/Loader/LoaderService";
|
||||||
|
import FolderService from "src/common/Api/LeCoffreApi/sdk/FolderService";
|
||||||
|
import PdfService, { CertificateData, Metadata } from "@Front/Services/PdfService";
|
||||||
|
import MessageBus from "src/sdk/MessageBus";
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
customerUid: string;
|
customerUid: string;
|
||||||
@ -61,6 +64,12 @@ export default function DocumentTables(props: IProps) {
|
|||||||
// FilterBy folder.uid & depositor.uid
|
// FilterBy folder.uid & depositor.uid
|
||||||
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor && document.depositor.uid === customerUid);
|
documents = documents.filter((document: any) => document.folder.uid === folderUid && document.depositor && document.depositor.uid === customerUid);
|
||||||
|
|
||||||
|
console.log('[DocumentTables] fetchDocuments: all documents for this folder/customer:', documents.map(doc => ({
|
||||||
|
uid: doc.uid,
|
||||||
|
status: doc.document_status,
|
||||||
|
type: doc.document_type?.name
|
||||||
|
})));
|
||||||
|
|
||||||
for (const document of documents) {
|
for (const document of documents) {
|
||||||
if (document.document_type) {
|
if (document.document_type) {
|
||||||
document.document_type = (await DocumentTypeService.getDocumentTypeByUid(document.document_type.uid)).processData;
|
document.document_type = (await DocumentTypeService.getDocumentTypeByUid(document.document_type.uid)).processData;
|
||||||
@ -176,6 +185,71 @@ export default function DocumentTables(props: IProps) {
|
|||||||
}).catch((e) => console.warn(e));
|
}).catch((e) => console.warn(e));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDownloadCertificate = useCallback(async (doc: any) => {
|
||||||
|
try {
|
||||||
|
console.log('[DocumentTables] onDownloadCertificate: doc', doc);
|
||||||
|
const certificateData: CertificateData = {
|
||||||
|
customer: {
|
||||||
|
firstName: doc.depositor.first_name || doc.depositor.firstName || "N/A",
|
||||||
|
lastName: doc.depositor.last_name || doc.depositor.lastName || "N/A",
|
||||||
|
postalAddress: doc.depositor.postal_address || doc.depositor.address || doc.depositor.postalAddress || "N/A",
|
||||||
|
email: doc.depositor.email || "N/A"
|
||||||
|
},
|
||||||
|
notary: {
|
||||||
|
name: "N/A"
|
||||||
|
},
|
||||||
|
folderUid: folderUid,
|
||||||
|
documentHash: "N/A",
|
||||||
|
metadata: {
|
||||||
|
fileName: "N/A",
|
||||||
|
isDeleted: false,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
commitmentId: "N/A",
|
||||||
|
createdAt: new Date(),
|
||||||
|
documentUid: "N/A",
|
||||||
|
documentType: "N/A",
|
||||||
|
merkleProof: "N/A"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the specific document that was clicked
|
||||||
|
const documentProcess = await DocumentService.getDocumentByUid(doc.uid);
|
||||||
|
if (!documentProcess) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process only the files for this specific document
|
||||||
|
for (const file of documentProcess.processData.files) {
|
||||||
|
const fileProcess = await FileService.getFileByUid(file.uid);
|
||||||
|
console.log('[DocumentTables] onDownloadCertificate: fileProcess', fileProcess);
|
||||||
|
|
||||||
|
const hash = fileProcess.lastUpdatedFileState.pcd_commitment.file_blob;
|
||||||
|
certificateData.documentHash = hash;
|
||||||
|
|
||||||
|
const proof = await MessageBus.getInstance().generateMerkleProof(fileProcess.lastUpdatedFileState, 'file_blob');
|
||||||
|
console.log('[DocumentTables] onDownloadCertificate: proof', proof);
|
||||||
|
|
||||||
|
const metadata: Metadata = {
|
||||||
|
fileName: fileProcess.processData.file_name,
|
||||||
|
isDeleted: false,
|
||||||
|
updatedAt: new Date(fileProcess.processData.updated_at),
|
||||||
|
commitmentId: fileProcess.lastUpdatedFileState.commited_in,
|
||||||
|
createdAt: new Date(fileProcess.processData.created_at),
|
||||||
|
documentUid: doc.uid,
|
||||||
|
documentType: doc.document_type.name,
|
||||||
|
merkleProof: proof
|
||||||
|
};
|
||||||
|
certificateData.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DocumentTables] onDownloadCertificate: certificateData', certificateData);
|
||||||
|
|
||||||
|
await PdfService.getInstance().downloadCertificate(certificateData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading certificate:', error);
|
||||||
|
}
|
||||||
|
}, [folderUid, customerUid]);
|
||||||
|
|
||||||
const askedDocuments: IRowProps[] = useMemo(
|
const askedDocuments: IRowProps[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
documents
|
documents
|
||||||
@ -263,8 +337,8 @@ export default function DocumentTables(props: IProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validatedDocuments: IRowProps[] = useMemo(
|
const validatedDocuments: IRowProps[] = useMemo(
|
||||||
() =>
|
() => {
|
||||||
documents
|
const validated = documents
|
||||||
.map((document) => {
|
.map((document) => {
|
||||||
if (document.document_status !== EDocumentStatus.VALIDATED) return null;
|
if (document.document_status !== EDocumentStatus.VALIDATED) return null;
|
||||||
return {
|
return {
|
||||||
@ -300,13 +374,17 @@ export default function DocumentTables(props: IProps) {
|
|||||||
<IconButton icon={<EyeIcon />} />
|
<IconButton icon={<EyeIcon />} />
|
||||||
</Link>
|
</Link>
|
||||||
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
|
<IconButton onClick={() => onDownload(document)} icon={<ArrowDownTrayIcon />} />
|
||||||
|
<IconButton onClick={() => onDownloadCertificate(document)} icon={<DocumentTextIcon />} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((document) => document !== null) as IRowProps[],
|
.filter((document) => document !== null) as IRowProps[];
|
||||||
[documents, folderUid, onDownload],
|
|
||||||
|
return validated;
|
||||||
|
},
|
||||||
|
[documents, folderUid, onDownload, onDownloadCertificate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const refusedDocuments: IRowProps[] = useMemo(
|
const refusedDocuments: IRowProps[] = useMemo(
|
||||||
|
@ -5,11 +5,12 @@ import { IItem } from "@Front/Components/DesignSystem/Menu/MenuItem";
|
|||||||
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
|
import Tag, { ETagColor } from "@Front/Components/DesignSystem/Tag";
|
||||||
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
|
||||||
import Module from "@Front/Config/Module";
|
import Module from "@Front/Config/Module";
|
||||||
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PaperAirplaneIcon, PencilSquareIcon, UsersIcon } from "@heroicons/react/24/outline";
|
import { ArchiveBoxIcon, EllipsisHorizontalIcon, PaperAirplaneIcon, PencilSquareIcon, ShieldCheckIcon, UsersIcon } from "@heroicons/react/24/outline";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
|
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { AnchorStatus } from "..";
|
import { AnchorStatus } from "..";
|
||||||
import classes from "./classes.module.scss";
|
import classes from "./classes.module.scss";
|
||||||
@ -24,6 +25,7 @@ type IProps = {
|
|||||||
|
|
||||||
export default function InformationSection(props: IProps) {
|
export default function InformationSection(props: IProps) {
|
||||||
const { folder, progress, onArchive, anchorStatus, isArchived } = props;
|
const { folder, progress, onArchive, anchorStatus, isArchived } = props;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const menuItemsDekstop = useMemo(() => {
|
const menuItemsDekstop = useMemo(() => {
|
||||||
let elements: IItem[] = [];
|
let elements: IItem[] = [];
|
||||||
@ -43,6 +45,17 @@ export default function InformationSection(props: IProps) {
|
|||||||
link: Module.getInstance()
|
link: Module.getInstance()
|
||||||
.get()
|
.get()
|
||||||
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""),
|
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""),
|
||||||
|
hasSeparator: true,
|
||||||
|
};
|
||||||
|
const verifyDocumentElement = {
|
||||||
|
icon: <ShieldCheckIcon />,
|
||||||
|
text: "Vérifier le document",
|
||||||
|
onClick: () => {
|
||||||
|
const verifyPath = Module.getInstance()
|
||||||
|
.get()
|
||||||
|
.modules.pages.Folder.pages.VerifyDocuments.props.path.replace("[folderUid]", folder?.uid ?? "");
|
||||||
|
router.push(verifyPath);
|
||||||
|
},
|
||||||
hasSeparator: false,
|
hasSeparator: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,8 +65,11 @@ export default function InformationSection(props: IProps) {
|
|||||||
elements.push(modifyInformationsElement);
|
elements.push(modifyInformationsElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add verify document option
|
||||||
|
elements.push(verifyDocumentElement);
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
}, [anchorStatus, folder?.uid]);
|
}, [anchorStatus, folder?.uid, router]);
|
||||||
|
|
||||||
const menuItemsMobile = useMemo(() => {
|
const menuItemsMobile = useMemo(() => {
|
||||||
let elements: IItem[] = [];
|
let elements: IItem[] = [];
|
||||||
@ -75,6 +91,17 @@ export default function InformationSection(props: IProps) {
|
|||||||
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""),
|
.modules.pages.Folder.pages.EditInformations.props.path.replace("[folderUid]", folder?.uid ?? ""),
|
||||||
hasSeparator: true,
|
hasSeparator: true,
|
||||||
};
|
};
|
||||||
|
const verifyDocumentElement = {
|
||||||
|
icon: <ShieldCheckIcon />,
|
||||||
|
text: "Vérifier le document",
|
||||||
|
onClick: () => {
|
||||||
|
const verifyPath = Module.getInstance()
|
||||||
|
.get()
|
||||||
|
.modules.pages.Folder.pages.VerifyDocuments.props.path.replace("[folderUid]", folder?.uid ?? "");
|
||||||
|
router.push(verifyPath);
|
||||||
|
},
|
||||||
|
hasSeparator: true,
|
||||||
|
};
|
||||||
|
|
||||||
// If the folder is not anchored, we can modify the collaborators and the informations
|
// If the folder is not anchored, we can modify the collaborators and the informations
|
||||||
if (anchorStatus === AnchorStatus.NOT_ANCHORED) {
|
if (anchorStatus === AnchorStatus.NOT_ANCHORED) {
|
||||||
@ -82,6 +109,9 @@ export default function InformationSection(props: IProps) {
|
|||||||
elements.push(modifyInformationsElement);
|
elements.push(modifyInformationsElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add verify document option
|
||||||
|
elements.push(verifyDocumentElement);
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
icon: <PaperAirplaneIcon />,
|
icon: <PaperAirplaneIcon />,
|
||||||
text: "Envoyer des documents",
|
text: "Envoyer des documents",
|
||||||
@ -101,7 +131,7 @@ export default function InformationSection(props: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
}, [anchorStatus, folder?.uid, isArchived, onArchive]);
|
}, [anchorStatus, folder?.uid, isArchived, onArchive, router]);
|
||||||
return (
|
return (
|
||||||
<section className={classes["root"]}>
|
<section className={classes["root"]}>
|
||||||
<div className={classes["info-box1"]}>
|
<div className={classes["info-box1"]}>
|
||||||
|
@ -19,6 +19,7 @@ import MessageBox from "@Front/Components/Elements/MessageBox";
|
|||||||
|
|
||||||
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
import DocumentService from "src/common/Api/LeCoffreApi/sdk/DocumentService";
|
||||||
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
import FileService from "src/common/Api/LeCoffreApi/sdk/FileService";
|
||||||
|
import { FileBlob } from "@Front/Api/Entities/types";
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {};
|
||||||
type IPropsClass = {
|
type IPropsClass = {
|
||||||
@ -32,7 +33,7 @@ type IState = {
|
|||||||
isValidateModalVisible: boolean;
|
isValidateModalVisible: boolean;
|
||||||
refuseText: string;
|
refuseText: string;
|
||||||
selectedFileIndex: number;
|
selectedFileIndex: number;
|
||||||
selectedFile: any; // File | null; TODO: review
|
selectedFile: { uid: string; file_name: string; file_blob: FileBlob } | null;
|
||||||
validatedPercentage: number;
|
validatedPercentage: number;
|
||||||
document: Document | null;
|
document: Document | null;
|
||||||
fileBlob: Blob | null;
|
fileBlob: Blob | null;
|
||||||
@ -237,6 +238,8 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
|
|||||||
|
|
||||||
private async getFilePreview(): Promise<void> {
|
private async getFilePreview(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!this.state.selectedFile) return;
|
||||||
|
|
||||||
const fileBlob: Blob = new Blob([this.state.selectedFile.file_blob.data], { type: this.state.selectedFile.file_blob.type });
|
const fileBlob: Blob = new Blob([this.state.selectedFile.file_blob.data], { type: this.state.selectedFile.file_blob.type });
|
||||||
this.setState({
|
this.setState({
|
||||||
fileBlob,
|
fileBlob,
|
||||||
@ -247,7 +250,7 @@ class ViewDocumentsClass extends BasePage<IPropsClass, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private downloadFile() {
|
private downloadFile() {
|
||||||
if (!this.state.fileBlob) return;
|
if (!this.state.fileBlob || !this.state.selectedFile) return;
|
||||||
|
|
||||||
const url = URL.createObjectURL(this.state.fileBlob);
|
const url = URL.createObjectURL(this.state.fileBlob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
@ -160,6 +160,13 @@
|
|||||||
"path": "/folders/select",
|
"path": "/folders/select",
|
||||||
"labelKey": "select_folder"
|
"labelKey": "select_folder"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"VerifyDocuments": {
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"path": "/folders/[folderUid]/verify-documents",
|
||||||
|
"labelKey": "verify_documents"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -160,6 +160,13 @@
|
|||||||
"path": "/folders/select",
|
"path": "/folders/select",
|
||||||
"labelKey": "select_folder"
|
"labelKey": "select_folder"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"VerifyDocuments": {
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"path": "/folders/[folderUid]/verify-documents",
|
||||||
|
"labelKey": "verify_documents"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -160,6 +160,13 @@
|
|||||||
"path": "/folders/select",
|
"path": "/folders/select",
|
||||||
"labelKey": "select_folder"
|
"labelKey": "select_folder"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"VerifyDocuments": {
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"path": "/folders/[folderUid]/verify-documents",
|
||||||
|
"labelKey": "verify_documents"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -160,6 +160,13 @@
|
|||||||
"path": "/folders/select",
|
"path": "/folders/select",
|
||||||
"labelKey": "select_folder"
|
"labelKey": "select_folder"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"VerifyDocuments": {
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"path": "/folders/[folderUid]/verify-documents",
|
||||||
|
"labelKey": "verify_documents"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
347
src/front/Services/PdfService/index.ts
Normal file
347
src/front/Services/PdfService/index.ts
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||||
|
|
||||||
|
const separator = '\x1f';
|
||||||
|
|
||||||
|
export interface CustomerInfo {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
postalAddress: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotaryInfo {
|
||||||
|
name: string;
|
||||||
|
// Add more notary fields as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
fileName: string,
|
||||||
|
createdAt: Date,
|
||||||
|
isDeleted: boolean,
|
||||||
|
updatedAt: Date,
|
||||||
|
commitmentId: string,
|
||||||
|
documentUid: string;
|
||||||
|
documentType: string;
|
||||||
|
merkleProof: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertificateData {
|
||||||
|
customer: CustomerInfo;
|
||||||
|
notary: NotaryInfo;
|
||||||
|
folderUid: string;
|
||||||
|
documentHash: string;
|
||||||
|
metadata: Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PdfService {
|
||||||
|
private static instance: PdfService;
|
||||||
|
|
||||||
|
public static getInstance(): PdfService {
|
||||||
|
if (!PdfService.instance) {
|
||||||
|
PdfService.instance = new PdfService();
|
||||||
|
}
|
||||||
|
return PdfService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a certificate PDF for a document
|
||||||
|
* @param certificateData - Data needed for the certificate
|
||||||
|
* @returns Promise<Blob> - The generated PDF as a blob
|
||||||
|
*/
|
||||||
|
public async generateCertificate(certificateData: CertificateData): Promise<Blob> {
|
||||||
|
try {
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
const page = pdfDoc.addPage([595, 842]); // A4 size
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||||
|
const helveticaObliqueFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
|
||||||
|
|
||||||
|
let y = height - 40; // Start 40pt from the top
|
||||||
|
const leftMargin = 20;
|
||||||
|
const rightSectionX = 320;
|
||||||
|
const lineSpacing = 16;
|
||||||
|
|
||||||
|
// Notary Information Section (Top Left)
|
||||||
|
page.drawText('Notaire Validateur:', {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaBoldFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(certificateData.notary.name, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Customer Information Section (Top Right)
|
||||||
|
let yRight = height - 40;
|
||||||
|
page.drawText('Fournisseur de Document:', {
|
||||||
|
x: rightSectionX,
|
||||||
|
y: yRight,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaBoldFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
yRight -= lineSpacing;
|
||||||
|
page.drawText(`${certificateData.customer.firstName} ${certificateData.customer.lastName}`, {
|
||||||
|
x: rightSectionX,
|
||||||
|
y: yRight,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
yRight -= lineSpacing;
|
||||||
|
page.drawText(certificateData.customer.email, {
|
||||||
|
x: rightSectionX,
|
||||||
|
y: yRight,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
yRight -= lineSpacing;
|
||||||
|
page.drawText(certificateData.customer.postalAddress, {
|
||||||
|
x: rightSectionX,
|
||||||
|
y: yRight,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Centered Title
|
||||||
|
y -= 4 * lineSpacing; // Add more space between header and title
|
||||||
|
const titleText = 'Certificat de Validation de Document';
|
||||||
|
const titleWidth = helveticaBoldFont.widthOfTextAtSize(titleText, 20);
|
||||||
|
page.drawText(titleText, {
|
||||||
|
x: (width - titleWidth) / 2,
|
||||||
|
y: y,
|
||||||
|
size: 20,
|
||||||
|
font: helveticaBoldFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
|
||||||
|
// Add a line separator
|
||||||
|
page.drawLine({
|
||||||
|
start: { x: leftMargin, y: y },
|
||||||
|
end: { x: width - leftMargin, y: y },
|
||||||
|
thickness: 0.5,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
|
||||||
|
// Document Information Section
|
||||||
|
page.drawText('Informations du Document', {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaBoldFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(`UID du Dossier: ${certificateData.folderUid}`, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(`Type de Document: ${certificateData.metadata.documentType}`, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(`Nom du fichier: ${certificateData.metadata.fileName}`, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(`Créé le: ${certificateData.metadata.createdAt.toLocaleDateString('fr-FR')}`, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
page.drawText(`Dernière modification: ${certificateData.metadata.updatedAt.toLocaleDateString('fr-FR')}`, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y,
|
||||||
|
size: 12,
|
||||||
|
font: helveticaFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
y -= lineSpacing;
|
||||||
|
|
||||||
|
const keywords = [
|
||||||
|
certificateData.documentHash,
|
||||||
|
certificateData.metadata.commitmentId,
|
||||||
|
JSON.stringify(certificateData.metadata.merkleProof)
|
||||||
|
];
|
||||||
|
pdfDoc.setKeywords([keywords.join(separator)]);
|
||||||
|
|
||||||
|
// Add explanatory text about certificate usage
|
||||||
|
const explanatoryText = "Ce certificat doit être utilisé avec le document qu'il certifie pour être vérifié sur la page dédiée. Le document correspondant à ce certificat doit être téléchargé depuis LeCoffre et peut être conservé avec le certificat tant qu'il n'est pas modifié.";
|
||||||
|
const explanatoryLines = this.splitTextToFit(helveticaObliqueFont, explanatoryText, width - (2 * leftMargin), 11);
|
||||||
|
y -= (explanatoryLines.length * 12) + 40; // Space after verification data
|
||||||
|
explanatoryLines.forEach((line, index) => {
|
||||||
|
page.drawText(line, {
|
||||||
|
x: leftMargin,
|
||||||
|
y: y - (index * 14), // Slightly more spacing for readability
|
||||||
|
size: 11,
|
||||||
|
font: helveticaObliqueFont,
|
||||||
|
color: rgb(0.3, 0.3, 0.3) // Slightly grayed out to distinguish from main content
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
const footerText1 = `Page 1 sur 1`;
|
||||||
|
const footerText2 = `Généré le ${new Date().toLocaleString('fr-FR')}`;
|
||||||
|
const footerWidth1 = helveticaObliqueFont.widthOfTextAtSize(footerText1, 10);
|
||||||
|
const footerWidth2 = helveticaObliqueFont.widthOfTextAtSize(footerText2, 10);
|
||||||
|
page.drawText(footerText1, {
|
||||||
|
x: (width - footerWidth1) / 2,
|
||||||
|
y: 35,
|
||||||
|
size: 10,
|
||||||
|
font: helveticaObliqueFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
page.drawText(footerText2, {
|
||||||
|
x: (width - footerWidth2) / 2,
|
||||||
|
y: 25,
|
||||||
|
size: 10,
|
||||||
|
font: helveticaObliqueFont,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
return new Blob([pdfBytes], { type: 'application/pdf' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating certificate:', error);
|
||||||
|
throw new Error('Failed to generate certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split text to fit within a specified width
|
||||||
|
* @param font - PDF font instance
|
||||||
|
* @param text - Text to split
|
||||||
|
* @param maxWidth - Maximum width in points
|
||||||
|
* @param fontSize - Font size
|
||||||
|
* @returns Array of text lines
|
||||||
|
*/
|
||||||
|
private splitTextToFit(font: any, text: string, maxWidth: number, fontSize: number): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
let currentLine = '';
|
||||||
|
|
||||||
|
// Split by characters (pdf-lib doesn't have word-level text measurement)
|
||||||
|
const chars = text.split('');
|
||||||
|
|
||||||
|
for (const char of chars) {
|
||||||
|
const testLine = currentLine + char;
|
||||||
|
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
|
||||||
|
|
||||||
|
if (testWidth <= maxWidth) {
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = char;
|
||||||
|
} else {
|
||||||
|
// If even a single character is too wide, force a break
|
||||||
|
lines.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a certificate PDF
|
||||||
|
* @param certificateData - Data needed for the certificate
|
||||||
|
* @param filename - Optional filename for the download
|
||||||
|
*/
|
||||||
|
public async downloadCertificate(certificateData: CertificateData, filename?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const pdfBlob = await this.generateCertificate(certificateData);
|
||||||
|
const defaultFilename = `certificate_${certificateData.metadata.documentUid}_${new Date().toISOString().split('T')[0]}.pdf`;
|
||||||
|
saveAs(pdfBlob, filename || defaultFilename);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading certificate:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a PDF certificate and extract document information
|
||||||
|
* @param pdfBlob - The PDF file as a blob
|
||||||
|
* @returns Promise<{documentHash: string, documentUid: string, commitmentId: string, merkleProof?: string}> - Extracted information
|
||||||
|
*/
|
||||||
|
public async parseCertificate(certificateFile: File): Promise<{documentHash: string, commitmentId: string, merkleProof: any}> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
|
console.log("certificateFile", certificateFile);
|
||||||
|
|
||||||
|
fileReader.onload = async () => {
|
||||||
|
try {
|
||||||
|
// Read the metadata and get the validation data from the keywords
|
||||||
|
const pdfDoc = await PDFDocument.load(await certificateFile.arrayBuffer());
|
||||||
|
const keywords = pdfDoc.getKeywords()?.split(separator);
|
||||||
|
|
||||||
|
if (!keywords) {
|
||||||
|
throw new Error("No keywords found in certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(keywords);
|
||||||
|
|
||||||
|
if (keywords.length !== 3) {
|
||||||
|
throw new Error("Invalid keywords found in certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentHash = keywords[0];
|
||||||
|
const commitmentId = keywords[1];
|
||||||
|
const merkleProof = keywords[2];
|
||||||
|
|
||||||
|
if (!documentHash || !commitmentId || !merkleProof) {
|
||||||
|
throw new Error("Invalid keywords found in certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("documentHash", documentHash);
|
||||||
|
console.log("commitmentId", commitmentId);
|
||||||
|
console.log("merkleProof", merkleProof);
|
||||||
|
|
||||||
|
|
||||||
|
resolve({ documentHash, commitmentId, merkleProof });
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.onerror = () => {
|
||||||
|
reject(new Error('Failed to read PDF file'));
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsArrayBuffer(certificateFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
211
src/front/Services/WatermarkService/index.ts
Normal file
211
src/front/Services/WatermarkService/index.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
export default class WatermarkService {
|
||||||
|
private static instance: WatermarkService;
|
||||||
|
|
||||||
|
public static getInstance(): WatermarkService {
|
||||||
|
if (!WatermarkService.instance) {
|
||||||
|
WatermarkService.instance = new WatermarkService();
|
||||||
|
}
|
||||||
|
return WatermarkService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a watermark to a file based on its type
|
||||||
|
* @param file - The original file
|
||||||
|
* @returns Promise<File> - The file with watermark added
|
||||||
|
*/
|
||||||
|
public async addWatermark(file: File): Promise<File> {
|
||||||
|
const fileType = file.type;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
return await this.addWatermarkToImage(file);
|
||||||
|
} else if (fileType === 'application/pdf') {
|
||||||
|
return await this.addWatermarkToPdf(file);
|
||||||
|
} else {
|
||||||
|
// For other file types, return the original file
|
||||||
|
console.log(`Watermark not supported for file type: ${fileType}`);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding watermark:', error);
|
||||||
|
// Return original file if watermarking fails
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add watermark to image files
|
||||||
|
* @param file - Image file
|
||||||
|
* @returns Promise<File> - Image with watermark
|
||||||
|
*/
|
||||||
|
private async addWatermarkToImage(file: File): Promise<File> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
// Create canvas
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error('Could not get canvas context'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas size to image size
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
|
||||||
|
// Draw original image
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// Add watermark
|
||||||
|
this.addImageWatermark(ctx, img.width, img.height);
|
||||||
|
|
||||||
|
// Convert to blob
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
const watermarkedFile = new File([blob], file.name, { type: file.type });
|
||||||
|
resolve(watermarkedFile);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Could not create blob from canvas'));
|
||||||
|
}
|
||||||
|
}, file.type);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = event.target?.result as string;
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add watermark to PDF files
|
||||||
|
* @param file - PDF file
|
||||||
|
* @returns Promise<File> - PDF with watermark
|
||||||
|
*/
|
||||||
|
private async addWatermarkToPdf(file: File): Promise<File> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = event.target?.result as ArrayBuffer;
|
||||||
|
|
||||||
|
// Import pdf-lib dynamically to avoid SSR issues
|
||||||
|
const { PDFDocument, rgb } = await import('pdf-lib');
|
||||||
|
|
||||||
|
// Load the existing PDF
|
||||||
|
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||||
|
|
||||||
|
// Get all pages
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
// Add watermark to each page
|
||||||
|
for (const page of pages) {
|
||||||
|
this.addPdfWatermark(page, rgb(0.8, 0.2, 0.2)); // Pale red color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the modified PDF
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const watermarkedFile = new File([pdfBytes], file.name, { type: file.type });
|
||||||
|
resolve(watermarkedFile);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding watermark to PDF:', error);
|
||||||
|
// If PDF watermarking fails, return original file
|
||||||
|
resolve(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add watermark to image using Canvas
|
||||||
|
* @param ctx - Canvas 2D context
|
||||||
|
* @param width - Image width
|
||||||
|
* @param height - Image height
|
||||||
|
*/
|
||||||
|
private addImageWatermark(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
||||||
|
// Save current state
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Set watermark properties
|
||||||
|
ctx.fillStyle = 'rgba(128, 128, 128, 0.7)'; // Semi-transparent gray
|
||||||
|
ctx.font = '12px Arial';
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.textBaseline = 'bottom';
|
||||||
|
|
||||||
|
// Position watermark in bottom-right corner
|
||||||
|
const text = 'Processed by LeCoffre';
|
||||||
|
const x = width - 10; // 10 pixels from right edge
|
||||||
|
const y = height - 10; // 10 pixels from bottom
|
||||||
|
|
||||||
|
// Add watermark text
|
||||||
|
ctx.fillText(text, x, y);
|
||||||
|
|
||||||
|
// Restore state
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add watermark to PDF using pdf-lib
|
||||||
|
* @param page - PDF page from pdf-lib
|
||||||
|
* @param color - Color for the watermark
|
||||||
|
*/
|
||||||
|
private addPdfWatermark(page: any, color: any): void {
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
// Calculate watermark position (bottom-right corner)
|
||||||
|
const text = 'Processed by LeCoffre';
|
||||||
|
const dateTime = new Date().toLocaleString('fr-FR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const fontSize = 10;
|
||||||
|
const lineHeight = 12; // Space between lines
|
||||||
|
|
||||||
|
// Calculate text widths (approximate - pdf-lib doesn't have a direct method for this)
|
||||||
|
// Using a conservative estimate: ~6 points per character for 10pt font
|
||||||
|
const estimatedTextWidth1 = text.length * 6;
|
||||||
|
const estimatedTextWidth2 = dateTime.length * 6;
|
||||||
|
const maxTextWidth = Math.max(estimatedTextWidth1, estimatedTextWidth2);
|
||||||
|
|
||||||
|
// Position watermark with proper margins (20 points from edges)
|
||||||
|
const x = width - maxTextWidth - 20; // 20 points from right edge
|
||||||
|
const y = 20; // 20 points from bottom
|
||||||
|
|
||||||
|
// Add watermark text with transparency (first line)
|
||||||
|
page.drawText(text, {
|
||||||
|
x: x,
|
||||||
|
y: y + lineHeight, // Second line (top)
|
||||||
|
size: fontSize,
|
||||||
|
color: color,
|
||||||
|
opacity: 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add date/time with transparency (second line)
|
||||||
|
page.drawText(dateTime, {
|
||||||
|
x: x,
|
||||||
|
y: y, // First line (bottom)
|
||||||
|
size: fontSize,
|
||||||
|
color: color,
|
||||||
|
opacity: 0.7
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
src/pages/folders/[folderUid]/verify-documents.tsx
Normal file
20
src/pages/folders/[folderUid]/verify-documents.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import DefaultNotaryDashboard from "@Front/Components/LayoutTemplates/DefaultNotaryDashboard";
|
||||||
|
import DocumentVerification from "@Front/Components/Layouts/Folder/DocumentVerification";
|
||||||
|
|
||||||
|
export default function VerifyDocuments() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { folderUid } = router.query;
|
||||||
|
|
||||||
|
if (!folderUid || Array.isArray(folderUid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultNotaryDashboard>
|
||||||
|
<DocumentVerification folderUid={folderUid} />
|
||||||
|
</DefaultNotaryDashboard>
|
||||||
|
);
|
||||||
|
}
|
@ -6,6 +6,7 @@ import EventBus from './EventBus';
|
|||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
import MapUtils from './MapUtils';
|
import MapUtils from './MapUtils';
|
||||||
|
import { FileBlob } from '../front/Api/Entities/types';
|
||||||
|
|
||||||
export default class MessageBus {
|
export default class MessageBus {
|
||||||
private static instance: MessageBus;
|
private static instance: MessageBus;
|
||||||
@ -113,13 +114,15 @@ export default class MessageBus {
|
|||||||
|
|
||||||
const publicDataDecoded: { [key: string]: any } = {};
|
const publicDataDecoded: { [key: string]: any } = {};
|
||||||
|
|
||||||
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
|
// We only care about the public data as they are in the last commited state
|
||||||
const state = process.states[stateId];
|
const processTip = process.states[process.states.length - 1].commited_in;
|
||||||
if (!state) {
|
const lastCommitedState = process.states.findLast((state: any) => state.commited_in !== processTip);
|
||||||
|
|
||||||
|
if (!lastCommitedState) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicDataEncoded = state.public_data;
|
const publicDataEncoded = lastCommitedState.public_data;
|
||||||
if (!publicDataEncoded) {
|
if (!publicDataEncoded) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -127,52 +130,53 @@ export default class MessageBus {
|
|||||||
for (const key of Object.keys(publicDataEncoded)) {
|
for (const key of Object.keys(publicDataEncoded)) {
|
||||||
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
|
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!(publicDataDecoded['uid'] && publicDataDecoded['uid'] === uid && publicDataDecoded['utype'] && publicDataDecoded['utype'] === 'file')) {
|
if (!(publicDataDecoded['uid'] && publicDataDecoded['uid'] === uid && publicDataDecoded['utype'] && publicDataDecoded['utype'] === 'file')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We take the file in it's latest commited state
|
||||||
let file: any;
|
let file: any;
|
||||||
|
|
||||||
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
|
// Which is the last state that updated file_blob?
|
||||||
const lastState = process.states[stateId];
|
const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']);
|
||||||
if (!lastState) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastStateId = lastState.state_id;
|
if (!lastUpdatedFileState) {
|
||||||
if (!lastStateId) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const processData = await this.getData(processId, lastStateId);
|
const processData = await this.getData(processId, lastUpdatedFileState.state_id);
|
||||||
const isEmpty = Object.keys(processData).length === 0;
|
const isEmpty = Object.keys(processData).length === 0;
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of Object.keys(publicDataDecoded)) {
|
const publicDataEncoded = lastUpdatedFileState.public_data;
|
||||||
processData[key] = publicDataDecoded[key];
|
if (!publicDataEncoded) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
file = {
|
file = {
|
||||||
processId,
|
processId,
|
||||||
lastStateId,
|
lastUpdatedFileState,
|
||||||
processData,
|
processData,
|
||||||
|
publicDataDecoded,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
for (const key of Object.keys(processData)) {
|
for (const key of Object.keys(processData)) {
|
||||||
file.processData[key] = processData[key];
|
file.processData[key] = processData[key];
|
||||||
}
|
}
|
||||||
file.lastStateId = lastStateId;
|
file.lastUpdatedFileState = lastUpdatedFileState;
|
||||||
|
for (const key of Object.keys(publicDataDecoded)) {
|
||||||
|
file.publicDataDecoded[key] = publicDataDecoded[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
files.push(file);
|
files.push(file);
|
||||||
break;
|
break;
|
||||||
@ -561,6 +565,119 @@ export default class MessageBus {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a document file using SHA-256
|
||||||
|
* @param fileBlob - The file blob to hash
|
||||||
|
* @returns Promise<string> - The SHA-256 hash of the file
|
||||||
|
*/
|
||||||
|
public hashDocument(fileBlob: FileBlob, commitedIn: string): Promise<string> {
|
||||||
|
return new Promise<string>((resolve: (hash: string) => void, reject: (error: string) => void) => {
|
||||||
|
this.checkToken().then(() => {
|
||||||
|
const messageId = `HASH_VALUE_${uuidv4()}`;
|
||||||
|
|
||||||
|
const unsubscribe = EventBus.getInstance().on('VALUE_HASHED', (responseId: string, hash: string) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
resolve(hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribeError = EventBus.getInstance().on('ERROR_VALUE_HASHED', (responseId: string, error: string) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribeError();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = User.getInstance();
|
||||||
|
const accessToken = user.getAccessToken()!;
|
||||||
|
|
||||||
|
const label = 'file_blob';
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
type: 'HASH_VALUE',
|
||||||
|
accessToken,
|
||||||
|
commitedIn,
|
||||||
|
label,
|
||||||
|
fileBlob,
|
||||||
|
messageId
|
||||||
|
});
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateMerkleProof(processState: any, attributeName: string): Promise<any> {
|
||||||
|
return new Promise<any>((resolve: (proof: any) => void, reject: (error: string) => void) => {
|
||||||
|
this.checkToken().then(() => {
|
||||||
|
const messageId = `GET_MERKLE_PROOF_${uuidv4()}`;
|
||||||
|
|
||||||
|
const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_RETRIEVED', (responseId: string, proof: any) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
resolve(proof);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_RETRIEVED', (responseId: string, error: string) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribeError();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = User.getInstance();
|
||||||
|
const accessToken = user.getAccessToken()!;
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
type: 'GET_MERKLE_PROOF',
|
||||||
|
accessToken,
|
||||||
|
processState,
|
||||||
|
attributeName,
|
||||||
|
messageId
|
||||||
|
});
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyMerkleProof(merkleProof: string, documentHash: string): Promise<boolean> {
|
||||||
|
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
|
||||||
|
this.checkToken().then(() => {
|
||||||
|
const messageId = `VALIDATE_MERKLE_PROOF_${uuidv4()}`;
|
||||||
|
|
||||||
|
const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_VALIDATED', (responseId: string, isValid: boolean) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
resolve(isValid);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_VALIDATED', (responseId: string, error: string) => {
|
||||||
|
if (responseId !== messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribeError();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = User.getInstance();
|
||||||
|
const accessToken = user.getAccessToken()!;
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
type: 'VALIDATE_MERKLE_PROOF',
|
||||||
|
accessToken,
|
||||||
|
merkleProof,
|
||||||
|
documentHash,
|
||||||
|
messageId
|
||||||
|
});
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private validateToken(): Promise<boolean> {
|
private validateToken(): Promise<boolean> {
|
||||||
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
|
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
|
||||||
const messageId = `VALIDATE_TOKEN_${uuidv4()}`;
|
const messageId = `VALIDATE_TOKEN_${uuidv4()}`;
|
||||||
@ -651,7 +768,9 @@ export default class MessageBus {
|
|||||||
console.error('[MessageBus] sendMessage: iframe not found');
|
console.error('[MessageBus] sendMessage: iframe not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (message.type === 'VALIDATE_MERKLE_PROOF') {
|
||||||
console.log('[MessageBus] sendMessage:', message);
|
console.log('[MessageBus] sendMessage:', message);
|
||||||
|
}
|
||||||
iframe.contentWindow?.postMessage(message, targetOrigin);
|
iframe.contentWindow?.postMessage(message, targetOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +807,7 @@ export default class MessageBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
console.log('[MessageBus] handleMessage:', message);
|
// console.log('[MessageBus] handleMessage:', message);
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'LISTENING':
|
case 'LISTENING':
|
||||||
@ -750,6 +869,18 @@ export default class MessageBus {
|
|||||||
this.doHandleMessage(message.messageId, 'STATE_VALIDATED', message, (message: any) => message.validatedProcess);
|
this.doHandleMessage(message.messageId, 'STATE_VALIDATED', message, (message: any) => message.validatedProcess);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'VALUE_HASHED': // HASH_VALUE
|
||||||
|
this.doHandleMessage(message.messageId, 'VALUE_HASHED', message, (message: any) => message.hash);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MERKLE_PROOF_RETRIEVED': // GET_MERKLE_PROOF
|
||||||
|
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_RETRIEVED', message, (message: any) => message.proof);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MERKLE_PROOF_VALIDATED': // VALIDATE_MERKLE_PROOF
|
||||||
|
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_VALIDATED', message, (message: any) => message.isValid);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
console.error('Error:', message);
|
console.error('Error:', message);
|
||||||
this.errors[message.messageId] = message.error;
|
this.errors[message.messageId] = message.error;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user