2025-07-03 18:09:02 +02:00

260 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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