From 96ee5b03e6dd73eeb41ed10861447fce579ce88d Mon Sep 17 00:00:00 2001 From: NicolasCantu Date: Fri, 28 Nov 2025 00:13:51 +0100 Subject: [PATCH] refactor(iframe-controller): standardize import statements, enhance logging, and improve error handling in IframeController service --- src/services/iframe-controller.service.ts | 331 +++++++++++++++------- 1 file changed, 225 insertions(+), 106 deletions(-) diff --git a/src/services/iframe-controller.service.ts b/src/services/iframe-controller.service.ts index 006c208..8f1dbf4 100644 --- a/src/services/iframe-controller.service.ts +++ b/src/services/iframe-controller.service.ts @@ -1,9 +1,9 @@ -import { MessageType } from '../types/index'; -import Services from './service'; -import TokenService from './token.service'; -import { cleanSubscriptions } from '../utils/subscription.utils'; -import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils'; -import { MerkleProofResult } from '../../pkg/sdk_client'; +import { MessageType } from "../types/index"; +import Services from "./service"; +import TokenService from "./token.service"; +import { cleanSubscriptions } from "../utils/subscription.utils"; +import { splitPrivateData, isValid32ByteHex } from "../utils/service.utils"; +import { MerkleProofResult } from "../../pkg/sdk_client"; export class IframeController { private static isInitialized = false; // <--- VERROU @@ -13,39 +13,57 @@ export class IframeController { // On ne lance l'écoute que si on est dans une iframe if (window.self !== window.top) { - console.log('[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API...'); + console.log( + "[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API..." + ); await IframeController.registerAllListeners(); } else { - console.log("[IframeController] ℹ️ Mode Standalone (pas d'iframe). Listeners API inactifs."); + console.log( + "[IframeController] ℹ️ Mode Standalone (pas d'iframe). Listeners API inactifs." + ); } } private static async registerAllListeners() { - console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...'); + console.log( + "[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)..." + ); const services = await Services.getInstance(); const tokenService = await TokenService.getInstance(); /** * Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A). */ - const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { - console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`); + const errorResponse = ( + errorMsg: string, + origin: string, + messageId?: string + ) => { + console.error( + `[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})` + ); window.parent.postMessage( { type: MessageType.ERROR, error: errorMsg, messageId, }, - origin, + origin ); }; // Helper pour vérifier le token avant chaque action sensible - const withToken = async (event: MessageEvent, action: () => Promise) => { + const withToken = async ( + event: MessageEvent, + action: () => Promise + ) => { const { accessToken } = event.data; // On vérifie si le token est présent ET valide pour l'origine de l'iframe - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); + if ( + !accessToken || + !(await tokenService.validateToken(accessToken, event.origin)) + ) { + throw new Error("Invalid or expired session token"); } // Si tout est bon, on exécute l'action await action(); @@ -54,35 +72,53 @@ export class IframeController { // --- Définitions des gestionnaires (Handlers) --- const handleRequestLink = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`); + console.log( + `[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}` + ); // 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion) const device = await services.getDeviceFromDatabase(); if (device && device.pairing_process_commitment) { - console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts."); + console.log( + "[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts." + ); // On saute l'attente et on passe directement à la suite. } else { // 2. Cas de la 1ère connexion (appareil non appairé) // On doit attendre que home.ts (auto-pairing) ait fini son travail. - console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...'); - const maxWait = 5000; // 5 sec - let waited = 0; - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + await new Promise((resolve, reject) => { + // Fonction de nettoyage pour éviter les fuites de mémoire + const cleanup = () => { + document.removeEventListener( + "app:pairing-ready", + handler as EventListener + ); + clearTimeout(timeoutId); + }; - // On attend le drapeau global - while (!(window as any).__PAIRING_READY && waited < maxWait) { - await delay(100); - waited += 100; - } + // Le gestionnaire de l'événement + const handler = (e: CustomEvent) => { + cleanup(); + if (e.detail && e.detail.success) { + resolve(); + } else { + reject(new Error(e.detail?.error || "Auto-pairing failed")); + } + }; - // 3. Vérifier le résultat de l'attente - if ((window as any).__PAIRING_READY === 'error') { - throw new Error('Auto-pairing failed'); - } - if (!(window as any).__PAIRING_READY) { - throw new Error('Auto-pairing timed out'); - } + // Timeout de sécurité (5 secondes) + const timeoutId = setTimeout(() => { + cleanup(); + reject(new Error("Auto-pairing timed out (Event not received)")); + }, 5000); + + // On écoute l'événement qu'on a créé dans Home.ts + document.addEventListener( + "app:pairing-ready", + handler as EventListener + ); + }); console.log(`[Router:API] Feu vert de home.ts reçu !`); } @@ -98,50 +134,69 @@ export class IframeController { refreshToken: tokens.refreshToken, messageId: event.data.messageId, }, - event.origin, + event.origin + ); + console.log( + `[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.` ); - console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`); }; const handleCreatePairing = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`); if (services.isPaired()) { - throw new Error('Device already paired — ignoring CREATE_PAIRING request'); + throw new Error( + "Device already paired — ignoring CREATE_PAIRING request" + ); } await withToken(event, async () => { console.log("[Router:API] 🚀 Démarrage du processus d'appairage..."); const myAddress = services.getDeviceAddress(); - console.log('[Router:API] 1/7: Création du processus de pairing...'); - const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); + console.log("[Router:API] 1/7: Création du processus de pairing..."); + const createPairingProcessReturn = await services.createPairingProcess( + "", + [myAddress] + ); - const pairingId = createPairingProcessReturn.updated_process?.process_id; - const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; + const pairingId = + createPairingProcessReturn.updated_process?.process_id; + const stateId = createPairingProcessReturn.updated_process + ?.current_process?.states[0]?.state_id as string; if (!pairingId || !stateId) { - throw new Error('Pairing process creation failed to return valid IDs'); + throw new Error( + "Pairing process creation failed to return valid IDs" + ); } console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`); console.log("[Router:API] 3/7: Enregistrement local de l'appareil..."); services.pairDevice(pairingId, [myAddress]); - console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...'); + console.log( + "[Router:API] 4/7: Traitement du retour (handleApiReturn)..." + ); await services.handleApiReturn(createPairingProcessReturn); - console.log('[Router:API] 5/7: Création de la mise à jour PRD...'); - const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); + console.log("[Router:API] 5/7: Création de la mise à jour PRD..."); + const createPrdUpdateReturn = await services.createPrdUpdate( + pairingId, + stateId + ); await services.handleApiReturn(createPrdUpdateReturn); - console.log('[Router:API] 6/7: Approbation du changement...'); - const approveChangeReturn = await services.approveChange(pairingId, stateId); + console.log("[Router:API] 6/7: Approbation du changement..."); + const approveChangeReturn = await services.approveChange( + pairingId, + stateId + ); await services.handleApiReturn(approveChangeReturn); - console.log('[Router:API] 7/7: Confirmation finale du pairing...'); + console.log("[Router:API] 7/7: Confirmation finale du pairing..."); // await services.confirmPairing(); - console.log('[Router:API] 🎉 Appairage terminé avec succès !'); + console.log("[Router:API] 🎉 Appairage terminé avec succès !"); const successMsg = { type: MessageType.PAIRING_CREATED, @@ -153,8 +208,10 @@ export class IframeController { }; const handleGetMyProcesses = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + console.log( + `[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu` + ); + if (!services.isPaired()) throw new Error("Device not paired"); await withToken(event, async () => { const myProcesses = await services.getMyProcesses(); @@ -165,14 +222,14 @@ export class IframeController { myProcesses, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleGetProcesses = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); await withToken(event, async () => { const processes = await services.getProcesses(); @@ -183,14 +240,14 @@ export class IframeController { processes, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleDecryptState = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); const { processId, stateId } = event.data; @@ -199,22 +256,36 @@ export class IframeController { if (!process) throw new Error("Can't find process"); const state = services.getStateFromId(process, stateId); - if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`); + if (!state) + throw new Error(`Unknown state ${stateId} for process ${processId}`); - console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`); + console.log( + `[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}` + ); await services.ensureConnections(process, stateId); const res: Record = {}; for (const attribute of Object.keys(state.pcd_commitment)) { - if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) { + if ( + attribute === "roles" || + (state.public_data && state.public_data[attribute]) + ) { continue; } - const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); + const decryptedAttribute = await services.decryptAttribute( + processId, + state, + attribute + ); if (decryptedAttribute) { res[attribute] = decryptedAttribute; } } - console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`); + console.log( + `[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${ + Object.keys(res).length + } attribut(s) déchiffré(s).` + ); window.parent.postMessage( { @@ -222,7 +293,7 @@ export class IframeController { data: res, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; @@ -232,10 +303,13 @@ export class IframeController { const accessToken = event.data.accessToken; const refreshToken = event.data.refreshToken; if (!accessToken || !refreshToken) { - throw new Error('Missing access, refresh token or both'); + throw new Error("Missing access, refresh token or both"); } - const isValid = await tokenService.validateToken(accessToken, event.origin); + const isValid = await tokenService.validateToken( + accessToken, + event.origin + ); console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`); window.parent.postMessage( { @@ -245,17 +319,21 @@ export class IframeController { isValid: isValid, messageId: event.data.messageId, }, - event.origin, + event.origin ); }; const handleRenewToken = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`); const refreshToken = event.data.refreshToken; - if (!refreshToken) throw new Error('No refresh token provided'); + if (!refreshToken) throw new Error("No refresh token provided"); - const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); - if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)'); + const newAccessToken = await tokenService.refreshAccessToken( + refreshToken, + event.origin + ); + if (!newAccessToken) + throw new Error("Failed to refresh token (invalid refresh token)"); console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`); window.parent.postMessage( @@ -265,7 +343,7 @@ export class IframeController { refreshToken: refreshToken, messageId: event.data.messageId, }, - event.origin, + event.origin ); }; @@ -285,19 +363,29 @@ export class IframeController { if (device && device.pairing_process_commitment) { // SUCCÈS ! L'ID est dans la BDD pairingId = device.pairing_process_commitment; - console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`); + console.log( + `[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${ + i + 1 + }/${maxRetries})` + ); break; // On sort de la boucle } // Si non trouvé, on patiente - console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`); + console.warn( + `[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${ + i + 1 + }/${maxRetries})` + ); await new Promise((resolve) => setTimeout(resolve, retryDelay)); } // Si la boucle se termine sans succès if (!pairingId) { - console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`); - throw new Error('Device not paired'); + console.error( + `[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.` + ); + throw new Error("Device not paired"); } await withToken(event, async () => { @@ -307,31 +395,42 @@ export class IframeController { userPairingId: pairingId, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleCreateProcess = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); const { processData, privateFields, roles } = event.data; await withToken(event, async () => { - console.log('[Router:API] 🚀 Démarrage de la création de processus standard...'); - const { privateData, publicData } = splitPrivateData(processData, privateFields); + console.log( + "[Router:API] 🚀 Démarrage de la création de processus standard..." + ); + const { privateData, publicData } = splitPrivateData( + processData, + privateFields + ); - console.log('[Router:API] 1/2: Création du processus...'); - const createProcessReturn = await services.createProcess(privateData, publicData, roles); + console.log("[Router:API] 1/2: Création du processus..."); + const createProcessReturn = await services.createProcess( + privateData, + publicData, + roles + ); if (!createProcessReturn.updated_process) { - throw new Error('Empty updated_process in createProcessReturn'); + throw new Error("Empty updated_process in createProcessReturn"); } const processId = createProcessReturn.updated_process.process_id; const process = createProcessReturn.updated_process.current_process; const stateId = process.states[0].state_id; - console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`); + console.log( + `[Router:API] 2/2: Processus ${processId} créé. Traitement...` + ); await services.handleApiReturn(createProcessReturn); console.log(`[Router:API] 🎉 Processus ${processId} créé.`); @@ -348,19 +447,19 @@ export class IframeController { processCreated: res, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleNotifyUpdate = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); const { processId, stateId } = event.data; await withToken(event, async () => { - if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id'); + if (!isValid32ByteHex(stateId)) throw new Error("Invalid state id"); const res = await services.createPrdUpdate(processId, stateId); await services.handleApiReturn(res); @@ -370,14 +469,14 @@ export class IframeController { type: MessageType.UPDATE_NOTIFIED, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleValidateState = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); const { processId, stateId } = event.data; @@ -391,22 +490,29 @@ export class IframeController { validatedProcess: res.updated_process, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleUpdateProcess = async (event: MessageEvent) => { console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + if (!services.isPaired()) throw new Error("Device not paired"); const { processId, newData, privateFields, roles } = event.data; await withToken(event, async () => { - console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`); + console.log( + `[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...` + ); // Le service gère maintenant tout : récupération, réparation d'état, et mise à jour. - const res = await services.updateProcess(processId, newData, privateFields, roles); + const res = await services.updateProcess( + processId, + newData, + privateFields, + roles + ); // Nous appelons handleApiReturn ici, comme avant. await services.handleApiReturn(res); @@ -418,14 +524,16 @@ export class IframeController { updatedProcess: res.updated_process, // res vient directement de l'appel service messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleDecodePublicData = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); + console.log( + `[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu` + ); + if (!services.isPaired()) throw new Error("Device not paired"); const { encodedData } = event.data; @@ -437,7 +545,7 @@ export class IframeController { decodedData, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; @@ -454,30 +562,37 @@ export class IframeController { hash, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleGetMerkleProof = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`); + console.log( + `[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu` + ); const { processState, attributeName } = event.data; await withToken(event, async () => { - const proof = services.getMerkleProofForFile(processState, attributeName); + const proof = services.getMerkleProofForFile( + processState, + attributeName + ); window.parent.postMessage( { type: MessageType.MERKLE_PROOF_RETRIEVED, proof, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; const handleValidateMerkleProof = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`); + console.log( + `[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu` + ); const { merkleProof, documentHash } = event.data; await withToken(event, async () => { @@ -485,25 +600,28 @@ export class IframeController { try { parsedMerkleProof = JSON.parse(merkleProof); } catch (e) { - throw new Error('Provided merkleProof is not a valid json object'); + throw new Error("Provided merkleProof is not a valid json object"); } - const res = services.validateMerkleProof(parsedMerkleProof, documentHash); + const res = services.validateMerkleProof( + parsedMerkleProof, + documentHash + ); window.parent.postMessage( { type: MessageType.MERKLE_PROOF_VALIDATED, isValid: res, messageId: event.data.messageId, }, - event.origin, + event.origin ); }); }; // --- Le "Switchyard" : il reçoit tous les messages et les dispatche --- - window.removeEventListener('message', handleMessage); - window.addEventListener('message', handleMessage); + window.removeEventListener("message", handleMessage); + window.addEventListener("message", handleMessage); async function handleMessage(event: MessageEvent) { try { @@ -557,7 +675,7 @@ export class IframeController { await handleValidateMerkleProof(event); break; default: - console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data); + console.warn("[Router:API] ⚠️ Message non géré reçu:", event.data); } } catch (error: any) { const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`; @@ -569,9 +687,10 @@ export class IframeController { { type: MessageType.LISTENING, }, - '*', + "*" + ); + console.log( + "[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent." ); - console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.'); } } -