Compare commits

..

4 Commits

8 changed files with 835 additions and 653 deletions

View File

@ -1,6 +1,3 @@
VITE_API_URL=https://api.example.com
VITE_API_KEY=your_api_key
VITE_JWT_SECRET_KEY=your_secret_key
VITE_BASEURL="your_base_url" VITE_BASEURL="your_base_url"
VITE_BOOTSTRAPURL="your_bootstrap_url" VITE_BOOTSTRAPURL="your_bootstrap_url"
VITE_STORAGEURL="your_storage_url" VITE_STORAGEURL="your_storage_url"

View File

@ -1,82 +1,75 @@
const EMPTY32BYTES = String('').padStart(64, '0'); // public/data.worker.js
const DB_NAME = "4nk";
const DB_VERSION = 1;
const EMPTY32BYTES = String("").padStart(64, "0");
// ============================================ // ============================================
// SERVICE WORKER LIFECYCLE // SERVICE WORKER LIFECYCLE
// ============================================ // ============================================
self.addEventListener('install', (event) => { self.addEventListener("install", (event) => {
event.waitUntil(self.skipWaiting()); event.waitUntil(self.skipWaiting());
}); });
self.addEventListener('activate', (event) => { self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim()); event.waitUntil(self.clients.claim());
}); });
// ============================================ // ============================================
// MESSAGE HANDLER // INDEXEDDB DIRECT ACCESS (READ-ONLY)
// ============================================ // ============================================
self.addEventListener('message', async (event) => { /**
const data = event.data; * Ouvre une connexion à la BDD directement depuis le Service Worker
console.log('[Service Worker] Message received:', data.type); */
function openDB() {
if (data.type === 'SCAN') {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length != 0) {
const scanResult = await scanMissingData(myProcessesId, event.source);
if (scanResult.toDownload.length != 0) {
console.log('[Service Worker] Sending TO_DOWNLOAD message');
event.source.postMessage({ type: 'TO_DOWNLOAD', data: scanResult.toDownload });
}
if (scanResult.diffsToCreate.length > 0) {
console.log('[Service Worker] Sending DIFFS_TO_CREATE message');
event.source.postMessage({ type: 'DIFFS_TO_CREATE', data: scanResult.diffsToCreate });
}
} else {
event.source.postMessage({ status: 'error', message: 'Empty lists' });
}
} catch (error) {
console.error('[Service Worker] Scan error:', error);
event.source.postMessage({ status: 'error', message: error.message });
}
}
});
// ============================================
// DATABASE COMMUNICATION
// ============================================
async function requestFromMainThread(client, action, payload) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const messageId = `sw_${Date.now()}_${Math.random()}`; const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
const messageHandler = (event) => { request.onsuccess = () => resolve(request.result);
if (event.data.id === messageId) {
self.removeEventListener('message', messageHandler);
if (event.data.type === 'DB_RESPONSE') {
resolve(event.data.result);
} else if (event.data.type === 'DB_ERROR') {
reject(new Error(event.data.error));
}
}
};
self.addEventListener('message', messageHandler);
client.postMessage({
type: 'DB_REQUEST',
id: messageId,
action,
payload
}); });
}
setTimeout(() => { /**
self.removeEventListener('message', messageHandler); * Récupère un objet spécifique (équivalent à GET_OBJECT)
reject(new Error('Database request timeout')); */
}, 10000); function getObject(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS)
* Optimisé pour utiliser une seule transaction.
*/
function getMultipleObjects(db, storeName, keys) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const results = [];
let completed = 0;
if (keys.length === 0) resolve([]);
keys.forEach((key) => {
const request = store.get(key);
request.onsuccess = () => {
if (request.result) results.push(request.result);
completed++;
if (completed === keys.length) resolve(results);
};
request.onerror = () => {
console.warn(`[SW] Erreur lecture clé ${key}`);
completed++;
if (completed === keys.length) resolve(results);
};
});
}); });
} }
@ -84,39 +77,56 @@ async function requestFromMainThread(client, action, payload) {
// SCAN LOGIC // SCAN LOGIC
// ============================================ // ============================================
async function scanMissingData(processesToScan, client) { async function scanMissingData(processesToScan) {
console.log('[Service Worker] Scanning for missing data...'); console.log("[Service Worker] 🚀 Scanning with DIRECT DB ACCESS...");
const myProcesses = await requestFromMainThread(client, 'GET_MULTIPLE_OBJECTS', { let db;
storeName: 'processes', try {
keys: processesToScan db = await openDB();
}); } catch (e) {
console.error("[SW] Impossible d'ouvrir la BDD:", e);
return { toDownload: [], diffsToCreate: [] };
}
// 1. Récupération directe des processus
const myProcesses = await getMultipleObjects(
db,
"processes",
processesToScan
);
let toDownload = new Set(); let toDownload = new Set();
let diffsToCreate = []; let diffsToCreate = [];
if (myProcesses && myProcesses.length != 0) { if (myProcesses && myProcesses.length !== 0) {
for (const process of myProcesses) { for (const process of myProcesses) {
if (!process || !process.states) continue;
const firstState = process.states[0]; const firstState = process.states[0];
// Sécurisation : on vérifie que firstState existe
if (!firstState) continue;
const processId = firstState.commited_in; const processId = firstState.commited_in;
for (const state of process.states) { for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue; if (state.state_id === EMPTY32BYTES) continue;
for (const [field, hash] of Object.entries(state.pcd_commitment)) { for (const [field, hash] of Object.entries(state.pcd_commitment)) {
if (state.public_data[field] !== undefined || field === 'roles') continue; // On ignore les données publiques ou les rôles
if (
(state.public_data && state.public_data[field] !== undefined) ||
field === "roles"
)
continue;
const existingData = await requestFromMainThread(client, 'GET_OBJECT', { // 2. Vérification directe dans 'data'
storeName: 'data', const existingData = await getObject(db, "data", hash);
key: hash
});
if (!existingData) { if (!existingData) {
toDownload.add(hash); toDownload.add(hash);
const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', { // 3. Vérification directe dans 'diffs'
storeName: 'diffs', const existingDiff = await getObject(db, "diffs", hash);
key: hash
});
if (!existingDiff) { if (!existingDiff) {
diffsToCreate.push({ diffsToCreate.push({
@ -130,12 +140,13 @@ async function scanMissingData(processesToScan, client) {
new_value: null, new_value: null,
notify_user: false, notify_user: false,
need_validation: false, need_validation: false,
validation_status: 'None' validation_status: "None",
}); });
} }
} else { } else {
if (toDownload.delete(hash)) { // Si on a trouvé la donnée, on est sûr de ne pas avoir besoin de la télécharger
console.log(`[Service Worker] Removing ${hash} from the set`); if (toDownload.has(hash)) {
toDownload.delete(hash);
} }
} }
} }
@ -143,10 +154,50 @@ async function scanMissingData(processesToScan, client) {
} }
} }
console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length }); // On ferme la connexion BDD pour libérer les ressources
db.close();
console.log("[Service Worker] Scan complete:", {
toDownload: toDownload.size,
diffsToCreate: diffsToCreate.length,
});
return { return {
toDownload: Array.from(toDownload), toDownload: Array.from(toDownload),
diffsToCreate: diffsToCreate diffsToCreate: diffsToCreate,
}; };
} }
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener("message", async (event) => {
const data = event.data;
if (data.type === "SCAN") {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length !== 0) {
// Appel direct de la nouvelle fonction optimisée
const scanResult = await scanMissingData(myProcessesId);
if (scanResult.toDownload.length !== 0) {
event.source.postMessage({
type: "TO_DOWNLOAD",
data: scanResult.toDownload,
});
}
if (scanResult.diffsToCreate.length > 0) {
event.source.postMessage({
type: "DIFFS_TO_CREATE",
data: scanResult.diffsToCreate,
});
}
}
} catch (error) {
console.error("[Service Worker] Scan error:", error);
// On évite de spammer l'UI avec des erreurs internes du worker
}
}
});

View File

@ -1,14 +1,19 @@
// src/pages/process/Home.ts // src/pages/process/Home.ts
import Services from '../../services/service'; import Services from "../../services/service";
import globalCss from '../../assets/styles/style.css?inline'; import globalCss from "../../assets/styles/style.css?inline";
import homeHtml from './home.html?raw'; import homeHtml from "./home.html?raw";
import { displayEmojis, generateCreateBtn, prepareAndSendPairingTx, addressToEmoji } from '../../utils/sp-address.utils'; import {
import { Router } from '../../router/index'; displayEmojis,
generateCreateBtn,
prepareAndSendPairingTx,
addressToEmoji,
} from "../../utils/sp-address.utils";
import { Router } from "../../router/index";
export class HomePage extends HTMLElement { export class HomePage extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
} }
connectedCallback() { connectedCallback() {
@ -120,26 +125,36 @@ export class HomePage extends HTMLElement {
const container = this.shadowRoot; const container = this.shadowRoot;
if (!container) return; if (!container) return;
(window as any).__PAIRING_READY = false; const loaderDiv = container.querySelector(
const loaderDiv = container.querySelector('#iframe-loader') as HTMLDivElement; "#iframe-loader"
const mainContentDiv = container.querySelector('#main-content') as HTMLDivElement; ) as HTMLDivElement;
const tabs = container.querySelectorAll('.tab'); const mainContentDiv = container.querySelector(
"#main-content"
) as HTMLDivElement;
const tabs = container.querySelectorAll(".tab");
tabs.forEach((tab) => { tabs.forEach((tab) => {
tab.addEventListener('click', () => { tab.addEventListener("click", () => {
// Remplacement de addSubscription pour simplifier ici // Remplacement de addSubscription pour simplifier ici
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); container
tab.classList.add('active'); .querySelectorAll(".tab")
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active')); .forEach((t) => t.classList.remove("active"));
container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active'); tab.classList.add("active");
container
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
container
.querySelector(`#${tab.getAttribute("data-tab") as string}`)
?.classList.add("active");
}); });
}); });
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
try { try {
await delay(500); await delay(500);
this.addLoaderStep('Initialisation des services...'); this.addLoaderStep("Initialisation des services...");
const service = await Services.getInstance(); const service = await Services.getInstance();
await delay(700); await delay(700);
@ -149,42 +164,51 @@ export class HomePage extends HTMLElement {
if (pairingId) { if (pairingId) {
await delay(300); await delay(300);
this.addLoaderStep('Appairage existant trouvé.'); this.addLoaderStep("Appairage existant trouvé.");
service.setProcessId(pairingId); service.setProcessId(pairingId);
} else { } else {
await delay(300); await delay(300);
this.addLoaderStep("Création d'un appairage sécurisé..."); this.addLoaderStep("Création d'un appairage sécurisé...");
await prepareAndSendPairingTx(); await prepareAndSendPairingTx();
this.addLoaderStep('Appairage créé avec succès.'); this.addLoaderStep("Appairage créé avec succès.");
} }
// --- SUCCÈS --- // --- SUCCÈS ---
(window as any).__PAIRING_READY = true; console.log("[Home] Auto-pairing terminé avec succès.");
console.log('[Home] Auto-pairing terminé avec succès.'); document.dispatchEvent(
new CustomEvent("app:pairing-ready", {
detail: { success: true },
})
);
if (window.self !== window.top) { if (window.self !== window.top) {
// CAS IFRAME : On ne bouge pas ! // CAS IFRAME : On ne bouge pas !
// On affiche juste un état "Prêt" dans le loader pour le debug visuel // On affiche juste un état "Prêt" dans le loader pour le debug visuel
this.addLoaderStep("Prêt. En attente de l'application parente..."); this.addLoaderStep("Prêt. En attente de l'application parente...");
console.log('[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API.'); console.log(
"[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API."
);
} else { } else {
// CAS STANDALONE : On redirige // CAS STANDALONE : On redirige
console.log('[Home] 🚀 Mode Standalone : Redirection vers /process...'); console.log("[Home] 🚀 Mode Standalone : Redirection vers /process...");
await delay(500); await delay(500);
// On nettoie l'UI avant de partir // On nettoie l'UI avant de partir
if (loaderDiv) loaderDiv.style.display = 'none'; if (loaderDiv) loaderDiv.style.display = "none";
if (mainContentDiv) mainContentDiv.style.display = 'block'; if (mainContentDiv) mainContentDiv.style.display = "block";
// Hop, on navigue // Hop, on navigue
Router.navigate('process'); Router.navigate("process");
} }
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); container
container.querySelector('[data-tab="tab2"]')?.classList.add('active'); .querySelectorAll(".tab")
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active')); .forEach((t) => t.classList.remove("active"));
container.querySelector('#tab2')?.classList.add('active'); container.querySelector('[data-tab="tab2"]')?.classList.add("active");
container
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
container.querySelector("#tab2")?.classList.add("active");
const spAddress = await service.getDeviceAddress(); const spAddress = await service.getDeviceAddress();
generateCreateBtn(); generateCreateBtn();
@ -193,28 +217,35 @@ export class HomePage extends HTMLElement {
await delay(1000); await delay(1000);
if (loaderDiv) loaderDiv.style.display = 'none'; if (loaderDiv) loaderDiv.style.display = "none";
if (mainContentDiv) mainContentDiv.style.display = 'block'; if (mainContentDiv) mainContentDiv.style.display = "block";
console.log('[Home] Init terminée.'); console.log("[Home] Init terminée.");
(window as any).__PAIRING_READY = true;
} catch (e: any) { } catch (e: any) {
console.error('[Home] Erreur:', e); console.error("[Home] Erreur:", e);
this.addLoaderStep(`Erreur: ${e.message}`); this.addLoaderStep(`Erreur: ${e.message}`);
(window as any).__PAIRING_READY = 'error'; document.dispatchEvent(
new CustomEvent("app:pairing-ready", {
detail: { success: false, error: e.message },
})
);
} }
} }
addLoaderStep(text: string) { addLoaderStep(text: string) {
const container = this.shadowRoot; const container = this.shadowRoot;
if (!container) return; if (!container) return;
const currentStep = container.querySelector('.loader-step.active') as HTMLParagraphElement; const currentStep = container.querySelector(
if (currentStep) currentStep.classList.remove('active'); ".loader-step.active"
) as HTMLParagraphElement;
if (currentStep) currentStep.classList.remove("active");
const stepsContainer = container.querySelector('#loader-steps-container') as HTMLDivElement; const stepsContainer = container.querySelector(
"#loader-steps-container"
) as HTMLDivElement;
if (stepsContainer) { if (stepsContainer) {
const newStep = document.createElement('p'); const newStep = document.createElement("p");
newStep.className = 'loader-step active'; newStep.className = "loader-step active";
newStep.textContent = text; newStep.textContent = text;
stepsContainer.appendChild(newStep); stepsContainer.appendChild(newStep);
} }
@ -223,7 +254,9 @@ export class HomePage extends HTMLElement {
async populateMemberSelect() { async populateMemberSelect() {
const container = this.shadowRoot; const container = this.shadowRoot;
if (!container) return; if (!container) return;
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement; const memberSelect = container.querySelector(
"#memberSelect"
) as HTMLSelectElement;
if (!memberSelect) return; if (!memberSelect) return;
const service = await Services.getInstance(); const service = await Services.getInstance();
@ -231,7 +264,7 @@ export class HomePage extends HTMLElement {
for (const [processId, member] of Object.entries(members)) { for (const [processId, member] of Object.entries(members)) {
const emojis = await addressToEmoji(processId); const emojis = await addressToEmoji(processId);
const option = document.createElement('option'); const option = document.createElement("option");
option.value = processId; option.value = processId;
option.textContent = `Member (${emojis})`; option.textContent = `Member (${emojis})`;
memberSelect.appendChild(option); memberSelect.appendChild(option);
@ -239,5 +272,4 @@ export class HomePage extends HTMLElement {
} }
} }
customElements.define('home-page', HomePage); customElements.define("home-page", HomePage);

View File

@ -1,7 +1,8 @@
import processHtml from './process.html?raw'; // src/pages/process/ProcessList.ts
import globalCss from '../../assets/styles/style.css?inline';
import Services from '../../services/service'; import processHtml from "./process.html?raw";
import { Router } from '../../router'; import globalCss from "../../assets/styles/style.css?inline";
import Services from "../../services/service";
export class ProcessListPage extends HTMLElement { export class ProcessListPage extends HTMLElement {
private services!: Services; private services!: Services;
@ -16,187 +17,54 @@ export class ProcessListPage extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
} }
async connectedCallback() { async connectedCallback() {
this.services = await Services.getInstance(); this.services = await Services.getInstance();
this.render(); this.render();
// Petit délai pour s'assurer que le DOM est prêt
setTimeout(() => this.initLogic(), 0); setTimeout(() => this.initLogic(), 0);
} }
render() { render() {
if (this.shadowRoot) { if (this.shadowRoot) {
// Le CSS et le HTML de base sont statiques, donc innerHTML est OK ici.
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
${globalCss} ${globalCss}
/* ... (styles identiques à ton fichier d'origine, je ne les répète pas pour abréger) ... */
:host { :host { display: block; width: 100%; }
display: block; .process-layout { padding: 2rem; display: flex; justify-content: center; }
width: 100%; .dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
}
.process-layout {
padding: 2rem;
display: flex;
justify-content: center;
}
.dashboard-container {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 1.5rem;
max-height: 85vh; /* Pour scroller dedans si besoin */
overflow-y: auto;
}
.dashboard-header { text-align: center; } .dashboard-header { text-align: center; }
.subtitle { color: var(--text-muted); margin-top: -0.5rem; } .subtitle { color: var(--text-muted); margin-top: -0.5rem; }
.search-input-container { position: relative; display: flex; align-items: center; }
/* Search Styles */ .search-input-container input { padding-right: 40px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); transition: all 0.3s; }
.search-input-container { .search-input-container input:focus { background: rgba(255,255,255,0.1); border-color: var(--primary); }
position: relative; .search-icon { position: absolute; right: 12px; opacity: 0.5; }
display: flex; .autocomplete-dropdown { list-style: none; margin-top: 5px; padding: 0; background: #1e293b; border: 1px solid var(--glass-border); border-radius: var(--radius-sm); max-height: 200px; overflow-y: auto; display: none; position: absolute; width: 100%; z-index: 10; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
align-items: center;
}
.search-input-container input {
padding-right: 40px; /* Place pour l'icone */
background: rgba(255,255,255,0.05);
border: 1px solid var(--glass-border);
transition: all 0.3s;
}
.search-input-container input:focus {
background: rgba(255,255,255,0.1);
border-color: var(--primary);
}
.search-icon {
position: absolute;
right: 12px;
opacity: 0.5;
}
/* Autocomplete List */
.autocomplete-dropdown {
list-style: none;
margin-top: 5px;
padding: 0;
background: #1e293b; /* Fond opaque pour lisibilité */
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
max-height: 200px;
overflow-y: auto;
display: none; /* Caché par défaut */
position: absolute;
width: 100%;
z-index: 10;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
/* Position relative pour que le dropdown s'aligne */
.custom-select-wrapper { position: relative; } .custom-select-wrapper { position: relative; }
.autocomplete-dropdown li { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.2s; color: var(--text-main); }
.autocomplete-dropdown li { .autocomplete-dropdown li:hover { background: var(--primary); color: white; }
padding: 10px 15px; .autocomplete-dropdown li.my-process { border-left: 3px solid var(--accent); }
cursor: pointer; .tags-container { display: flex; flex-wrap: wrap; gap: 8px; min-height: 30px; }
border-bottom: 1px solid rgba(255,255,255,0.05); .tag { background: rgba(var(--primary-hue), 50, 50, 0.3); border: 1px solid var(--primary); color: white; padding: 4px 10px; border-radius: 20px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px; animation: popIn 0.2s ease-out; }
transition: background 0.2s; .tag-close { cursor: pointer; opacity: 0.7; font-weight: bold; }
color: var(--text-main);
}
.autocomplete-dropdown li:hover {
background: var(--primary);
color: white;
}
.autocomplete-dropdown li.my-process {
border-left: 3px solid var(--accent);
}
/* Tags */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 30px;
}
.tag {
background: rgba(var(--primary-hue), 50, 50, 0.3);
border: 1px solid var(--primary);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 8px;
animation: popIn 0.2s ease-out;
}
.tag-close {
cursor: pointer;
opacity: 0.7;
font-weight: bold;
}
.tag-close:hover { opacity: 1; } .tag-close:hover { opacity: 1; }
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; } .divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
.details-content { background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); padding: 1rem; min-height: 100px; }
/* Process Details */
.details-content {
background: rgba(0,0,0,0.2);
border-radius: var(--radius-sm);
padding: 1rem;
min-height: 100px;
}
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; } .empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
.process-item { margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem; }
.process-item { .process-title-display { font-size: 1.1rem; font-weight: bold; color: var(--accent); margin-bottom: 0.5rem; }
margin-bottom: 1rem; .state-element { background: rgba(255,255,255,0.05); padding: 8px 12px; margin-top: 5px; border-radius: 4px; cursor: pointer; transition: background 0.2s; border: 1px solid transparent; font-family: monospace; font-size: 0.9rem; }
border-bottom: 1px solid var(--glass-border);
padding-bottom: 1rem;
}
.process-title-display {
font-size: 1.1rem;
font-weight: bold;
color: var(--accent);
margin-bottom: 0.5rem;
}
.state-element {
background: rgba(255,255,255,0.05);
padding: 8px 12px;
margin-top: 5px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
border: 1px solid transparent;
font-family: monospace;
font-size: 0.9rem;
}
.state-element:hover { background: rgba(255,255,255,0.1); } .state-element:hover { background: rgba(255,255,255,0.1); }
.state-element.selected { background: rgba(var(--success), 0.2); border-color: var(--success); }
.state-element.selected { .dashboard-footer { display: flex; justify-content: flex-end; }
background: rgba(var(--success), 0.2);
border-color: var(--success);
}
.dashboard-footer {
display: flex;
justify-content: flex-end;
}
/* Scrollbar custom */
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); } ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
</style> </style>
${processHtml} ${processHtml}
`; `;
@ -207,47 +75,50 @@ export class ProcessListPage extends HTMLElement {
const root = this.shadowRoot; const root = this.shadowRoot;
if (!root) return; if (!root) return;
// Récupération des éléments this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
this.wrapper = root.querySelector('#autocomplete-wrapper') as HTMLElement; this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
this.inputInput = root.querySelector('#process-input') as HTMLInputElement; this.autocompleteList = root.querySelector(
this.autocompleteList = root.querySelector('#autocomplete-list') as HTMLUListElement; "#autocomplete-list"
this.tagsContainer = root.querySelector('#selected-tags-container') as HTMLElement; ) as HTMLUListElement;
this.detailsContainer = root.querySelector('#process-details') as HTMLElement; this.tagsContainer = root.querySelector(
this.okButton = root.querySelector('#go-to-process-btn') as HTMLButtonElement; "#selected-tags-container"
) as HTMLElement;
this.detailsContainer = root.querySelector(
"#process-details"
) as HTMLElement;
this.okButton = root.querySelector(
"#go-to-process-btn"
) as HTMLButtonElement;
// Listeners this.inputInput.addEventListener("keyup", () => this.handleInput());
this.inputInput.addEventListener('keyup', () => this.handleInput()); this.inputInput.addEventListener("click", () => this.openDropdown());
this.inputInput.addEventListener('click', () => this.openDropdown());
// Fermeture du dropdown au clic extérieur document.addEventListener("click", (e) => {
document.addEventListener('click', (e) => {
const path = e.composedPath(); const path = e.composedPath();
if (!path.includes(this.wrapper)) { if (!path.includes(this.wrapper)) {
this.closeDropdown(); this.closeDropdown();
} }
}); });
this.okButton.addEventListener('click', () => this.goToProcess()); this.okButton.addEventListener("click", () => this.goToProcess());
// Écoute des mises à jour du service document.addEventListener("processes-updated", async () => {
document.addEventListener('processes-updated', async () => {
await this.populateList(this.inputInput.value); await this.populateList(this.inputInput.value);
}); });
// Chargement initial await this.populateList("");
await this.populateList('');
} }
// --- Logique Autocomplete --- // --- Logique Autocomplete Sécurisée ---
async populateList(query: string) { async populateList(query: string) {
this.autocompleteList.innerHTML = ''; this.autocompleteList.innerHTML = ""; // Vide la liste proprement
const mineArray = (await this.services.getMyProcesses()) ?? []; const mineArray = (await this.services.getMyProcesses()) ?? [];
const allProcesses = await this.services.getProcesses(); const allProcesses = await this.services.getProcesses();
const otherProcesses = Object.keys(allProcesses).filter(
// On combine tout, en mettant les miens d'abord (id) => !mineArray.includes(id)
const otherProcesses = Object.keys(allProcesses).filter((id) => !mineArray.includes(id)); );
const listToShow = [...mineArray, ...otherProcesses]; const listToShow = [...mineArray, ...otherProcesses];
let count = 0; let count = 0;
@ -258,22 +129,33 @@ export class ProcessListPage extends HTMLElement {
const name = this.services.getProcessName(process) || pid; const name = this.services.getProcessName(process) || pid;
// Filtre if (
if (query && !name.toLowerCase().includes(query.toLowerCase()) && !pid.includes(query)) { query &&
!name.toLowerCase().includes(query.toLowerCase()) &&
!pid.includes(query)
) {
continue; continue;
} }
count++; count++;
const li = document.createElement('li'); const li = document.createElement("li");
li.textContent = name;
const nameSpan = document.createElement("span");
nameSpan.textContent = name;
li.appendChild(nameSpan);
if (mineArray.includes(pid)) { if (mineArray.includes(pid)) {
li.classList.add('my-process'); li.classList.add("my-process");
li.innerHTML += ' <small style="opacity:0.6">(Mien)</small>'; const small = document.createElement("small");
small.style.opacity = "0.6";
small.style.marginLeft = "8px";
small.textContent = "(Mien)"; // Texte statique sûr
li.appendChild(small);
} }
li.addEventListener('click', () => { li.addEventListener("click", () => {
this.addTag(pid, name); this.addTag(pid, name);
this.inputInput.value = ''; this.inputInput.value = "";
this.showProcessDetails(pid); this.showProcessDetails(pid);
this.closeDropdown(); this.closeDropdown();
}); });
@ -282,10 +164,10 @@ export class ProcessListPage extends HTMLElement {
} }
if (count === 0) { if (count === 0) {
const empty = document.createElement('li'); const empty = document.createElement("li");
empty.textContent = 'Aucun résultat'; empty.textContent = "Aucun résultat";
empty.style.cursor = 'default'; empty.style.cursor = "default";
empty.style.opacity = '0.5'; empty.style.opacity = "0.5";
this.autocompleteList.appendChild(empty); this.autocompleteList.appendChild(empty);
} }
} }
@ -296,84 +178,121 @@ export class ProcessListPage extends HTMLElement {
} }
openDropdown() { openDropdown() {
this.autocompleteList.style.display = 'block'; this.autocompleteList.style.display = "block";
} }
closeDropdown() { closeDropdown() {
this.autocompleteList.style.display = 'none'; this.autocompleteList.style.display = "none";
} }
// --- Gestion des Tags --- // --- Gestion des Tags Sécurisée ---
addTag(pid: string, name: string) { addTag(pid: string, name: string) {
// On nettoie les anciens tags (mode single select pour l'instant, ou multi si tu veux) this.tagsContainer.innerHTML = "";
this.tagsContainer.innerHTML = '';
const tag = document.createElement('div'); const tag = document.createElement("div");
tag.className = 'tag'; tag.className = "tag";
tag.innerHTML = `
<span>${name}</span>
<span class="tag-close">&times;</span>
`;
tag.querySelector('.tag-close')?.addEventListener('click', (e) => { const spanName = document.createElement("span");
spanName.textContent = name;
tag.appendChild(spanName);
const closeBtn = document.createElement("span");
closeBtn.className = "tag-close";
closeBtn.innerHTML = "&times;";
closeBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.removeTag(); this.removeTag();
}); });
tag.appendChild(closeBtn);
this.tagsContainer.appendChild(tag); this.tagsContainer.appendChild(tag);
} }
removeTag() { removeTag() {
this.tagsContainer.innerHTML = ''; this.tagsContainer.innerHTML = "";
this.detailsContainer.innerHTML = '<div class="empty-state"><p>Aucun processus sélectionné.</p></div>';
this.detailsContainer.innerHTML = "";
const emptyState = document.createElement("div");
emptyState.className = "empty-state";
const p = document.createElement("p");
p.textContent = "Aucun processus sélectionné.";
emptyState.appendChild(p);
this.detailsContainer.appendChild(emptyState);
this.okButton.disabled = true; this.okButton.disabled = true;
this.okButton.classList.add('disabled'); this.okButton.classList.add("disabled");
} }
// --- Détails du processus --- // --- Détails du processus Sécurisés ---
async showProcessDetails(pid: string) { async showProcessDetails(pid: string) {
this.detailsContainer.innerHTML = '<p style="text-align:center">Chargement...</p>'; this.detailsContainer.textContent = "Chargement..."; // Safe loader
const process = await this.services.getProcess(pid); const process = await this.services.getProcess(pid);
if (!process) return; if (!process) return;
this.detailsContainer.innerHTML = ''; // Clear this.detailsContainer.innerHTML = "";
const name = this.services.getProcessName(process) || 'Sans nom'; const name = this.services.getProcessName(process) || "Sans nom";
// Description // Description
let description = 'Pas de description'; let description = "Pas de description";
const lastState = this.services.getLastCommitedState(process); const lastState = this.services.getLastCommitedState(process);
if (lastState?.pcd_commitment['description']) { if (lastState?.pcd_commitment["description"]) {
const diff = await this.services.getDiffByValue(lastState.pcd_commitment['description']); const diff = await this.services.getDiffByValue(
lastState.pcd_commitment["description"]
);
if (diff) description = diff.value_commitment; if (diff) description = diff.value_commitment;
} }
const containerDiv = document.createElement('div'); const containerDiv = document.createElement("div");
containerDiv.className = 'process-item'; containerDiv.className = "process-item";
containerDiv.innerHTML = `
<div class="process-title-display">${name}</div> // Titre
<div style="font-size:0.9rem; margin-bottom:10px">${description}</div> const titleDiv = document.createElement("div");
<div style="font-size:0.8rem; opacity:0.7; margin-bottom:10px">ID: ${pid}</div> titleDiv.className = "process-title-display";
<div style="font-weight:bold; margin-top:15px">États en attente :</div> titleDiv.textContent = name; // Safe
`; containerDiv.appendChild(titleDiv);
// Description
const descDiv = document.createElement("div");
descDiv.style.fontSize = "0.9rem";
descDiv.style.marginBottom = "10px";
descDiv.textContent = description; // Safe
containerDiv.appendChild(descDiv);
// ID
const idDiv = document.createElement("div");
idDiv.style.fontSize = "0.8rem";
idDiv.style.opacity = "0.7";
idDiv.style.marginBottom = "10px";
idDiv.textContent = `ID: ${pid}`; // Safe
containerDiv.appendChild(idDiv);
// Label "États en attente"
const labelDiv = document.createElement("div");
labelDiv.style.fontWeight = "bold";
labelDiv.style.marginTop = "15px";
labelDiv.textContent = "États en attente :";
containerDiv.appendChild(labelDiv);
const uncommitted = this.services.getUncommitedStates(process); const uncommitted = this.services.getUncommitedStates(process);
if (uncommitted.length > 0) { if (uncommitted.length > 0) {
uncommitted.forEach((state) => { uncommitted.forEach((state) => {
const el = document.createElement('div'); const el = document.createElement("div");
el.className = 'state-element'; el.className = "state-element";
// textContent ici aussi, même si state_id est technique
el.textContent = `État: ${state.state_id.substring(0, 16)}...`; el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
el.addEventListener('click', () => { el.addEventListener("click", () => {
// Gestion de la sélection this.shadowRoot
this.shadowRoot?.querySelectorAll('.state-element').forEach((x) => x.classList.remove('selected')); ?.querySelectorAll(".state-element")
el.classList.add('selected'); .forEach((x) => x.classList.remove("selected"));
el.classList.add("selected");
// Activation du bouton
this.okButton.disabled = false; this.okButton.disabled = false;
this.okButton.dataset.target = `${pid}/${state.state_id}`; this.okButton.dataset.target = `${pid}/${state.state_id}`;
}); });
@ -381,10 +300,10 @@ export class ProcessListPage extends HTMLElement {
containerDiv.appendChild(el); containerDiv.appendChild(el);
}); });
} else { } else {
const empty = document.createElement('div'); const empty = document.createElement("div");
empty.style.padding = '10px'; empty.style.padding = "10px";
empty.style.opacity = '0.6'; empty.style.opacity = "0.6";
empty.textContent = 'Aucun état en attente de validation.'; empty.textContent = "Aucun état en attente de validation.";
containerDiv.appendChild(empty); containerDiv.appendChild(empty);
} }
@ -394,11 +313,10 @@ export class ProcessListPage extends HTMLElement {
goToProcess() { goToProcess() {
const target = this.okButton.dataset.target; const target = this.okButton.dataset.target;
if (target) { if (target) {
console.log('Navigation vers', target); console.log("Navigation vers", target);
alert('Navigation vers la page de détail du processus (à implémenter): ' + target); alert("Navigation vers : " + target);
// Router.navigate(`process-detail/${target}`);
} }
} }
} }
customElements.define('process-list-page', ProcessListPage); customElements.define("process-list-page", ProcessListPage);

View File

@ -1,4 +1,4 @@
import Services from './service'; import Services from "./service";
/** /**
* Database service managing IndexedDB operations via Web Worker and Service Worker * Database service managing IndexedDB operations via Web Worker and Service Worker
@ -13,7 +13,10 @@ export class Database {
private serviceWorkerCheckIntervalId: number | null = null; private serviceWorkerCheckIntervalId: number | null = null;
private indexedDBWorker: Worker | null = null; private indexedDBWorker: Worker | null = null;
private messageIdCounter: number = 0; private messageIdCounter: number = 0;
private pendingMessages: Map<number, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map(); private pendingMessages: Map<
number,
{ resolve: (value: any) => void; reject: (error: any) => void }
> = new Map();
// ============================================ // ============================================
// INITIALIZATION & SINGLETON // INITIALIZATION & SINGLETON
@ -37,7 +40,10 @@ export class Database {
// ============================================ // ============================================
private initIndexedDBWorker(): void { private initIndexedDBWorker(): void {
this.indexedDBWorker = new Worker(new URL('../workers/database.worker.ts', import.meta.url), { type: 'module' }); this.indexedDBWorker = new Worker(
new URL("../workers/database.worker.ts", import.meta.url),
{ type: "module" }
);
this.indexedDBWorker.onmessage = (event) => { this.indexedDBWorker.onmessage = (event) => {
const { id, type, result, error } = event.data; const { id, type, result, error } = event.data;
@ -46,27 +52,27 @@ export class Database {
if (pending) { if (pending) {
this.pendingMessages.delete(id); this.pendingMessages.delete(id);
if (type === 'SUCCESS') { if (type === "SUCCESS") {
pending.resolve(result); pending.resolve(result);
} else if (type === 'ERROR') { } else if (type === "ERROR") {
pending.reject(new Error(error)); pending.reject(new Error(error));
} }
} }
}; };
this.indexedDBWorker.onerror = (error) => { this.indexedDBWorker.onerror = (error) => {
console.error('[Database] IndexedDB Worker error:', error); console.error("[Database] IndexedDB Worker error:", error);
}; };
} }
private async waitForWorkerReady(): Promise<void> { private async waitForWorkerReady(): Promise<void> {
return this.sendMessageToWorker('INIT', {}); return this.sendMessageToWorker("INIT", {});
} }
private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> { private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.indexedDBWorker) { if (!this.indexedDBWorker) {
reject(new Error('IndexedDB Worker not initialized')); reject(new Error("IndexedDB Worker not initialized"));
return; return;
} }
@ -90,74 +96,93 @@ export class Database {
// ============================================ // ============================================
private initServiceWorker(): void { private initServiceWorker(): void {
this.registerServiceWorker('/data.worker.js'); this.registerServiceWorker("/data.worker.js");
} }
private async registerServiceWorker(path: string): Promise<void> { private async registerServiceWorker(path: string): Promise<void> {
if (!('serviceWorker' in navigator)) return; if (!("serviceWorker" in navigator)) return;
console.log('[Database] Initializing Service Worker:', path); console.log("[Database] Initializing Service Worker:", path);
try { try {
const registrations = await navigator.serviceWorker.getRegistrations(); const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) { for (const registration of registrations) {
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL; const scriptURL =
registration.active?.scriptURL ||
registration.installing?.scriptURL ||
registration.waiting?.scriptURL;
const scope = registration.scope; const scope = registration.scope;
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) { if (
scope.includes("/src/service-workers/") ||
(scriptURL && scriptURL.includes("/src/service-workers/"))
) {
console.warn(`[Database] Removing old Service Worker (${scope})`); console.warn(`[Database] Removing old Service Worker (${scope})`);
await registration.unregister(); await registration.unregister();
} }
} }
const existingValidWorker = registrations.find((r) => { const existingValidWorker = registrations.find((r) => {
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; const url =
return url && url.endsWith(path.replace(/^\//,'')); r.active?.scriptURL ||
r.installing?.scriptURL ||
r.waiting?.scriptURL;
return url && url.endsWith(path.replace(/^\//, ""));
}); });
if (!existingValidWorker) { if (!existingValidWorker) {
console.log('[Database] Registering new Service Worker'); console.log("[Database] Registering new Service Worker");
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' }); this.serviceWorkerRegistration = await navigator.serviceWorker.register(
path,
{ type: "module", scope: "/" }
);
} else { } else {
console.log('[Database] Service Worker already active'); console.log("[Database] Service Worker already active");
this.serviceWorkerRegistration = existingValidWorker; this.serviceWorkerRegistration = existingValidWorker;
await this.serviceWorkerRegistration.update(); await this.serviceWorkerRegistration.update();
} }
navigator.serviceWorker.addEventListener('message', async (event) => { navigator.serviceWorker.addEventListener("message", async (event) => {
if (event.data.type === 'DB_REQUEST') { // ✅ SIMPLIFICATION : Plus besoin de gérer les "DB_REQUEST"
await this.handleDatabaseRequest(event.data);
return;
}
await this.handleServiceWorkerMessage(event.data); await this.handleServiceWorkerMessage(event.data);
}); });
if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); if (this.serviceWorkerCheckIntervalId)
clearInterval(this.serviceWorkerCheckIntervalId);
this.serviceWorkerCheckIntervalId = window.setInterval(async () => { this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); const activeWorker =
this.serviceWorkerRegistration?.active ||
(await this.waitForServiceWorkerActivation(
this.serviceWorkerRegistration!
));
const service = await Services.getInstance(); const service = await Services.getInstance();
const payload = await service.getMyProcesses(); const payload = await service.getMyProcesses();
if (payload && payload.length != 0) { if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload }); activeWorker?.postMessage({ type: "SCAN", payload });
} }
}, 5000); }, 5000);
} catch (error) { } catch (error) {
console.error('[Database] Service Worker error:', error); console.error("[Database] Service Worker error:", error);
} }
} }
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> { private async waitForServiceWorkerActivation(
registration: ServiceWorkerRegistration
): Promise<ServiceWorker | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (registration.active) { if (registration.active) {
resolve(registration.active); resolve(registration.active);
} else { } else {
const listener = () => { const listener = () => {
if (registration.active) { if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener); navigator.serviceWorker.removeEventListener(
"controllerchange",
listener
);
resolve(registration.active); resolve(registration.active);
} }
}; };
navigator.serviceWorker.addEventListener('controllerchange', listener); navigator.serviceWorker.addEventListener("controllerchange", listener);
} }
}); });
} }
@ -168,10 +193,12 @@ export class Database {
await this.serviceWorkerRegistration.update(); await this.serviceWorkerRegistration.update();
if (this.serviceWorkerRegistration.waiting) { if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); this.serviceWorkerRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
} }
} catch (error) { } catch (error) {
console.error('Error checking for service worker updates:', error); console.error("Error checking for service worker updates:", error);
} }
} }
} }
@ -179,73 +206,34 @@ export class Database {
// ============================================ // ============================================
// SERVICE WORKER MESSAGE HANDLERS // SERVICE WORKER MESSAGE HANDLERS
// ============================================ // ============================================
private async handleDatabaseRequest(request: any): Promise<void> {
const { id, action, payload } = request;
try { // ✅ NETTOYAGE : handleDatabaseRequest() a été supprimé
let result;
switch (action) {
case 'GET_OBJECT':
result = await this.getObject(payload.storeName, payload.key);
break;
case 'GET_MULTIPLE_OBJECTS':
result = await this.sendMessageToWorker('GET_MULTIPLE_OBJECTS', payload);
break;
case 'GET_ALL_OBJECTS':
result = await this.sendMessageToWorker('GET_ALL_OBJECTS', payload);
break;
case 'GET_ALL_OBJECTS_WITH_FILTER':
result = await this.sendMessageToWorker('GET_ALL_OBJECTS_WITH_FILTER', payload);
break;
default:
throw new Error(`Unknown database action: ${action}`);
}
if (this.serviceWorkerRegistration?.active) {
this.serviceWorkerRegistration.active.postMessage({
type: 'DB_RESPONSE',
id,
result
});
}
} catch (error: any) {
console.error('[Database] Error handling database request:', error);
if (this.serviceWorkerRegistration?.active) {
this.serviceWorkerRegistration.active.postMessage({
type: 'DB_ERROR',
id,
error: error.message || String(error)
});
}
}
}
private async handleServiceWorkerMessage(message: any) { private async handleServiceWorkerMessage(message: any) {
switch (message.type) { switch (message.type) {
case 'TO_DOWNLOAD': case "TO_DOWNLOAD":
await this.handleDownloadList(message.data); await this.handleDownloadList(message.data);
break; break;
case 'DIFFS_TO_CREATE': case "DIFFS_TO_CREATE":
await this.handleDiffsToCreate(message.data); await this.handleDiffsToCreate(message.data);
break; break;
default: default:
console.warn('Unknown message type received from service worker:', message); console.warn(
"Unknown message type received from service worker:",
message
);
} }
} }
private async handleDiffsToCreate(diffs: any[]): Promise<void> { private async handleDiffsToCreate(diffs: any[]): Promise<void> {
console.log(`[Database] Creating ${diffs.length} diffs from Service Worker scan`); console.log(
`[Database] Creating ${diffs.length} diffs from Service Worker scan`
);
try { try {
await this.saveDiffs(diffs); await this.saveDiffs(diffs);
console.log('[Database] Diffs created successfully'); console.log("[Database] Diffs created successfully");
} catch (error) { } catch (error) {
console.error('[Database] Error creating diffs:', error); console.error("[Database] Error creating diffs:", error);
} }
} }
@ -264,15 +252,17 @@ export class Database {
try { try {
const valueBytes = await service.fetchValueFromStorage(hash); const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) { if (valueBytes) {
const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); const blob = new Blob([valueBytes], {
type: "application/octet-stream",
});
await service.saveBlobToDb(hash, blob); await service.saveBlobToDb(hash, blob);
document.dispatchEvent( document.dispatchEvent(
new CustomEvent('newDataReceived', { new CustomEvent("newDataReceived", {
detail: { processId, stateId, hash }, detail: { processId, stateId, hash },
}), })
); );
} else { } else {
console.log('Request data from managers of the process'); console.log("Request data from managers of the process");
if (!requestedStateId.includes(stateId)) { if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]); await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId); requestedStateId.push(stateId);
@ -289,35 +279,50 @@ export class Database {
// ============================================ // ============================================
public async getStoreList(): Promise<{ [key: string]: string }> { public async getStoreList(): Promise<{ [key: string]: string }> {
return this.sendMessageToWorker('GET_STORE_LIST', {}); return this.sendMessageToWorker("GET_STORE_LIST", {});
} }
public async addObject(payload: { storeName: string; object: any; key: any }): Promise<void> { public async addObject(payload: {
await this.sendMessageToWorker('ADD_OBJECT', payload); storeName: string;
object: any;
key: any;
}): Promise<void> {
await this.sendMessageToWorker("ADD_OBJECT", payload);
} }
public async batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> { public async batchWriting(payload: {
await this.sendMessageToWorker('BATCH_WRITING', payload); storeName: string;
objects: { key: any; object: any }[];
}): Promise<void> {
await this.sendMessageToWorker("BATCH_WRITING", payload);
} }
public async getObject(storeName: string, key: string): Promise<any | null> { public async getObject(storeName: string, key: string): Promise<any | null> {
return this.sendMessageToWorker('GET_OBJECT', { storeName, key }); return this.sendMessageToWorker("GET_OBJECT", { storeName, key });
} }
public async dumpStore(storeName: string): Promise<Record<string, any>> { public async dumpStore(storeName: string): Promise<Record<string, any>> {
return this.sendMessageToWorker('DUMP_STORE', { storeName }); return this.sendMessageToWorker("DUMP_STORE", { storeName });
} }
public async deleteObject(storeName: string, key: string): Promise<void> { public async deleteObject(storeName: string, key: string): Promise<void> {
await this.sendMessageToWorker('DELETE_OBJECT', { storeName, key }); await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key });
} }
public async clearStore(storeName: string): Promise<void> { public async clearStore(storeName: string): Promise<void> {
await this.sendMessageToWorker('CLEAR_STORE', { storeName }); await this.sendMessageToWorker("CLEAR_STORE", { storeName });
} }
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> { public async requestStoreByIndex(
return this.sendMessageToWorker('REQUEST_STORE_BY_INDEX', { storeName, indexName, request }); storeName: string,
indexName: string,
request: string
): Promise<any[]> {
return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", {
storeName,
indexName,
request,
});
} }
public async clearMultipleStores(storeNames: string[]): Promise<void> { public async clearMultipleStores(storeNames: string[]): Promise<void> {
@ -332,24 +337,22 @@ export class Database {
public async saveDevice(device: any): Promise<void> { public async saveDevice(device: any): Promise<void> {
try { try {
const existing = await this.getObject('wallet', '1'); const existing = await this.getObject("wallet", "1");
if (existing) { if (existing) {
await this.deleteObject('wallet', '1'); await this.deleteObject("wallet", "1");
} }
} catch (e) {} } catch (e) {}
await this.addObject({ await this.addObject({
storeName: 'wallet', storeName: "wallet",
object: { pre_id: '1', device }, object: { pre_id: "1", device },
key: null, key: null,
}); });
} }
public async getDevice(): Promise<any | null> { public async getDevice(): Promise<any | null> {
const result = await this.getObject('wallet', '1'); const result = await this.getObject("wallet", "1");
console.log(result); return result ? result["device"] : null;
return result ? result['device'] : null;
} }
// ============================================ // ============================================
@ -358,27 +361,32 @@ export class Database {
public async saveProcess(processId: string, process: any): Promise<void> { public async saveProcess(processId: string, process: any): Promise<void> {
await this.addObject({ await this.addObject({
storeName: 'processes', storeName: "processes",
object: process, object: process,
key: processId, key: processId,
}); });
} }
public async saveProcessesBatch(processes: Record<string, any>): Promise<void> { public async saveProcessesBatch(
processes: Record<string, any>
): Promise<void> {
if (Object.keys(processes).length === 0) return; if (Object.keys(processes).length === 0) return;
await this.batchWriting({ await this.batchWriting({
storeName: 'processes', storeName: "processes",
objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })), objects: Object.entries(processes).map(([key, value]) => ({
key,
object: value,
})),
}); });
} }
public async getProcess(processId: string): Promise<any | null> { public async getProcess(processId: string): Promise<any | null> {
return this.getObject('processes', processId); return this.getObject("processes", processId);
} }
public async getAllProcesses(): Promise<Record<string, any>> { public async getAllProcesses(): Promise<Record<string, any>> {
return this.dumpStore('processes'); return this.dumpStore("processes");
} }
// ============================================ // ============================================
@ -387,14 +395,14 @@ export class Database {
public async saveBlob(hash: string, data: Blob): Promise<void> { public async saveBlob(hash: string, data: Blob): Promise<void> {
await this.addObject({ await this.addObject({
storeName: 'data', storeName: "data",
object: data, object: data,
key: hash, key: hash,
}); });
} }
public async getBlob(hash: string): Promise<Blob | null> { public async getBlob(hash: string): Promise<Blob | null> {
return this.getObject('data', hash); return this.getObject("data", hash);
} }
// ============================================ // ============================================
@ -406,7 +414,7 @@ export class Database {
for (const diff of diffs) { for (const diff of diffs) {
await this.addObject({ await this.addObject({
storeName: 'diffs', storeName: "diffs",
object: diff, object: diff,
key: null, key: null,
}); });
@ -414,11 +422,11 @@ export class Database {
} }
public async getDiff(hash: string): Promise<any | null> { public async getDiff(hash: string): Promise<any | null> {
return this.getObject('diffs', hash); return this.getObject("diffs", hash);
} }
public async getAllDiffs(): Promise<Record<string, any>> { public async getAllDiffs(): Promise<Record<string, any>> {
return this.dumpStore('diffs'); return this.dumpStore("diffs");
} }
// ============================================ // ============================================
@ -426,14 +434,17 @@ export class Database {
// ============================================ // ============================================
public async getSharedSecret(address: string): Promise<string | null> { public async getSharedSecret(address: string): Promise<string | null> {
return this.getObject('shared_secrets', address); return this.getObject("shared_secrets", address);
} }
public async saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void> { public async saveSecretsBatch(
unconfirmedSecrets: any[],
sharedSecrets: { key: string; value: any }[]
): Promise<void> {
if (unconfirmedSecrets && unconfirmedSecrets.length > 0) { if (unconfirmedSecrets && unconfirmedSecrets.length > 0) {
for (const secret of unconfirmedSecrets) { for (const secret of unconfirmedSecrets) {
await this.addObject({ await this.addObject({
storeName: 'unconfirmed_secrets', storeName: "unconfirmed_secrets",
object: secret, object: secret,
key: null, key: null,
}); });
@ -443,7 +454,7 @@ export class Database {
if (sharedSecrets && sharedSecrets.length > 0) { if (sharedSecrets && sharedSecrets.length > 0) {
for (const { key, value } of sharedSecrets) { for (const { key, value } of sharedSecrets) {
await this.addObject({ await this.addObject({
storeName: 'shared_secrets', storeName: "shared_secrets",
object: value, object: value,
key: key, key: key,
}); });
@ -451,9 +462,12 @@ export class Database {
} }
} }
public async getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }> { public async getAllSecrets(): Promise<{
const sharedSecrets = await this.dumpStore('shared_secrets'); shared_secrets: Record<string, any>;
const unconfirmedSecrets = await this.dumpStore('unconfirmed_secrets'); unconfirmed_secrets: any[];
}> {
const sharedSecrets = await this.dumpStore("shared_secrets");
const unconfirmedSecrets = await this.dumpStore("unconfirmed_secrets");
return { return {
shared_secrets: sharedSecrets, shared_secrets: sharedSecrets,

View File

@ -1,9 +1,9 @@
import { MessageType } from '../types/index'; import { MessageType } from "../types/index";
import Services from './service'; import Services from "./service";
import TokenService from './token.service'; import TokenService from "./token.service";
import { cleanSubscriptions } from '../utils/subscription.utils'; import { cleanSubscriptions } from "../utils/subscription.utils";
import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils'; import { splitPrivateData, isValid32ByteHex } from "../utils/service.utils";
import { MerkleProofResult } from '../../pkg/sdk_client'; import { MerkleProofResult } from "../../pkg/sdk_client";
export class IframeController { export class IframeController {
private static isInitialized = false; // <--- VERROU private static isInitialized = false; // <--- VERROU
@ -13,39 +13,57 @@ export class IframeController {
// On ne lance l'écoute que si on est dans une iframe // On ne lance l'écoute que si on est dans une iframe
if (window.self !== window.top) { 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(); await IframeController.registerAllListeners();
} else { } 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() { 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 services = await Services.getInstance();
const tokenService = await TokenService.getInstance(); const tokenService = await TokenService.getInstance();
/** /**
* Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A). * 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) => { const errorResponse = (
console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`); errorMsg: string,
origin: string,
messageId?: string
) => {
console.error(
`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`
);
window.parent.postMessage( window.parent.postMessage(
{ {
type: MessageType.ERROR, type: MessageType.ERROR,
error: errorMsg, error: errorMsg,
messageId, messageId,
}, },
origin, origin
); );
}; };
// Helper pour vérifier le token avant chaque action sensible // Helper pour vérifier le token avant chaque action sensible
const withToken = async (event: MessageEvent, action: () => Promise<void>) => { const withToken = async (
event: MessageEvent,
action: () => Promise<void>
) => {
const { accessToken } = event.data; const { accessToken } = event.data;
// On vérifie si le token est présent ET valide pour l'origine de l'iframe // 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))) { if (
throw new Error('Invalid or expired session token'); !accessToken ||
!(await tokenService.validateToken(accessToken, event.origin))
) {
throw new Error("Invalid or expired session token");
} }
// Si tout est bon, on exécute l'action // Si tout est bon, on exécute l'action
await action(); await action();
@ -54,35 +72,53 @@ export class IframeController {
// --- Définitions des gestionnaires (Handlers) --- // --- Définitions des gestionnaires (Handlers) ---
const handleRequestLink = async (event: MessageEvent) => { 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) // 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion)
const device = await services.getDeviceFromDatabase(); const device = await services.getDeviceFromDatabase();
if (device && device.pairing_process_commitment) { 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. // On saute l'attente et on passe directement à la suite.
} else { } else {
// 2. Cas de la 1ère connexion (appareil non appairé) // 2. Cas de la 1ère connexion (appareil non appairé)
// On doit attendre que home.ts (auto-pairing) ait fini son travail. // 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...'); await new Promise<void>((resolve, reject) => {
const maxWait = 5000; // 5 sec // Fonction de nettoyage pour éviter les fuites de mémoire
let waited = 0; const cleanup = () => {
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); document.removeEventListener(
"app:pairing-ready",
handler as EventListener
);
clearTimeout(timeoutId);
};
// On attend le drapeau global // Le gestionnaire de l'événement
while (!(window as any).__PAIRING_READY && waited < maxWait) { const handler = (e: CustomEvent) => {
await delay(100); cleanup();
waited += 100; 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 // Timeout de sécurité (5 secondes)
if ((window as any).__PAIRING_READY === 'error') { const timeoutId = setTimeout(() => {
throw new Error('Auto-pairing failed'); cleanup();
} reject(new Error("Auto-pairing timed out (Event not received)"));
if (!(window as any).__PAIRING_READY) { }, 5000);
throw new Error('Auto-pairing timed out');
} // 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 !`); console.log(`[Router:API] Feu vert de home.ts reçu !`);
} }
@ -98,50 +134,69 @@ export class IframeController {
refreshToken: tokens.refreshToken, refreshToken: tokens.refreshToken,
messageId: event.data.messageId, 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) => { const handleCreatePairing = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`); console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
if (services.isPaired()) { 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 () => { await withToken(event, async () => {
console.log("[Router:API] 🚀 Démarrage du processus d'appairage..."); console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
const myAddress = services.getDeviceAddress(); const myAddress = services.getDeviceAddress();
console.log('[Router:API] 1/7: Création du processus de pairing...'); console.log("[Router:API] 1/7: Création du processus de pairing...");
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); const createPairingProcessReturn = await services.createPairingProcess(
"",
[myAddress]
);
const pairingId = createPairingProcessReturn.updated_process?.process_id; const pairingId =
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; createPairingProcessReturn.updated_process?.process_id;
const stateId = createPairingProcessReturn.updated_process
?.current_process?.states[0]?.state_id as string;
if (!pairingId || !stateId) { 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] 2/7: Processus ${pairingId} créé.`);
console.log("[Router:API] 3/7: Enregistrement local de l'appareil..."); console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
services.pairDevice(pairingId, [myAddress]); 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); await services.handleApiReturn(createPairingProcessReturn);
console.log('[Router:API] 5/7: Création de la mise à jour PRD...'); console.log("[Router:API] 5/7: Création de la mise à jour PRD...");
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); const createPrdUpdateReturn = await services.createPrdUpdate(
pairingId,
stateId
);
await services.handleApiReturn(createPrdUpdateReturn); await services.handleApiReturn(createPrdUpdateReturn);
console.log('[Router:API] 6/7: Approbation du changement...'); console.log("[Router:API] 6/7: Approbation du changement...");
const approveChangeReturn = await services.approveChange(pairingId, stateId); const approveChangeReturn = await services.approveChange(
pairingId,
stateId
);
await services.handleApiReturn(approveChangeReturn); 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(); // await services.confirmPairing();
console.log('[Router:API] 🎉 Appairage terminé avec succès !'); console.log("[Router:API] 🎉 Appairage terminé avec succès !");
const successMsg = { const successMsg = {
type: MessageType.PAIRING_CREATED, type: MessageType.PAIRING_CREATED,
@ -153,8 +208,10 @@ export class IframeController {
}; };
const handleGetMyProcesses = async (event: MessageEvent) => { const handleGetMyProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`); console.log(
if (!services.isPaired()) throw new Error('Device not paired'); `[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`
);
if (!services.isPaired()) throw new Error("Device not paired");
await withToken(event, async () => { await withToken(event, async () => {
const myProcesses = await services.getMyProcesses(); const myProcesses = await services.getMyProcesses();
@ -165,14 +222,14 @@ export class IframeController {
myProcesses, myProcesses,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleGetProcesses = async (event: MessageEvent) => { const handleGetProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`); 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 () => { await withToken(event, async () => {
const processes = await services.getProcesses(); const processes = await services.getProcesses();
@ -183,14 +240,14 @@ export class IframeController {
processes, processes,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleDecryptState = async (event: MessageEvent) => { const handleDecryptState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`); 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; const { processId, stateId } = event.data;
@ -199,22 +256,36 @@ export class IframeController {
if (!process) throw new Error("Can't find process"); if (!process) throw new Error("Can't find process");
const state = services.getStateFromId(process, stateId); 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); await services.ensureConnections(process, stateId);
const res: Record<string, any> = {}; const res: Record<string, any> = {};
for (const attribute of Object.keys(state.pcd_commitment)) { 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; continue;
} }
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); const decryptedAttribute = await services.decryptAttribute(
processId,
state,
attribute
);
if (decryptedAttribute) { if (decryptedAttribute) {
res[attribute] = 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( window.parent.postMessage(
{ {
@ -222,7 +293,7 @@ export class IframeController {
data: res, data: res,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
@ -232,10 +303,13 @@ export class IframeController {
const accessToken = event.data.accessToken; const accessToken = event.data.accessToken;
const refreshToken = event.data.refreshToken; const refreshToken = event.data.refreshToken;
if (!accessToken || !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}`); console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
window.parent.postMessage( window.parent.postMessage(
{ {
@ -245,17 +319,21 @@ export class IframeController {
isValid: isValid, isValid: isValid,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}; };
const handleRenewToken = async (event: MessageEvent) => { const handleRenewToken = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`); console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
const refreshToken = event.data.refreshToken; 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); const newAccessToken = await tokenService.refreshAccessToken(
if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)'); refreshToken,
event.origin
);
if (!newAccessToken)
throw new Error("Failed to refresh token (invalid refresh token)");
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`); console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
window.parent.postMessage( window.parent.postMessage(
@ -265,7 +343,7 @@ export class IframeController {
refreshToken: refreshToken, refreshToken: refreshToken,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}; };
@ -285,19 +363,29 @@ export class IframeController {
if (device && device.pairing_process_commitment) { if (device && device.pairing_process_commitment) {
// SUCCÈS ! L'ID est dans la BDD // SUCCÈS ! L'ID est dans la BDD
pairingId = device.pairing_process_commitment; 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 break; // On sort de la boucle
} }
// Si non trouvé, on patiente // 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)); await new Promise((resolve) => setTimeout(resolve, retryDelay));
} }
// Si la boucle se termine sans succès // Si la boucle se termine sans succès
if (!pairingId) { if (!pairingId) {
console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`); console.error(
throw new Error('Device not paired'); `[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 () => { await withToken(event, async () => {
@ -307,31 +395,42 @@ export class IframeController {
userPairingId: pairingId, userPairingId: pairingId,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleCreateProcess = async (event: MessageEvent) => { const handleCreateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`); 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; const { processData, privateFields, roles } = event.data;
await withToken(event, async () => { await withToken(event, async () => {
console.log('[Router:API] 🚀 Démarrage de la création de processus standard...'); console.log(
const { privateData, publicData } = splitPrivateData(processData, privateFields); "[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...'); console.log("[Router:API] 1/2: Création du processus...");
const createProcessReturn = await services.createProcess(privateData, publicData, roles); const createProcessReturn = await services.createProcess(
privateData,
publicData,
roles
);
if (!createProcessReturn.updated_process) { 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 processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process; const process = createProcessReturn.updated_process.current_process;
const stateId = process.states[0].state_id; 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); await services.handleApiReturn(createProcessReturn);
console.log(`[Router:API] 🎉 Processus ${processId} créé.`); console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
@ -348,19 +447,19 @@ export class IframeController {
processCreated: res, processCreated: res,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleNotifyUpdate = async (event: MessageEvent) => { const handleNotifyUpdate = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`); 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; const { processId, stateId } = event.data;
await withToken(event, async () => { 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); const res = await services.createPrdUpdate(processId, stateId);
await services.handleApiReturn(res); await services.handleApiReturn(res);
@ -370,14 +469,14 @@ export class IframeController {
type: MessageType.UPDATE_NOTIFIED, type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleValidateState = async (event: MessageEvent) => { const handleValidateState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`); 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; const { processId, stateId } = event.data;
@ -391,22 +490,29 @@ export class IframeController {
validatedProcess: res.updated_process, validatedProcess: res.updated_process,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleUpdateProcess = async (event: MessageEvent) => { const handleUpdateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`); 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; const { processId, newData, privateFields, roles } = event.data;
await withToken(event, async () => { 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. // 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. // Nous appelons handleApiReturn ici, comme avant.
await services.handleApiReturn(res); await services.handleApiReturn(res);
@ -418,14 +524,16 @@ export class IframeController {
updatedProcess: res.updated_process, // res vient directement de l'appel service updatedProcess: res.updated_process, // res vient directement de l'appel service
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleDecodePublicData = async (event: MessageEvent) => { const handleDecodePublicData = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`); console.log(
if (!services.isPaired()) throw new Error('Device not paired'); `[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`
);
if (!services.isPaired()) throw new Error("Device not paired");
const { encodedData } = event.data; const { encodedData } = event.data;
@ -437,7 +545,7 @@ export class IframeController {
decodedData, decodedData,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
@ -454,30 +562,37 @@ export class IframeController {
hash, hash,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleGetMerkleProof = async (event: MessageEvent) => { 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; const { processState, attributeName } = event.data;
await withToken(event, async () => { await withToken(event, async () => {
const proof = services.getMerkleProofForFile(processState, attributeName); const proof = services.getMerkleProofForFile(
processState,
attributeName
);
window.parent.postMessage( window.parent.postMessage(
{ {
type: MessageType.MERKLE_PROOF_RETRIEVED, type: MessageType.MERKLE_PROOF_RETRIEVED,
proof, proof,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
const handleValidateMerkleProof = async (event: MessageEvent) => { 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; const { merkleProof, documentHash } = event.data;
await withToken(event, async () => { await withToken(event, async () => {
@ -485,25 +600,28 @@ export class IframeController {
try { try {
parsedMerkleProof = JSON.parse(merkleProof); parsedMerkleProof = JSON.parse(merkleProof);
} catch (e) { } 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( window.parent.postMessage(
{ {
type: MessageType.MERKLE_PROOF_VALIDATED, type: MessageType.MERKLE_PROOF_VALIDATED,
isValid: res, isValid: res,
messageId: event.data.messageId, messageId: event.data.messageId,
}, },
event.origin, event.origin
); );
}); });
}; };
// --- Le "Switchyard" : il reçoit tous les messages et les dispatche --- // --- Le "Switchyard" : il reçoit tous les messages et les dispatche ---
window.removeEventListener('message', handleMessage); window.removeEventListener("message", handleMessage);
window.addEventListener('message', handleMessage); window.addEventListener("message", handleMessage);
async function handleMessage(event: MessageEvent) { async function handleMessage(event: MessageEvent) {
try { try {
@ -557,7 +675,7 @@ export class IframeController {
await handleValidateMerkleProof(event); await handleValidateMerkleProof(event);
break; break;
default: 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) { } catch (error: any) {
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`; const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`;
@ -569,9 +687,10 @@ export class IframeController {
{ {
type: MessageType.LISTENING, 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.');
} }
} }

View File

@ -1,4 +1,4 @@
import * as jose from 'jose'; import * as jose from "jose";
interface TokenPair { interface TokenPair {
accessToken: string; accessToken: string;
@ -7,10 +7,14 @@ interface TokenPair {
export default class TokenService { export default class TokenService {
private static instance: TokenService; private static instance: TokenService;
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
private readonly ACCESS_TOKEN_EXPIRATION = '30s'; // Constantes
private readonly REFRESH_TOKEN_EXPIRATION = '7d'; private readonly STORAGE_KEY = "4NK_SECURE_SESSION_KEY";
private readonly encoder = new TextEncoder(); private readonly ACCESS_TOKEN_EXPIRATION = "30s";
private readonly REFRESH_TOKEN_EXPIRATION = "7d";
// Cache mémoire pour éviter de lire le localStorage à chaque appel
private secretKeyCache: Uint8Array | null = null;
private constructor() {} private constructor() {}
@ -21,17 +25,47 @@ export default class TokenService {
return TokenService.instance; return TokenService.instance;
} }
async generateSessionToken(origin: string): Promise<TokenPair> { /**
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); * Récupère la clé secrète existante ou en génère une nouvelle
* et la sauvegarde dans le localStorage pour survivre aux refresh.
*/
private getSecretKey(): Uint8Array {
if (this.secretKeyCache) return this.secretKeyCache;
const accessToken = await new jose.SignJWT({ origin, type: 'access' }) const storedKey = localStorage.getItem(this.STORAGE_KEY);
.setProtectedHeader({ alg: 'HS256' })
if (storedKey) {
// Restauration de la clé existante (Hex -> Uint8Array)
this.secretKeyCache = this.hexToBuffer(storedKey);
} else {
// Génération d'une nouvelle clé aléatoire de 32 octets (256 bits)
const newKey = new Uint8Array(32);
crypto.getRandomValues(newKey);
// Sauvegarde (Uint8Array -> Hex)
localStorage.setItem(this.STORAGE_KEY, this.bufferToHex(newKey));
this.secretKeyCache = newKey;
console.log(
"[TokenService] 🔐 Nouvelle clé de session générée et stockée."
);
}
return this.secretKeyCache;
}
// --- Méthodes Publiques ---
async generateSessionToken(origin: string): Promise<TokenPair> {
const secret = this.getSecretKey();
const accessToken = await new jose.SignJWT({ origin, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
const refreshToken = await new jose.SignJWT({ origin, type: 'refresh' }) const refreshToken = await new jose.SignJWT({ origin, type: "refresh" })
.setProtectedHeader({ alg: 'HS256' }) .setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.REFRESH_TOKEN_EXPIRATION) .setExpirationTime(this.REFRESH_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
@ -41,47 +75,64 @@ export default class TokenService {
async validateToken(token: string, origin: string): Promise<boolean> { async validateToken(token: string, origin: string): Promise<boolean> {
try { try {
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); const secret = this.getSecretKey();
const { payload } = await jose.jwtVerify(token, secret); const { payload } = await jose.jwtVerify(token, secret);
return payload.origin === origin; return payload.origin === origin;
} catch (error: any) { } catch (error: any) {
if (error?.code === 'ERR_JWT_EXPIRED') { // On ignore les erreurs d'expiration classiques pour ne pas polluer la console
console.log('Token expiré'); if (error?.code === "ERR_JWT_EXPIRED") {
return false; return false;
} }
console.error('Erreur de validation du token:', error); console.warn(
"[TokenService] Validation échouée:",
error.code || error.message
);
return false; return false;
} }
} }
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> { async refreshAccessToken(
refreshToken: string,
origin: string
): Promise<string | null> {
try { try {
// Vérifier si le refresh token est valide // Validation du token (vérifie signature + expiration)
const isValid = await this.validateToken(refreshToken, origin); const isValid = await this.validateToken(refreshToken, origin);
if (!isValid) { if (!isValid) return null;
return null;
}
// Vérifier le type du token const secret = this.getSecretKey();
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
const { payload } = await jose.jwtVerify(refreshToken, secret); const { payload } = await jose.jwtVerify(refreshToken, secret);
if (payload.type !== 'refresh') {
return null;
}
// Générer un nouveau access token if (payload.type !== "refresh") return null;
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
.setProtectedHeader({ alg: 'HS256' }) // Génération du nouveau token
return await new jose.SignJWT({ origin, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
return newAccessToken;
} catch (error) { } catch (error) {
console.error('Erreur lors du refresh du token:', error); console.error("[TokenService] Erreur refresh:", error);
return null; return null;
} }
} }
// --- Utilitaires de conversion ---
private bufferToHex(buffer: Uint8Array): string {
return Array.from(buffer)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
private hexToBuffer(hex: string): Uint8Array {
if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
} }

View File

@ -116,14 +116,14 @@ export function initAddressInput() {
if (address) { if (address) {
const emojis = await addressToEmoji(address); const emojis = await addressToEmoji(address);
if (emojiDisplay) { if (emojiDisplay) {
emojiDisplay.innerHTML = emojis; emojiDisplay.textContent = emojis;
} }
if (okButton) { if (okButton) {
okButton.style.display = 'inline-block'; okButton.style.display = 'inline-block';
} }
} else { } else {
if (emojiDisplay) { if (emojiDisplay) {
emojiDisplay.innerHTML = ''; emojiDisplay.textContent = '';
} }
if (okButton) { if (okButton) {
okButton.style.display = 'none'; okButton.style.display = 'none';