From 9e0f3d3b73b8a08473c39a8351a67a939ff68ae1 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 15 Jul 2025 20:41:26 +0200 Subject: [PATCH 1/6] Add StepImportProfile component --- .../StepImportProfile/classes.module.scss | 72 ++++++++++ .../Layouts/Login/StepImportProfile/index.tsx | 132 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/front/Components/Layouts/Login/StepImportProfile/classes.module.scss create mode 100644 src/front/Components/Layouts/Login/StepImportProfile/index.tsx diff --git a/src/front/Components/Layouts/Login/StepImportProfile/classes.module.scss b/src/front/Components/Layouts/Login/StepImportProfile/classes.module.scss new file mode 100644 index 00000000..6350bf78 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepImportProfile/classes.module.scss @@ -0,0 +1,72 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .description { + margin-top: 16px; + color: var(--color-neutral-700); + line-height: 1.5; + } + + .import-section { + margin-top: 32px; + } + + .error { + margin-top: 16px; + padding: 12px; + background-color: var(--color-error-50); + border: 1px solid var(--color-error-200); + border-radius: 6px; + } + + .profile-preview { + margin-top: 24px; + padding: 16px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 8px; + + .preview-title { + margin-bottom: 12px; + color: var(--color-neutral-800); + } + } + + .actions { + margin-top: 32px; + display: flex; + gap: 16px; + justify-content: flex-end; + + .back-button { + min-width: 100px; + } + + .submit-button { + min-width: 140px; + } + + @media (max-width: $screen-s) { + flex-direction: column; + + .back-button, + .submit-button { + width: 100%; + } + } + } +} \ No newline at end of file diff --git a/src/front/Components/Layouts/Login/StepImportProfile/index.tsx b/src/front/Components/Layouts/Login/StepImportProfile/index.tsx new file mode 100644 index 00000000..44ddfeb0 --- /dev/null +++ b/src/front/Components/Layouts/Login/StepImportProfile/index.tsx @@ -0,0 +1,132 @@ +import React, { useState, useCallback } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import DragAndDrop from "@Front/Components/DesignSystem/DragAndDrop"; +import { ValidationError } from "class-validator"; + +type IProps = { + onSubmit: (profileData: any) => void; + onBack: () => void; + validationErrors: ValidationError[]; +}; + +export default function StepImportProfile(props: IProps) { + const { onSubmit, onBack, validationErrors } = props; + const [importedFile, setImportedFile] = useState(null); + const [profileData, setProfileData] = useState(null); + const [error, setError] = useState(""); + + const validateProfileStructure = (data: any): boolean => { + // Basic validation for profile structure + return data && + typeof data === "object" && + data.version && + data.userData && + typeof data.userData === "object"; + }; + + const handleFileUpload = useCallback(async (files: File[]) => { + const file = files[0]; + if (!file) return; + + // Validate file type + if (file.type !== "application/json") { + setError("Veuillez sélectionner un fichier JSON valide."); + return; + } + + // Validate file size (max 1MB) + if (file.size > 1024 * 1024) { + setError("Le fichier est trop volumineux. Taille maximum : 1MB."); + return; + } + + setImportedFile(file); + setError(""); + + // Read and parse JSON + try { + const text = await file.text(); + const data = JSON.parse(text); + + // Validate profile structure + if (!validateProfileStructure(data)) { + setError("Le fichier ne contient pas un profil valide."); + return; + } + + setProfileData(data); + } catch (err) { + setError("Erreur lors de la lecture du fichier JSON."); + } + }, []); + + const handleSubmit = useCallback(() => { + if (profileData) { + onSubmit(profileData); + } + }, [profileData, onSubmit]); + + // Get validation error for import + const importError = validationErrors.find((error) => error.property === "import"); + + return ( +
+ +
Importer un profil
+
+ + + Sélectionnez un fichier JSON contenant vos données de profil pour restaurer votre session. + + +
+ +
+ + {(error || importError) && ( + + {error || (importError?.constraints && Object.values(importError.constraints)[0])} + + )} + + {profileData && ( +
+ + Profil détecté : + + + {profileData.userData?.email || "Email non disponible"} + + + Version : {profileData.version} + + {profileData.exportedAt && ( + + Exporté le : {new Date(profileData.exportedAt).toLocaleDateString('fr-FR')} + + )} +
+ )} + +
+ + +
+
+ ); +} \ No newline at end of file -- 2.39.5 From e4b9b0a56dc0be4124fe478c99f884f2ff41aa86 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 15 Jul 2025 20:42:30 +0200 Subject: [PATCH 2/6] Add exportBackupFile in MessageBus --- src/sdk/MessageBus.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/sdk/MessageBus.ts b/src/sdk/MessageBus.ts index aeb6316f..39d219a9 100644 --- a/src/sdk/MessageBus.ts +++ b/src/sdk/MessageBus.ts @@ -644,6 +644,39 @@ export default class MessageBus { }); } + public exportBackupFile(): Promise { + return new Promise((resolve: (backupFile: any) => void, reject: (error: string) => void) => { + this.checkToken().then(() => { + const messageId = `EXPORT_BACKUP_${uuidv4()}`; + + const unsubscribe = EventBus.getInstance().on('BACKUP_RETRIEVED', (responseId: string, backupFile: any) => { + if (responseId !== messageId) { + return; + } + unsubscribe(); + resolve(backupFile); + }); + + const unsubscribeError = EventBus.getInstance().on('ERROR_BACKUP_RETRIEVED', (responseId: string, error: string) => { + if (responseId !== messageId) { + return; + } + unsubscribeError(); + reject(error); + }); + + const user = User.getInstance(); + const accessToken = user.getAccessToken()!; + + this.sendMessage({ + type: 'EXPORT_BACKUP', + accessToken, + messageId + }); + }).catch(reject); + }); + } + public verifyMerkleProof(merkleProof: string, documentHash: string): Promise { return new Promise((resolve: (isValid: boolean) => void, reject: (error: string) => void) => { this.checkToken().then(() => { -- 2.39.5 From 23a75b68c641c135580b05ae7609d739e0e5dd23 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 15 Jul 2025 20:43:15 +0200 Subject: [PATCH 3/6] Add create backup button in my account --- .../Components/Layouts/MyAccount/index.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/front/Components/Layouts/MyAccount/index.tsx b/src/front/Components/Layouts/MyAccount/index.tsx index 7bbc65bc..937ef3c7 100644 --- a/src/front/Components/Layouts/MyAccount/index.tsx +++ b/src/front/Components/Layouts/MyAccount/index.tsx @@ -1,5 +1,6 @@ import backgroundImage from "@Assets/images/background_refonte.svg"; import Users from "@Front/Api/LeCoffreApi/Notary/Users/Users"; +import Button, { EButtonVariant, EButtonSize, EButtonstyletype } from "@Front/Components/DesignSystem/Button"; import Form from "@Front/Components/DesignSystem/Form"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography"; @@ -9,6 +10,7 @@ import User from "le-coffre-resources/dist/Notary"; import React, { useEffect } from "react"; import classes from "./classes.module.scss"; +import MessageBus from "src/sdk/MessageBus"; export default function MyAccount() { const [user, setUser] = React.useState(null); @@ -33,6 +35,14 @@ export default function MyAccount() { }); }, []); + const handleCreateRestorationFile = () => { + MessageBus.getInstance().exportBackupFile().then((backupFile) => { + console.log(backupFile); + }).catch((error) => { + console.error(error); + }); + }; + return (
@@ -104,6 +114,16 @@ export default function MyAccount() { canCopy /> + +
); -- 2.39.5 From 108c27fc483b2a6c05476d4e567e8ad701cc40a3 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 15 Jul 2025 20:44:15 +0200 Subject: [PATCH 4/6] Add import profile step in login --- .../Layouts/Login/StepEmail/index.tsx | 19 ++++++- src/front/Components/Layouts/Login/index.tsx | 57 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/front/Components/Layouts/Login/StepEmail/index.tsx b/src/front/Components/Layouts/Login/StepEmail/index.tsx index 17b108dd..d87a8cc1 100644 --- a/src/front/Components/Layouts/Login/StepEmail/index.tsx +++ b/src/front/Components/Layouts/Login/StepEmail/index.tsx @@ -4,7 +4,7 @@ import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Ty //import Image from "next/image"; import Form from "@Front/Components/DesignSystem/Form"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; -import Button from "@Front/Components/DesignSystem/Button"; +import Button, { EButtonVariant, EButtonstyletype } from "@Front/Components/DesignSystem/Button"; //import franceConnectLogo from "../france-connect.svg"; // import { useRouter } from "next/router"; // import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; @@ -20,10 +20,11 @@ import Confirm from "@Front/Components/DesignSystem/OldModal/Confirm"; type IProps = { onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; validationErrors: ValidationError[]; + onImportProfile?: () => void; }; export default function StepEmail(props: IProps) { - const { onSubmit, validationErrors } = props; + const { onSubmit, validationErrors, onImportProfile } = props; const [isErrorModalOpen, setIsErrorModalOpen] = useState(0); /* const router = useRouter(); @@ -111,6 +112,20 @@ export default function StepEmail(props: IProps) { + {onImportProfile && ( +
+ + Options avancées : + + +
+ )} (""); const [validationErrors, setValidationErrors] = useState([]); const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); // const openErrorModal = useCallback(() => { // setIsErrorModalOpen(true); @@ -241,10 +244,55 @@ export default function Login() { } }, [email, totpCodeUid]); + const onImportProfileSubmit = useCallback(async (profileData: any) => { + try { + setIsLoading(true); + setValidationErrors([]); + + // Call API to validate and import profile + // Note: You'll need to implement this method in your Auth service + // const response = await Auth.getInstance().importProfile(profileData); + + // For now, we'll simulate a successful import + // In a real implementation, you would: + // 1. Send the profile data to your backend + // 2. Validate the profile on the server + // 3. Return authentication tokens + // 4. Connect the user + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // For demo purposes, we'll just redirect to the dashboard + // In reality, you'd use the response from the API + // CustomerStore.instance.connect(response.accessToken, response.refreshToken); + + // Redirect to dashboard + router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); + } catch (error: any) { + setValidationErrors([ + { + property: "import", + constraints: { + [error.http_status || "500"]: error.message || "Erreur lors de l'import du profil", + }, + }, + ]); + } finally { + setIsLoading(false); + } + }, [router]); + return (
- {step === LoginStep.EMAIL && } + {step === LoginStep.EMAIL && ( + setStep(LoginStep.IMPORT_PROFILE)} + /> + )} {step === LoginStep.TOTP && ( } + {step === LoginStep.IMPORT_PROFILE && ( + setStep(LoginStep.EMAIL)} + validationErrors={validationErrors} + /> + )}
{/* Date: Tue, 15 Jul 2025 21:54:09 +0200 Subject: [PATCH 5/6] Download the backup json as a file --- .../Components/Layouts/MyAccount/index.tsx | 30 ++++++++++++++++++- src/sdk/MessageBus.ts | 6 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/front/Components/Layouts/MyAccount/index.tsx b/src/front/Components/Layouts/MyAccount/index.tsx index 937ef3c7..07c35853 100644 --- a/src/front/Components/Layouts/MyAccount/index.tsx +++ b/src/front/Components/Layouts/MyAccount/index.tsx @@ -35,9 +35,37 @@ export default function MyAccount() { }); }, []); + const downloadBackupFile = (backupFile: string, filename: string = 'backup.json') => { + try { + // Create a blob with the backup data + const blob = new Blob([backupFile], { type: 'application/json' }); + + // Create a download link + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + + // Trigger the download + document.body.appendChild(link); + link.click(); + + // Clean up + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading backup file:', error); + } + }; + const handleCreateRestorationFile = () => { MessageBus.getInstance().exportBackupFile().then((backupFile) => { - console.log(backupFile); + // Generate filename with current date + const now = new Date(); + const dateString = now.toISOString().split('T')[0]; // YYYY-MM-DD format + const filename = `backup_${dateString}.json`; + + downloadBackupFile(backupFile, filename); }).catch((error) => { console.error(error); }); diff --git a/src/sdk/MessageBus.ts b/src/sdk/MessageBus.ts index 39d219a9..715357e6 100644 --- a/src/sdk/MessageBus.ts +++ b/src/sdk/MessageBus.ts @@ -649,7 +649,7 @@ export default class MessageBus { this.checkToken().then(() => { const messageId = `EXPORT_BACKUP_${uuidv4()}`; - const unsubscribe = EventBus.getInstance().on('BACKUP_RETRIEVED', (responseId: string, backupFile: any) => { + const unsubscribe = EventBus.getInstance().on('BACKUP_RETRIEVED', (responseId: string, backupFile: string) => { if (responseId !== messageId) { return; } @@ -896,6 +896,10 @@ export default class MessageBus { this.doHandleMessage(message.messageId, 'MERKLE_PROOF_VALIDATED', message, (message: any) => message.isValid); break; + case 'BACKUP_RETRIEVED': // EXPORT_BACKUP + this.doHandleMessage(message.messageId, 'BACKUP_RETRIEVED', message, (message: any) => message.backupFile); + break; + case 'ERROR': console.error('Error:', message); this.errors[message.messageId] = message.error; -- 2.39.5 From c4fd8da72b0e405a7a6d9c99b524d7b5406b2583 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 16 Jul 2025 18:24:47 +0200 Subject: [PATCH 6/6] WIP --- .../DesignSystem/DragAndDrop/index.tsx | 4 ++ .../Layouts/Login/StepImportProfile/index.tsx | 54 +++++++------------ src/front/Components/Layouts/Login/index.tsx | 37 ++++++------- .../Layouts/LoginCallback/index.tsx | 46 +++------------- src/sdk/MessageBus.ts | 35 ++++++++++++ 5 files changed, 84 insertions(+), 92 deletions(-) diff --git a/src/front/Components/DesignSystem/DragAndDrop/index.tsx b/src/front/Components/DesignSystem/DragAndDrop/index.tsx index bc2a8021..a73ab98f 100644 --- a/src/front/Components/DesignSystem/DragAndDrop/index.tsx +++ b/src/front/Components/DesignSystem/DragAndDrop/index.tsx @@ -77,6 +77,10 @@ const mimeTypesAccepted: { [key: string]: IMimeTypes } = { extension: "txt", size: 104857600, // 100MB }, + "application/json": { + extension: "json", + size: 104857600, // 100MB + }, }; type IDocumentFileBase = { diff --git a/src/front/Components/Layouts/Login/StepImportProfile/index.tsx b/src/front/Components/Layouts/Login/StepImportProfile/index.tsx index 44ddfeb0..35d1e545 100644 --- a/src/front/Components/Layouts/Login/StepImportProfile/index.tsx +++ b/src/front/Components/Layouts/Login/StepImportProfile/index.tsx @@ -13,7 +13,6 @@ type IProps = { export default function StepImportProfile(props: IProps) { const { onSubmit, onBack, validationErrors } = props; - const [importedFile, setImportedFile] = useState(null); const [profileData, setProfileData] = useState(null); const [error, setError] = useState(""); @@ -22,44 +21,32 @@ export default function StepImportProfile(props: IProps) { return data && typeof data === "object" && data.version && - data.userData && - typeof data.userData === "object"; + data.user_data && + typeof data.user_data === "object"; }; - const handleFileUpload = useCallback(async (files: File[]) => { + const handleFileUpload = useCallback((files: File[]) => { const file = files[0]; if (!file) return; - // Validate file type - if (file.type !== "application/json") { - setError("Veuillez sélectionner un fichier JSON valide."); - return; - } - - // Validate file size (max 1MB) - if (file.size > 1024 * 1024) { - setError("Le fichier est trop volumineux. Taille maximum : 1MB."); - return; - } - - setImportedFile(file); setError(""); // Read and parse JSON - try { - const text = await file.text(); - const data = JSON.parse(text); - - // Validate profile structure - if (!validateProfileStructure(data)) { - setError("Le fichier ne contient pas un profil valide."); - return; - } + file.text() + .then((text) => { + const data = JSON.parse(text); + + // Validate profile structure + if (!validateProfileStructure(data)) { + setError('Le fichier ne contient pas un profil valide.'); + return; + } - setProfileData(data); - } catch (err) { - setError("Erreur lors de la lecture du fichier JSON."); - } + setProfileData(data); + }) + .catch((e) => { + setError(`Erreur lors de la lecture du fichier JSON: ${e}`); + }); }, []); const handleSubmit = useCallback(() => { @@ -100,15 +87,12 @@ export default function StepImportProfile(props: IProps) { Profil détecté : - - {profileData.userData?.email || "Email non disponible"} - Version : {profileData.version} - {profileData.exportedAt && ( + {profileData.exported_at && ( - Exporté le : {new Date(profileData.exportedAt).toLocaleDateString('fr-FR')} + Exporté le : {new Date(profileData.exported_at).toLocaleDateString('fr-FR')} )} diff --git a/src/front/Components/Layouts/Login/index.tsx b/src/front/Components/Layouts/Login/index.tsx index 8226512b..6005e476 100644 --- a/src/front/Components/Layouts/Login/index.tsx +++ b/src/front/Components/Layouts/Login/index.tsx @@ -22,6 +22,8 @@ import UserStore from "@Front/Stores/UserStore"; import AuthModal from "src/sdk/AuthModal"; import CustomerService from "src/common/Api/LeCoffreApi/sdk/CustomerService"; import StepImportProfile from "./StepImportProfile"; +import MessageBus from "src/sdk/MessageBus"; +import Iframe from "src/sdk/Iframe"; export enum LoginStep { EMAIL, @@ -46,6 +48,7 @@ export default function Login() { const [validationErrors, setValidationErrors] = useState([]); const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [showIframeForImport, setShowIframeForImport] = useState(false); // const openErrorModal = useCallback(() => { // setIsErrorModalOpen(true); @@ -248,26 +251,22 @@ export default function Login() { try { setIsLoading(true); setValidationErrors([]); + + // Show iframe for import operation (but hidden from view) + setShowIframeForImport(true); - // Call API to validate and import profile - // Note: You'll need to implement this method in your Auth service - // const response = await Auth.getInstance().importProfile(profileData); + // Initialize message listener for import operation + MessageBus.getInstance().initMessageListener(); - // For now, we'll simulate a successful import - // In a real implementation, you would: - // 1. Send the profile data to your backend - // 2. Validate the profile on the server - // 3. Return authentication tokens - // 4. Connect the user - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1000)); - - // For demo purposes, we'll just redirect to the dashboard - // In reality, you'd use the response from the API - // CustomerStore.instance.connect(response.accessToken, response.refreshToken); - - // Redirect to dashboard + // Wait for the iframe to be ready + await MessageBus.getInstance().isReady(); + + await MessageBus.getInstance().importBackup(profileData); + + // Clean up message listener after import + MessageBus.getInstance().destroyMessageListener(); + setShowIframeForImport(false); + router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); } catch (error: any) { setValidationErrors([ @@ -280,6 +279,7 @@ export default function Login() { ]); } finally { setIsLoading(false); + setShowIframeForImport(false); } }, [router]); @@ -337,6 +337,7 @@ export default function Login() { /> )} + {showIframeForImport &&