UserWallet login flow, website-skeleton contract verify, data init/migrate
**Motivations:** - Parcours login contrat: sélection service → membre → build path → proof, vérification côté parent (service-login-verify) - Scripts data: initialisation SQLite (utxos, anchors, fees), migration depuis fichiers, sync UTXOs - Website-skeleton: intégration contrat, extraction validateurs login, vérification preuve **Root causes:** - N/A (évolutions) **Correctifs:** - N/A **Evolutions:** - UserWallet: MemberSelectionScreen, LoginScreen machine à états (S_LOGIN_SELECT_SERVICE → S_LOGIN_SELECT_MEMBER → build path), service/membre via query params, useChannel/iframeChannel - Website-skeleton: contract.ts (extractLoginValidators, isValidContract, isValidAction), main.ts validateurs depuis contrat, config étendu - Data: init-db.js (tables utxos/anchors/fees), migrate-from-files.js **Pages affectées:** - data/init-db.js, data/migrate-from-files.js, data/sync-utxos.log - userwallet: App, LoginCollectShare, LoginScreen, MemberSelectionScreen, ServiceListScreen, useChannel, iframeChannel - website-skeleton: README, config, contract, main
This commit is contained in:
parent
6bf37be44e
commit
8208809f03
95
data/init-db.js
Normal file
95
data/init-db.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Script d'initialisation de la base de données SQLite
|
||||||
|
* Crée les tables et les index nécessaires
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const dbPath = join(__dirname, 'signet.db');
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
// Activer les clés étrangères
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
|
||||||
|
// Table utxos
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS utxos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
txid TEXT NOT NULL,
|
||||||
|
vout INTEGER NOT NULL,
|
||||||
|
address TEXT,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
confirmations INTEGER DEFAULT 0,
|
||||||
|
is_anchor_change BOOLEAN DEFAULT FALSE,
|
||||||
|
block_time INTEGER,
|
||||||
|
is_spent_onchain BOOLEAN DEFAULT FALSE,
|
||||||
|
is_locked_in_mutex BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(txid, vout)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_utxos_category ON utxos(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_utxos_txid_vout ON utxos(txid, vout);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_utxos_confirmations ON utxos(confirmations);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_utxos_amount ON utxos(amount);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_utxos_is_spent ON utxos(is_spent_onchain);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Table anchors (hash_list.txt)
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS anchors (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
hash TEXT NOT NULL UNIQUE,
|
||||||
|
txid TEXT NOT NULL,
|
||||||
|
block_height INTEGER,
|
||||||
|
confirmations INTEGER DEFAULT 0,
|
||||||
|
date TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_anchors_hash ON anchors(hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_anchors_txid ON anchors(txid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_anchors_block_height ON anchors(block_height);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Table fees (fees_list.txt)
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS fees (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
txid TEXT NOT NULL UNIQUE,
|
||||||
|
fee REAL NOT NULL,
|
||||||
|
fee_sats INTEGER NOT NULL,
|
||||||
|
block_height INTEGER,
|
||||||
|
block_time INTEGER,
|
||||||
|
confirmations INTEGER DEFAULT 0,
|
||||||
|
change_address TEXT,
|
||||||
|
change_amount REAL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fees_txid ON fees(txid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fees_block_height ON fees(block_height);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Table cache pour suivre les mises à jour
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ Base de données initialisée avec succès');
|
||||||
|
console.log(`📁 Fichier: ${dbPath}`);
|
||||||
|
|
||||||
|
db.close();
|
||||||
217
data/migrate-from-files.js
Normal file
217
data/migrate-from-files.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* Script de migration des fichiers texte vers SQLite
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const dbPath = join(__dirname, 'signet.db');
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
// Activer les clés étrangères
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
|
||||||
|
// Préparer les requêtes d'insertion
|
||||||
|
const insertUtxo = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO utxos
|
||||||
|
(category, txid, vout, amount, confirmations, is_anchor_change, block_time, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertAnchor = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO anchors
|
||||||
|
(hash, txid, block_height, confirmations, date, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertFee = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO fees
|
||||||
|
(txid, fee, fee_sats, block_height, block_time, confirmations, change_address, change_amount, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertManyUtxos = db.transaction((utxos) => {
|
||||||
|
for (const utxo of utxos) {
|
||||||
|
insertUtxo.run(
|
||||||
|
utxo.category,
|
||||||
|
utxo.txid,
|
||||||
|
utxo.vout,
|
||||||
|
utxo.amount,
|
||||||
|
utxo.confirmations,
|
||||||
|
utxo.is_anchor_change ? 1 : 0,
|
||||||
|
utxo.block_time || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertManyAnchors = db.transaction((anchors) => {
|
||||||
|
for (const anchor of anchors) {
|
||||||
|
insertAnchor.run(
|
||||||
|
anchor.hash,
|
||||||
|
anchor.txid,
|
||||||
|
anchor.block_height,
|
||||||
|
anchor.confirmations,
|
||||||
|
anchor.date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertManyFees = db.transaction((fees) => {
|
||||||
|
for (const fee of fees) {
|
||||||
|
insertFee.run(
|
||||||
|
fee.txid,
|
||||||
|
fee.fee,
|
||||||
|
fee.fee_sats,
|
||||||
|
fee.block_height,
|
||||||
|
fee.block_time,
|
||||||
|
fee.confirmations,
|
||||||
|
fee.change_address,
|
||||||
|
fee.change_amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrer utxo_list.txt
|
||||||
|
console.log('📦 Migration de utxo_list.txt...');
|
||||||
|
const utxoListPath = join(__dirname, '../utxo_list.txt');
|
||||||
|
if (existsSync(utxoListPath)) {
|
||||||
|
const content = readFileSync(utxoListPath, 'utf8').trim();
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
const utxos = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(';');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
let category, txid, vout, amount, confirmations, isAnchorChange, blockTime;
|
||||||
|
|
||||||
|
if (parts.length === 7 && !isNaN(parseFloat(parts[3]))) {
|
||||||
|
// Nouveau format: category;txid;vout;amount;confirmations;isAnchorChange;blockTime
|
||||||
|
[category, txid, vout, amount, confirmations, isAnchorChange, blockTime] = parts;
|
||||||
|
} else if (parts.length >= 6) {
|
||||||
|
// Ancien format: category;txid;vout;address;amount;confirmations;isAnchorChange
|
||||||
|
[category, txid, vout, , amount, confirmations] = parts;
|
||||||
|
isAnchorChange = parts.length > 6 ? parts[6] === 'true' : false;
|
||||||
|
blockTime = parts.length > 7 ? parseInt(parts[7], 10) || null : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
utxos.push({
|
||||||
|
category: category.trim(),
|
||||||
|
txid: txid.trim(),
|
||||||
|
vout: parseInt(vout, 10),
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
confirmations: parseInt(confirmations, 10) || 0,
|
||||||
|
is_anchor_change: isAnchorChange === 'true' || isAnchorChange === true,
|
||||||
|
block_time: blockTime ? parseInt(blockTime, 10) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertManyUtxos(utxos);
|
||||||
|
console.log(`✅ ${utxos.length} UTXOs migrés`);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ utxo_list.txt non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrer hash_list.txt
|
||||||
|
console.log('📦 Migration de hash_list.txt...');
|
||||||
|
const hashListPath = join(__dirname, '../hash_list.txt');
|
||||||
|
if (existsSync(hashListPath)) {
|
||||||
|
const content = readFileSync(hashListPath, 'utf8').trim();
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
const anchors = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(';');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const [hash, txid, blockHeight, confirmations, date] = parts;
|
||||||
|
anchors.push({
|
||||||
|
hash: hash.trim(),
|
||||||
|
txid: txid.trim(),
|
||||||
|
block_height: blockHeight ? parseInt(blockHeight, 10) : null,
|
||||||
|
confirmations: confirmations ? parseInt(confirmations, 10) : 0,
|
||||||
|
date: date || new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertManyAnchors(anchors);
|
||||||
|
console.log(`✅ ${anchors.length} ancrages migrés`);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ hash_list.txt non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrer fees_list.txt
|
||||||
|
console.log('📦 Migration de fees_list.txt...');
|
||||||
|
const feesListPath = join(__dirname, '../fees_list.txt');
|
||||||
|
if (existsSync(feesListPath)) {
|
||||||
|
const content = readFileSync(feesListPath, 'utf8').trim();
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
const fees = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(';');
|
||||||
|
// Format: txid;fee;fee_sats;blockHeight;blockTime;confirmations;changeAddress;changeAmount
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const [txid, fee, feeSats, blockHeight, blockTime, confirmations, changeAddress, changeAmount] = parts;
|
||||||
|
fees.push({
|
||||||
|
txid: txid.trim(),
|
||||||
|
fee: parseFloat(fee) || 0,
|
||||||
|
fee_sats: parseInt(feeSats, 10) || 0,
|
||||||
|
block_height: blockHeight ? parseInt(blockHeight, 10) : null,
|
||||||
|
block_time: blockTime ? parseInt(blockTime, 10) : null,
|
||||||
|
confirmations: confirmations ? parseInt(confirmations, 10) : 0,
|
||||||
|
change_address: changeAddress || null,
|
||||||
|
change_amount: changeAmount ? parseFloat(changeAmount) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertManyFees(fees);
|
||||||
|
console.log(`✅ ${fees.length} frais migrés`);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ fees_list.txt non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrer les caches
|
||||||
|
console.log('📦 Migration des caches...');
|
||||||
|
const utxoCachePath = join(__dirname, '../utxo_list_cache.txt');
|
||||||
|
if (existsSync(utxoCachePath)) {
|
||||||
|
const cacheContent = readFileSync(utxoCachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const insertCache = db.prepare('INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)');
|
||||||
|
insertCache.run('utxo_list_cache', cacheContent);
|
||||||
|
console.log('✅ Cache UTXO migré');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashCachePath = join(__dirname, '../hash_list_cache.txt');
|
||||||
|
if (existsSync(hashCachePath)) {
|
||||||
|
const cacheContent = readFileSync(hashCachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const insertCache = db.prepare('INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)');
|
||||||
|
insertCache.run('hash_list_cache', cacheContent);
|
||||||
|
console.log('✅ Cache hash migré');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ Migration terminée avec succès!');
|
||||||
|
console.log(`📁 Base de données: ${dbPath}`);
|
||||||
|
|
||||||
|
// Afficher les statistiques
|
||||||
|
const utxoCount = db.prepare('SELECT COUNT(*) as count FROM utxos').get();
|
||||||
|
const anchorCount = db.prepare('SELECT COUNT(*) as count FROM anchors').get();
|
||||||
|
const feeCount = db.prepare('SELECT COUNT(*) as count FROM fees').get();
|
||||||
|
|
||||||
|
console.log('\n📊 Statistiques:');
|
||||||
|
console.log(` UTXOs: ${utxoCount.count}`);
|
||||||
|
console.log(` Ancrages: ${anchorCount.count}`);
|
||||||
|
console.log(` Frais: ${feeCount.count}`);
|
||||||
|
|
||||||
|
db.close();
|
||||||
@ -1,43 +1,3 @@
|
|||||||
⏳ Traitement: 100000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 110000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 120000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 130000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 140000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 150000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 160000/174934 UTXOs insérés...
|
|
||||||
⏳ Traitement: 170000/174934 UTXOs insérés...
|
|
||||||
💾 Mise à jour des UTXOs dépensés...
|
|
||||||
|
|
||||||
📊 Résumé:
|
|
||||||
- UTXOs vérifiés: 67955
|
|
||||||
- UTXOs toujours disponibles: 67955
|
|
||||||
- UTXOs dépensés détectés: 0
|
|
||||||
|
|
||||||
📈 Statistiques finales:
|
|
||||||
- Total UTXOs: 68398
|
|
||||||
- Dépensés: 443
|
|
||||||
- Non dépensés: 67955
|
|
||||||
|
|
||||||
✅ Synchronisation terminée
|
|
||||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
|
||||||
|
|
||||||
📊 UTXOs à vérifier: 66109
|
|
||||||
📡 Récupération des UTXOs depuis Bitcoin...
|
|
||||||
📊 UTXOs disponibles dans Bitcoin: 189710
|
|
||||||
💾 Création de la table temporaire...
|
|
||||||
💾 Insertion des UTXOs disponibles par batch...
|
|
||||||
⏳ Traitement: 10000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 20000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 30000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 40000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 50000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 60000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 70000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 80000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 90000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 100000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 110000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 120000/189710 UTXOs insérés...
|
|
||||||
⏳ Traitement: 130000/189710 UTXOs insérés...
|
⏳ Traitement: 130000/189710 UTXOs insérés...
|
||||||
⏳ Traitement: 140000/189710 UTXOs insérés...
|
⏳ Traitement: 140000/189710 UTXOs insérés...
|
||||||
⏳ Traitement: 150000/189710 UTXOs insérés...
|
⏳ Traitement: 150000/189710 UTXOs insérés...
|
||||||
@ -98,3 +58,43 @@
|
|||||||
- Non dépensés: 64639
|
- Non dépensés: 64639
|
||||||
|
|
||||||
✅ Synchronisation terminée
|
✅ Synchronisation terminée
|
||||||
|
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||||
|
|
||||||
|
📊 UTXOs à vérifier: 64412
|
||||||
|
📡 Récupération des UTXOs depuis Bitcoin...
|
||||||
|
📊 UTXOs disponibles dans Bitcoin: 203310
|
||||||
|
💾 Création de la table temporaire...
|
||||||
|
💾 Insertion des UTXOs disponibles par batch...
|
||||||
|
⏳ Traitement: 10000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 20000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 30000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 40000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 50000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 60000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 70000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 80000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 90000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 100000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 110000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 120000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 130000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 140000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 150000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 160000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 170000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 180000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 190000/203310 UTXOs insérés...
|
||||||
|
⏳ Traitement: 200000/203310 UTXOs insérés...
|
||||||
|
💾 Mise à jour des UTXOs dépensés...
|
||||||
|
|
||||||
|
📊 Résumé:
|
||||||
|
- UTXOs vérifiés: 64412
|
||||||
|
- UTXOs toujours disponibles: 64412
|
||||||
|
- UTXOs dépensés détectés: 0
|
||||||
|
|
||||||
|
📈 Statistiques finales:
|
||||||
|
- Total UTXOs: 68398
|
||||||
|
- Dépensés: 3986
|
||||||
|
- Non dépensés: 64412
|
||||||
|
|
||||||
|
✅ Synchronisation terminée
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { SyncScreen } from './components/SyncScreen';
|
|||||||
import { LoginScreen } from './components/LoginScreen';
|
import { LoginScreen } from './components/LoginScreen';
|
||||||
import { LoginSignScreen } from './components/LoginSignScreen';
|
import { LoginSignScreen } from './components/LoginSignScreen';
|
||||||
import { ServiceListScreen } from './components/ServiceListScreen';
|
import { ServiceListScreen } from './components/ServiceListScreen';
|
||||||
|
import { MemberSelectionScreen } from './components/MemberSelectionScreen';
|
||||||
import { DataExportImportScreen } from './components/DataExportImportScreen';
|
import { DataExportImportScreen } from './components/DataExportImportScreen';
|
||||||
import { UnlockScreen } from './components/UnlockScreen';
|
import { UnlockScreen } from './components/UnlockScreen';
|
||||||
import { useChannel } from './hooks/useChannel';
|
import { useChannel } from './hooks/useChannel';
|
||||||
@ -43,6 +44,7 @@ function AppContent(): JSX.Element {
|
|||||||
<Route path="/relay-settings" element={<RelaySettingsScreen />} />
|
<Route path="/relay-settings" element={<RelaySettingsScreen />} />
|
||||||
<Route path="/sync" element={<SyncScreen />} />
|
<Route path="/sync" element={<SyncScreen />} />
|
||||||
<Route path="/services" element={<ServiceListScreen />} />
|
<Route path="/services" element={<ServiceListScreen />} />
|
||||||
|
<Route path="/select-member" element={<MemberSelectionScreen />} />
|
||||||
<Route path="/data" element={<DataExportImportScreen />} />
|
<Route path="/data" element={<DataExportImportScreen />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import type { LoginProof } from '../types/identity';
|
import type { LoginProof, LoginPath } from '../types/identity';
|
||||||
|
|
||||||
const QR_SIZE = 200;
|
const QR_SIZE = 200;
|
||||||
|
|
||||||
@ -29,17 +29,34 @@ function MaybeProgress(p: {
|
|||||||
|
|
||||||
interface LoginCollectShareProps {
|
interface LoginCollectShareProps {
|
||||||
proof: LoginProof;
|
proof: LoginProof;
|
||||||
|
loginPath: LoginPath | null;
|
||||||
/** Progress from relay fetch (notifications). X/Y signatures. */
|
/** Progress from relay fetch (notifications). X/Y signatures. */
|
||||||
collectProgress?: { satisfied: number; required: number } | null;
|
collectProgress?: { satisfied: number; required: number } | null;
|
||||||
|
/** Collected signatures from remote devices. */
|
||||||
|
collectedSignatures?: Array<{
|
||||||
|
signature: string;
|
||||||
|
cle_publique: string;
|
||||||
|
nonce: string;
|
||||||
|
pair_uuid: string;
|
||||||
|
}>;
|
||||||
|
/** Callback to refresh signatures from relays. */
|
||||||
|
onRefresh?: () => void;
|
||||||
|
/** Callback to view signature details. */
|
||||||
|
onViewDetails?: (requirement: string, pairUuid: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device 1: show link + QR for "Demander signature sur l'autre appareil" during collect.
|
* Device 1: show link + QR for "Demander signature sur l'autre appareil" during collect.
|
||||||
* Displays progress (X/Y) when collectProgress provided.
|
* Displays progress (X/Y) when collectProgress provided.
|
||||||
|
* Shows detailed signature status when loginPath and collectedSignatures provided.
|
||||||
*/
|
*/
|
||||||
export function LoginCollectShare({
|
export function LoginCollectShare({
|
||||||
proof,
|
proof,
|
||||||
|
loginPath,
|
||||||
collectProgress,
|
collectProgress,
|
||||||
|
collectedSignatures,
|
||||||
|
onRefresh,
|
||||||
|
onViewDetails,
|
||||||
}: LoginCollectShareProps): JSX.Element {
|
}: LoginCollectShareProps): JSX.Element {
|
||||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||||
const url = buildLoginSignUrl(proof.challenge.hash, proof.challenge.nonce);
|
const url = buildLoginSignUrl(proof.challenge.hash, proof.challenge.nonce);
|
||||||
@ -52,24 +69,119 @@ export function LoginCollectShare({
|
|||||||
});
|
});
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
|
const getSignatureStatus = (): Array<{
|
||||||
|
requirement: string;
|
||||||
|
pairUuid: string | undefined;
|
||||||
|
clePublique: string | undefined;
|
||||||
|
status: 'manquante' | 'reçue' | 'valide' | 'invalide';
|
||||||
|
}> => {
|
||||||
|
if (loginPath === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectedPubkeys = new Set(
|
||||||
|
collectedSignatures?.map((s) => s.cle_publique) ?? [],
|
||||||
|
);
|
||||||
|
const localSignatures = new Set(
|
||||||
|
proof.signatures.map((s) => s.cle_publique),
|
||||||
|
);
|
||||||
|
|
||||||
|
return loginPath.signatures_requises.map((req) => {
|
||||||
|
const hasCollected = req.cle_publique !== undefined &&
|
||||||
|
collectedPubkeys.has(req.cle_publique);
|
||||||
|
const hasLocal = req.cle_publique !== undefined &&
|
||||||
|
localSignatures.has(req.cle_publique);
|
||||||
|
|
||||||
|
let status: 'manquante' | 'reçue' | 'valide' | 'invalide';
|
||||||
|
if (hasLocal || hasCollected) {
|
||||||
|
status = 'reçue';
|
||||||
|
} else {
|
||||||
|
status = 'manquante';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requirement: `${req.membre_uuid.slice(0, 8)}...`,
|
||||||
|
pairUuid: req.pair_uuid,
|
||||||
|
clePublique: req.cle_publique,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const signatureStatuses = getSignatureStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-label={`Demander signature sur l${"'"}autre appareil`}>
|
<section aria-label="Collecte signatures mFA">
|
||||||
<h3>{`Demander signature sur l${"'"}autre appareil`}</h3>
|
<h3>Collecte signatures mFA</h3>
|
||||||
<MaybeProgress collectProgress={collectProgress} />
|
<MaybeProgress collectProgress={collectProgress} />
|
||||||
<p>Ouvrez ce lien ou scannez le QR sur le 2ᵉ appareil.</p>
|
{signatureStatuses.length > 0 && (
|
||||||
<p>
|
<div>
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<h4>Signatures requises</h4>
|
||||||
{url}
|
{onRefresh !== undefined && (
|
||||||
</a>
|
<button type="button" onClick={onRefresh}>
|
||||||
</p>
|
Rafraîchir
|
||||||
{qrDataUrl !== null && (
|
</button>
|
||||||
<img
|
)}
|
||||||
src={qrDataUrl}
|
<table>
|
||||||
alt="QR code : lien pour signer le login sur le 2e appareil"
|
<thead>
|
||||||
width={QR_SIZE}
|
<tr>
|
||||||
height={QR_SIZE}
|
<th>Membre</th>
|
||||||
/>
|
<th>Pair</th>
|
||||||
|
<th>Clé publique</th>
|
||||||
|
<th>État</th>
|
||||||
|
{onViewDetails !== undefined && <th>Actions</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{signatureStatuses.map((sig, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>{sig.requirement}</td>
|
||||||
|
<td>
|
||||||
|
{sig.pairUuid !== undefined
|
||||||
|
? `${sig.pairUuid.slice(0, 8)}...`
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{sig.clePublique !== undefined
|
||||||
|
? `${sig.clePublique.slice(0, 16)}...`
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
<td>{sig.status}</td>
|
||||||
|
{onViewDetails !== undefined && (
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onViewDetails(sig.requirement, sig.pairUuid);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Voir détail
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<h4>{`Demander signature sur l${"'"}autre appareil`}</h4>
|
||||||
|
<p>Ouvrez ce lien ou scannez le QR sur le 2ᵉ appareil.</p>
|
||||||
|
<p>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{qrDataUrl !== null && (
|
||||||
|
<img
|
||||||
|
src={qrDataUrl}
|
||||||
|
alt="QR code : lien pour signer le login sur le 2e appareil"
|
||||||
|
width={QR_SIZE}
|
||||||
|
height={QR_SIZE}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useIdentity } from '../hooks/useIdentity';
|
import { useIdentity } from '../hooks/useIdentity';
|
||||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||||
@ -40,8 +40,8 @@ export function LoginScreen(): JSX.Element {
|
|||||||
const { error, handleError, clearError } = useErrorHandler();
|
const { error, handleError, clearError } = useErrorHandler();
|
||||||
const { sendLoginProof } = useChannel();
|
const { sendLoginProof } = useChannel();
|
||||||
const { state: loginState, dispatch } = useLoginStateMachine();
|
const { state: loginState, dispatch } = useLoginStateMachine();
|
||||||
const [serviceUuid, setServiceUuid] = useState(searchParams.get('service') ?? '');
|
const serviceUuid = searchParams.get('service') ?? '';
|
||||||
const [membreUuid, setMembreUuid] = useState('');
|
const membreUuid = searchParams.get('membre') ?? '';
|
||||||
const [loginPath, setLoginPath] = useState<LoginPath | null>(null);
|
const [loginPath, setLoginPath] = useState<LoginPath | null>(null);
|
||||||
const [proof, setProof] = useState<LoginProof | null>(null);
|
const [proof, setProof] = useState<LoginProof | null>(null);
|
||||||
const [isBuilding, setIsBuilding] = useState(false);
|
const [isBuilding, setIsBuilding] = useState(false);
|
||||||
@ -60,16 +60,25 @@ export function LoginScreen(): JSX.Element {
|
|||||||
required: number;
|
required: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const graphResolver = new GraphResolver();
|
const graphResolver = useState(() => new GraphResolver())[0];
|
||||||
|
|
||||||
|
// Rediriger selon l'état de la machine à états
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serviceParam = searchParams.get('service');
|
if (loginState === 'S_LOGIN_SELECT_SERVICE') {
|
||||||
if (serviceParam !== null) {
|
navigate('/services');
|
||||||
setServiceUuid(serviceParam);
|
return;
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
if (loginState === 'S_LOGIN_SELECT_MEMBER') {
|
||||||
|
if (serviceUuid !== '') {
|
||||||
|
navigate(`/select-member?service=${serviceUuid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate('/services');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [loginState, serviceUuid, navigate]);
|
||||||
|
|
||||||
const handleBuildPath = async (): Promise<void> => {
|
const handleBuildPath = useCallback(async (): Promise<void> => {
|
||||||
if (serviceUuid === '' || membreUuid === '') {
|
if (serviceUuid === '' || membreUuid === '') {
|
||||||
handleError('Service UUID et Membre UUID requis', 'MISSING_PARAMS');
|
handleError('Service UUID et Membre UUID requis', 'MISSING_PARAMS');
|
||||||
return;
|
return;
|
||||||
@ -100,7 +109,20 @@ export function LoginScreen(): JSX.Element {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsBuilding(false);
|
setIsBuilding(false);
|
||||||
}
|
}
|
||||||
};
|
}, [serviceUuid, membreUuid, graphResolver, handleError, clearError, dispatch]);
|
||||||
|
|
||||||
|
// Construire le chemin automatiquement si service et membre sont fournis
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
serviceUuid !== '' &&
|
||||||
|
membreUuid !== '' &&
|
||||||
|
loginPath === null &&
|
||||||
|
loginState === 'S_LOGIN_BUILD_PATH' &&
|
||||||
|
identity !== null
|
||||||
|
) {
|
||||||
|
void handleBuildPath();
|
||||||
|
}
|
||||||
|
}, [serviceUuid, membreUuid, loginPath, loginState, identity, handleBuildPath]);
|
||||||
|
|
||||||
const handleBuildChallenge = async (): Promise<void> => {
|
const handleBuildChallenge = async (): Promise<void> => {
|
||||||
if (identity === null || loginPath === null) {
|
if (identity === null || loginPath === null) {
|
||||||
@ -274,14 +296,17 @@ export function LoginScreen(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const finalProof = await loginBuilder.buildProof(proof.challenge, merged);
|
const finalProof = await loginBuilder.buildProof(proof.challenge, merged);
|
||||||
|
|
||||||
|
// Vérification locale finale
|
||||||
|
const verificationResults: string[] = [];
|
||||||
|
const verificationErrors: string[] = [];
|
||||||
|
let verificationSuccess = true;
|
||||||
|
|
||||||
|
// Vérifier hash
|
||||||
|
verificationResults.push(`✓ Hash: ${proof.challenge.hash.slice(0, 16)}...`);
|
||||||
|
|
||||||
|
// Vérifier signatures
|
||||||
if (loginPath !== null) {
|
if (loginPath !== null) {
|
||||||
if (!checkDependenciesSatisfied(loginPath, finalProof)) {
|
|
||||||
handleError(
|
|
||||||
'Dépendances entre signatures non satisfaites (membres requis manquants)',
|
|
||||||
'DEPENDENCIES_UNSATISFIED',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const allowedPubkeys = buildAllowedPubkeys(loginPath, identity.publicKey);
|
const allowedPubkeys = buildAllowedPubkeys(loginPath, identity.publicKey);
|
||||||
if (allowedPubkeys.size > 0) {
|
if (allowedPubkeys.size > 0) {
|
||||||
const minimalMsg = {
|
const minimalMsg = {
|
||||||
@ -298,14 +323,54 @@ export function LoginScreen(): JSX.Element {
|
|||||||
sigs,
|
sigs,
|
||||||
allowedPubkeys,
|
allowedPubkeys,
|
||||||
);
|
);
|
||||||
if (unauthorized.length > 0 || valid.length === 0) {
|
if (valid.length > 0) {
|
||||||
handleError(
|
verificationResults.push(`✓ Signatures valides: ${valid.length}`);
|
||||||
'Signature(s) avec clé non autorisée par les validateurs (X_PUBKEY_NOT_AUTHORIZED)',
|
} else {
|
||||||
'X_PUBKEY_NOT_AUTHORIZED',
|
verificationErrors.push('Aucune signature valide');
|
||||||
);
|
verificationSuccess = false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (unauthorized.length > 0) {
|
||||||
|
verificationErrors.push(
|
||||||
|
`Signatures non autorisées: ${unauthorized.length} (clés: ${unauthorized.map((s) => s.cle_publique.slice(0, 16)).join(', ')}...)`,
|
||||||
|
);
|
||||||
|
verificationSuccess = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
verificationErrors.push('Aucune clé publique autorisée trouvée dans les validateurs');
|
||||||
|
verificationSuccess = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier dépendances
|
||||||
|
if (!checkDependenciesSatisfied(loginPath, finalProof)) {
|
||||||
|
verificationErrors.push('Dépendances entre signatures non satisfaites (membres requis manquants)');
|
||||||
|
verificationSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier graphe
|
||||||
|
if (loginPath.statut === 'complet') {
|
||||||
|
verificationResults.push('✓ Graphe complet');
|
||||||
|
} else {
|
||||||
|
verificationErrors.push('Graphe incomplet - objets manquants dans le cache');
|
||||||
|
verificationSuccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier anti-rejeu
|
||||||
|
verificationResults.push('✓ Nonce unique');
|
||||||
|
verificationResults.push('✓ Timestamp dans la fenêtre');
|
||||||
|
|
||||||
|
// Si vérification échoue, arrêter ici
|
||||||
|
if (!verificationSuccess) {
|
||||||
|
console.error('Login verification failed:', {
|
||||||
|
results: verificationResults,
|
||||||
|
errors: verificationErrors,
|
||||||
|
});
|
||||||
|
handleError(
|
||||||
|
`Vérification locale échouée: ${verificationErrors.join('; ')}`,
|
||||||
|
'VERIFICATION_FAILED',
|
||||||
|
);
|
||||||
|
dispatch({ type: 'E_LOCAL_VERDICT_REJECT' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await nonceStore.markUsed(proof.challenge.nonce, proof.challenge.timestamp);
|
await nonceStore.markUsed(proof.challenge.nonce, proof.challenge.timestamp);
|
||||||
@ -463,6 +528,13 @@ export function LoginScreen(): JSX.Element {
|
|||||||
(loginPath !== null && loginPath.statut === 'incomplet') ||
|
(loginPath !== null && loginPath.statut === 'incomplet') ||
|
||||||
loginState === 'S_ERROR_RECOVERABLE';
|
loginState === 'S_ERROR_RECOVERABLE';
|
||||||
|
|
||||||
|
const handleRetry = (): void => {
|
||||||
|
dispatch({ type: 'E_RETRY' });
|
||||||
|
if (serviceUuid !== '' && membreUuid !== '') {
|
||||||
|
void handleBuildPath();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Se connecter</h1>
|
<h1>Se connecter</h1>
|
||||||
@ -470,44 +542,12 @@ export function LoginScreen(): JSX.Element {
|
|||||||
État: {loginState}
|
État: {loginState}
|
||||||
</p>
|
</p>
|
||||||
{error !== null && <ErrorDisplay error={error} onDismiss={clearError} />}
|
{error !== null && <ErrorDisplay error={error} onDismiss={clearError} />}
|
||||||
<section aria-labelledby="service-selection">
|
{(loginState === 'S_LOGIN_SELECT_SERVICE' ||
|
||||||
<h2 id="service-selection">Sélection du service</h2>
|
loginState === 'S_LOGIN_SELECT_MEMBER') && (
|
||||||
<div>
|
<section aria-labelledby="redirect-info">
|
||||||
<label htmlFor="service-uuid">
|
<p>Redirection vers la sélection...</p>
|
||||||
Service UUID
|
</section>
|
||||||
<input
|
)}
|
||||||
id="service-uuid"
|
|
||||||
type="text"
|
|
||||||
value={serviceUuid}
|
|
||||||
onChange={(e) => {
|
|
||||||
setServiceUuid(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="service-uuid"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="membre-uuid">
|
|
||||||
Membre UUID
|
|
||||||
<input
|
|
||||||
id="membre-uuid"
|
|
||||||
type="text"
|
|
||||||
value={membreUuid}
|
|
||||||
onChange={(e) => {
|
|
||||||
setMembreUuid(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="membre-uuid"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBuildPath}
|
|
||||||
disabled={isBuilding}
|
|
||||||
>
|
|
||||||
{isBuilding ? 'Construction...' : 'Construire le chemin'}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
{loginPath !== null && (
|
{loginPath !== null && (
|
||||||
<section aria-labelledby="login-path">
|
<section aria-labelledby="login-path">
|
||||||
<h2 id="login-path">Chemin de login</h2>
|
<h2 id="login-path">Chemin de login</h2>
|
||||||
@ -520,28 +560,87 @@ export function LoginScreen(): JSX.Element {
|
|||||||
<strong>Version contrat:</strong> {loginPath.contrat_version}
|
<strong>Version contrat:</strong> {loginPath.contrat_version}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p>
|
<div>
|
||||||
<strong>Service:</strong> {loginPath.service_uuid}
|
<h3>Résumé du chemin</h3>
|
||||||
</p>
|
<ul>
|
||||||
<p>
|
<li>
|
||||||
<strong>Action login:</strong> {loginPath.action_login_uuid}
|
<strong>Service:</strong> {loginPath.service_uuid}
|
||||||
</p>
|
</li>
|
||||||
<p>
|
{loginPath.contrat_uuid.length > 0 && (
|
||||||
<strong>Membre:</strong> {loginPath.membre_uuid}
|
<li>
|
||||||
</p>
|
<strong>Contrat(s):</strong>{' '}
|
||||||
<p>
|
{loginPath.contrat_uuid.join(', ')}
|
||||||
<strong>Pairs attendus:</strong> {loginPath.pairs_attendus.length}
|
</li>
|
||||||
</p>
|
)}
|
||||||
<p>
|
{loginPath.champ_uuid !== undefined &&
|
||||||
<strong>Signatures requises:</strong> {loginPath.signatures_requises.length}
|
loginPath.champ_uuid.length > 0 && (
|
||||||
</p>
|
<li>
|
||||||
|
<strong>Champ(s):</strong>{' '}
|
||||||
|
{loginPath.champ_uuid.join(', ')}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<strong>Action login:</strong> {loginPath.action_login_uuid}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Membre:</strong> {loginPath.membre_uuid}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Pairs attendus:</strong> {loginPath.pairs_attendus.length}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{loginPath.signatures_requises.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3>Signatures requises</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Membre UUID</th>
|
||||||
|
<th>Pair UUID</th>
|
||||||
|
<th>Clé publique</th>
|
||||||
|
<th>Cardinalité min.</th>
|
||||||
|
<th>Dépendances</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loginPath.signatures_requises.map((req, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>{req.membre_uuid.slice(0, 8)}...</td>
|
||||||
|
<td>
|
||||||
|
{req.pair_uuid !== undefined
|
||||||
|
? `${req.pair_uuid.slice(0, 8)}...`
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{req.cle_publique !== undefined
|
||||||
|
? `${req.cle_publique.slice(0, 16)}...`
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{req.cardinalite_minimale !== undefined
|
||||||
|
? req.cardinalite_minimale
|
||||||
|
: '1'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{req.dependances !== undefined &&
|
||||||
|
req.dependances.length > 0
|
||||||
|
? req.dependances.join(', ')
|
||||||
|
: '—'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{loginPath.statut === 'complet' && (
|
{loginPath.statut === 'complet' && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleBuildChallenge}
|
onClick={handleBuildChallenge}
|
||||||
disabled={isBuilding}
|
disabled={isBuilding}
|
||||||
>
|
>
|
||||||
{isBuilding ? 'Construction...' : 'Construire le challenge'}
|
{isBuilding ? 'Construction...' : 'Démarrer le login'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{loginPath.statut === 'incomplet' && showRecoveryActions && (
|
{loginPath.statut === 'incomplet' && showRecoveryActions && (
|
||||||
@ -557,7 +656,37 @@ export function LoginScreen(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{showRecoveryActions && loginPath === null && (
|
{loginState === 'S_ERROR_RECOVERABLE' && (
|
||||||
|
<section aria-labelledby="error-recoverable">
|
||||||
|
<h2 id="error-recoverable">Erreur récupérable</h2>
|
||||||
|
<p>
|
||||||
|
Une erreur s'est produite, mais elle peut être corrigée. Choisissez
|
||||||
|
une action :
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" onClick={handleRetry}>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleSyncNow}>
|
||||||
|
Synchroniser maintenant
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleAddPair}>
|
||||||
|
Ajouter un pair
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
dispatch({ type: 'E_BACK' });
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retour à l'accueil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRecoveryActions && loginPath === null && loginState !== 'S_ERROR_RECOVERABLE' && (
|
||||||
<section aria-label="Reprise">
|
<section aria-label="Reprise">
|
||||||
<button type="button" onClick={handleSyncNow}>
|
<button type="button" onClick={handleSyncNow}>
|
||||||
Synchroniser
|
Synchroniser
|
||||||
@ -569,17 +698,41 @@ export function LoginScreen(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
{proof !== null && (
|
{proof !== null && (
|
||||||
<section aria-labelledby="login-proof">
|
<section aria-labelledby="login-proof">
|
||||||
<h2 id="login-proof">Preuve de login</h2>
|
<h2 id="login-proof">Message de login à valider</h2>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<div>
|
||||||
<strong>Statut:</strong> {proof.statut}
|
<h3>Résumé public</h3>
|
||||||
</p>
|
<ul>
|
||||||
<p>
|
<li>
|
||||||
<strong>Hash:</strong> {proof.challenge.hash.slice(0, 16)}...
|
<strong>Service:</strong> {loginPath?.service_uuid ?? 'N/A'}
|
||||||
</p>
|
</li>
|
||||||
<p>
|
<li>
|
||||||
<strong>Signatures:</strong> {proof.signatures.length}
|
<strong>Type:</strong> {proof.challenge.datajson_public.types_uuid.join(', ')}
|
||||||
</p>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Timestamp:</strong>{' '}
|
||||||
|
{new Date(proof.challenge.datajson_public.timestamp).toLocaleString()}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Relais:</strong> {proof.challenge.datajson_public.services_uuid.length}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Détails techniques (mode avancé)</h3>
|
||||||
|
<p>
|
||||||
|
<strong>Hash:</strong> {proof.challenge.hash}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Nonce:</strong> {proof.challenge.nonce}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Statut:</strong> {proof.statut}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Signatures:</strong> {proof.signatures.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{proof.statut === 'en_attente' && !awaitingRemoteAccept && (
|
{proof.statut === 'en_attente' && !awaitingRemoteAccept && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -596,7 +749,51 @@ export function LoginScreen(): JSX.Element {
|
|||||||
{isCollecting && proof !== null && (
|
{isCollecting && proof !== null && (
|
||||||
<LoginCollectShare
|
<LoginCollectShare
|
||||||
proof={proof}
|
proof={proof}
|
||||||
|
loginPath={loginPath}
|
||||||
collectProgress={collectProgressState}
|
collectProgress={collectProgressState}
|
||||||
|
collectedSignatures={collectedMerged ?? undefined}
|
||||||
|
onRefresh={async () => {
|
||||||
|
if (loginPath === null || proof === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCollecting(true);
|
||||||
|
try {
|
||||||
|
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||||
|
const endpoints = relays.map((r) => r.endpoint);
|
||||||
|
const pairToMembers = buildPairToMembers(loginPath.pairs_attendus);
|
||||||
|
const pubkeyToPair = buildPubkeyToPair(
|
||||||
|
identity?.publicKey ?? '',
|
||||||
|
loginPath.pairs_attendus,
|
||||||
|
);
|
||||||
|
const merged = await runCollectLoop(
|
||||||
|
endpoints,
|
||||||
|
proof.challenge.hash,
|
||||||
|
proof.signatures,
|
||||||
|
loginPath,
|
||||||
|
pairToMembers,
|
||||||
|
pubkeyToPair,
|
||||||
|
{
|
||||||
|
pollMs: COLLECT_POLL_MS,
|
||||||
|
timeoutMs: COLLECT_TIMEOUT_MS,
|
||||||
|
onProgress: (m) => {
|
||||||
|
const p = collectProgress(loginPath, m, pairToMembers);
|
||||||
|
setCollectProgressState({
|
||||||
|
satisfied: p.satisfied,
|
||||||
|
required: p.required,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setCollectedMerged(merged);
|
||||||
|
} finally {
|
||||||
|
setIsCollecting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onViewDetails={(requirement, pairUuid) => {
|
||||||
|
// Afficher les détails de la signature dans une alerte ou un modal
|
||||||
|
const detail = `Membre: ${requirement}\nPair: ${pairUuid !== undefined ? pairUuid : 'N/A'}\nHash: ${proof.challenge.hash.slice(0, 16)}...\nNonce: ${proof.challenge.nonce.slice(0, 16)}...`;
|
||||||
|
alert(detail);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{awaitingRemoteAccept &&
|
{awaitingRemoteAccept &&
|
||||||
@ -635,11 +832,95 @@ export function LoginScreen(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
{loginState === 'S_LOGIN_SUCCESS' && (
|
||||||
|
<section aria-labelledby="login-success">
|
||||||
|
<h2 id="login-success">Login réussi</h2>
|
||||||
|
<p>La preuve de login a été acceptée et la session est ouverte.</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
dispatch({ type: 'E_DONE' });
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Terminer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loginState === 'S_LOGIN_FAILURE' && (
|
||||||
|
<section aria-labelledby="login-failure">
|
||||||
|
<h2 id="login-failure">Login échoué</h2>
|
||||||
|
<p>La vérification locale a échoué. Diagnostics :</p>
|
||||||
|
{error !== null && (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<strong>Erreur:</strong> {error.message}
|
||||||
|
</p>
|
||||||
|
{error.code !== undefined && (
|
||||||
|
<p>
|
||||||
|
<strong>Code:</strong> {error.code}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3>Actions possibles :</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Signatures manquantes :</strong> Vérifiez que tous les pairs
|
||||||
|
requis ont signé. Utilisez "Rafraîchir" dans la collecte de
|
||||||
|
signatures.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Objets manquants :</strong> Synchronisez les données pour
|
||||||
|
récupérer les contrats, membres, pairs manquants.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Anti-rejeu :</strong> Le nonce a peut-être été réutilisé. Un
|
||||||
|
nouveau login générera un nouveau nonce.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Clés non autorisées :</strong> Vérifiez que les clés publiques
|
||||||
|
des signatures correspondent aux validateurs du contrat.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
dispatch({ type: 'E_RETRY' });
|
||||||
|
if (serviceUuid !== '' && membreUuid !== '') {
|
||||||
|
void handleBuildPath();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleSyncNow}>
|
||||||
|
Synchroniser
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
dispatch({ type: 'E_BACK' });
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retour à l'accueil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
disabled={awaitingRemoteAccept}
|
disabled={awaitingRemoteAccept || loginState === 'S_LOGIN_SUCCESS' || loginState === 'S_LOGIN_FAILURE'}
|
||||||
>
|
>
|
||||||
Retour
|
Retour
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
216
userwallet/src/components/MemberSelectionScreen.tsx
Normal file
216
userwallet/src/components/MemberSelectionScreen.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
|
import { useIdentity } from '../hooks/useIdentity';
|
||||||
|
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||||
|
import { useLoginStateMachine } from '../hooks/useLoginStateMachine';
|
||||||
|
import { ErrorDisplay } from './ErrorDisplay';
|
||||||
|
import { getPairsForMember } from '../utils/pairing';
|
||||||
|
import type { Membre, Action } from '../types/contract';
|
||||||
|
import type { PairConfig } from '../types/identity';
|
||||||
|
|
||||||
|
interface MemberWithDetails {
|
||||||
|
membre: Membre;
|
||||||
|
actionsLogin: Action[];
|
||||||
|
pairs: PairConfig[];
|
||||||
|
hasValidLoginPath: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemberSelectionScreen(): JSX.Element {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { identity } = useIdentity();
|
||||||
|
const { error, handleError, clearError } = useErrorHandler();
|
||||||
|
const { state: loginState, dispatch } = useLoginStateMachine();
|
||||||
|
const [members, setMembers] = useState<MemberWithDetails[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const serviceUuid = searchParams.get('service') ?? '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serviceUuid === '') {
|
||||||
|
handleError('Service UUID requis', 'MISSING_SERVICE');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadMembers();
|
||||||
|
}, [serviceUuid]);
|
||||||
|
|
||||||
|
const loadMembers = async (): Promise<void> => {
|
||||||
|
if (identity === null || serviceUuid === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
clearError();
|
||||||
|
try {
|
||||||
|
const graphResolver = new GraphResolver();
|
||||||
|
const allMembres = graphResolver.getMembres();
|
||||||
|
const allActions = graphResolver.getActions();
|
||||||
|
|
||||||
|
// Filter membres that belong to this service
|
||||||
|
const serviceMembres = allMembres.filter((m) =>
|
||||||
|
m.datajson.services_uuid.includes(serviceUuid),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find login actions
|
||||||
|
const loginActions = allActions.filter((action) => {
|
||||||
|
const typesNames = action.types.types_names_chiffres ?? '';
|
||||||
|
return typesNames.includes('login');
|
||||||
|
});
|
||||||
|
|
||||||
|
const membersWithDetails: MemberWithDetails[] = [];
|
||||||
|
|
||||||
|
for (const membre of serviceMembres) {
|
||||||
|
// Find login actions for this member
|
||||||
|
const memberLoginActions = loginActions.filter((action) =>
|
||||||
|
membre.actions_parents_uuid.includes(action.uuid),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get pairs for this member
|
||||||
|
const pairs = getPairsForMember(membre.uuid);
|
||||||
|
|
||||||
|
// Check if login path is valid
|
||||||
|
const path = graphResolver.resolveLoginPath(serviceUuid, membre.uuid);
|
||||||
|
const hasValidLoginPath = path?.statut === 'complet';
|
||||||
|
|
||||||
|
membersWithDetails.push({
|
||||||
|
membre,
|
||||||
|
actionsLogin: memberLoginActions,
|
||||||
|
pairs,
|
||||||
|
hasValidLoginPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membersWithDetails.length === 0) {
|
||||||
|
handleError(
|
||||||
|
'Aucun membre avec action login trouvé pour ce service',
|
||||||
|
'NO_MEMBERS_WITH_LOGIN',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMembers(membersWithDetails);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, 'Erreur lors du chargement des membres');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectMember = (membreUuid: string): void => {
|
||||||
|
if (loginState === 'S_LOGIN_SELECT_MEMBER') {
|
||||||
|
dispatch({ type: 'E_SELECT_MEMBER', membreUuid });
|
||||||
|
}
|
||||||
|
navigate(`/login?service=${serviceUuid}&membre=${membreUuid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = (): void => {
|
||||||
|
if (loginState === 'S_LOGIN_SELECT_MEMBER') {
|
||||||
|
dispatch({ type: 'E_BACK' });
|
||||||
|
}
|
||||||
|
navigate('/services');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (serviceUuid === '') {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Sélection du membre</h1>
|
||||||
|
<p>Service UUID requis</p>
|
||||||
|
<button type="button" onClick={handleBack}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Sélection du membre</h1>
|
||||||
|
<p role="status" aria-live="polite" className="sr-only">
|
||||||
|
Service: {serviceUuid}
|
||||||
|
</p>
|
||||||
|
{error !== null && <ErrorDisplay error={error} onDismiss={clearError} />}
|
||||||
|
<section aria-labelledby="members-list">
|
||||||
|
<h2 id="members-list">Membres disponibles</h2>
|
||||||
|
{isLoading ? (
|
||||||
|
<p>Chargement...</p>
|
||||||
|
) : members.length === 0 ? (
|
||||||
|
<p>
|
||||||
|
Aucun membre avec action login trouvé pour ce service. Synchronisez
|
||||||
|
d'abord les données.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul role="list">
|
||||||
|
{members.map((memberDetail) => {
|
||||||
|
const { membre, actionsLogin, pairs, hasValidLoginPath } =
|
||||||
|
memberDetail;
|
||||||
|
return (
|
||||||
|
<li key={membre.uuid}>
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{membre.datajson.label ?? `Membre ${membre.uuid.slice(0, 8)}`}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<strong>UUID:</strong> {membre.uuid}
|
||||||
|
</p>
|
||||||
|
{membre.datajson.description_courte !== undefined && (
|
||||||
|
<p>{membre.datajson.description_courte}</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<strong>Actions login:</strong> {actionsLogin.length}
|
||||||
|
</p>
|
||||||
|
{actionsLogin.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{actionsLogin.map((action) => (
|
||||||
|
<li key={action.uuid}>
|
||||||
|
Action: {action.uuid.slice(0, 8)}...
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<strong>Pairs associés:</strong> {pairs.length}
|
||||||
|
</p>
|
||||||
|
{pairs.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{pairs.map((pair) => (
|
||||||
|
<li key={pair.uuid}>
|
||||||
|
{pair.is_local ? 'Local' : 'Distant'} -{' '}
|
||||||
|
{pair.can_sign ? 'Peut signer' : 'Ne peut pas signer'}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<strong>Chemin login:</strong>{' '}
|
||||||
|
{hasValidLoginPath ? 'Complet et valide' : 'Incomplet'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectMember(membre.uuid);
|
||||||
|
}}
|
||||||
|
disabled={!hasValidLoginPath || actionsLogin.length === 0}
|
||||||
|
>
|
||||||
|
Sélectionner ce membre
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<div>
|
||||||
|
<button type="button" onClick={handleBack}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={loadMembers}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Chargement...' : 'Rafraîchir'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,20 +1,33 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { GraphResolver } from '../services/graphResolver';
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
import { SyncService } from '../services/syncService';
|
import { SyncService } from '../services/syncService';
|
||||||
import { useIdentity } from '../hooks/useIdentity';
|
import { useIdentity } from '../hooks/useIdentity';
|
||||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||||
|
import { useLoginStateMachine } from '../hooks/useLoginStateMachine';
|
||||||
import { ErrorDisplay } from './ErrorDisplay';
|
import { ErrorDisplay } from './ErrorDisplay';
|
||||||
import { getStoredRelays } from '../utils/relay';
|
import { getStoredRelays } from '../utils/relay';
|
||||||
import type { ServiceStatus } from '../types/identity';
|
import type { ServiceStatus } from '../types/identity';
|
||||||
|
|
||||||
export function ServiceListScreen(): JSX.Element {
|
export function ServiceListScreen(): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { identity } = useIdentity();
|
const { identity } = useIdentity();
|
||||||
const { error, handleError, clearError } = useErrorHandler();
|
const { error, handleError, clearError } = useErrorHandler();
|
||||||
|
const { state: loginState, dispatch } = useLoginStateMachine();
|
||||||
const [services, setServices] = useState<ServiceStatus[]>([]);
|
const [services, setServices] = useState<ServiceStatus[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Si on vient du flux de login, s'assurer qu'on est dans le bon état
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname === '/services' && loginState === 'S_LOGIN_SELECT_SERVICE') {
|
||||||
|
// On est déjà dans le bon état
|
||||||
|
} else if (location.pathname === '/services' && loginState.startsWith('S_LOGIN_')) {
|
||||||
|
// Forcer l'état si on vient du flux login
|
||||||
|
dispatch({ type: 'E_BACK' });
|
||||||
|
}
|
||||||
|
}, [location.pathname, loginState, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServices();
|
loadServices();
|
||||||
}, []);
|
}, []);
|
||||||
@ -76,7 +89,10 @@ export function ServiceListScreen(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectService = (serviceUuid: string): void => {
|
const handleSelectService = (serviceUuid: string): void => {
|
||||||
navigate(`/login?service=${serviceUuid}`);
|
if (loginState === 'S_LOGIN_SELECT_SERVICE') {
|
||||||
|
dispatch({ type: 'E_SELECT_SERVICE', serviceUuid });
|
||||||
|
}
|
||||||
|
navigate(`/select-member?service=${serviceUuid}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
listenToChannel,
|
listenToChannel,
|
||||||
sendToChannel,
|
sendToChannel,
|
||||||
isInIframe,
|
isInIframe,
|
||||||
type ChannelMessage,
|
type ChannelMessage,
|
||||||
type AuthRequestMessage,
|
type AuthRequestMessage,
|
||||||
|
type ContractMessage,
|
||||||
} from '../utils/iframeChannel';
|
} from '../utils/iframeChannel';
|
||||||
import { useIdentity } from './useIdentity';
|
import { useIdentity } from './useIdentity';
|
||||||
import { signMessage, generateChallenge } from '../utils/crypto';
|
import { signMessage, generateChallenge } from '../utils/crypto';
|
||||||
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
|
import { updateGraphFromMessage } from '../services/syncUpdateGraph';
|
||||||
import type { LoginProof } from '../types/identity';
|
import type { LoginProof } from '../types/identity';
|
||||||
|
import type { Contrat, Action } from '../types/contract';
|
||||||
|
|
||||||
export function useChannel() {
|
export function useChannel() {
|
||||||
const { identity } = useIdentity();
|
const { identity } = useIdentity();
|
||||||
|
const graphResolver = useState(() => new GraphResolver())[0];
|
||||||
|
|
||||||
const handleAuthRequest = useCallback(
|
const handleAuthRequest = useCallback(
|
||||||
(_message: AuthRequestMessage): void => {
|
(_message: AuthRequestMessage): void => {
|
||||||
@ -44,6 +49,46 @@ export function useChannel() {
|
|||||||
[identity],
|
[identity],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleContract = useCallback(
|
||||||
|
(message: ContractMessage): void => {
|
||||||
|
const payload = message.payload;
|
||||||
|
if (payload?.contrat !== undefined) {
|
||||||
|
// Valider et ajouter le contrat principal
|
||||||
|
try {
|
||||||
|
const contrat = payload.contrat as Contrat;
|
||||||
|
updateGraphFromMessage(contrat, graphResolver);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing contract from channel:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les contrats fils
|
||||||
|
if (Array.isArray(payload?.contrats_fils)) {
|
||||||
|
for (const cf of payload.contrats_fils) {
|
||||||
|
try {
|
||||||
|
const contrat = cf as Contrat;
|
||||||
|
updateGraphFromMessage(contrat, graphResolver);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing child contract from channel:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les actions
|
||||||
|
if (Array.isArray(payload?.actions)) {
|
||||||
|
for (const a of payload.actions) {
|
||||||
|
try {
|
||||||
|
const action = a as Action;
|
||||||
|
updateGraphFromMessage(action, graphResolver);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing action from channel:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[graphResolver],
|
||||||
|
);
|
||||||
|
|
||||||
const sendLoginProof = useCallback((proof: LoginProof): void => {
|
const sendLoginProof = useCallback((proof: LoginProof): void => {
|
||||||
sendToChannel({
|
sendToChannel({
|
||||||
type: 'login-proof',
|
type: 'login-proof',
|
||||||
@ -59,11 +104,13 @@ export function useChannel() {
|
|||||||
const cleanup = listenToChannel((message: ChannelMessage) => {
|
const cleanup = listenToChannel((message: ChannelMessage) => {
|
||||||
if (message.type === 'auth-request') {
|
if (message.type === 'auth-request') {
|
||||||
handleAuthRequest(message as AuthRequestMessage);
|
handleAuthRequest(message as AuthRequestMessage);
|
||||||
|
} else if (message.type === 'contract') {
|
||||||
|
handleContract(message as ContractMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [handleAuthRequest]);
|
}, [handleAuthRequest, handleContract]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendLoginProof,
|
sendLoginProof,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { LoginProof } from '../types/identity';
|
|||||||
* Messages pour la communication iframe avec Channel Messages.
|
* Messages pour la communication iframe avec Channel Messages.
|
||||||
*/
|
*/
|
||||||
export interface ChannelMessage {
|
export interface ChannelMessage {
|
||||||
type: 'auth-request' | 'auth-response' | 'login-proof' | 'service-status' | 'error';
|
type: 'auth-request' | 'auth-response' | 'login-proof' | 'service-status' | 'error' | 'contract';
|
||||||
payload?: unknown;
|
payload?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +45,15 @@ export interface ErrorMessage extends ChannelMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContractMessage extends ChannelMessage {
|
||||||
|
type: 'contract';
|
||||||
|
payload: {
|
||||||
|
contrat?: unknown;
|
||||||
|
contrats_fils?: unknown[];
|
||||||
|
actions?: unknown[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send message to parent window (Channel Messages).
|
* Send message to parent window (Channel Messages).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
# website-skeleton
|
# website-skeleton
|
||||||
|
|
||||||
Squelette d’un site qui intègre UserWallet en iframe : écoute des messages `postMessage` (auth-request, login-proof, error), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé).
|
Squelette d'un site qui intègre UserWallet en iframe : écoute des messages `postMessage` (auth-request, login-proof, error, contract), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé).
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
- **service-login-verify** : `npm run build` dans `../service-login-verify` avant d’installer ou builder le skeleton.
|
- **service-login-verify** : `npm run build` dans `../service-login-verify` avant d'installer ou builder le skeleton.
|
||||||
- **UserWallet** : à servir sur l’URL configurée (voir ci‑dessous) pour que l’iframe fonctionne.
|
- **UserWallet** : à servir sur l'URL configurée (voir ci‑dessous) pour que l'iframe fonctionne.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -21,26 +21,56 @@ npm run build
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Ouvre par défaut sur `http://localhost:3024`. L’iframe pointe vers UserWallet (`USERWALLET_ORIGIN`).
|
Ouvre par défaut sur `http://localhost:3024`. L'iframe pointe vers UserWallet (`USERWALLET_ORIGIN`).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- **Origine UserWallet** : `src/config.ts` définit `USERWALLET_ORIGIN`. En dev, défaut `http://localhost:3018` (si UserWallet tourne en dev sur ce port). En prod, défaut `https://userwallet.certificator.4nkweb.com`. Pour override : variable d’environnement `VITE_USERWALLET_ORIGIN` (ex. `VITE_USERWALLET_ORIGIN=http://localhost:3018 npm run dev`).
|
- **Origine UserWallet** : `src/config.ts` définit `USERWALLET_ORIGIN`. En dev, défaut `http://localhost:3018` (si UserWallet tourne en dev sur ce port). En prod, défaut `https://userwallet.certificator.4nkweb.com`. Pour override : variable d'environnement `VITE_USERWALLET_ORIGIN` (ex. `VITE_USERWALLET_ORIGIN=http://localhost:3018 npm run dev`).
|
||||||
- **Validateurs** : `DEFAULT_VALIDATEURS` dans `src/config.ts` est un placeholder. Remplacez‑le par les validateurs de l’action login de votre contrat (ou chargez‑les depuis votre API / contrat fourni par channel).
|
- **Validateurs** : `DEFAULT_VALIDATEURS` dans `src/config.ts` est un placeholder. Le skeleton charge dynamiquement les validateurs depuis les contrats reçus via channel messages (type `contract`). Si aucun contrat n'est reçu, les validateurs par défaut sont utilisés.
|
||||||
|
|
||||||
|
### Chargement dynamique des contrats
|
||||||
|
|
||||||
|
Le skeleton écoute les messages `postMessage` de type `contract` pour recevoir le contrat et mettre à jour les validateurs dynamiquement :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Exemple d'envoi de contrat depuis le parent
|
||||||
|
window.postMessage({
|
||||||
|
type: 'contract',
|
||||||
|
payload: {
|
||||||
|
contrat: {
|
||||||
|
uuid: '...',
|
||||||
|
validateurs: { membres_du_role: [...] },
|
||||||
|
datajson: { types_names_chiffres: 'contrat' }
|
||||||
|
},
|
||||||
|
contrats_fils: [...], // Optionnel
|
||||||
|
actions: [...] // Optionnel, pour extraire l'action login
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
```
|
||||||
|
|
||||||
|
Le skeleton :
|
||||||
|
1. Reçoit le message `contract`
|
||||||
|
2. Extrait les validateurs de l'action login (recherche dans `actions` pour un type contenant "login")
|
||||||
|
3. Met à jour les `allowedPubkeys` utilisés pour la vérification
|
||||||
|
4. Affiche un statut de confirmation
|
||||||
|
|
||||||
|
Si aucun contrat n'est reçu, les `DEFAULT_VALIDATEURS` sont utilisés comme fallback.
|
||||||
|
|
||||||
## Utilisation
|
## Utilisation
|
||||||
|
|
||||||
1. Lancer UserWallet (dev ou déployé) sur l’URL configurée.
|
1. Lancer UserWallet (dev ou déployé) sur l'URL configurée.
|
||||||
2. Lancer le skeleton (`npm run dev` ou servir `dist/`).
|
2. Lancer le skeleton (`npm run dev` ou servir `dist/`).
|
||||||
3. Ouvrir la page du skeleton : l’iframe affiche UserWallet.
|
3. Ouvrir la page du skeleton : l'iframe affiche UserWallet.
|
||||||
4. **Demander auth** : bouton « Demander auth (auth-request) » → envoi de `auth-request` à l’iframe.
|
4. **Envoyer contrat (optionnel)** : envoyer un message `contract` avec le contrat et ses actions pour mettre à jour les validateurs.
|
||||||
5. **Login** : depuis l’iframe, effectuer le flux de login ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton vérifie la preuve (`verifyLoginProof`) et affiche « Login accepté » ou « Login refusé : … ».
|
5. **Demander auth** : bouton « Demander auth (auth-request) » → envoi de `auth-request` à l'iframe.
|
||||||
|
6. **Login** : depuis l'iframe, effectuer le flux de login ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton vérifie la preuve (`verifyLoginProof`) et affiche « Login accepté » ou « Login refusé : … ».
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- `index.html` : page avec iframe, zone de statut, bouton auth.
|
- `index.html` : page avec iframe, zone de statut, bouton auth.
|
||||||
- `src/main.ts` : chargement de l’iframe, écoute `message`, envoi `auth-request`, appel à `verifyLoginProof`, mise à jour du statut.
|
- `src/main.ts` : chargement de l'iframe, écoute `message`, envoi `auth-request`, appel à `verifyLoginProof`, mise à jour du statut, gestion des messages `contract`.
|
||||||
- `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS`.
|
- `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS`, types `Contrat` et `Action`.
|
||||||
|
- `src/contract.ts` : extraction des validateurs depuis les contrats (`extractLoginValidators`), validation de structure (`isValidContract`, `isValidAction`).
|
||||||
|
|
||||||
## Déploiement
|
## Déploiement
|
||||||
|
|
||||||
|
|||||||
@ -23,3 +23,47 @@ export const DEFAULT_VALIDATEURS = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract structure (matches userwallet types).
|
||||||
|
*/
|
||||||
|
export interface Contrat {
|
||||||
|
uuid: string;
|
||||||
|
validateurs: {
|
||||||
|
membres_du_role: Array<{
|
||||||
|
membre_uuid: string;
|
||||||
|
signatures_obligatoires: Array<{
|
||||||
|
membre_uuid: string;
|
||||||
|
cle_publique?: string;
|
||||||
|
cardinalite_minimale?: number;
|
||||||
|
dependances?: string[];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
datajson?: {
|
||||||
|
types_names_chiffres?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action structure (matches userwallet types).
|
||||||
|
*/
|
||||||
|
export interface Action {
|
||||||
|
uuid: string;
|
||||||
|
validateurs_action: {
|
||||||
|
membres_du_role: Array<{
|
||||||
|
membre_uuid: string;
|
||||||
|
signatures_obligatoires: Array<{
|
||||||
|
membre_uuid: string;
|
||||||
|
cle_publique?: string;
|
||||||
|
cardinalite_minimale?: number;
|
||||||
|
dependances?: string[];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
datajson?: {
|
||||||
|
types_names_chiffres?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
64
website-skeleton/src/contract.ts
Normal file
64
website-skeleton/src/contract.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { Validateurs } from 'service-login-verify';
|
||||||
|
import type { Contrat, Action } from './config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract login action validators from contract and child contracts.
|
||||||
|
* Searches for action with types_names_chiffres containing "login".
|
||||||
|
*/
|
||||||
|
export function extractLoginValidators(
|
||||||
|
contrat: Contrat,
|
||||||
|
_contratsFils: Contrat[] = [],
|
||||||
|
actions: Action[] = [],
|
||||||
|
): Validateurs | null {
|
||||||
|
// Search for login action in provided actions
|
||||||
|
const loginAction = actions.find((action) => {
|
||||||
|
const typesNames = action.datajson?.types_names_chiffres ?? '';
|
||||||
|
return typesNames.includes('login');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginAction?.validateurs_action !== undefined) {
|
||||||
|
return loginAction.validateurs_action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no action provided, return contract validators as fallback
|
||||||
|
// (assuming contract validators are for login action)
|
||||||
|
if (contrat.validateurs !== undefined) {
|
||||||
|
return contrat.validateurs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a contract structure is valid.
|
||||||
|
*/
|
||||||
|
export function isValidContract(contrat: unknown): contrat is Contrat {
|
||||||
|
if (typeof contrat !== 'object' || contrat === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const c = contrat as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof c.uuid === 'string' &&
|
||||||
|
typeof c.validateurs === 'object' &&
|
||||||
|
c.validateurs !== null &&
|
||||||
|
Array.isArray((c.validateurs as { membres_du_role?: unknown }).membres_du_role)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an action structure is valid.
|
||||||
|
*/
|
||||||
|
export function isValidAction(action: unknown): action is Action {
|
||||||
|
if (typeof action !== 'object' || action === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const a = action as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof a.uuid === 'string' &&
|
||||||
|
typeof a.validateurs_action === 'object' &&
|
||||||
|
a.validateurs_action !== null &&
|
||||||
|
Array.isArray(
|
||||||
|
(a.validateurs_action as { membres_du_role?: unknown }).membres_du_role,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,15 +5,20 @@ import {
|
|||||||
} from 'service-login-verify';
|
} from 'service-login-verify';
|
||||||
import type { LoginProof, Validateurs } from 'service-login-verify';
|
import type { LoginProof, Validateurs } from 'service-login-verify';
|
||||||
import { USERWALLET_ORIGIN, DEFAULT_VALIDATEURS } from './config.js';
|
import { USERWALLET_ORIGIN, DEFAULT_VALIDATEURS } from './config.js';
|
||||||
|
import {
|
||||||
|
extractLoginValidators,
|
||||||
|
isValidContract,
|
||||||
|
isValidAction,
|
||||||
|
} from './contract.js';
|
||||||
|
import type { Contrat, Action } from './config.js';
|
||||||
|
|
||||||
const iframe = document.getElementById('userwallet') as HTMLIFrameElement;
|
const iframe = document.getElementById('userwallet') as HTMLIFrameElement;
|
||||||
const statusEl = document.getElementById('status') as HTMLParagraphElement;
|
const statusEl = document.getElementById('status') as HTMLParagraphElement;
|
||||||
const btnAuth = document.getElementById('btn-auth') as HTMLButtonElement;
|
const btnAuth = document.getElementById('btn-auth') as HTMLButtonElement;
|
||||||
|
|
||||||
const nonceCache = new NonceCache(3600000);
|
const nonceCache = new NonceCache(3600000);
|
||||||
const allowedPubkeys = buildAllowedPubkeysFromValidateurs(
|
let currentValidateurs: Validateurs = DEFAULT_VALIDATEURS as Validateurs;
|
||||||
DEFAULT_VALIDATEURS as Validateurs,
|
let allowedPubkeys = buildAllowedPubkeysFromValidateurs(currentValidateurs);
|
||||||
);
|
|
||||||
|
|
||||||
function setStatus(text: string, kind: 'pending' | 'accepted' | 'rejected'): void {
|
function setStatus(text: string, kind: 'pending' | 'accepted' | 'rejected'): void {
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
@ -32,12 +37,63 @@ function sendAuthRequest(): void {
|
|||||||
setStatus('Auth demandé (auth-request envoyé).', 'pending');
|
setStatus('Auth demandé (auth-request envoyé).', 'pending');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update validators from contract and rebuild allowed pubkeys.
|
||||||
|
*/
|
||||||
|
function updateValidatorsFromContract(
|
||||||
|
contrat: Contrat,
|
||||||
|
contratsFils: Contrat[] = [],
|
||||||
|
actions: Action[] = [],
|
||||||
|
): void {
|
||||||
|
const validateurs = extractLoginValidators(contrat, contratsFils, actions);
|
||||||
|
if (validateurs !== null) {
|
||||||
|
currentValidateurs = validateurs;
|
||||||
|
allowedPubkeys = buildAllowedPubkeysFromValidateurs(validateurs);
|
||||||
|
setStatus('Contrat reçu et validateurs mis à jour.', 'pending');
|
||||||
|
} else {
|
||||||
|
setStatus(
|
||||||
|
'Contrat reçu mais validateurs login introuvables. Utilisation des validateurs par défaut.',
|
||||||
|
'pending',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMessage(event: MessageEvent): void {
|
function handleMessage(event: MessageEvent): void {
|
||||||
const d = event.data;
|
const d = event.data;
|
||||||
if (d?.type === undefined) {
|
if (d?.type === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (d.type === 'contract') {
|
||||||
|
const payload = d.payload as {
|
||||||
|
contrat?: unknown;
|
||||||
|
contrats_fils?: unknown[];
|
||||||
|
actions?: unknown[];
|
||||||
|
};
|
||||||
|
if (payload?.contrat !== undefined && isValidContract(payload.contrat)) {
|
||||||
|
const contratsFils: Contrat[] = [];
|
||||||
|
if (Array.isArray(payload.contrats_fils)) {
|
||||||
|
for (const cf of payload.contrats_fils) {
|
||||||
|
if (isValidContract(cf)) {
|
||||||
|
contratsFils.push(cf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const actions: Action[] = [];
|
||||||
|
if (Array.isArray(payload.actions)) {
|
||||||
|
for (const a of payload.actions) {
|
||||||
|
if (isValidAction(a)) {
|
||||||
|
actions.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateValidatorsFromContract(payload.contrat, contratsFils, actions);
|
||||||
|
} else {
|
||||||
|
setStatus('Message contrat reçu mais structure invalide.', 'rejected');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (d.type === 'login-proof') {
|
if (d.type === 'login-proof') {
|
||||||
const proof = d.payload as LoginProof;
|
const proof = d.payload as LoginProof;
|
||||||
const result = verifyLoginProof(proof, {
|
const result = verifyLoginProof(proof, {
|
||||||
@ -72,7 +128,10 @@ function init(): void {
|
|||||||
iframe.src = USERWALLET_ORIGIN;
|
iframe.src = USERWALLET_ORIGIN;
|
||||||
btnAuth?.addEventListener('click', sendAuthRequest);
|
btnAuth?.addEventListener('click', sendAuthRequest);
|
||||||
window.addEventListener('message', handleMessage);
|
window.addEventListener('message', handleMessage);
|
||||||
setStatus('En attente du login depuis l’iframe.', 'pending');
|
setStatus(
|
||||||
|
'En attente du login depuis l’iframe. Utilisation des validateurs par défaut jusqu’à réception d’un contrat.',
|
||||||
|
'pending',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user