260 lines
8.7 KiB
TypeScript
260 lines
8.7 KiB
TypeScript
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>
|
||
);
|
||
}
|