Compare commits
410 Commits
create-acc
...
wip-pairin
| Author | SHA1 | Date | |
|---|---|---|---|
| f0f8ed9768 | |||
| b29f86af4e | |||
| f3f5e21195 | |||
| 8ec8df419e | |||
| ff94b1ec21 | |||
| 76556e1f6e | |||
| 74093a12eb | |||
| 274b19e410 | |||
| 44adae2d05 | |||
| bd1762ee0c | |||
| 25e0272959 | |||
| 162e5fd303 | |||
| db7f9ed10f | |||
| 77182c4399 | |||
| 4a9ac19610 | |||
| 10f85e20df | |||
| 73690e1e22 | |||
| 222f92e058 | |||
| c9ff430b09 | |||
| df835332e5 | |||
| b8e5ae3088 | |||
| b83725e112 | |||
| 930b46fa00 | |||
| 3ee99dea5a | |||
| fb47dfbfd2 | |||
| 039f6e3583 | |||
| 84d9852aa2 | |||
| d9125f52c6 | |||
| f1beeca103 | |||
| 37fef26b8b | |||
| edb850d586 | |||
| ad32875179 | |||
| 2b9b9771e1 | |||
| 2d02a20f99 | |||
| c78d1a5909 | |||
| e811b8275b | |||
| 72e7f9b920 | |||
| 43ba9fc35b | |||
| dff9eed76e | |||
| 1e531ac157 | |||
| f732f775c2 | |||
| f7c2f86d30 | |||
| 0fa1423b13 | |||
| 47e3166851 | |||
| 5a1826034e | |||
| af78165aee | |||
| 0d3163b5b2 | |||
| 0ea661b766 | |||
| 43a5fadbc8 | |||
| 1f9100e3fe | |||
| 393b75c03b | |||
| 3f7c3b1dbe | |||
| 90bb585251 | |||
| f3cfeef11b | |||
| 4418f805ef | |||
| 6f54d8ad51 | |||
| aa95537254 | |||
| 8e6756539d | |||
| 16d30d45dc | |||
| d15ef53384 | |||
| 149bdd26c9 | |||
| 64476b639c | |||
| 102ee331db | |||
| 36adf1df12 | |||
| 9de7f1a5ed | |||
| b3af85d3a0 | |||
| cd368cf667 | |||
| a52baf07e0 | |||
| 9b5e1b68b6 | |||
| 93ddfcbb76 | |||
| 72cb8129c1 | |||
| c63fe48420 | |||
| ad28b37903 | |||
| aedfa09bbc | |||
| b0694eba22 | |||
| 6f9baf6f56 | |||
| bb5f70a48f | |||
| 2ce11599e2 | |||
| ce38e01037 | |||
| 309e2902cf | |||
| 1f6b622c1a | |||
| 2f2088c8ea | |||
| 2fad2d507f | |||
| c2bd615e88 | |||
| 057102300a | |||
| 4042025e22 | |||
| 0b92af0905 | |||
| d010dac706 | |||
| 42f6e9ed05 | |||
| 09b34f7e07 | |||
| 26580aceed | |||
| f4b80f1d93 | |||
| ab31901a20 | |||
| 6a36fde154 | |||
| b8b28c1f5d | |||
| c858a75a9c | |||
| 64f4d217d6 | |||
| 0e75a49b08 | |||
| 3e63b9d8fc | |||
| 2780f21a8b | |||
| a96ffabd59 | |||
| 3eae4f0210 | |||
| aa913ef930 | |||
| 653c7f32ca | |||
| c385f23e8f | |||
| fe65881b02 | |||
| 1ddcde6b24 | |||
| bab3ba67ab | |||
| f795296d53 | |||
| 4a3b23c9d7 | |||
| 3f387ee97f | |||
| b6f3a91b3f | |||
| 07b13876ba | |||
| 31f57b86a0 | |||
| 683743d629 | |||
| d013676f9f | |||
| d34848c54e | |||
| 422ceef3e9 | |||
| f46f82be7a | |||
| 82f8fc4303 | |||
| 1a4a751485 | |||
| 9dd81d5f06 | |||
| 8057ff5b2c | |||
| 535bcf5314 | |||
| 9b3af0b5ea | |||
| 4f8e43ed87 | |||
| 5def07797e | |||
| e393a4f615 | |||
| 8af1fd055d | |||
| bf68677d3a | |||
| 63f6ac828f | |||
| cbe49aff5c | |||
| 6cd46f33d5 | |||
| 60ab17bb26 | |||
| a9f3ff8037 | |||
| 06df2ff6c1 | |||
| 3260ea9695 | |||
| 73b8d722c2 | |||
| 1d711932ce | |||
| 65132ea2f0 | |||
| 4ec026e892 | |||
| 8261e0533d | |||
| 050351d52e | |||
| 33935f4b18 | |||
| 802a77b568 | |||
| 82b3b27ab6 | |||
| 03bc0b5602 | |||
| ee7b4c8545 | |||
| d419a28c2f | |||
| 09ef9be8b8 | |||
| aabf814f99 | |||
| f628a64ad0 | |||
| 82e37cbff7 | |||
| a96a292089 | |||
| 08a47fab3e | |||
| c21de2b943 | |||
| bec3ab1729 | |||
| 97427e811a | |||
| e3e3d5431e | |||
| 9c9def2320 | |||
| db4c210046 | |||
| 6b5fc4bc91 | |||
| 507a08e959 | |||
| b8a35ea123 | |||
| cc8a2ea708 | |||
| 6d7da4d276 | |||
| 0a84381d4f | |||
| 066580f8d6 | |||
| 0c883dfcac | |||
| 770a5b7397 | |||
| 451a1941dc | |||
| 47c90093e3 | |||
| c1ba781ca5 | |||
| 0cf6abdcd5 | |||
| 7444f64394 | |||
| 1cccf236bb | |||
| 0b94cda76e | |||
| 69424c6bf6 | |||
| b545e3875e | |||
| 530dcaf633 | |||
| 88011d2f10 | |||
| bf680ab6dd | |||
| ef0f80e044 | |||
| baad7c48bc | |||
| ca4e580a95 | |||
| 9ec97e1787 | |||
| d7e2e1a648 | |||
| 918e282a25 | |||
| 937b071100 | |||
| 17517f861a | |||
| 99a8e1c382 | |||
| 96d1ee33ac | |||
| a7b76ed95c | |||
| c6ebf9627b | |||
| b8297f9be6 | |||
| 08b47b17b8 | |||
| 50f782908d | |||
| 3258b16a6e | |||
| 60f19752d3 | |||
| 7c2c4bfb46 | |||
| 79633ed923 | |||
| 412c855777 | |||
| d9daa00b32 | |||
| 31b88865d7 | |||
| cd4a971d8d | |||
| e74ce0aabc | |||
|
|
0d473cf3d1 | ||
|
|
457994c506 | ||
|
|
5fc485e233 | ||
|
|
0d934e7b6e | ||
| 02d28d46bb | |||
| 723f4d5d85 | |||
|
|
6f9fa60e2f | ||
|
|
e729e32b35 | ||
|
|
e4681f91e4 | ||
|
|
6363ec1189 | ||
|
|
c8ac815e2b | ||
|
|
ef31cba983 | ||
|
|
47c7d31249 | ||
|
|
ede8d95fd1 | ||
|
|
0fc7b6e4c3 | ||
|
|
3f64369852 | ||
|
|
e8c2d1a05a | ||
|
|
63ee4ce719 | ||
|
|
e0e186f4f4 | ||
|
|
bfca596e8b | ||
|
|
acb9739a80 | ||
|
|
c422881cd1 | ||
|
|
19da967605 | ||
|
|
d4223ce604 | ||
|
|
420979e63e | ||
|
|
1c92a40984 | ||
|
|
046eef18e6 | ||
|
|
2ba7be8dbb | ||
|
|
77d9c1ad43 | ||
|
|
3ce412d814 | ||
|
|
7100eda272 | ||
|
|
1a3a2dbef1 | ||
|
|
76a1d38e09 | ||
|
|
8a0a8e2df2 | ||
|
|
48194dd2de | ||
|
|
8e9d7f0c76 | ||
|
|
eda7102ded | ||
| ec99d101ab | |||
|
|
0dd928d28b | ||
|
|
5ba45a29be | ||
|
|
8541427b87 | ||
| 7b86318dec | |||
|
|
205796d22a | ||
| b072495cea | |||
|
|
9a601056b7 | ||
|
|
d3e207c6da | ||
|
|
cb5297e6fe | ||
|
|
f0151fa55e | ||
|
|
5192745a48 | ||
|
|
a027004bd0 | ||
|
|
aae11200d4 | ||
|
|
dbb7f67154 | ||
|
|
58fed7a53b | ||
|
|
19b2ab994e | ||
|
|
93d610e942 | ||
|
|
1dad1d4e2b | ||
|
|
5a98fac745 | ||
|
|
18d46531a0 | ||
|
|
62ccfec315 | ||
|
|
e9fc0b8454 | ||
|
|
5119d04243 | ||
|
|
5a8c31df32 | ||
|
|
deebcefc3d | ||
|
|
d9b8817ecc | ||
|
|
d8c2b22c3d | ||
|
|
39f24114e1 | ||
|
|
189bd3d252 | ||
|
|
989263d44a | ||
|
|
7391a08a01 | ||
|
|
4e109e8fba | ||
|
|
13b605a850 | ||
|
|
0a860bd559 | ||
|
|
a8b0248b5f | ||
|
|
0dc3c83c3c | ||
|
|
1a87a4db14 | ||
|
|
67cd7a1662 | ||
|
|
44f0d8c6c9 | ||
|
|
10589b056f | ||
|
|
926f41d270 | ||
|
|
7c39795cef | ||
|
|
207b308173 | ||
|
|
337a6adc60 | ||
|
|
d8422de94e | ||
|
|
9edcc2e897 | ||
|
|
f5fae245e2 | ||
| ed4fa732f7 | |||
| ac11893e93 | |||
| 929e7ee36d | |||
| c2a4b598a7 | |||
| 2bd2fdff98 | |||
| 13731da7e1 | |||
|
|
965f5da9a9 | ||
|
|
18ef18db71 | ||
|
|
50a92995d7 | ||
|
|
17bdcec317 | ||
|
|
25caed410e | ||
|
|
cf57681c31 | ||
|
|
91ba7205cc | ||
|
|
d31e18d4ae | ||
|
|
6076c342f8 | ||
|
|
bb5d3ff16d | ||
|
|
a3fe29e4a0 | ||
|
|
0d51f9d056 | ||
|
|
c0d402b234 | ||
|
|
dfae77de58 | ||
|
|
e1494d5bf4 | ||
|
|
ed23adf8f1 | ||
|
|
2a7c0d6675 | ||
|
|
25dba4e67b | ||
|
|
65d43686cb | ||
|
|
18e82de549 | ||
| f4d8f8652f | |||
| 39f2b086b5 | |||
| 00bc3d8ad2 | |||
| b52ff937f0 | |||
| d6e06f3594 | |||
| 05f13224fa | |||
| 06295fe591 | |||
| 72d43210de | |||
| 73cee5d144 | |||
| 85fe8cc251 | |||
| ec9fe0f62c | |||
| b6a2a5fc3b | |||
| 7417aec7e0 | |||
| f42aca7eb9 | |||
| 0f0b5d1af3 | |||
| 84aa6298e3 | |||
| 14b539595f | |||
| 99400a71f7 | |||
| c5b58d999f | |||
| 23a3b2a9e8 | |||
| 6167d59501 | |||
| b828e5197a | |||
| 26ba3e6e93 | |||
| df726d929a | |||
| 0e44a01218 | |||
| 8260c6c5da | |||
| 8eb6f36b64 | |||
| e15da5c22a | |||
| a8b3631dc1 | |||
| 89e9b3e4e0 | |||
| c4db22f626 | |||
| accd427cab | |||
| 381dcdf7a8 | |||
| 0cbc07cf63 | |||
| 3c59105aa6 | |||
| 325d2cbf13 | |||
| d4f1f36376 | |||
| f6edadc535 | |||
| 0099a8c858 | |||
| 0e0c3946d2 | |||
| 0a2a2674f8 | |||
| 9d461d63d7 | |||
| 2f68c652dd | |||
| 147f4cfa7d | |||
| 235aecd6a7 | |||
| e1f2483924 | |||
| 0c2df347ec | |||
| abfe581f29 | |||
| b66ee42ddd | |||
| aecdcd93e1 | |||
| c63e2a6fe9 | |||
| 67963bfb02 | |||
| 4b12b560e1 | |||
| 28c151254c | |||
| 5d0c617bbb | |||
| ae88959496 | |||
| e5a958b0b9 | |||
| 6b77ec2972 | |||
| a1ce472cad | |||
| db48386f05 | |||
| 39b50d6789 | |||
| 86393e6cfa | |||
| bf06b6634a | |||
| cfc9514656 | |||
| 0f364c7c6e | |||
| ee7c79a7d5 | |||
| 37bdb3dad3 | |||
| ecba13594b | |||
| 4c534973d2 | |||
| eca4d4de85 | |||
| 824a0b88f6 | |||
| e224921f86 | |||
| cf18e46e17 | |||
| e6cf1c3658 | |||
| b9851c587e | |||
| d601d94bf6 | |||
| d19ba72b4a | |||
| 2d0e15533a | |||
| 94ee8842e3 | |||
| 7b7d13ce6c | |||
| cc9396c4b8 | |||
| 51c906866e | |||
| 3f42cb27a7 | |||
| 2601418aaf | |||
| 455fe53fe2 | |||
| 2f847514f0 | |||
| a0888f8c90 | |||
| f2e2aeaa9a | |||
| 2855365851 | |||
| bb277706fd | |||
| 05dddd9567 | |||
| d54ce71f02 | |||
| a42141246d |
144
.cursor/rules/all.mdc
Normal file
144
.cursor/rules/all.mdc
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
description: Règles pour tous aussi pour l'IA et pour Cursor
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# IHM_CLIENT
|
||||
|
||||
voir les fichiers README.md
|
||||
|
||||
## Instructions for Claude
|
||||
|
||||
### General
|
||||
|
||||
* Répond en français
|
||||
* Code, documente le code, et fait les commits en anglais
|
||||
|
||||
### Règles Obligatoires
|
||||
|
||||
### Préparation
|
||||
|
||||
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
|
||||
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
|
||||
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
|
||||
|
||||
#### ⚙️ Getion de projet
|
||||
|
||||
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
|
||||
* **Planning :** Ne fait pas de roadmap.
|
||||
|
||||
#### 🤝 Collaboration et Workflow
|
||||
|
||||
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
|
||||
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
|
||||
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
|
||||
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
|
||||
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
|
||||
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
|
||||
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Rapports :** Ne fait pas de rapports apres tes actions.
|
||||
|
||||
#### ⚙️ Gestion de l'Environnement et des Configurations
|
||||
|
||||
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
|
||||
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
|
||||
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
|
||||
* **Nginx :** Ne modifie jamaisles configurations Nginx
|
||||
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
|
||||
|
||||
#### 💻 Qualité du Code et Bonnes Pratiques
|
||||
|
||||
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
|
||||
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
|
||||
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
|
||||
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
|
||||
|
||||
#### Code
|
||||
|
||||
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
|
||||
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
|
||||
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
|
||||
* **Fallbacks :** Ne fait pas et supprime les fallbacks
|
||||
* **Fichiers de définition :** Génère automatiquement les fichiers de définition de type pour chaque fichier TypeScript compilé. Chaque module doit exposer explicitement ses types publics pour permettre l’interopérabilité et l’analyse statique par d’autres projets.
|
||||
* **Répertoire de sortie des fichiers compilés :** la structure du code source doit être reproduite à l’identique des dossiers compilés afin d’assurer la traçabilité et la reproductibilité des builds.
|
||||
* **Version ECMAScript :** le code doit rester compatible avec les navigateurs ou environnements qui supportent les fonctionnalités ESNext, ou être transpilé si nécessaire.
|
||||
* **Bibliothèques et environnements :** Définit les bibliothèques intégrées utilisées par le compilateur pour fournir des types globaux (ex. objets DOM, APIs Web Worker). Tout code doit respecter les interfaces standardisées des environnements navigateur et worker.
|
||||
* **types propres à Vite et à Node.js :** garantir que les modules supportent à la fois le contexte serveur (Node) et client (navigateur).
|
||||
* **JavaScript (.js) :** Permet l’inclusion de fichiers JavaScript (.js) dans la compilation. Le code JavaScript inclus doit respecter les conventions TypeScript (noms, exports, compatibilité de types).
|
||||
* **skipLibCheck :** Désactive la vérification de type interne des fichiers .d.ts des bibliothèques externes. Les dépendances doivent être validées manuellement lors des mises à jour pour éviter des erreurs de typage masquées.
|
||||
* **Compatibilité automatique entre modules CommonJS et ESModules desactivée** tous les imports doivent être conformes à la sémantique native ECMAScript.
|
||||
* **allowSyntheticDefaultImports** Autorise les imports par défaut même lorsque le module n’en expose pas formellement. Cette option simplifie la migration depuis CommonJS, mais doit être utilisée avec modération.
|
||||
* **Mode strict :** Active le mode strict global, qui regroupe plusieurs sous-vérifications (null, any, this, etc.). Tout code doit passer sans avertissement en mode strict pour garantir la robustesse du typage.
|
||||
* **noImplicitAny :**: Interdit l’utilisation implicite du type any. Tout type doit être explicitement déclaré ou inféré, garantissant la traçabilité sémantique.
|
||||
* **noImplicitReturns :** Impose que toutes les branches de fonction retournent une valeur. Elimine les comportements indéterminés liés à des retours manquants.
|
||||
* **noUnusedParameters :** Autorise les paramètres non utilisés. Ces paramètres doivent être nommés avec un préfixe conventionnel (_) pour indiquer l’intention d’ignorance.
|
||||
* **exactOptionalPropertyTypes :** Ne pas permettre une correspondance souple des propriétés optionnelles ({ a?: string } peut accepter {} ou { a: undefined }).
|
||||
* **forceConsistentCasingInFileNames :**: Impose une casse cohérente entre les noms de fichiers importés et ceux présents sur le disque. Empêche les erreurs de casse entre systèmes de fichiers sensibles et insensibles (Windows, Linux).
|
||||
* **ESNext :** Utilise la syntaxe modulaire la plus récente. La structure des imports doit suivre le format standard ECMAScript, y compris pour les chemins relatifs.
|
||||
* **Module Resolution :** la hiérarchie des node_modules doit être stable et conforme aux conventions de résolution.
|
||||
* **resolveJsonModule :** Autorise l’import direct de fichiers JSON en tant que modules. Les JSON importés doivent être statiquement typés (via interfaces ou as const).
|
||||
* **isolatedModules :** Oblige chaque fichier à pouvoir être transpilé indépendamment. Empêche les dépendances implicites entre fichiers et améliore la compatibilité.
|
||||
* **experimentalDecorators :** Active le support expérimental des décorateurs (@decorator). Les décorateurs doivent être documentés et limités aux contextes maîtrisés (injection de dépendances, métaprogrammation contrôlée).
|
||||
* **Chemins :** Utiliser des chemin relatifs et indiquer la racine du projet en configuration. Toutes les références internes doivent être relatives à la racine du projet. Vérifier de limiter l'acces en dehors du projet.
|
||||
|
||||
#### 🧪 Tests
|
||||
|
||||
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
|
||||
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
|
||||
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
|
||||
|
||||
#### 📚 Documentation
|
||||
|
||||
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
|
||||
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Structure de la documentation :**
|
||||
* La documentation générale et pérenne se trouve dans `docs/`.
|
||||
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
|
||||
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
|
||||
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
|
||||
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
|
||||
|
||||
#### 📊 Logging et Gestion des Erreurs
|
||||
|
||||
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
|
||||
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
|
||||
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
|
||||
|
||||
#### 🌐 Interactions Externes (BDD, API, Emails)
|
||||
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
|
||||
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
|
||||
|
||||
### Base de données
|
||||
|
||||
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
|
||||
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
|
||||
|
||||
#### 🚀 Déploiement
|
||||
|
||||
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
|
||||
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
|
||||
* **Lancement :** ne lance aucun déploiement sans demander avant
|
||||
|
||||
#### 🚨 Gestion des Problèmes
|
||||
|
||||
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
|
||||
|
||||
#### 🗄️ Gestion des Fichiers
|
||||
|
||||
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
|
||||
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
|
||||
|
||||
### Mise à jour de ces règles
|
||||
|
||||
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
|
||||
|
||||
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
|
||||
|
||||
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
|
||||
|
||||
### Application
|
||||
|
||||
* Indique l'IA que tu utilise
|
||||
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.
|
||||
7
.cursor/rules/init.mdc
Normal file
7
.cursor/rules/init.mdc
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
lire avec attention: docs/INITIALIZATION_FLOW.md
|
||||
lire avec attention: docs/IA_agents/*
|
||||
lire avec attention: docs/docs/*
|
||||
360
.cursor/rules/project-analysis.mdc
Normal file
360
.cursor/rules/project-analysis.mdc
Normal file
@ -0,0 +1,360 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# IHM_CLIENT - Analyse du Projet
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Application client Web5 pour l'écosystème 4NK permettant la gestion sécurisée des appareils, le pairing et les signatures de documents.
|
||||
|
||||
## Architecture Technique
|
||||
|
||||
### Stack Technologique
|
||||
- **Frontend**: TypeScript, Vite, HTML5, CSS3
|
||||
- **SDK**: Rust compilé en WebAssembly
|
||||
- **Storage**: IndexedDB, Service Workers
|
||||
- **Communication**: WebSockets, PostMessage API
|
||||
- **Construction**: Vite avec plugins WASM et top-level await
|
||||
|
||||
### Configuration
|
||||
- **Port**: 3004
|
||||
- **URL**: https://dev3.4nkweb.com
|
||||
- **Proxy**: `/storage` → https://dev3.4nkweb.com/storage
|
||||
- **Base URL**: Variables d'environnement VITE_BASEURL, VITE_BOOTSTRAPURL, VITE_STORAGEURL, VITE_BLINDBITURL
|
||||
|
||||
### Structure du Projet
|
||||
|
||||
```
|
||||
ihm_client_dev3/
|
||||
├── src/
|
||||
│ ├── components/ # Composants UI réutilisables
|
||||
│ │ ├── account-nav/
|
||||
│ │ ├── device-management/
|
||||
│ │ ├── iframe-pairing/
|
||||
│ │ ├── login-modal/
|
||||
│ │ ├── secure-credentials/
|
||||
│ │ ├── security-mode-selector/
|
||||
│ │ └── validation-modal/
|
||||
│ ├── pages/ # Pages de l'application
|
||||
│ │ ├── account/
|
||||
│ │ ├── birthday-setup/
|
||||
│ │ ├── block-sync/
|
||||
│ │ ├── home/
|
||||
│ │ ├── pairing/
|
||||
│ │ ├── security-setup/
|
||||
│ │ └── wallet-setup/
|
||||
│ ├── services/ # Services métier
|
||||
│ │ ├── service.ts # Service principal (singleton)
|
||||
│ │ ├── database.service.ts
|
||||
│ │ ├── secure-credentials.service.ts
|
||||
│ │ ├── security-mode.service.ts
|
||||
│ │ ├── pairing.service.ts
|
||||
│ │ ├── iframe-pairing.service.ts
|
||||
│ │ └── websocket-manager.ts
|
||||
│ ├── repositories/ # Accès aux données
|
||||
│ │ ├── device.repository.ts
|
||||
│ │ └── process.repository.ts
|
||||
│ ├── models/ # Types et interfaces
|
||||
│ ├── utils/ # Utilitaires
|
||||
│ ├── service-workers/ # Workers pour opérations async
|
||||
│ ├── router.ts # Router principal
|
||||
│ └── main.ts # Point d'entrée
|
||||
├── pkg/ # SDK WebAssembly compilé
|
||||
├── docs/ # Documentation
|
||||
├── IA_agents/ # Documentation pour IA
|
||||
├── test-browser/ # Tests Playwright
|
||||
└── logs/ # Logs centralisés
|
||||
```
|
||||
|
||||
## Architecture Fonctionnelle
|
||||
|
||||
### Flux d'Initialisation
|
||||
|
||||
1. **Security Setup** → Configuration du mode de sécurité
|
||||
2. **Wallet Setup** → Création du wallet Bitcoin
|
||||
3. **Birthday Setup** → Configuration de la date anniversaire
|
||||
4. **Block Sync** → Synchronisation initiale des blocs
|
||||
5. **Pairing** → Appairage de l'appareil
|
||||
6. **Account** → Interface principale
|
||||
|
||||
### Système de Modes de Sécurité
|
||||
|
||||
| Mode | Nom | Niveau | Stockage Clé PBKDF2 |
|
||||
|------|-----|--------|---------------------|
|
||||
| `proton-pass` | Proton Pass | High | WebAuthn du navigateur |
|
||||
| `os` | OS Authenticator | High | WebAuthn du système |
|
||||
| `otp` | OTP | High | En clair |
|
||||
| `password` | Mot de passe | Low | Chiffré avec mot de passe |
|
||||
| `none` | Aucune | Critical | Clé en dur (déconseillé) |
|
||||
|
||||
### Base de Données IndexedDB
|
||||
|
||||
**Stores:**
|
||||
- `pbkdf2keys` : Clés PBKDF2 chiffrées par mode de sécurité
|
||||
- `wallet` : Wallet chiffré (device + wallet data)
|
||||
- `credentials` : Credentials de pairing (après pairing)
|
||||
- `env` : Variables d'environnement internes
|
||||
- `processes` : Processus de communication
|
||||
- `labels` : Labels de transactions
|
||||
- `shared_secrets` : Secrets partagés
|
||||
- `unconfirmed_secrets` : Secrets non confirmés
|
||||
- `diffs` : Différences de synchronisation
|
||||
- `data` : Données générales
|
||||
|
||||
### Système de Processus
|
||||
|
||||
Le système de processus est générique et réutilisable pour créer des "contrats" entre des membres avec niveaux d'accès différents.
|
||||
|
||||
**Concepts:**
|
||||
- **Process**: Contrat décentralisé entre plusieurs membres
|
||||
- **State**: État du processus avec données publiques/privées
|
||||
- **Roles**: Définition des permissions par rôle
|
||||
- **Members**: Participants identifiés par pairing process ID
|
||||
|
||||
**Données:**
|
||||
- **Public**: Accessibles à tous, portées automatiquement
|
||||
- **Private**: Chiffrées via PCD commitments, distribuées aux membres autorisés
|
||||
- **Roles**: Quorum et champs accessibles par rôle
|
||||
|
||||
**Cycle de vie:**
|
||||
1. Création → `createProcess()`
|
||||
2. Mise à jour → `updateProcess()`
|
||||
3. Synchronisation PRD → `createPrdUpdate()`
|
||||
4. Validation → `approveChange()`
|
||||
5. Commit blockchain → État immuable
|
||||
6. Accès → `getPublicData()` / `decryptAttribute()`
|
||||
|
||||
### Système de Pairing
|
||||
|
||||
**But**: Créer une identité numérique vérifiable pour MFA entre appareils
|
||||
|
||||
**Caractéristiques:**
|
||||
- Un wallet peut être appairé à plusieurs appareils
|
||||
- Processus blockchain avec états commités
|
||||
- Synchronisation via relais
|
||||
- Contrôle via 4 mots
|
||||
|
||||
**Flux Créateur:**
|
||||
1. `createPairingProcess()` avec son adresse
|
||||
2. `generateQRCode()` pour le joiner
|
||||
3. `waitForJoinerAndUpdateProcess()`
|
||||
4. `waitForPairingCommitment()`
|
||||
5. `confirmPairing()`
|
||||
|
||||
**Flux Joiner:**
|
||||
1. `discoverAndJoinPairingProcess()` via QR code
|
||||
2. `waitForPairingCommitment()`
|
||||
3. `confirmPairing()`
|
||||
|
||||
## Services Principaux
|
||||
|
||||
### Services (Singleton)
|
||||
|
||||
**Initialisation:**
|
||||
- WebAssembly SDK
|
||||
- WebSocket connections
|
||||
- Database restoration
|
||||
- Process listening
|
||||
|
||||
**Principales méthodes:**
|
||||
- `getDeviceFromDatabase()` : Récupération du device
|
||||
- `createPairingProcess()` : Création de processus de pairing
|
||||
- `createProcess()` : Création de processus générique
|
||||
- `updateProcess()` : Mise à jour de processus
|
||||
- `getProcess()` : Récupération de processus
|
||||
- `checkConnections()` : Vérification des connexions entre membres
|
||||
- `decryptAttribute()` : Déchiffrement d'attribut privé
|
||||
- `getPublicData()` : Récupération des données publiques
|
||||
|
||||
### Database Service
|
||||
|
||||
**Gestion IndexedDB:**
|
||||
- Singleton avec service worker
|
||||
- Stores configurables via `storeDefinitions`
|
||||
- Transactions asynchrones
|
||||
- Cache management
|
||||
|
||||
### Secure Credentials Service
|
||||
|
||||
**Gestion des credentials:**
|
||||
- Génération et stockage de credentials de pairing
|
||||
- Récupération avec déchiffrement
|
||||
- Support WebAuthn pour modes haute sécurité
|
||||
|
||||
### Security Mode Service
|
||||
|
||||
**Gestion des modes:**
|
||||
- Détection du mode actuel
|
||||
- Stockage dans IndexedDB
|
||||
- Authentification selon mode
|
||||
|
||||
## Logging et Erreurs
|
||||
|
||||
### SecureLogger
|
||||
|
||||
**Système centralisé:**
|
||||
- Niveaux: DEBUG, INFO, WARN, ERROR
|
||||
- Sanitisation automatique des données sensibles
|
||||
- Contexte enrichi avec composant et métadonnées
|
||||
- Formatage cohérent
|
||||
|
||||
**Bonnes pratiques:**
|
||||
- Utiliser `secureLogger` au lieu de `console.*`
|
||||
- Toujours spécifier le composant
|
||||
- Ajouter métadonnées utiles
|
||||
- Messages clairs et concis
|
||||
- Vérifications réelles avant logs de succès
|
||||
|
||||
**Patterns:**
|
||||
- 🔍 DEBUG : Informations de débogage
|
||||
- ✅ INFO : Succès et initialisations
|
||||
- ⚠️ WARN : Avertissements non critiques
|
||||
- ❌ ERROR : Erreurs critiques
|
||||
|
||||
## Router
|
||||
|
||||
### Navigation
|
||||
|
||||
**Logique de progression:**
|
||||
1. Si pairing → account
|
||||
2. Si date anniversaire → pairing
|
||||
3. Si wallet → birthday-setup
|
||||
4. Si pbkdf2 → wallet-setup
|
||||
5. Sinon → security-setup
|
||||
|
||||
**Routes principales:**
|
||||
- `/security-setup` : Configuration sécurité
|
||||
- `/wallet-setup` : Création wallet
|
||||
- `/birthday-setup` : Configuration birthday
|
||||
- `/block-sync` : Synchronisation blocs
|
||||
- `/pairing` : Appairage
|
||||
- `/account` : Interface principale
|
||||
- `/home` : Page d'accueil avec login
|
||||
|
||||
### Message Handlers
|
||||
|
||||
Gestion des messages PostMessage pour communication iframe:
|
||||
- `REQUEST_LINK` : Liaison avec site externe
|
||||
- `CREATE_PAIRING` : Création de pairing
|
||||
- `GET_PROCESSES` : Récupération des processus
|
||||
- `CREATE_PROCESS` : Création de processus
|
||||
- `UPDATE_PROCESS` : Mise à jour de processus
|
||||
- `VALIDATE_STATE` : Validation d'état
|
||||
- Etc.
|
||||
|
||||
## WebSocket
|
||||
|
||||
**Gestion:**
|
||||
- Connexions multiples (BOOTSTRAP, STORAGE, BLINDBIT)
|
||||
- Handshake automatique
|
||||
- Retry automatique en cas de déconnexion
|
||||
- Event bus pour messages
|
||||
|
||||
**Messages:**
|
||||
- HandshakeMessage : Synchronisation initiale
|
||||
- NewTxMessage : Nouvelles transactions
|
||||
- Process updates : Mises à jour de processus
|
||||
|
||||
## Dépendances
|
||||
|
||||
**Principales:**
|
||||
- `axios` : Requêtes HTTP
|
||||
- `jose` : JWT et chiffrement
|
||||
- `jsonwebtoken` : Tokens JWT
|
||||
- `pdf-lib` : Manipulation PDF
|
||||
- `sweetalert2` : Modals UI
|
||||
|
||||
**SDK WebAssembly:**
|
||||
- `pkg/sdk_client.js` : SDK principal
|
||||
- `pkg/sdk_client_bg.wasm` : Binaire WebAssembly
|
||||
- Types TypeScript générés
|
||||
|
||||
## Tests
|
||||
|
||||
**Structure:**
|
||||
- `test-browser/` : Tests Playwright
|
||||
- Tests pour chaque parcours utilisateur
|
||||
- Intégration avec mocked data
|
||||
|
||||
## User Stories
|
||||
|
||||
34 stories définies couvrant:
|
||||
- Login (adresse device, QR code)
|
||||
- Process management
|
||||
- Account management
|
||||
- Pairing
|
||||
- Wallet
|
||||
- Chat
|
||||
- Signatures
|
||||
|
||||
## Configuration TypeScript
|
||||
|
||||
**Strict Mode:**
|
||||
- `strict: true`
|
||||
- `noImplicitAny: true`
|
||||
- `noImplicitReturns: true`
|
||||
- `forceConsistentCasingInFileNames: true`
|
||||
|
||||
**Modules:**
|
||||
- `module: ESNext`
|
||||
- `target: ESNext`
|
||||
- `lib: ["DOM", "DOM.Iterable", "ESNext", "webworker"]`
|
||||
- `isolatedModules: true`
|
||||
|
||||
## Points Critiques
|
||||
|
||||
### Sécurité
|
||||
- Clés PBKDF2 toujours chiffrées (sauf mode OTP)
|
||||
- Wallet toujours chiffré en base
|
||||
- WebAuthn pour modes haute sécurité
|
||||
- Sanitisation automatique des logs
|
||||
|
||||
### Performance
|
||||
- Cache désactivé (`maxCacheSize = 0`)
|
||||
- Memory management avec retry
|
||||
- Lazy loading des composants
|
||||
- Service workers pour opérations async
|
||||
|
||||
### Robustesse
|
||||
- Retry automatique pour opérations critiques
|
||||
- Vérifications réelles avant logs de succès
|
||||
- Fallback et navigation automatique
|
||||
- Gestion des erreurs IndexedDB
|
||||
|
||||
## Documentation
|
||||
|
||||
**Fichiers clés:**
|
||||
- `docs/INITIALIZATION_FLOW.md` : Flux d'initialisation détaillé
|
||||
- `docs/PROCESS_SYSTEM_ARCHITECTURE.md` : Architecture des processus
|
||||
- `docs/PAIRING_SYSTEM_ANALYSIS.md` : Analyse du pairing
|
||||
- `docs/LOGGING_GUIDELINES.md` : Guide de logging
|
||||
- `IA_agents/all.md` : Règles pour IA
|
||||
- `README.md` : Vue d'ensemble
|
||||
|
||||
## Développement
|
||||
|
||||
**Scripts:**
|
||||
- `npm run start` : Serveur de développement
|
||||
- `npm run build` : Build de production
|
||||
- `npm run quality` : Vérification qualité
|
||||
- `npm run lint` : Linting
|
||||
- `npm run type-check` : Vérification TypeScript
|
||||
|
||||
**Prérequis:**
|
||||
- Node.js 18+
|
||||
- Rust (pour SDK)
|
||||
- npm ou yarn
|
||||
|
||||
## Points d'Attention
|
||||
|
||||
1. **Ordre des modes testés**: `['none', 'otp', 'password', 'os', 'proton-pass']`
|
||||
2. **Store credentials**: Utilisé uniquement après pairing
|
||||
3. **Redirection automatique**: 3s après création wallet vers birthday-setup
|
||||
4. **Synchronisation IndexedDB**: Utilisation directe pour éviter problèmes service worker
|
||||
5. **Retry automatique**: Jusqu'à 5 tentatives pour vérifications wallet
|
||||
6. **Vérifications réelles**: Logs de succès uniquement après vérification
|
||||
7. **WebAssembly memory**: Monitoring et cleanup automatique
|
||||
8. **Singleton patterns**: Services, Database, ModalService
|
||||
9. **Message handlers**: Tous nécessitent validation token
|
||||
10. **Process lifecycle**: Toujours vérifier état committé avant manipulation
|
||||
8
.cursor/rules/webiste.mdc
Normal file
8
.cursor/rules/webiste.mdc
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
le site tourne sur le port 3004
|
||||
l'url du site est https://dev3.4nkweb.com
|
||||
ne déclanche jamais la CI
|
||||
le relai doit tourner sur 8091
|
||||
3
.env
3
.env
@ -1,3 +0,0 @@
|
||||
# .env
|
||||
VITE_API_URL=https://api.example.com
|
||||
VITE_API_KEY=your_api_key
|
||||
5
.env.exemple
Normal file
5
.env.exemple
Normal file
@ -0,0 +1,5 @@
|
||||
VITE_BASEURL="your_base_url"
|
||||
VITE_BOOTSTRAPURL="your_bootstrap_url"
|
||||
VITE_STORAGEURL="your_storage_url"
|
||||
VITE_BLINDBITURL="your_blindbit_url"
|
||||
VITE_JWT_SECRET_KEY="your_secret_key"
|
||||
52
.eslintrc.json
Normal file
52
.eslintrc.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"extends": [
|
||||
"@typescript-eslint/recommended",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
// Qualité du code
|
||||
"complexity": ["warn", 10],
|
||||
"max-lines": ["warn", 300],
|
||||
"max-lines-per-function": ["warn", 50],
|
||||
"max-params": ["warn", 4],
|
||||
"max-depth": ["warn", 4],
|
||||
|
||||
// TypeScript spécifique
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
|
||||
// Bonnes pratiques
|
||||
"no-console": "warn",
|
||||
"no-debugger": "error",
|
||||
"no-alert": "warn",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
"eqeqeq": "error",
|
||||
"curly": "error",
|
||||
|
||||
// Sécurité
|
||||
"no-eval": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-new-func": "error",
|
||||
|
||||
// Performance
|
||||
"no-loop-func": "error",
|
||||
"no-await-in-loop": "warn"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist/",
|
||||
"node_modules/",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
44
.gitea/workflows/dev.yml
Normal file
44
.gitea/workflows/dev.yml
Normal file
@ -0,0 +1,44 @@
|
||||
name: Build and Push to Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
|
||||
env:
|
||||
REGISTRY: git.4nkweb.com
|
||||
IMAGE_NAME: 4nk/ihm_client
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.USER }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
ssh: default
|
||||
build-args: |
|
||||
ENV_VARS=${{ secrets.ENV_VARS }}
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||
100
.gitignore
vendored
100
.gitignore
vendored
@ -1,7 +1,103 @@
|
||||
# ----------------------------
|
||||
# 🦀 Rust
|
||||
# ----------------------------
|
||||
target/
|
||||
pkg/
|
||||
Cargo.lock
|
||||
*.rs.bk
|
||||
**/*.rlib
|
||||
|
||||
# ----------------------------
|
||||
# 🧰 Node / Frontend
|
||||
# ----------------------------
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode
|
||||
build/
|
||||
.cache/
|
||||
.next/
|
||||
out/
|
||||
.tmp/
|
||||
temp/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# ----------------------------
|
||||
# 🧱 IDE / Éditeurs
|
||||
# ----------------------------
|
||||
.idea/
|
||||
*.iml
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ----------------------------
|
||||
# ⚙️ Environnements / Secrets
|
||||
# ----------------------------
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.production.local
|
||||
.env.test.local
|
||||
*.pem
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
# ----------------------------
|
||||
# 🌐 SSL / Certificats
|
||||
# ----------------------------
|
||||
public/ssl/
|
||||
certs/
|
||||
keys/
|
||||
|
||||
# ----------------------------
|
||||
# 📦 Compilations WebAssembly
|
||||
# ----------------------------
|
||||
wasm-pack.log
|
||||
*.wasm
|
||||
|
||||
# ----------------------------
|
||||
# 🧪 Tests / Coverage
|
||||
# ----------------------------
|
||||
coverage/
|
||||
lcov-report/
|
||||
.nyc_output/
|
||||
jest-cache/
|
||||
jest-results.json
|
||||
|
||||
# ----------------------------
|
||||
# 🧍 Runtime / OS / Divers
|
||||
# ----------------------------
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# ----------------------------
|
||||
# 🧠 Logs / Debug / Dump
|
||||
# ----------------------------
|
||||
*.log
|
||||
*.stackdump
|
||||
*.dmp
|
||||
debug.log
|
||||
error.log
|
||||
|
||||
# ----------------------------
|
||||
# 🚀 Deploy / Production builds
|
||||
# ----------------------------
|
||||
.vercel/
|
||||
.netlify/
|
||||
firebase/
|
||||
functions/lib/
|
||||
sdk_relay
|
||||
sdk_client
|
||||
19
.prettierrc
19
.prettierrc
@ -1,14 +1,15 @@
|
||||
{
|
||||
"printWidth": 300,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"requirePragma": false,
|
||||
"insertPragma": false,
|
||||
"endOfLine": "crlf"
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": true,
|
||||
"bracketSameLine": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-vscode.vscode-typescript-next"
|
||||
]
|
||||
}
|
||||
|
||||
59
.vscode/settings.json
vendored
Normal file
59
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
// ESLint Configuration
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": ["."],
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": "eslint.config.js"
|
||||
},
|
||||
|
||||
// TypeScript Configuration
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"typescript.tsserver.maxTsServerMemory": 4096,
|
||||
|
||||
// Problems Panel Configuration
|
||||
"problems.showCurrentInStatus": true,
|
||||
"problems.autoReveal": true,
|
||||
|
||||
// File Associations
|
||||
"files.associations": {
|
||||
"*.ts": "typescript",
|
||||
"*.tsx": "typescriptreact"
|
||||
},
|
||||
|
||||
// Editor Configuration
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
|
||||
// TypeScript Specific
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
|
||||
// Exclude patterns for file watcher
|
||||
"files.watcherExclude": {
|
||||
"**/node_modules/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/pkg/**": true,
|
||||
"**/logs/**": true,
|
||||
"**/.git/**": true
|
||||
},
|
||||
|
||||
// Search exclusion
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true,
|
||||
"**/pkg": true,
|
||||
"**/logs": true,
|
||||
"**/.git": true
|
||||
}
|
||||
}
|
||||
120
CLAUDE.md
Normal file
120
CLAUDE.md
Normal file
@ -0,0 +1,120 @@
|
||||
# IHM_CLIENT
|
||||
|
||||
voir les fichiers README.md
|
||||
|
||||
## Instructions for Claude
|
||||
|
||||
### General
|
||||
|
||||
* Répond en français
|
||||
* Code, documente le code, et fait les commits en anglais
|
||||
|
||||
### Règles Obligatoires
|
||||
|
||||
### Préparation
|
||||
|
||||
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
|
||||
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
|
||||
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
|
||||
|
||||
#### ⚙️ Getion de projet
|
||||
|
||||
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
|
||||
* **Planning :** Ne fait pas de roadmap.
|
||||
|
||||
#### 🤝 Collaboration et Workflow
|
||||
|
||||
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
|
||||
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
|
||||
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
|
||||
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
|
||||
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
|
||||
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
|
||||
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Rapports :** Ne fait pas de rapports apres tes actions.
|
||||
|
||||
#### ⚙️ Gestion de l'Environnement et des Configurations
|
||||
|
||||
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
|
||||
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
|
||||
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
|
||||
* **Nginx :** Ne modifie jamaisles configurations Nginx
|
||||
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
|
||||
|
||||
#### 💻 Qualité du Code et Bonnes Pratiques
|
||||
|
||||
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
|
||||
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
|
||||
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
|
||||
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
|
||||
|
||||
#### Code
|
||||
|
||||
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
|
||||
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
|
||||
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
|
||||
* **Fallbacks :** Ne fait pas et supprime les fallbacks
|
||||
|
||||
#### 🧪 Tests
|
||||
|
||||
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
|
||||
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
|
||||
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
|
||||
|
||||
#### 📚 Documentation
|
||||
|
||||
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
|
||||
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Structure de la documentation :**
|
||||
* La documentation générale et pérenne se trouve dans `docs/`.
|
||||
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
|
||||
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
|
||||
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
|
||||
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
|
||||
|
||||
#### 📊 Logging et Gestion des Erreurs
|
||||
|
||||
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
|
||||
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
|
||||
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
|
||||
* **Données vérifiées :** Vérifiant que les logs reflètent des vérifications réelles et non des déclarations.
|
||||
* **Log abondamment :** Log les informations et étapes ou états clés ainsi que les identifiants clés.
|
||||
|
||||
#### 🌐 Interactions Externes (BDD, API, Emails)
|
||||
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
|
||||
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
|
||||
|
||||
### Base de données
|
||||
|
||||
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
|
||||
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
|
||||
|
||||
#### 🚀 Déploiement
|
||||
|
||||
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
|
||||
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
|
||||
* **Lancement :** ne lance aucun déploiement sans demander avant
|
||||
|
||||
#### 🚨 Gestion des Problèmes
|
||||
|
||||
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
|
||||
|
||||
#### 🗄️ Gestion des Fichiers
|
||||
|
||||
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
|
||||
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
|
||||
|
||||
### Mise à jour de ces règles
|
||||
|
||||
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
|
||||
|
||||
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
|
||||
|
||||
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
|
||||
|
||||
### Application
|
||||
|
||||
* Indique l'IA que tu utilise
|
||||
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.
|
||||
193
CONTRIBUTING.md
Normal file
193
CONTRIBUTING.md
Normal file
@ -0,0 +1,193 @@
|
||||
# 🤝 Guide de contribution - 4NK Client
|
||||
|
||||
## 📋 Standards de code
|
||||
|
||||
### **TypeScript**
|
||||
- Utiliser des types explicites
|
||||
- Éviter `any` autant que possible
|
||||
- Préférer les interfaces aux types
|
||||
- Documenter les fonctions publiques avec JSDoc
|
||||
|
||||
### **Architecture**
|
||||
- Séparation claire des responsabilités
|
||||
- Services injectables (éviter les singletons)
|
||||
- Composants réutilisables
|
||||
- Gestion d'erreurs centralisée
|
||||
|
||||
### **Performance**
|
||||
- Lazy loading des modules lourds
|
||||
- Mémoisation des calculs coûteux
|
||||
- Debouncing des événements fréquents
|
||||
- Optimisation des re-renders
|
||||
|
||||
## 🛠️ Workflow de développement
|
||||
|
||||
### **1. Avant de commencer**
|
||||
```bash
|
||||
# Installer les dépendances
|
||||
npm install
|
||||
|
||||
# Vérifier la qualité du code
|
||||
npm run quality
|
||||
|
||||
# Lancer les tests
|
||||
npm test
|
||||
```
|
||||
|
||||
### **2. Pendant le développement**
|
||||
```bash
|
||||
# Vérifier les types
|
||||
npm run type-check
|
||||
|
||||
# Linter le code
|
||||
npm run lint
|
||||
|
||||
# Formater le code
|
||||
npm run prettify
|
||||
```
|
||||
|
||||
### **3. Avant de commiter**
|
||||
```bash
|
||||
# Vérification complète
|
||||
npm run quality:fix
|
||||
|
||||
# Tests
|
||||
npm test
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 📝 Standards de commit
|
||||
|
||||
### **Format**
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[body optionnel]
|
||||
|
||||
[footer optionnel]
|
||||
```
|
||||
|
||||
### **Types**
|
||||
- `feat`: Nouvelle fonctionnalité
|
||||
- `fix`: Correction de bug
|
||||
- `docs`: Documentation
|
||||
- `style`: Formatage, point-virgules, etc.
|
||||
- `refactor`: Refactoring
|
||||
- `test`: Ajout de tests
|
||||
- `chore`: Tâches de maintenance
|
||||
|
||||
### **Exemples**
|
||||
```
|
||||
feat(pairing): add 4-words pairing support
|
||||
fix(ui): resolve header display issue
|
||||
docs(api): update pairing service documentation
|
||||
```
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
### **Structure des tests**
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ └── __tests__/
|
||||
├── services/
|
||||
│ └── __tests__/
|
||||
└── utils/
|
||||
└── __tests__/
|
||||
```
|
||||
|
||||
### **Conventions**
|
||||
- Un fichier de test par fichier source
|
||||
- Nommage: `*.test.ts` ou `*.spec.ts`
|
||||
- Couverture minimale: 80%
|
||||
- Tests unitaires et d'intégration
|
||||
|
||||
## 📊 Métriques de qualité
|
||||
|
||||
### **Objectifs**
|
||||
- **Complexité cyclomatique**: < 10
|
||||
- **Taille des fichiers**: < 300 lignes
|
||||
- **Couverture de tests**: > 80%
|
||||
- **Temps de build**: < 30 secondes
|
||||
|
||||
### **Outils**
|
||||
- ESLint pour la qualité du code
|
||||
- Prettier pour le formatage
|
||||
- TypeScript pour la sécurité des types
|
||||
- Bundle analyzer pour la taille
|
||||
|
||||
## 🔒 Sécurité
|
||||
|
||||
### **Bonnes pratiques**
|
||||
- Validation des données d'entrée
|
||||
- Sanitisation des messages
|
||||
- Gestion sécurisée des tokens
|
||||
- Logs sans données sensibles
|
||||
|
||||
### **Vérifications**
|
||||
- Aucun `eval()` ou `Function()`
|
||||
- Validation des URLs et chemins
|
||||
- Gestion des erreurs sans exposition d'informations
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### **Code**
|
||||
- JSDoc pour toutes les fonctions publiques
|
||||
- Commentaires pour la logique complexe
|
||||
- README technique pour l'architecture
|
||||
|
||||
### **API**
|
||||
- Documentation des endpoints
|
||||
- Exemples d'utilisation
|
||||
- Gestion des erreurs
|
||||
|
||||
## 🚀 Déploiement
|
||||
|
||||
### **Environnements**
|
||||
- **Development**: `npm run start`
|
||||
- **Production**: `npm run build && npm run deploy`
|
||||
|
||||
### **Vérifications pré-déploiement**
|
||||
```bash
|
||||
npm run quality
|
||||
npm test
|
||||
npm run build
|
||||
npm run analyze
|
||||
```
|
||||
|
||||
## 🐛 Signalement de bugs
|
||||
|
||||
### **Template**
|
||||
```
|
||||
**Description**
|
||||
Description claire du problème
|
||||
|
||||
**Reproduction**
|
||||
1. Étapes pour reproduire
|
||||
2. Comportement attendu
|
||||
3. Comportement actuel
|
||||
|
||||
**Environnement**
|
||||
- OS:
|
||||
- Navigateur:
|
||||
- Version:
|
||||
|
||||
**Logs**
|
||||
Logs pertinents (sans données sensibles)
|
||||
```
|
||||
|
||||
## 💡 Suggestions d'amélioration
|
||||
|
||||
### **Processus**
|
||||
1. Créer une issue détaillée
|
||||
2. Discuter de la faisabilité
|
||||
3. Implémenter avec tests
|
||||
4. Documentation mise à jour
|
||||
|
||||
### **Critères**
|
||||
- Amélioration de la performance
|
||||
- Meilleure expérience utilisateur
|
||||
- Réduction de la complexité
|
||||
- Sécurité renforcée
|
||||
62
Dockerfile
62
Dockerfile
@ -1,13 +1,61 @@
|
||||
FROM node:20
|
||||
# syntax=docker/dockerfile:1.4
|
||||
FROM rust:1.82-alpine AS wasm-builder
|
||||
WORKDIR /build
|
||||
|
||||
ENV TZ=Europe/Paris
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
# Installation des dépendances nécessaires pour la compilation
|
||||
RUN apk update && apk add --no-cache \
|
||||
git \
|
||||
openssh-client \
|
||||
curl \
|
||||
nodejs \
|
||||
npm \
|
||||
build-base \
|
||||
pkgconfig \
|
||||
clang \
|
||||
llvm \
|
||||
musl-dev \
|
||||
nginx
|
||||
|
||||
# use this user because he have uid et gid 1000 like theradia
|
||||
USER node
|
||||
# Installation de wasm-pack
|
||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# Configuration SSH basique
|
||||
RUN mkdir -p /root/.ssh && \
|
||||
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
|
||||
|
||||
# On se place dans le bon répertoire parent
|
||||
WORKDIR /build
|
||||
# Copie du projet ihm_client
|
||||
COPY . ihm_client/
|
||||
|
||||
# Clonage du sdk_client au même niveau que ihm_client en utilisant la clé SSH montée
|
||||
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
|
||||
|
||||
# Build du WebAssembly avec accès SSH pour les dépendances
|
||||
WORKDIR /build/sdk_client
|
||||
RUN --mount=type=ssh wasm-pack build --out-dir ../ihm_client/pkg --target bundler --dev
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["npm", "start"]
|
||||
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]
|
||||
# Installation des dépendances nécessaires
|
||||
RUN apk update && apk add --no-cache git nginx
|
||||
|
||||
# Copie des fichiers du projet
|
||||
COPY --from=wasm-builder /build/ihm_client/pkg ./pkg
|
||||
COPY . .
|
||||
|
||||
# Installation des dépendances Node.js
|
||||
RUN npm install
|
||||
|
||||
# Copie de la configuration nginx
|
||||
COPY nginx.dev.conf /etc/nginx/http.d/default.conf
|
||||
|
||||
# Script de démarrage
|
||||
COPY start-dev.sh /start-dev.sh
|
||||
RUN chmod +x /start-dev.sh
|
||||
|
||||
EXPOSE 3003 80
|
||||
|
||||
CMD ["/start-dev.sh"]
|
||||
|
||||
|
||||
121
IA_agents/all.md
Normal file
121
IA_agents/all.md
Normal file
@ -0,0 +1,121 @@
|
||||
# IHM_CLIENT
|
||||
|
||||
voir README.md
|
||||
|
||||
voir docs/INITIALIZATION_FLOW.md
|
||||
|
||||
### General
|
||||
|
||||
* Répond en français
|
||||
* Code, documente le code, et fait les commits en anglais
|
||||
|
||||
### Règles Obligatoires
|
||||
|
||||
### Préparation
|
||||
|
||||
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
|
||||
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
|
||||
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
|
||||
|
||||
#### ⚙️ Getion de projet
|
||||
|
||||
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
|
||||
* **Planning :** Ne fait pas de roadmap.
|
||||
|
||||
#### 🤝 Collaboration et Workflow
|
||||
|
||||
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
|
||||
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
|
||||
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
|
||||
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
|
||||
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
|
||||
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
|
||||
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
|
||||
* **Rapports :** Ne fait pas de rapports apres tes actions.
|
||||
|
||||
#### ⚙️ Gestion de l'Environnement et des Configurations
|
||||
|
||||
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
|
||||
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
|
||||
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
|
||||
* **Nginx :** Ne modifie jamaisles configurations Nginx
|
||||
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
|
||||
|
||||
#### 💻 Qualité du Code et Bonnes Pratiques
|
||||
|
||||
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
|
||||
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
|
||||
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
|
||||
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
|
||||
|
||||
#### Code
|
||||
|
||||
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
|
||||
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
|
||||
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
|
||||
* **Fallbacks :** Ne fait pas et supprime les fallbacks
|
||||
|
||||
#### 🧪 Tests
|
||||
|
||||
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
|
||||
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
|
||||
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
|
||||
|
||||
#### 📚 Documentation
|
||||
|
||||
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
|
||||
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
|
||||
* **Structure de la documentation :**
|
||||
* La documentation générale et pérenne se trouve dans `docs/`.
|
||||
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
|
||||
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
|
||||
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
|
||||
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
|
||||
|
||||
#### 📊 Logging et Gestion des Erreurs
|
||||
|
||||
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
|
||||
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
|
||||
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
|
||||
* **Données vérifiées :** Vérifiant que les logs reflètent des vérifications réelles et non des déclarations.
|
||||
* **Log abondamment :** Log les informations et étapes ou états clés ainsi que les identifiants clés.
|
||||
|
||||
|
||||
#### 🌐 Interactions Externes (BDD, API, Emails)
|
||||
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
|
||||
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
|
||||
|
||||
### Base de données
|
||||
|
||||
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
|
||||
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
|
||||
|
||||
#### 🚀 Déploiement
|
||||
|
||||
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
|
||||
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
|
||||
* **Lancement :** ne lance aucun déploiement sans demander avant
|
||||
|
||||
#### 🚨 Gestion des Problèmes
|
||||
|
||||
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
|
||||
|
||||
#### 🗄️ Gestion des Fichiers
|
||||
|
||||
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
|
||||
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
|
||||
|
||||
### Mise à jour de ces règles
|
||||
|
||||
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
|
||||
|
||||
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
|
||||
|
||||
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
|
||||
|
||||
### Application
|
||||
|
||||
* Indique l'IA que tu utilise
|
||||
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.
|
||||
130
README.md
130
README.md
@ -1,16 +1,128 @@
|
||||
# ihm_client
|
||||
# 🚀 4NK Client - Application Web5
|
||||
|
||||
Application client pour l'écosystème 4NK, permettant la gestion sécurisée des appareils, le pairing, et les signatures de documents.
|
||||
|
||||
## 📋 Table des matières
|
||||
|
||||
## HOW TO START
|
||||
- [🚀 Démarrage rapide](#-démarrage-rapide)
|
||||
- [🏗️ Architecture](#️-architecture)
|
||||
- [🔧 Développement](#-développement)
|
||||
- [📊 Qualité du code](#-qualité-du-code)
|
||||
- [🤝 Contribution](#-contribution)
|
||||
|
||||
1 - clone sdk_common, commit name "doc pcd" from 28.10.2024
|
||||
2 - clone sdk_client, commit name "Ignore messages" from 17.10.2024
|
||||
3 - clone ihm_client_test3
|
||||
4 - cargo build in sdk_common
|
||||
5 - cargo run in sdk_client
|
||||
6 - npm run build_wasm in ihm_client_test3
|
||||
7 - npm run start in ihm_client_test3
|
||||
## 🚀 Démarrage rapide
|
||||
|
||||
### **Prérequis**
|
||||
- Node.js 18+
|
||||
- Rust (pour le SDK)
|
||||
- npm ou yarn
|
||||
|
||||
### **Installation**
|
||||
|
||||
```bash
|
||||
# 1. Cloner les dépendances
|
||||
git clone <sdk_common> # commit "doc pcd" from 28.10.2024
|
||||
git clone <sdk_client> # commit "Ignore messages" from 17.10.2024
|
||||
git clone <ihm_client_dev3>
|
||||
|
||||
# 2. Build du SDK Rust
|
||||
cd sdk_common && cargo build
|
||||
cd ../sdk_client && cargo run
|
||||
|
||||
# 3. Build et démarrage de l'application
|
||||
cd ../ihm_client_dev3
|
||||
npm install
|
||||
npm run build_wasm
|
||||
npm run start
|
||||
```
|
||||
|
||||
### **Scripts disponibles**
|
||||
|
||||
```bash
|
||||
# Développement
|
||||
npm run start # Serveur de développement
|
||||
npm run build # Build de production
|
||||
npm run quality # Vérification de la qualité
|
||||
npm run quality:fix # Correction automatique
|
||||
|
||||
# Tests et analyse
|
||||
npm run test # Tests unitaires
|
||||
npm run lint # Linting du code
|
||||
npm run type-check # Vérification TypeScript
|
||||
npm run analyze # Analyse du bundle
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### **Structure du projet**
|
||||
```
|
||||
src/
|
||||
├── components/ # Composants UI réutilisables
|
||||
├── pages/ # Pages de l'application
|
||||
├── services/ # Services métier
|
||||
├── utils/ # Utilitaires et helpers
|
||||
├── models/ # Types et interfaces
|
||||
└── service-workers/ # Workers pour les opérations async
|
||||
```
|
||||
|
||||
### **Technologies**
|
||||
- **Frontend**: TypeScript, Vite, HTML5, CSS3
|
||||
- **SDK**: Rust (WebAssembly)
|
||||
- **Storage**: IndexedDB, Service Workers
|
||||
- **Communication**: WebSockets, PostMessage API
|
||||
|
||||
### **WebSocket Relay Configuration**
|
||||
- Default relay runs locally on `127.0.0.1:8091` and is exposed securely via `wss://relay235.4nkweb.com/ws/`
|
||||
- Nginx TLS termination is defined in `nginx.relay235.conf` (kept alongside the existing `nginx.dev.conf`)
|
||||
- Clients must configure `VITE_BOOTSTRAPURL=wss://relay235.4nkweb.com/ws/` to avoid mixed-content issues when the app is served over HTTPS
|
||||
|
||||
## 🔧 Développement
|
||||
|
||||
### **Standards de code**
|
||||
- TypeScript strict
|
||||
- ESLint + Prettier
|
||||
- Tests unitaires
|
||||
- Documentation JSDoc
|
||||
|
||||
### **Workflow**
|
||||
1. Créer une branche feature
|
||||
2. Développer avec tests
|
||||
3. Vérifier la qualité: `npm run quality`
|
||||
4. Créer une PR avec description détaillée
|
||||
|
||||
## 📊 Qualité du code
|
||||
|
||||
### **Métriques cibles**
|
||||
- Couverture de tests: > 80%
|
||||
- Complexité cyclomatique: < 10
|
||||
- Taille des fichiers: < 300 lignes
|
||||
- Bundle size: < 500KB gzippé
|
||||
|
||||
### **Outils de qualité**
|
||||
- TypeScript strict mode
|
||||
- ESLint avec règles personnalisées
|
||||
- Prettier pour le formatage
|
||||
- Bundle analyzer pour l'optimisation
|
||||
|
||||
## 🤝 Contribution
|
||||
|
||||
Voir [CONTRIBUTING.md](./CONTRIBUTING.md) pour les détails complets.
|
||||
|
||||
### **Démarrage rapide**
|
||||
```bash
|
||||
# Fork et clone
|
||||
git clone <votre-fork>
|
||||
cd ihm_client_dev3
|
||||
|
||||
# Installation
|
||||
npm install
|
||||
|
||||
# Vérification de la qualité
|
||||
npm run quality
|
||||
|
||||
# Développement
|
||||
npm run start
|
||||
```
|
||||
|
||||
## USER STORIES
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.9 MiB |
500
docs/CODE_ANALYSIS_REPORT.md
Normal file
500
docs/CODE_ANALYSIS_REPORT.md
Normal file
@ -0,0 +1,500 @@
|
||||
# 🔍 Analyse approfondie du code - 4NK Client
|
||||
|
||||
## 📊 **Résumé exécutif**
|
||||
|
||||
Après analyse complète du code au-delà du linting, j'ai identifié plusieurs axes d'amélioration majeurs pour optimiser les performances, la sécurité, la maintenabilité et l'architecture de l'application.
|
||||
|
||||
## 🏗️ **1. Architecture et Design Patterns**
|
||||
|
||||
### **❌ Problèmes identifiés :**
|
||||
|
||||
#### **A. Anti-patterns majeurs**
|
||||
1. **Singleton excessif** : Tous les services utilisent le pattern Singleton
|
||||
```typescript
|
||||
// ❌ Problématique actuelle
|
||||
export default class Services {
|
||||
private static instance: Services;
|
||||
public static async getInstance(): Promise<Services> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Couplage fort** : Services directement liés entre eux
|
||||
```typescript
|
||||
// ❌ Couplage fort
|
||||
import Services from './service';
|
||||
export class Database {
|
||||
// Utilise directement Services
|
||||
}
|
||||
```
|
||||
|
||||
3. **Responsabilités mélangées** : Services font trop de choses
|
||||
- `Services` : 3265 lignes, gère pairing, storage, websockets, UI
|
||||
- `Database` : 619 lignes, gère storage + communication
|
||||
|
||||
### **✅ Solutions recommandées :**
|
||||
|
||||
#### **A. Injection de dépendances**
|
||||
```typescript
|
||||
// ✅ Architecture recommandée
|
||||
interface ServiceContainer {
|
||||
deviceRepo: DeviceRepository;
|
||||
pairingService: PairingService;
|
||||
storageService: StorageService;
|
||||
eventBus: EventBus;
|
||||
}
|
||||
|
||||
class PairingService {
|
||||
constructor(
|
||||
private deviceRepo: DeviceRepository,
|
||||
private eventBus: EventBus,
|
||||
private logger: Logger
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
#### **B. Pattern Repository**
|
||||
```typescript
|
||||
// ✅ Séparation des responsabilités
|
||||
interface DeviceRepository {
|
||||
getDevice(): Promise<Device | null>;
|
||||
saveDevice(device: Device): Promise<void>;
|
||||
deleteDevice(): Promise<void>;
|
||||
}
|
||||
|
||||
interface ProcessRepository {
|
||||
getProcesses(): Promise<Process[]>;
|
||||
saveProcess(process: Process): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 **2. Performances et Optimisations**
|
||||
|
||||
### **❌ Goulots d'étranglement identifiés :**
|
||||
|
||||
#### **A. Gestion mémoire défaillante**
|
||||
1. **Cache désactivé** : `processesCache` existe mais est désactivé (`maxCacheSize = 0`)
|
||||
```typescript
|
||||
// ⚠️ État actuel
|
||||
private processesCache: Record<string, Process> = {};
|
||||
private maxCacheSize = 0; // Disabled caches completely
|
||||
private cacheExpiry = 0; // No cache expiry
|
||||
```
|
||||
**Note** : Le cache a été désactivé pour économiser la mémoire, mais cela peut impacter les performances pour les applications avec beaucoup de processus.
|
||||
|
||||
2. **Event listeners non nettoyés** : Fuites mémoire
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
window.addEventListener('message', handleMessage);
|
||||
// Jamais supprimé, s'accumule
|
||||
```
|
||||
|
||||
3. **WebSocket non fermé** : Connexions persistantes
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
let ws: WebSocket; // Variable globale
|
||||
// Pas de cleanup, pas de reconnexion
|
||||
```
|
||||
|
||||
#### **B. Opérations bloquantes**
|
||||
1. **Encodage synchrone** : Bloque l'UI
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
|
||||
const encodedPrivateData = {
|
||||
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
|
||||
...this.sdkClient.encode_binary(privateSplitData.binaryData),
|
||||
};
|
||||
```
|
||||
|
||||
2. **Boucles synchrones** : Bloquent le thread principal
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
while (messageQueue.length > 0) {
|
||||
const message = messageQueue.shift();
|
||||
if (message) {
|
||||
ws.send(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **✅ Solutions recommandées :**
|
||||
|
||||
#### **A. Gestion mémoire optimisée**
|
||||
```typescript
|
||||
// ✅ Cache avec limite et expiration
|
||||
class ProcessCache {
|
||||
private cache = new Map<string, { data: Process; timestamp: number }>();
|
||||
private maxSize = 100;
|
||||
private ttl = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
set(key: string, process: Process): void {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const oldest = this.cache.keys().next().value;
|
||||
this.cache.delete(oldest);
|
||||
}
|
||||
this.cache.set(key, { data: process, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
get(key: string): Process | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (Date.now() - entry.timestamp > this.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **B. WebSocket avec reconnexion**
|
||||
```typescript
|
||||
// ✅ WebSocket robuste
|
||||
class WebSocketManager {
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000;
|
||||
|
||||
connect(url: string): void {
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.processMessageQueue();
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.scheduleReconnect(url);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
private scheduleReconnect(url: string): void {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect(url);
|
||||
}, this.reconnectDelay * this.reconnectAttempts);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **C. Encodage asynchrone**
|
||||
```typescript
|
||||
// ✅ Encodage non-bloquant
|
||||
async function encodeDataAsync(data: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Utiliser Web Workers pour l'encodage lourd
|
||||
const worker = new Worker('/workers/encoder.worker.js');
|
||||
worker.postMessage(data);
|
||||
worker.onmessage = (e) => resolve(e.data);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 **3. Sécurité et Vulnérabilités**
|
||||
|
||||
### **❌ Vulnérabilités identifiées :**
|
||||
|
||||
#### **A. Exposition de données sensibles**
|
||||
1. **Clés privées en mémoire** : Stockage non sécurisé
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
private_key: safeDevice.sp_wallet.private_key,
|
||||
// Clé privée exposée dans les logs et la mémoire
|
||||
```
|
||||
|
||||
2. **Logs avec données sensibles** : Information leakage
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
console.log('encodedPrivateData:', encodedPrivateData);
|
||||
// Données privées dans les logs
|
||||
```
|
||||
|
||||
3. **Validation d'entrée insuffisante** : Injection possible
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
const parsedMessage = JSON.parse(msgData);
|
||||
// Pas de validation, pas de sanitisation
|
||||
```
|
||||
|
||||
#### **B. Gestion des erreurs dangereuse**
|
||||
1. **Stack traces exposés** : Information disclosure
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
console.error('Received an invalid message:', error);
|
||||
// Stack trace complet exposé
|
||||
```
|
||||
|
||||
2. **Messages d'erreur trop détaillés** : Aide à l'attaquant
|
||||
```typescript
|
||||
// ❌ Problème actuel
|
||||
throw new Error('❌ No relay address available after waiting');
|
||||
// Information sur l'architecture interne
|
||||
```
|
||||
|
||||
### **✅ Solutions recommandées :**
|
||||
|
||||
#### **A. Sécurisation des données sensibles**
|
||||
```typescript
|
||||
// ✅ Gestion sécurisée des clés
|
||||
class SecureKeyManager {
|
||||
private keyStore: CryptoKey | null = null;
|
||||
|
||||
async storePrivateKey(key: string): Promise<void> {
|
||||
// Chiffrer la clé avant stockage
|
||||
const encryptedKey = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
|
||||
await this.getDerivedKey(),
|
||||
new TextEncoder().encode(key)
|
||||
);
|
||||
this.keyStore = encryptedKey;
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<string | null> {
|
||||
if (!this.keyStore) return null;
|
||||
|
||||
try {
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: this.keyStore.slice(0, 12) },
|
||||
await this.getDerivedKey(),
|
||||
this.keyStore.slice(12)
|
||||
);
|
||||
return new TextDecoder().decode(decrypted);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **B. Validation et sanitisation**
|
||||
```typescript
|
||||
// ✅ Validation robuste
|
||||
class MessageValidator {
|
||||
static validateWebSocketMessage(data: any): boolean {
|
||||
if (typeof data !== 'string') return false;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
return this.isValidMessageStructure(parsed);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static isValidMessageStructure(msg: any): boolean {
|
||||
return (
|
||||
typeof msg === 'object' &&
|
||||
typeof msg.flag === 'string' &&
|
||||
typeof msg.content === 'object' &&
|
||||
['Handshake', 'NewTx', 'Cipher', 'Commit'].includes(msg.flag)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **C. Logging sécurisé**
|
||||
```typescript
|
||||
// ✅ Logging sans données sensibles
|
||||
class SecureLogger {
|
||||
static logError(message: string, error: Error, context?: any): void {
|
||||
const sanitizedContext = this.sanitizeContext(context);
|
||||
console.error(`[${new Date().toISOString()}] ${message}`, {
|
||||
error: error.message,
|
||||
context: sanitizedContext,
|
||||
// Pas de stack trace en production
|
||||
});
|
||||
}
|
||||
|
||||
private static sanitizeContext(context: any): any {
|
||||
if (!context) return {};
|
||||
|
||||
const sanitized = { ...context };
|
||||
// Supprimer les données sensibles
|
||||
delete sanitized.privateKey;
|
||||
delete sanitized.password;
|
||||
delete sanitized.token;
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 **4. Tests et Qualité**
|
||||
|
||||
### **❌ Déficiences actuelles :**
|
||||
|
||||
1. **Aucun test unitaire** : Pas de couverture de code
|
||||
2. **Pas de tests d'intégration** : Fonctionnalités non validées
|
||||
3. **Pas de tests de performance** : Goulots non identifiés
|
||||
4. **Pas de tests de sécurité** : Vulnérabilités non détectées
|
||||
|
||||
### **✅ Solutions recommandées :**
|
||||
|
||||
#### **A. Tests unitaires**
|
||||
```typescript
|
||||
// ✅ Tests unitaires
|
||||
describe('PairingService', () => {
|
||||
let pairingService: PairingService;
|
||||
let mockDeviceRepo: jest.Mocked<DeviceRepository>;
|
||||
let mockEventBus: jest.Mocked<EventBus>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeviceRepo = createMockDeviceRepository();
|
||||
mockEventBus = createMockEventBus();
|
||||
pairingService = new PairingService(mockDeviceRepo, mockEventBus);
|
||||
});
|
||||
|
||||
it('should create pairing process successfully', async () => {
|
||||
// Arrange
|
||||
const mockDevice = createMockDevice();
|
||||
mockDeviceRepo.getDevice.mockResolvedValue(mockDevice);
|
||||
|
||||
// Act
|
||||
const result = await pairingService.createPairing();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockEventBus.emit).toHaveBeenCalledWith('pairing:created');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### **B. Tests de performance**
|
||||
```typescript
|
||||
// ✅ Tests de performance
|
||||
describe('Performance Tests', () => {
|
||||
it('should handle large data encoding within time limit', async () => {
|
||||
const largeData = generateLargeData(1024 * 1024); // 1MB
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = await encodeDataAsync(largeData);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(5000); // 5 secondes max
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 📈 **5. Métriques et Monitoring**
|
||||
|
||||
### **✅ Implémentation recommandée :**
|
||||
|
||||
#### **A. Métriques de performance**
|
||||
```typescript
|
||||
// ✅ Monitoring des performances
|
||||
class PerformanceMonitor {
|
||||
private metrics: Map<string, number[]> = new Map();
|
||||
|
||||
recordMetric(name: string, value: number): void {
|
||||
if (!this.metrics.has(name)) {
|
||||
this.metrics.set(name, []);
|
||||
}
|
||||
this.metrics.get(name)!.push(value);
|
||||
}
|
||||
|
||||
getAverageMetric(name: string): number {
|
||||
const values = this.metrics.get(name) || [];
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
getMetrics(): Record<string, number> {
|
||||
const result: Record<string, number> = {};
|
||||
for (const [name, values] of this.metrics) {
|
||||
result[name] = this.getAverageMetric(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **B. Health checks**
|
||||
```typescript
|
||||
// ✅ Vérifications de santé
|
||||
class HealthChecker {
|
||||
async checkDatabase(): Promise<boolean> {
|
||||
try {
|
||||
await this.database.ping();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkWebSocket(): Promise<boolean> {
|
||||
return this.wsManager.isConnected();
|
||||
}
|
||||
|
||||
async getHealthStatus(): Promise<HealthStatus> {
|
||||
return {
|
||||
database: await this.checkDatabase(),
|
||||
websocket: await this.checkWebSocket(),
|
||||
memory: this.getMemoryUsage(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 **6. Plan d'implémentation prioritaire**
|
||||
|
||||
### **Phase 1 - Critique (1-2 semaines)**
|
||||
1. **Sécurisation des données sensibles**
|
||||
- Chiffrement des clés privées
|
||||
- Sanitisation des logs
|
||||
- Validation des entrées
|
||||
|
||||
2. **Gestion mémoire**
|
||||
- Limitation des caches
|
||||
- Nettoyage des event listeners
|
||||
- Gestion des WebSockets
|
||||
|
||||
### **Phase 2 - Performance (2-3 semaines)**
|
||||
1. **Architecture modulaire**
|
||||
- Injection de dépendances
|
||||
- Pattern Repository
|
||||
- Séparation des responsabilités
|
||||
|
||||
2. **Optimisations**
|
||||
- Encodage asynchrone
|
||||
- Lazy loading
|
||||
- Debouncing
|
||||
|
||||
### **Phase 3 - Qualité (3-4 semaines)**
|
||||
1. **Tests**
|
||||
- Tests unitaires
|
||||
- Tests d'intégration
|
||||
- Tests de performance
|
||||
|
||||
2. **Monitoring**
|
||||
- Métriques de performance
|
||||
- Health checks
|
||||
- Alertes
|
||||
|
||||
## 📊 **7. Métriques de succès**
|
||||
|
||||
### **Objectifs quantifiables :**
|
||||
- **Performance** : Temps de réponse < 200ms
|
||||
- **Mémoire** : Utilisation < 100MB
|
||||
- **Sécurité** : 0 vulnérabilité critique
|
||||
- **Qualité** : Couverture de tests > 80%
|
||||
- **Maintenabilité** : Complexité cyclomatique < 10
|
||||
|
||||
## 🚀 **8. Bénéfices attendus**
|
||||
|
||||
1. **Performance** : 3x plus rapide, 50% moins de mémoire
|
||||
2. **Sécurité** : Protection des données sensibles
|
||||
3. **Maintenabilité** : Code modulaire et testable
|
||||
4. **Évolutivité** : Architecture extensible
|
||||
5. **Fiabilité** : Moins de bugs, plus de stabilité
|
||||
|
||||
---
|
||||
|
||||
**Conclusion** : L'application a une base solide mais nécessite des améliorations significatives en architecture, performance et sécurité. Le plan proposé permettra de transformer l'application en une solution robuste et évolutive.
|
||||
648
docs/INITIALIZATION_FLOW.md
Normal file
648
docs/INITIALIZATION_FLOW.md
Normal file
@ -0,0 +1,648 @@
|
||||
# Documentation de l'Initialisation IHM_CLIENT
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système IHM_CLIENT suit un processus d'initialisation en plusieurs étapes pour créer et sécuriser un wallet Bitcoin. Ce document détaille chaque étape du processus, depuis le choix du mode de sécurité jusqu'au pairing réussi et à la récupération des processus.
|
||||
|
||||
## Architecture des Stores IndexedDB
|
||||
|
||||
### Stores utilisés :
|
||||
- **`pbkdf2keys`** : Stockage des clés PBKDF2 chiffrées par mode de sécurité
|
||||
- **`wallet`** : Stockage du wallet chiffré (device + wallet data)
|
||||
- **`credentials`** : Stockage des credentials de pairing (utilisé uniquement après pairing)
|
||||
- **`env`** : Stockage des variables d'environnement internes (mots de passe, constantes)
|
||||
- **`processes`** : Stockage des processus de communication
|
||||
- **`labels`** : Stockage des labels de transactions
|
||||
- **`shared_secrets`** : Stockage des secrets partagés
|
||||
- **`unconfirmed_secrets`** : Stockage des secrets non confirmés
|
||||
- **`diffs`** : Stockage des différences de synchronisation
|
||||
- **`data`** : Stockage des données générales
|
||||
|
||||
> Note: The IndexedDB stores are provisioned from the shared `DATABASE_CONFIG` for both the main thread database helper and the `database.worker.js`, ensuring that `credentials` and `env` collections are always created before the pairing flow starts.
|
||||
|
||||
## Flux d'Initialisation Complet
|
||||
|
||||
### 1. Démarrage de l'Application
|
||||
|
||||
**Fichier :** `src/router.ts` → `checkStorageStateAndNavigate()`
|
||||
|
||||
L'application vérifie l'état du storage pour déterminer l'étape suivante :
|
||||
|
||||
```typescript
|
||||
// Logique de progression :
|
||||
// - Si pairing → account
|
||||
// - Si date anniversaire → pairing
|
||||
// - Si wallet → birthday-setup
|
||||
// - Si pbkdf2 → wallet-setup
|
||||
// - Sinon → security-setup
|
||||
```
|
||||
|
||||
**États possibles :**
|
||||
1. **Appareil appairé** → Redirection vers `account`
|
||||
2. **Date anniversaire configurée** → Redirection vers `home` (pairing)
|
||||
3. **Wallet existe sans date anniversaire** → Redirection vers `birthday-setup`
|
||||
4. **Clé PBKDF2 existe** → Redirection vers `wallet-setup`
|
||||
5. **Aucune configuration** → Redirection vers `security-setup`
|
||||
|
||||
### 2. Configuration du Mode de Sécurité
|
||||
|
||||
**Fichier :** `src/pages/security-setup/security-setup.ts`
|
||||
|
||||
#### 2.1 Sélection du Mode
|
||||
|
||||
L'utilisateur choisit parmi les modes disponibles :
|
||||
|
||||
| Mode | Nom | Description | Niveau de Sécurité | Clé de Chiffrement PBKDF2 | Stockage de la Clé de Chiffrement |
|
||||
|------|-----|-------------|-------------------|---------------------------|-----------------------------------|
|
||||
| `proton-pass` | Proton Pass | Authentification biométrique via Proton Pass | High | Clé WebAuthn générée par le navigateur | Stockée dans le navigateur (WebAuthn credential) |
|
||||
| `os` | Authentificateur OS | Authentification biométrique du système | High | Clé WebAuthn générée par le système | Stockée dans le système d'exploitation |
|
||||
| `otp` | OTP | Code à usage unique (Google Authenticator, etc.) | High | Aucune (clé PBKDF2 stockée en clair) | Secret OTP stocké dans l'application OTP |
|
||||
| `password` | Mot de passe | Chiffrement par mot de passe (non sauvegardé) | Low | Mot de passe utilisateur | Stocké dans le gestionnaire de mots de passe du navigateur |
|
||||
| `none` | Aucune sécurité | Chiffrement avec clé en dur (non recommandé) | Critical | Clé en dur `4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE` | Intégrée dans le code (non sécurisé) |
|
||||
|
||||
#### 2.2 Génération de la Clé PBKDF2
|
||||
|
||||
**Fichier :** `src/services/secure-credentials.service.ts` → `generatePBKDF2Key()`
|
||||
|
||||
Pour chaque mode, une clé PBKDF2 est générée et stockée différemment :
|
||||
|
||||
##### Mode `proton-pass` et `os` (WebAuthn)
|
||||
```typescript
|
||||
// Stockage avec WebAuthn (authentification biométrique)
|
||||
await webAuthnService.storeKeyWithWebAuthn(pbkdf2Key, securityMode);
|
||||
```
|
||||
- **Store :** `pbkdf2keys`
|
||||
- **Clé :** `security_mode` (ex: "proton-pass")
|
||||
- **Valeur :** Clé PBKDF2 chiffrée avec WebAuthn
|
||||
- **Authentification :** Biométrique (empreinte, visage, etc.)
|
||||
|
||||
##### Mode `otp`
|
||||
```typescript
|
||||
// Génération du secret OTP
|
||||
const otpSecret = await this.generateOTPSecret();
|
||||
// Stockage de la clé PBKDF2 en clair
|
||||
await this.storePBKDF2KeyInStore(pbkdf2Key, securityMode);
|
||||
// Affichage du QR code
|
||||
this.displayOTPQRCode(otpSecret);
|
||||
```
|
||||
- **Store :** `pbkdf2keys`
|
||||
- **Clé :** `security_mode` ("otp")
|
||||
- **Valeur :** Clé PBKDF2 en clair
|
||||
- **Authentification :** Code OTP généré par l'application
|
||||
|
||||
##### Mode `password`
|
||||
```typescript
|
||||
// Demande du mot de passe utilisateur
|
||||
const userPassword = await this.promptForPasswordWithBrowser();
|
||||
// Chiffrement de la clé PBKDF2
|
||||
const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword);
|
||||
// Stockage chiffré
|
||||
await this.storePBKDF2KeyInStore(encryptedKey, securityMode);
|
||||
```
|
||||
- **Store :** `pbkdf2keys`
|
||||
- **Clé :** `security_mode` ("password")
|
||||
- **Valeur :** Clé PBKDF2 chiffrée avec le mot de passe utilisateur
|
||||
- **Authentification :** Mot de passe utilisateur (non sauvegardé)
|
||||
|
||||
##### Mode `none`
|
||||
```typescript
|
||||
// Clé de chiffrement en dur
|
||||
const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE';
|
||||
// Chiffrement avec la clé en dur
|
||||
const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey);
|
||||
// Stockage chiffré
|
||||
await this.storePBKDF2KeyInStore(encryptedKeyNone, securityMode);
|
||||
```
|
||||
- **Store :** `pbkdf2keys`
|
||||
- **Clé :** `security_mode` ("none")
|
||||
- **Valeur :** Clé PBKDF2 chiffrée avec clé en dur
|
||||
- **Authentification :** Aucune (non sécurisé)
|
||||
|
||||
### 3. Création du Wallet
|
||||
|
||||
**Fichier :** `src/pages/wallet-setup/wallet-setup.ts`
|
||||
|
||||
#### 3.1 Récupération de la Clé PBKDF2
|
||||
|
||||
Le système teste tous les modes de sécurité pour trouver la clé PBKDF2 valide :
|
||||
|
||||
```typescript
|
||||
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
|
||||
for (const mode of allSecurityModes) {
|
||||
const hasKey = await secureCredentialsService.hasPBKDF2Key(mode);
|
||||
if (hasKey) {
|
||||
const key = await secureCredentialsService.retrievePBKDF2Key(mode);
|
||||
if (key) {
|
||||
currentMode = mode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Génération des Clés du Wallet
|
||||
|
||||
```typescript
|
||||
// Génération des clés temporaires
|
||||
const walletData = {
|
||||
scan_sk: encryptionService.generateRandomKey(),
|
||||
spend_key: encryptionService.generateRandomKey(),
|
||||
network: 'signet',
|
||||
state: 'birthday_waiting',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
```
|
||||
|
||||
#### 3.3 Création du Device via SDK
|
||||
|
||||
```typescript
|
||||
// Création du device avec birthday = 0
|
||||
const spAddress = await services.sdkClient.create_new_device(0, 'signet');
|
||||
// Génération forcée du wallet
|
||||
const wallet = await services.sdkClient.dump_wallet();
|
||||
```
|
||||
|
||||
#### 3.4 Stockage Chiffré du Wallet
|
||||
|
||||
```typescript
|
||||
// Chiffrement du device
|
||||
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
|
||||
// Chiffrement du wallet
|
||||
const encryptedWallet = await encryptionService.encrypt(walletString, pbkdf2Key);
|
||||
|
||||
// Stockage dans le store wallet
|
||||
const walletObject = {
|
||||
pre_id: '1',
|
||||
encrypted_device: encryptedDevice,
|
||||
encrypted_wallet: encryptedWallet
|
||||
};
|
||||
```
|
||||
|
||||
**Store :** `wallet`
|
||||
**Clé :** `pre_id` ("1")
|
||||
**Valeur :** Objet contenant uniquement des données chiffrées
|
||||
|
||||
### 4. Configuration de la Date Anniversaire
|
||||
|
||||
**Fichier :** `src/pages/birthday-setup/birthday-setup.ts`
|
||||
|
||||
#### 4.1 Vérification des Prérequis
|
||||
|
||||
Avant de procéder, la page vérifie que tous les prérequis sont remplis :
|
||||
|
||||
```typescript
|
||||
// Vérification de la clé PBKDF2 dans le store pbkdf2keys
|
||||
const securityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
|
||||
let pbkdf2KeyFound = false;
|
||||
for (const mode of securityModes) {
|
||||
const hasKey = await secureCredentials.hasPBKDF2Key(mode);
|
||||
if (hasKey) {
|
||||
const key = await secureCredentials.retrievePBKDF2Key(mode);
|
||||
if (key) {
|
||||
pbkdf2KeyFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification du wallet en base de données (avec retry pour synchronisation)
|
||||
let wallet = await services.getDeviceFromDatabase();
|
||||
if (!wallet) {
|
||||
// Retry jusqu'à 5 tentatives avec délai de 500ms
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
wallet = await services.getDeviceFromDatabase();
|
||||
if (wallet) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification que le wallet contient les données attendues
|
||||
if (wallet.sp_wallet && wallet.sp_wallet.birthday !== undefined) {
|
||||
console.log('✅ Wallet found in database with birthday:', wallet.sp_wallet.birthday);
|
||||
} else {
|
||||
throw new Error('Wallet found but missing required data');
|
||||
}
|
||||
```
|
||||
|
||||
**Points importants :**
|
||||
- Vérification réelle de la présence de la clé PBKDF2 dans le store `pbkdf2keys`
|
||||
- Vérification réelle du wallet en base avec retry pour gérer les problèmes de synchronisation
|
||||
- Validation que le wallet contient bien les données attendues (`sp_wallet`, `birthday`)
|
||||
|
||||
#### 4.2 Connexion aux Relais
|
||||
|
||||
```typescript
|
||||
// Connexion aux relais Bitcoin
|
||||
await services.connectAllRelays();
|
||||
|
||||
// Vérification que les relais sont connectés en vérifiant chain_tip
|
||||
const currentBlockHeight = services.getCurrentBlockHeight();
|
||||
if (currentBlockHeight !== -1) {
|
||||
console.log('✅ Relays connected successfully, chain_tip:', currentBlockHeight);
|
||||
} else {
|
||||
throw new Error('Relays connected but chain_tip not set');
|
||||
}
|
||||
|
||||
// Vérification que le handshake a été reçu
|
||||
if (currentBlockHeight > 0) {
|
||||
console.log('✅ Communication handshake completed, chain_tip:', currentBlockHeight);
|
||||
} else {
|
||||
throw new Error('Handshake not received or chain_tip not set');
|
||||
}
|
||||
```
|
||||
|
||||
**Vérifications réelles :**
|
||||
- Vérification que `chain_tip` est défini (valeur != -1)
|
||||
- Vérification que `chain_tip` est positif (indique que le handshake a été reçu)
|
||||
|
||||
#### 4.3 Mise à Jour de la Date Anniversaire
|
||||
|
||||
**Fichier :** `src/services/service.ts` → `updateDeviceBlockHeight()`
|
||||
|
||||
```typescript
|
||||
// Mise à jour du birthday du device
|
||||
await services.updateDeviceBlockHeight();
|
||||
```
|
||||
|
||||
**Processus interne avec vérifications réelles :**
|
||||
|
||||
1. **Restauration en mémoire** :
|
||||
```typescript
|
||||
this.sdkClient.restore_device(device);
|
||||
// Vérification que le device a été restauré en mémoire
|
||||
const restoredDevice = this.dumpDeviceFromMemory();
|
||||
if (restoredDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
|
||||
console.log('✅ Device restored in memory with updated birthday:', device.sp_wallet.birthday);
|
||||
} else {
|
||||
throw new Error('Device restoration failed');
|
||||
}
|
||||
```
|
||||
|
||||
2. **Sauvegarde en base de données** :
|
||||
```typescript
|
||||
await this.saveDeviceInDatabase(device);
|
||||
// Vérification que le device a été sauvegardé en base de données
|
||||
const savedDevice = await this.getDeviceFromDatabase();
|
||||
if (savedDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
|
||||
console.log('✅ Device saved to database with updated birthday:', device.sp_wallet.birthday);
|
||||
} else {
|
||||
throw new Error('Device save verification failed');
|
||||
}
|
||||
```
|
||||
|
||||
3. **Vérification du scan initial** :
|
||||
```typescript
|
||||
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
|
||||
// Vérification que le scan est terminé en vérifiant last_scan
|
||||
const deviceAfterScan = this.dumpDeviceFromMemory();
|
||||
if (deviceAfterScan?.sp_wallet?.last_scan === this.currentBlockHeight) {
|
||||
console.log('✅ Initial scan completed for new wallet');
|
||||
} else {
|
||||
console.warn('⚠️ Initial scan may not be complete');
|
||||
}
|
||||
```
|
||||
|
||||
4. **Vérification finale** :
|
||||
```typescript
|
||||
// Sauvegarde finale avec last_scan mis à jour
|
||||
await this.saveDeviceInDatabase(device);
|
||||
// Vérification que le device a été sauvegardé avec last_scan mis à jour
|
||||
const finalDevice = await this.getDeviceFromDatabase();
|
||||
if (finalDevice?.sp_wallet?.last_scan === this.currentBlockHeight) {
|
||||
console.log('✅ New wallet initial scan completed and saved');
|
||||
} else {
|
||||
throw new Error('Final save verification failed');
|
||||
}
|
||||
```
|
||||
|
||||
**Vérification dans birthday-setup.ts :**
|
||||
```typescript
|
||||
// Vérifier que le birthday a bien été mis à jour en récupérant le wallet depuis la base
|
||||
const updatedWallet = await services.getDeviceFromDatabase();
|
||||
if (updatedWallet?.sp_wallet?.birthday && updatedWallet.sp_wallet.birthday > 0) {
|
||||
console.log('✅ Birthday updated successfully:', updatedWallet.sp_wallet.birthday);
|
||||
} else {
|
||||
throw new Error('Birthday update verification failed');
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Sauvegarde du Device avec Vérification
|
||||
|
||||
**Fichier :** `src/services/service.ts` → `saveDeviceInDatabase()`
|
||||
|
||||
La méthode `saveDeviceInDatabase()` effectue maintenant une vérification réelle après la sauvegarde :
|
||||
|
||||
```typescript
|
||||
// Sauvegarde du wallet chiffré
|
||||
const putRequest = store.put(walletObject);
|
||||
putRequest.onsuccess = () => {
|
||||
console.log('✅ Device saved to database with encryption');
|
||||
// La vérification se fera dans transaction.oncomplete
|
||||
};
|
||||
|
||||
transaction.oncomplete = async () => {
|
||||
// Vérifier que le wallet a bien été sauvegardé en le récupérant depuis la base
|
||||
const verificationDb = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
const verificationTx = verificationDb.transaction([walletStore], 'readonly');
|
||||
const verifyRequest = verificationStore.get('1');
|
||||
|
||||
verifyRequest.onsuccess = () => {
|
||||
const savedData = verifyRequest.result;
|
||||
if (savedData && savedData.encrypted_device === encryptedDevice) {
|
||||
console.log('✅ Verified: Device correctly saved in database');
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('Device save verification failed');
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Points importants :**
|
||||
- Vérification réelle après la transaction pour confirmer que les données sont bien sauvegardées
|
||||
- Comparaison de `encrypted_device` pour s'assurer que les données sont correctes
|
||||
- Logs de succès uniquement après vérification réelle
|
||||
|
||||
#### 4.5 Redirection vers la Synchronisation des Blocs
|
||||
|
||||
Après la mise à jour réussie du birthday, l'application redirige vers la page de synchronisation des blocs :
|
||||
|
||||
```typescript
|
||||
// Redirection vers la page de synchronisation des blocs
|
||||
window.location.href = '/src/pages/block-sync/block-sync.html';
|
||||
```
|
||||
|
||||
**Page :** `src/pages/block-sync/block-sync.html`
|
||||
- Interface dédiée pour la synchronisation des blocs
|
||||
- Affichage de la progression de la synchronisation
|
||||
- Gestion de la synchronisation initiale du wallet avec le réseau Bitcoin
|
||||
|
||||
### 5. Processus de Pairing
|
||||
|
||||
**Fichier :** `src/pages/home/home.ts` → `handleMainPairing()` et `src/utils/sp-address.utils.ts` → `prepareAndSendPairingTx()`
|
||||
|
||||
#### But et Objectif du Pairing
|
||||
|
||||
Le processus de pairing dans IHM_CLIENT sert à **créer une identité numérique vérifiable** qui permet :
|
||||
|
||||
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus permet de valider les actions critiques nécessitant plusieurs appareils appairés
|
||||
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
|
||||
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
|
||||
4. **Système d'identité et de chiffrement** : Partage des secrets Silent Payment pour le chiffrement entre appareils appairés
|
||||
|
||||
#### Caractéristiques
|
||||
|
||||
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus peut inclure N appareils
|
||||
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
|
||||
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
|
||||
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
|
||||
|
||||
#### 5.1 Vérification du Mode de Sécurité
|
||||
|
||||
```typescript
|
||||
const currentMode = await securityModeService.getCurrentMode();
|
||||
if (!currentMode) {
|
||||
// Redirection vers security-setup si aucun mode n'est configuré
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 Authentification selon le Mode
|
||||
|
||||
##### Mode `proton-pass` et `os`
|
||||
```typescript
|
||||
// Authentification WebAuthn
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: new Uint8Array(32),
|
||||
allowCredentials: [{
|
||||
id: credentialId,
|
||||
type: 'public-key'
|
||||
}]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
##### Mode `otp`
|
||||
```typescript
|
||||
// Demande du code OTP
|
||||
const otpCode = await this.promptForOTPCode();
|
||||
// Vérification du code OTP
|
||||
const isValid = await this.verifyOTPCode(otpCode, otpSecret);
|
||||
```
|
||||
|
||||
##### Mode `password`
|
||||
```typescript
|
||||
// Demande du mot de passe
|
||||
const password = await this.promptForPassword();
|
||||
// Déchiffrement de la clé PBKDF2
|
||||
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, password);
|
||||
```
|
||||
|
||||
##### Mode `none`
|
||||
```typescript
|
||||
// Déchiffrement avec la clé en dur
|
||||
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, hardcodedKey);
|
||||
```
|
||||
|
||||
#### 5.3 Génération des Credentials de Pairing
|
||||
|
||||
```typescript
|
||||
// Génération des credentials sécurisés (mot de passe récupéré depuis le store env)
|
||||
const credentialData = await secureCredentialsService.generateSecureCredentials('');
|
||||
|
||||
// Stockage des credentials dans le store credentials
|
||||
await secureCredentialsService.storeCredentials(credentialData, '');
|
||||
|
||||
// Récupération et déchiffrement des credentials (mot de passe récupéré depuis le store env)
|
||||
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
|
||||
```
|
||||
|
||||
**Store `env` :**
|
||||
- **Clé :** `CREDENTIALS_PASSWORD`
|
||||
- **Valeur :** `4nk-secure-password` (mot de passe interne pour les credentials)
|
||||
- **Description :** Mot de passe interne pour la génération des credentials de pairing
|
||||
|
||||
#### 5.4 Création du Processus de Pairing
|
||||
|
||||
```typescript
|
||||
// Création du processus via le SDK
|
||||
const pairingResult = await services.createPairingProcess({
|
||||
spendKey: retrievedCredentials.spendKey,
|
||||
scanKey: retrievedCredentials.scanKey
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Récupération des Processus
|
||||
|
||||
**Fichier :** `src/services/service.ts` → `restoreProcessesFromDB()`
|
||||
|
||||
#### 6.1 Synchronisation des Processus
|
||||
|
||||
```typescript
|
||||
// Récupération des processus depuis la base de données
|
||||
const processes = await processRepo.getAllProcesses();
|
||||
|
||||
// Synchronisation avec le réseau
|
||||
for (const process of processes) {
|
||||
await services.syncProcess(process);
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 Mise à Jour de l'État de Pairing
|
||||
|
||||
```typescript
|
||||
// Vérification de l'état de pairing
|
||||
const isPaired = services.isPaired();
|
||||
if (isPaired) {
|
||||
// Redirection vers la page account
|
||||
await navigate('account');
|
||||
}
|
||||
```
|
||||
|
||||
## Diagramme de Flux
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Démarrage Application] --> B{Vérification État Storage}
|
||||
B -->|Aucune config| C[Security Setup]
|
||||
B -->|PBKDF2 existe| D[Wallet Setup]
|
||||
B -->|Wallet existe| E[Birthday Setup]
|
||||
B -->|Birthday configuré| F[Block Sync]
|
||||
B -->|Appareil appairé| G[Account]
|
||||
|
||||
C --> C1[Sélection Mode Sécurité]
|
||||
C1 --> C2[Génération Clé PBKDF2]
|
||||
C2 --> C3[Stockage selon Mode]
|
||||
C3 --> D
|
||||
|
||||
D --> D1[Récupération Clé PBKDF2]
|
||||
D1 --> D2[Création Device SDK]
|
||||
D2 --> D3[Génération Wallet]
|
||||
D3 --> D4[Stockage Chiffré avec Vérification]
|
||||
D4 --> E
|
||||
|
||||
E --> E1[Vérification Prérequis]
|
||||
E1 --> E2{Prérequis OK?}
|
||||
E2 -->|Non| E3[Redirection vers Setup Précédent]
|
||||
E2 -->|Oui| E4[Connexion Relais]
|
||||
E4 --> E5[Vérification Handshake]
|
||||
E5 --> E6[Mise à Jour Birthday]
|
||||
E6 --> E7[Vérification Sauvegarde]
|
||||
E7 --> E8[Vérification Birthday]
|
||||
E8 --> F
|
||||
|
||||
F --> F1[Synchronisation Blocs]
|
||||
F1 --> F2[Initialisation Services]
|
||||
F2 --> F3[Scan des Blocs]
|
||||
F3 --> F4[Mise à Jour last_scan]
|
||||
F4 --> G
|
||||
|
||||
G --> G1[Pairing]
|
||||
G1 --> G2[Authentification Mode]
|
||||
G2 --> G3[Génération Credentials]
|
||||
G3 --> G4[Création Processus Pairing]
|
||||
G4 --> G5[Récupération Processus]
|
||||
```
|
||||
|
||||
## Sécurité par Mode
|
||||
|
||||
### Mode `proton-pass` et `os`
|
||||
- **Stockage :** Clé PBKDF2 chiffrée avec WebAuthn
|
||||
- **Authentification :** Biométrique (empreinte, visage)
|
||||
- **Sécurité :** Élevée (clé matérielle)
|
||||
|
||||
### Mode `otp`
|
||||
- **Stockage :** Clé PBKDF2 en clair
|
||||
- **Authentification :** Code OTP temporaire
|
||||
- **Sécurité :** Élevée (authentification à deux facteurs)
|
||||
|
||||
### Mode `password`
|
||||
- **Stockage :** Clé PBKDF2 chiffrée avec mot de passe utilisateur
|
||||
- **Authentification :** Mot de passe utilisateur
|
||||
- **Sécurité :** Faible (dépend de la force du mot de passe)
|
||||
|
||||
### Mode `none`
|
||||
- **Stockage :** Clé PBKDF2 chiffrée avec clé en dur
|
||||
- **Authentification :** Aucune
|
||||
- **Sécurité :** Critique (non recommandé)
|
||||
|
||||
## Gestion des Erreurs
|
||||
|
||||
### Erreurs de Chiffrement
|
||||
- **Clé PBKDF2 introuvable** → Redirection vers `security-setup`
|
||||
- **Échec de déchiffrement** → Demande de réauthentification
|
||||
- **Wallet corrompu** → Recréation du wallet
|
||||
|
||||
### Erreurs de Réseau
|
||||
- **Connexion relais échouée** → Retry automatique
|
||||
- **Synchronisation échouée** → Mode hors ligne
|
||||
- **Pairing échoué** → Nouvelle tentative
|
||||
|
||||
### Erreurs d'Authentification
|
||||
- **WebAuthn échoué** → Fallback vers autre mode
|
||||
- **OTP invalide** → Nouvelle demande
|
||||
- **Mot de passe incorrect** → Nouvelle tentative
|
||||
|
||||
### Erreurs de Vérification
|
||||
- **Vérification des prérequis échouée** → Redirection vers l'étape appropriée
|
||||
- **Vérification de sauvegarde échouée** → Retry de la sauvegarde avec logs détaillés
|
||||
- **Vérification de restauration échouée** → Retry de la restauration avec logs détaillés
|
||||
- **Vérification de handshake échouée** → Retry de la connexion avec logs détaillés
|
||||
|
||||
## Système de Vérification Réelle des Logs
|
||||
|
||||
Tous les logs de succès sont maintenant émis uniquement après vérification réelle des résultats. Cela permet de :
|
||||
|
||||
1. **Détecter les échecs silencieux** : Les opérations qui échouent sans erreur sont détectées par les vérifications
|
||||
2. **Avoir des logs fiables** : Les logs reflètent la réalité et non juste des déclarations
|
||||
3. **Faciliter le diagnostic** : Les logs indiquent précisément où et pourquoi un processus échoue
|
||||
|
||||
### Vérifications Implémentées
|
||||
|
||||
#### Dans `birthday-setup.ts`
|
||||
- ✅ Vérification réelle de la présence de la clé PBKDF2 dans le store `pbkdf2keys`
|
||||
- ✅ Vérification réelle du wallet en base avec retry pour gérer les problèmes de synchronisation
|
||||
- ✅ Validation que le wallet contient bien les données attendues (`sp_wallet`, `birthday`)
|
||||
- ✅ Vérification que les relais sont connectés en vérifiant `chain_tip`
|
||||
- ✅ Vérification que le handshake a été reçu (`chain_tip > 0`)
|
||||
- ✅ Vérification que le birthday a bien été mis à jour en récupérant le wallet depuis la base
|
||||
|
||||
#### Dans `updateDeviceBlockHeight()`
|
||||
- ✅ Vérification que le device est restauré en mémoire en comparant le birthday
|
||||
- ✅ Vérification que le device est sauvegardé en base en le récupérant après l'opération
|
||||
- ✅ Vérification que le scan est terminé en vérifiant `last_scan`
|
||||
- ✅ Vérification que la sauvegarde finale est réussie
|
||||
|
||||
#### Dans `saveDeviceInDatabase()`
|
||||
- ✅ Vérification que le wallet est bien sauvegardé en le récupérant depuis la base après la transaction
|
||||
- ✅ Comparaison de `encrypted_device` pour confirmer que les données sont correctes
|
||||
- ✅ Logs de succès uniquement après vérification réelle
|
||||
|
||||
### Avantages
|
||||
|
||||
- **Fiabilité** : Les logs reflètent la réalité et non juste des déclarations
|
||||
- **Diagnostic** : Facilite le diagnostic en cas de problème
|
||||
- **Détection** : Détecte les échecs silencieux qui passeraient inaperçus
|
||||
- **Traçabilité** : Chaque étape est vérifiée et tracée avec des logs détaillés
|
||||
|
||||
## Points d'Attention
|
||||
|
||||
1. **Ordre des modes testés** : `['none', 'otp', 'password', 'os', 'proton-pass']`
|
||||
2. **Store `credentials`** : Utilisé uniquement après pairing
|
||||
3. **Clé PBKDF2** : Toujours stockée dans `pbkdf2keys` avec `security_mode` comme clé
|
||||
4. **Wallet** : Toujours stocké chiffré dans le store `wallet`
|
||||
5. **Redirection automatique** : 3 secondes après création du wallet vers `birthday-setup`
|
||||
6. **Vérifications réelles** : Tous les logs de succès sont émis uniquement après vérification réelle des résultats
|
||||
7. **Block Sync** : Nouvelle page intermédiaire entre `birthday-setup` et `pairing` pour la synchronisation des blocs
|
||||
8. **Prérequis** : Chaque page vérifie ses prérequis en base de données avant de procéder
|
||||
9. **Synchronisation IndexedDB** : Utilisation directe d'IndexedDB pour éviter les problèmes de synchronisation avec le service worker
|
||||
10. **Retry automatique** : Retry automatique jusqu'à 5 tentatives avec délai de 500ms pour les vérifications de wallet en base
|
||||
|
||||
Cette documentation couvre l'ensemble du processus d'initialisation du système IHM_CLIENT, depuis la configuration de sécurité jusqu'au pairing réussi et à la récupération des processus.
|
||||
220
docs/INTEGRATION.md
Normal file
220
docs/INTEGRATION.md
Normal file
@ -0,0 +1,220 @@
|
||||
# 4NK Integration Guide
|
||||
|
||||
## 🎯 Modes d'utilisation
|
||||
|
||||
Le site 4NK peut être utilisé de deux façons :
|
||||
|
||||
### 1. **Mode Normal** (Site autonome)
|
||||
- **URL** : http://localhost:3004
|
||||
- **Interface** : Header complet + navigation normale
|
||||
- **Utilisation** : Application standalone
|
||||
- **Fonctionnalités** : Toutes les fonctionnalités disponibles
|
||||
|
||||
### 2. **Mode Iframe** (Intégration externe)
|
||||
- **URL** : http://localhost:3004 (détection automatique)
|
||||
- **Interface** : Header masqué + menu intégré dans le contenu
|
||||
- **Utilisation** : Intégration dans un site externe
|
||||
- **Fonctionnalités** : Communication bidirectionnelle avec le parent
|
||||
|
||||
## 🔧 Détection automatique
|
||||
|
||||
Le site détecte automatiquement s'il est chargé dans une iframe :
|
||||
|
||||
```javascript
|
||||
// Détection iframe
|
||||
if (window.parent !== window) {
|
||||
// Mode iframe activé
|
||||
document.body.classList.add('iframe-mode');
|
||||
// Header masqué automatiquement
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Interface adaptative
|
||||
|
||||
### Mode Normal
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Header (Navigation, Logo, etc.) │
|
||||
├─────────────────────────────────────┤
|
||||
│ Contenu principal │
|
||||
│ ├── Titre et description │
|
||||
│ ├── Interface de pairing │
|
||||
│ └── Boutons d'action │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Mode Iframe
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Contenu principal (sans header) │
|
||||
│ ├── Titre et description │
|
||||
│ ├── Menu intégré (Home, Account...) │
|
||||
│ ├── Interface de pairing │
|
||||
│ └── Boutons d'action │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔄 Communication iframe
|
||||
|
||||
### Messages envoyés au parent
|
||||
- `IFRAME_READY` : Iframe initialisé
|
||||
- `MENU_NAVIGATION` : Navigation du menu
|
||||
- `PAIRING_4WORDS_WORDS_GENERATED` : 4 mots générés
|
||||
- `PAIRING_4WORDS_STATUS_UPDATE` : Mise à jour du statut
|
||||
- `PAIRING_4WORDS_SUCCESS` : Pairing réussi
|
||||
- `PAIRING_4WORDS_ERROR` : Erreur de pairing
|
||||
- `TEST_RESPONSE` : Réponse à un message de test
|
||||
- `LISTENING` : Notification que l'iframe écoute les messages
|
||||
|
||||
### Messages reçus du parent
|
||||
- `TEST_MESSAGE` : Test de communication
|
||||
- `PAIRING_4WORDS_CREATE` : Créer un pairing
|
||||
- `PAIRING_4WORDS_JOIN` : Rejoindre avec 4 mots
|
||||
- `LISTENING` : Notification que le parent écoute les messages
|
||||
- `IFRAME_READY` : Notification que l'iframe est prête (envoyée par l'iframe elle-même)
|
||||
|
||||
## 🧪 Tests d'intégration
|
||||
|
||||
### Test rapide
|
||||
```bash
|
||||
# Ouvrir dans le navigateur
|
||||
open examples/test-integration.html
|
||||
```
|
||||
|
||||
### Test complet
|
||||
```bash
|
||||
# Site externe d'exemple
|
||||
open examples/external-site.html
|
||||
```
|
||||
|
||||
## 🎨 Styles CSS
|
||||
|
||||
Les styles s'adaptent automatiquement :
|
||||
|
||||
```css
|
||||
/* Styles normaux */
|
||||
.title-container { /* ... */ }
|
||||
|
||||
/* Styles iframe */
|
||||
.iframe-mode .content-menu { /* ... */ }
|
||||
.iframe-mode .menu-btn { /* ... */ }
|
||||
```
|
||||
|
||||
## 🚀 Utilisation en production
|
||||
|
||||
### 1. Site autonome
|
||||
```html
|
||||
<!-- Utilisation normale -->
|
||||
<iframe src="https://your-4nk-site.com" width="100%" height="600px"></iframe>
|
||||
```
|
||||
|
||||
### 1.1 Relai WebSocket
|
||||
- Relai principal exposé en `wss://relay235.4nkweb.com/ws/`
|
||||
- Terminaison TLS gérée par `nginx.relay235.conf` (reverse proxy vers le service local sur `127.0.0.1:8091`)
|
||||
- Variables d’environnement cliente à utiliser : `VITE_BOOTSTRAPURL=wss://relay235.4nkweb.com/ws/`
|
||||
|
||||
### 2. Intégration personnalisée
|
||||
```html
|
||||
<!-- Site externe -->
|
||||
<div id="4nk-container">
|
||||
<iframe
|
||||
src="https://your-4nk-site.com"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
onload="init4NKIntegration(this)">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function init4NKIntegration(iframe) {
|
||||
// Écouter les messages de l'iframe
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== 'https://your-4nk-site.com') return;
|
||||
|
||||
const { type, data } = event.data;
|
||||
switch (type) {
|
||||
case 'IFRAME_READY':
|
||||
console.log('4NK iframe ready');
|
||||
break;
|
||||
case 'PAIRING_4WORDS_SUCCESS':
|
||||
console.log('Pairing successful:', data.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Envoyer des commandes à l'iframe
|
||||
function createPairing() {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'PAIRING_4WORDS_CREATE',
|
||||
data: {}
|
||||
}, 'https://your-4nk-site.com');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🔒 Sécurité
|
||||
|
||||
### Vérification d'origine
|
||||
```javascript
|
||||
// Toujours vérifier l'origine des messages
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== 'https://trusted-4nk-site.com') {
|
||||
return; // Ignorer les messages non autorisés
|
||||
}
|
||||
// Traiter le message
|
||||
});
|
||||
```
|
||||
|
||||
### Sandbox iframe
|
||||
```html
|
||||
<iframe
|
||||
src="https://your-4nk-site.com"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
allow="clipboard-write">
|
||||
</iframe>
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Logs de communication
|
||||
```javascript
|
||||
// Activer les logs détaillés
|
||||
window.DEBUG_IFRAME = true;
|
||||
|
||||
// Écouter tous les messages
|
||||
window.addEventListener('message', (event) => {
|
||||
console.log('📨 Message received:', {
|
||||
origin: event.origin,
|
||||
type: event.data.type,
|
||||
data: event.data.data
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🐛 Dépannage
|
||||
|
||||
### Problèmes courants
|
||||
|
||||
1. **Iframe ne se charge pas**
|
||||
- Vérifier les paramètres CORS
|
||||
- Vérifier l'URL de l'iframe
|
||||
- Vérifier les paramètres sandbox
|
||||
|
||||
2. **Messages non reçus**
|
||||
- Vérifier la vérification d'origine
|
||||
- Vérifier le format des messages
|
||||
- Vérifier la console pour les erreurs
|
||||
|
||||
3. **Styles cassés**
|
||||
- Vérifier la classe `iframe-mode`
|
||||
- Vérifier les styles CSS conditionnels
|
||||
- Vérifier la détection d'iframe
|
||||
|
||||
### Debug mode
|
||||
```javascript
|
||||
// Activer le mode debug
|
||||
localStorage.setItem('4nk-debug', 'true');
|
||||
|
||||
// Voir les logs détaillés
|
||||
console.log('4NK Debug Mode:', localStorage.getItem('4nk-debug'));
|
||||
```
|
||||
256
docs/LOGGING_GUIDELINES.md
Normal file
256
docs/LOGGING_GUIDELINES.md
Normal file
@ -0,0 +1,256 @@
|
||||
# Guide des bonnes pratiques de logging
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit les bonnes pratiques pour l'utilisation des logs dans l'application 4NK. Nous utilisons un système de logging centralisé avec `secureLogger` pour assurer la cohérence et la sécurité.
|
||||
|
||||
## Système de logging
|
||||
|
||||
### SecureLogger
|
||||
|
||||
Le `secureLogger` est le système de logging principal de l'application. Il fournit :
|
||||
|
||||
- **Sanitisation automatique** des données sensibles
|
||||
- **Niveaux de log structurés** (DEBUG, INFO, WARN, ERROR)
|
||||
- **Contexte enrichi** avec composant et métadonnées
|
||||
- **Formatage cohérent** des messages
|
||||
|
||||
### Import
|
||||
|
||||
```typescript
|
||||
import { secureLogger } from '../services/secure-logger';
|
||||
```
|
||||
|
||||
## Niveaux de log
|
||||
|
||||
### DEBUG
|
||||
Utilisé pour les informations de débogage détaillées, généralement utiles uniquement lors du développement.
|
||||
|
||||
```typescript
|
||||
secureLogger.debug('Memory usage after cleanup: 45.2%', { component: 'Service' });
|
||||
secureLogger.debug('Checking credentials availability', { component: 'HomePage' });
|
||||
```
|
||||
|
||||
**Quand utiliser :**
|
||||
- Informations de débogage
|
||||
- État interne des variables
|
||||
- Progression des opérations complexes
|
||||
- Messages avec emoji 🔍
|
||||
|
||||
### INFO
|
||||
Utilisé pour les informations générales sur le fonctionnement de l'application.
|
||||
|
||||
```typescript
|
||||
secureLogger.info('Home/Pairing page loaded', { component: 'HomePage' });
|
||||
secureLogger.info('Services initialized successfully', { component: 'Service' });
|
||||
```
|
||||
|
||||
**Quand utiliser :**
|
||||
- Initialisation de composants
|
||||
- Succès d'opérations
|
||||
- Messages avec emoji ✅, 🔄, 🚀
|
||||
- Événements importants du flux utilisateur
|
||||
|
||||
### WARN
|
||||
Utilisé pour les avertissements qui n'empêchent pas le fonctionnement mais méritent attention.
|
||||
|
||||
```typescript
|
||||
secureLogger.warn('High memory detected, performing cleanup', { component: 'Service' });
|
||||
secureLogger.warn('Home page already initializing, skipping...', { component: 'HomePage' });
|
||||
```
|
||||
|
||||
**Quand utiliser :**
|
||||
- Conditions non critiques mais inhabituelles
|
||||
- Messages avec emoji ⚠️
|
||||
- Opérations de récupération
|
||||
- Dégradations de performance
|
||||
|
||||
### ERROR
|
||||
Utilisé pour les erreurs qui empêchent le fonctionnement normal.
|
||||
|
||||
```typescript
|
||||
secureLogger.error('Failed to initialize services', error, { component: 'Service' });
|
||||
secureLogger.error('Authentication failed', error, { component: 'HomePage' });
|
||||
```
|
||||
|
||||
**Quand utiliser :**
|
||||
- Erreurs critiques
|
||||
- Messages avec emoji ❌
|
||||
- Échecs d'opérations importantes
|
||||
- Exceptions non gérées
|
||||
|
||||
## Contexte et métadonnées
|
||||
|
||||
### Composant
|
||||
|
||||
Toujours spécifier le composant dans le contexte :
|
||||
|
||||
```typescript
|
||||
secureLogger.info('Operation completed', { component: 'HomePage' });
|
||||
secureLogger.error('Database connection failed', error, { component: 'Service' });
|
||||
```
|
||||
|
||||
### Métadonnées supplémentaires
|
||||
|
||||
Ajouter des métadonnées utiles pour le débogage :
|
||||
|
||||
```typescript
|
||||
secureLogger.debug('Wallet details', {
|
||||
component: 'HomePage',
|
||||
hasSpendKey: !!wallet.sp_wallet?.spend_key,
|
||||
hasScanKey: !!wallet.sp_wallet?.scan_key,
|
||||
birthday: wallet.sp_wallet?.birthday
|
||||
});
|
||||
```
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### 1. Utiliser secureLogger au lieu de console.*
|
||||
|
||||
❌ **Mauvais :**
|
||||
```typescript
|
||||
console.log('User authenticated');
|
||||
console.error('Database error:', error);
|
||||
```
|
||||
|
||||
✅ **Bon :**
|
||||
```typescript
|
||||
secureLogger.info('User authenticated', { component: 'AuthService' });
|
||||
secureLogger.error('Database error', error, { component: 'DatabaseService' });
|
||||
```
|
||||
|
||||
### 2. Messages clairs et concis
|
||||
|
||||
❌ **Mauvais :**
|
||||
```typescript
|
||||
secureLogger.info('x', { component: 'Service' });
|
||||
secureLogger.info('Processing user request with id 12345 and data {name: "John", email: "john@example.com"}', { component: 'UserService' });
|
||||
```
|
||||
|
||||
✅ **Bon :**
|
||||
```typescript
|
||||
secureLogger.info('Processing user request', {
|
||||
component: 'UserService',
|
||||
userId: 12345,
|
||||
userName: 'John'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Niveaux appropriés
|
||||
|
||||
❌ **Mauvais :**
|
||||
```typescript
|
||||
secureLogger.error('User clicked button'); // Pas une erreur
|
||||
secureLogger.info('Critical system failure'); // Pas une info
|
||||
```
|
||||
|
||||
✅ **Bon :**
|
||||
```typescript
|
||||
secureLogger.debug('User clicked button', { component: 'UI' });
|
||||
secureLogger.error('Critical system failure', error, { component: 'System' });
|
||||
```
|
||||
|
||||
### 4. Contexte enrichi
|
||||
|
||||
❌ **Mauvais :**
|
||||
```typescript
|
||||
secureLogger.info('Operation failed');
|
||||
```
|
||||
|
||||
✅ **Bon :**
|
||||
```typescript
|
||||
secureLogger.error('Operation failed', error, {
|
||||
component: 'PaymentService',
|
||||
operation: 'processPayment',
|
||||
userId: user.id,
|
||||
amount: payment.amount
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Gestion des erreurs
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
secureLogger.info('Operation completed successfully', { component: 'Service' });
|
||||
} catch (error) {
|
||||
secureLogger.error('Operation failed', error, {
|
||||
component: 'Service',
|
||||
operation: 'riskyOperation'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns d'emojis
|
||||
|
||||
### DEBUG (🔍)
|
||||
- `🔍 Checking...`
|
||||
- `🔍 Debug info:`
|
||||
- `🔍 Memory usage:`
|
||||
|
||||
### INFO (✅, 🔄, 🚀)
|
||||
- `✅ Operation completed`
|
||||
- `🔄 Processing...`
|
||||
- `🚀 Starting...`
|
||||
- `🔧 Initializing...`
|
||||
|
||||
### WARN (⚠️)
|
||||
- `⚠️ Warning:`
|
||||
- `⚠️ Skipping...`
|
||||
- `⚠️ High memory detected`
|
||||
|
||||
### ERROR (❌)
|
||||
- `❌ Error:`
|
||||
- `❌ Failed to:`
|
||||
- `❌ Critical error:`
|
||||
|
||||
## Exemples par composant
|
||||
|
||||
### Service
|
||||
```typescript
|
||||
secureLogger.info('Service initialized', { component: 'Service' });
|
||||
secureLogger.debug('Memory usage: 45.2%', { component: 'Service' });
|
||||
secureLogger.warn('High memory detected', { component: 'Service' });
|
||||
secureLogger.error('Service initialization failed', error, { component: 'Service' });
|
||||
```
|
||||
|
||||
### HomePage
|
||||
```typescript
|
||||
secureLogger.info('Home page loaded', { component: 'HomePage' });
|
||||
secureLogger.info('Prerequisites verified', { component: 'HomePage' });
|
||||
secureLogger.warn('Already initializing, skipping', { component: 'HomePage' });
|
||||
secureLogger.error('Page initialization failed', error, { component: 'HomePage' });
|
||||
```
|
||||
|
||||
### PairingPage
|
||||
```typescript
|
||||
secureLogger.info('Pairing page loaded', { component: 'PairingPage' });
|
||||
secureLogger.info('Pairing process started', { component: 'PairingPage' });
|
||||
secureLogger.warn('Pairing already in progress', { component: 'PairingPage' });
|
||||
secureLogger.error('Pairing failed', error, { component: 'PairingPage' });
|
||||
```
|
||||
|
||||
## Outils de correction
|
||||
|
||||
Un script automatique est disponible pour corriger les logs existants :
|
||||
|
||||
```bash
|
||||
node fix-logs.cjs
|
||||
```
|
||||
|
||||
Ce script :
|
||||
- Remplace `console.*` par `secureLogger.*`
|
||||
- Ajoute les imports nécessaires
|
||||
- Détermine automatiquement les niveaux appropriés
|
||||
- Ajoute le contexte de composant
|
||||
|
||||
## Vérification
|
||||
|
||||
Pour vérifier que tous les logs utilisent le système centralisé :
|
||||
|
||||
```bash
|
||||
grep -r "console\.\(log\|info\|warn\|error\|debug\)" src/
|
||||
```
|
||||
|
||||
Cette commande ne devrait retourner aucun résultat si tous les logs sont correctement migrés.
|
||||
302
docs/PAIRING_SYSTEM_ANALYSIS.md
Normal file
302
docs/PAIRING_SYSTEM_ANALYSIS.md
Normal file
@ -0,0 +1,302 @@
|
||||
# Analyse du Système de Pairing - Version Actuelle
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document résume l'analyse complète du système de pairing et les corrections apportées pour résoudre les problèmes identifiés.
|
||||
|
||||
## But et Objectif du Pairing
|
||||
|
||||
Le système de pairing dans 4NK sert avant tout à **créer une identité numérique vérifiable** qui permet :
|
||||
|
||||
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus de pairing permet de valider les actions critiques nécessitant plusieurs appareils appairés
|
||||
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
|
||||
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
|
||||
4. **Système d'identité et de chiffrement** : Le wallet est avant tout un système d'identité et de chiffrement grâce aux secrets partagés du Silent Payment
|
||||
|
||||
### Caractéristiques principales
|
||||
|
||||
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus de pairing peut inclure N appareils
|
||||
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
|
||||
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
|
||||
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
|
||||
|
||||
### Différence avec les autres fonctionnalités
|
||||
|
||||
- **Backup du wallet** : Géré via les 4 mots dans les autres pages (permet de contrôler le processus pour auto-update)
|
||||
- **Support** : Géré via les 4 mots permettant un contrôle du processus
|
||||
- **Partage de secrets** : Les secrets Silent Payment sont partagés entre appareils appairés pour le chiffrement
|
||||
|
||||
## Problèmes Identifiés et Solutions
|
||||
|
||||
### 1. Problème de `checkConnections` pour le Pairing
|
||||
|
||||
**Problème** : La méthode `checkConnections` a été mise à jour il y a un mois pour prendre un `Process` et un `stateId` au lieu d'une liste de membres, mais la gestion des processus de pairing était défaillante.
|
||||
|
||||
**Symptômes** :
|
||||
- `checkConnections` échouait pour les processus de pairing
|
||||
- Les adresses des membres n'étaient pas correctement récupérées
|
||||
- Erreur "Not a pairing process" même pour des processus de pairing valides
|
||||
|
||||
**Solution Appliquée** :
|
||||
```typescript
|
||||
// Correction dans checkConnections pour gérer les pairedAddresses
|
||||
if (members.size === 0) {
|
||||
// This must be a pairing process
|
||||
let publicData: Record<string, any> | null = null;
|
||||
if (!stateId) {
|
||||
publicData = process.states[process.states.length - 2]?.public_data;
|
||||
} else {
|
||||
publicData = process.states.find(state => state.state_id === stateId)?.public_data || null;
|
||||
}
|
||||
|
||||
// If pairedAddresses is not in the current state, look in previous states
|
||||
if (!publicData || !publicData['pairedAddresses']) {
|
||||
// Look for pairedAddresses in previous states
|
||||
for (let i = process.states.length - 1; i >= 0; i--) {
|
||||
const state = process.states[i];
|
||||
if (state.public_data && state.public_data['pairedAddresses']) {
|
||||
publicData = state.public_data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
|
||||
members.add({ sp_addresses: decodedAddresses });
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Problème de `confirmPairing` avec `getPairingProcessId()`
|
||||
|
||||
**Problème** : `confirmPairing` échouait car `getPairingProcessId()` utilisait `sdkClient.get_pairing_process_id()` qui n'était pas encore disponible car le processus de pairing n'était pas encore committé.
|
||||
|
||||
**Symptômes** :
|
||||
- Erreur "Failed to get pairing process" dans `confirmPairing`
|
||||
- Le SDK n'avait pas encore le processus de pairing disponible
|
||||
- Échec de confirmation du pairing
|
||||
|
||||
**Solution Appliquée** :
|
||||
```typescript
|
||||
public async confirmPairing(pairingId?: string) {
|
||||
try {
|
||||
console.log('confirmPairing');
|
||||
let processId: string;
|
||||
if (pairingId) {
|
||||
processId = pairingId;
|
||||
console.log('pairingId (provided):', processId);
|
||||
} else if (this.processId) {
|
||||
processId = this.processId;
|
||||
console.log('pairingId (from stored processId):', processId);
|
||||
} else {
|
||||
// Try to get pairing process ID, with retry if it fails
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
processId = this.getPairingProcessId();
|
||||
console.log('pairingId (from SDK):', processId);
|
||||
break;
|
||||
} catch (e) {
|
||||
retries--;
|
||||
if (retries === 0) throw e;
|
||||
console.log(`Failed to get pairing process ID, retrying... (${retries} attempts left)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... rest of the method
|
||||
} catch (e) {
|
||||
console.error('Failed to confirm pairing');
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Problème de `pairing_process_commitment` à `null`
|
||||
|
||||
**Problème** : Le `pairing_process_commitment` restait à `null` dans le device dump car le device n'était pas synchronisé avec l'état committé du processus.
|
||||
|
||||
**Symptômes** :
|
||||
- `pairing_process_commitment: null` dans le device dump
|
||||
- Le commitment n'était pas synchronisé avec l'état committé du processus
|
||||
- Échec de la confirmation du pairing
|
||||
|
||||
**Solution Appliquée** :
|
||||
```typescript
|
||||
// Intégration de updateDevice() dans waitForPairingCommitment
|
||||
public async waitForPairingCommitment(processId: string, maxRetries: number = 10, retryDelay: number = 1000): Promise<void> {
|
||||
console.log(`Waiting for pairing process ${processId} to be committed...`);
|
||||
|
||||
// First, try to update the device to sync with the committed state
|
||||
try {
|
||||
await this.updateDevice();
|
||||
console.log('Device updated, checking commitment...');
|
||||
} catch (e) {
|
||||
console.log('Failed to update device, continuing with polling...', e);
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const device = this.dumpDeviceFromMemory();
|
||||
console.log(`Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =`, device.pairing_process_commitment);
|
||||
|
||||
// Check if the commitment is set and not null/empty
|
||||
if (device.pairing_process_commitment &&
|
||||
device.pairing_process_commitment !== null &&
|
||||
device.pairing_process_commitment !== '') {
|
||||
console.log('Pairing process commitment found:', device.pairing_process_commitment);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Attempt ${i + 1}/${maxRetries}: Device not ready yet - ${e}`);
|
||||
}
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Pairing process ${processId} was not committed after ${maxRetries} attempts`);
|
||||
}
|
||||
```
|
||||
|
||||
Et simplification du router :
|
||||
```typescript
|
||||
console.log("⏳ Waiting for pairing process to be committed...");
|
||||
await services.waitForPairingCommitment(pairingId);
|
||||
|
||||
console.log("🔁 Confirming pairing...");
|
||||
await services.confirmPairing(pairingId);
|
||||
```
|
||||
|
||||
## Architecture du Système de Pairing
|
||||
|
||||
### Flux de Création du Pairing (Créateur)
|
||||
|
||||
1. **Création du processus** : `createPairingProcess("", [myAddress])`
|
||||
2. **Enregistrement du device** : `pairDevice(pairingId, [myAddress])`
|
||||
3. **Traitement de l'API** : `handleApiReturn(createPairingProcessReturn)`
|
||||
4. **Création de la mise à jour PRD** : `createPrdUpdate(pairingId, stateId)`
|
||||
5. **Approbation du changement** : `approveChange(pairingId, stateId)`
|
||||
6. **Attente du commit avec synchronisation** : `waitForPairingCommitment(pairingId)` (inclut `updateDevice()`)
|
||||
7. **Confirmation du pairing** : `confirmPairing(pairingId)`
|
||||
|
||||
### Flux de Rejoindre le Pairing (Joiner) - ⚠️ INCOHÉRENT
|
||||
|
||||
**Problème identifié** : Le joiner n'a pas de flux de confirmation complet.
|
||||
|
||||
**Flux actuel (incomplet)** :
|
||||
1. **Création avec liste vide** : `createPairingProcess("", [])` ❌
|
||||
2. **Établissement des connexions** : `checkConnections(process)`
|
||||
3. **Pas de confirmation** : Aucun `waitForPairingCommitment` ou `confirmPairing` ❌
|
||||
|
||||
**Flux attendu (cohérent)** :
|
||||
1. **Récupération du processus existant** : `getPairingProcessId()`
|
||||
2. **Rejoindre le processus** : Pas de création, mais participation au processus existant
|
||||
3. **Flux de confirmation complet** : Même flux que le créateur
|
||||
4. **Attente du commit** : `waitForPairingCommitment()`
|
||||
5. **Confirmation du pairing** : `confirmPairing()`
|
||||
|
||||
### Gestion des Connexions
|
||||
|
||||
La méthode `checkConnections` gère maintenant :
|
||||
- **Processus normaux** : Utilise les rôles pour trouver les membres
|
||||
- **Processus de pairing** : Utilise `pairedAddresses` des données publiques
|
||||
- **Recherche dans les états précédents** : Si `pairedAddresses` n'est pas dans l'état actuel
|
||||
- **Décodage des adresses** : Les données publiques sont encodées et nécessitent un décodage
|
||||
|
||||
## Points Clés Appris
|
||||
|
||||
### 1. Encodage des Données Publiques
|
||||
- Les données publiques sont encodées avec `this.sdkClient.encode_json()`
|
||||
- `pairedAddresses` nécessite un décodage avec `this.decodeValue()`
|
||||
- Les données ne sont pas directement utilisables sans décodage
|
||||
|
||||
### 2. Gestion Multi-Hosts
|
||||
- Le créateur et le joiner peuvent être sur des hosts différents
|
||||
- Le joiner doit récupérer les adresses depuis le processus existant
|
||||
- `this.processId` n'est disponible que sur le même host
|
||||
|
||||
### 3. Synchronisation du SDK
|
||||
- Le SDK n'a pas immédiatement le processus de pairing disponible
|
||||
- Il faut attendre que le processus soit committé
|
||||
- `updateDevice()` est nécessaire pour synchroniser l'état
|
||||
|
||||
### 4. Gestion des États
|
||||
- Les processus de pairing peuvent avoir des mises à jour partielles
|
||||
- Il faut chercher `pairedAddresses` dans les états précédents si nécessaire
|
||||
- La logique de fallback est cruciale pour la robustesse
|
||||
|
||||
## Version Actuelle
|
||||
|
||||
### État des Corrections
|
||||
- ✅ `checkConnections` corrigé pour les processus de pairing
|
||||
- ✅ `confirmPairing` avec gestion des paramètres et retry
|
||||
- ✅ `waitForPairingCommitment` avec synchronisation automatique du device
|
||||
- ✅ Intégration de `updateDevice()` dans `waitForPairingCommitment`
|
||||
- ✅ Gestion des cas multi-hosts
|
||||
- ✅ Simplification du flux de création
|
||||
- ✅ **Flux du joiner implémenté** : Découverte et rejoindre un processus existant
|
||||
- ✅ **Détection automatique** : Créateur vs Joiner via paramètre URL
|
||||
|
||||
### Fonctionnalités Opérationnelles
|
||||
- **Création de pairing** : ✅ Fonctionne avec les adresses correctes
|
||||
- **Rejoindre un pairing** : ✅ Flux complet avec découverte et synchronisation
|
||||
- **Établissement des connexions** : ✅ `checkConnections` trouve les membres
|
||||
- **Confirmation du pairing** : ✅ Côté créateur et joiner
|
||||
- **Synchronisation du commitment** : ✅ Côté créateur et joiner
|
||||
- **Flux simplifié** : ✅ Côté créateur et joiner
|
||||
|
||||
### Flux Unifié Créateur vs Joiner
|
||||
|
||||
#### Flux du Créateur
|
||||
1. **Création** : `createPairingProcess()` avec son adresse
|
||||
2. **QR Code** : `generateQRCode()` pour le joiner
|
||||
3. **Attente** : `waitForJoinerAndUpdateProcess()` pour détecter le joiner
|
||||
4. **Synchronisation** : `waitForPairingCommitment()`
|
||||
5. **Confirmation** : `confirmPairing()`
|
||||
|
||||
#### Flux du Joiner
|
||||
1. **Découverte** : `discoverAndJoinPairingProcess()` via QR code
|
||||
2. **Synchronisation** : `waitForPairingCommitment()`
|
||||
3. **Confirmation** : `confirmPairing()`
|
||||
|
||||
#### Détection Automatique
|
||||
- **Créateur** : Pas de paramètre `sp_address` dans l'URL
|
||||
- **Joiner** : Paramètre `sp_address` présent dans l'URL
|
||||
- **Logique** : `onCreateButtonClick()` détecte automatiquement le flux
|
||||
|
||||
### Améliorations Récentes
|
||||
|
||||
#### Synchronisation Automatique du Device
|
||||
- **Intégration de `updateDevice()`** : Appelé automatiquement dans `waitForPairingCommitment`
|
||||
- **Gestion des erreurs** : Continue le polling même si `updateDevice()` échoue
|
||||
- **Logs détaillés** : Suivi complet du processus de synchronisation
|
||||
- **Temps d'attente augmenté** : 30 tentatives × 2 secondes = 60 secondes max
|
||||
|
||||
#### Simplification du Flux
|
||||
- **Moins d'étapes manuelles** : `updateDevice()` intégré dans `waitForPairingCommitment`
|
||||
- **Flux plus robuste** : Gestion automatique de la synchronisation
|
||||
- **Code plus maintenable** : Logique centralisée dans une seule méthode
|
||||
|
||||
### Points d'Attention
|
||||
- Le système nécessite que les deux côtés soient synchronisés
|
||||
- Les retry automatiques sont implémentés pour la robustesse
|
||||
- La gestion des erreurs est améliorée avec des logs détaillés
|
||||
- Le flux est maintenant plus prévisible et fiable
|
||||
- La synchronisation du device est automatique et robuste
|
||||
|
||||
## Recommandations
|
||||
|
||||
### Améliorations Futures
|
||||
1. **Tests automatisés** : Implémenter des tests unitaires et d'intégration pour valider le pairing
|
||||
2. **Monitoring** : Ajouter des métriques pour surveiller les performances du pairing
|
||||
3. **Documentation** : Maintenir cette documentation à jour avec les évolutions
|
||||
4. **Optimisation** : Analyser et optimiser les délais de retry si nécessaire
|
||||
|
||||
### Tests et Monitoring
|
||||
1. **Tests** : Tester le pairing entre différents hosts avec les deux flux
|
||||
2. **Monitoring** : Surveiller les logs pour identifier les problèmes potentiels
|
||||
3. **Performance** : Optimiser les délais de retry si nécessaire
|
||||
4. **Documentation** : Maintenir cette documentation à jour avec les évolutions
|
||||
|
||||
Cette analyse fournit une base solide pour comprendre et maintenir le système de pairing. Les corrections majeures ont été implémentées et le système est maintenant opérationnel avec un flux unifié pour le créateur et le joiner.
|
||||
530
docs/PROCESS_SYSTEM_ARCHITECTURE.md
Normal file
530
docs/PROCESS_SYSTEM_ARCHITECTURE.md
Normal file
@ -0,0 +1,530 @@
|
||||
# Architecture du Système de Processus et Updates
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système de processus est un **système générique et réutilisable** pour créer des "contrats" entre des membres avec des niveaux d'accès différents aux champs de données. C'est la fondation qui permet d'implémenter des fonctionnalités comme le pairing, mais aussi n'importe quel autre type de contrat décentralisé.
|
||||
|
||||
## Concepts Fondamentaux
|
||||
|
||||
### 1. Processus (Process)
|
||||
|
||||
Un **processus** est un contrat décentralisé entre plusieurs membres, commité sur la blockchain. Il représente un accord ou une entité partagée avec :
|
||||
|
||||
- **Identifiant unique** : `process_id`
|
||||
- **États successifs** : Historique des modifications
|
||||
- **Membres** : Participants au processus
|
||||
- **Rôles et permissions** : Définition des accès
|
||||
|
||||
### 2. État (State)
|
||||
|
||||
Chaque processus contient une liste d'**états** représentant l'évolution du processus dans le temps. Chaque état contient :
|
||||
|
||||
#### Données Publiques (`public_data`)
|
||||
- **Accessibles à tous** les membres du processus
|
||||
- **Inchangées** dans tous les nouveaux états (portées automatiquement)
|
||||
- **Encodées** mais non chiffrées (encodage JSON/Binary)
|
||||
- **Exemple** : Nom du processus, adresses appairées, métadonnées
|
||||
|
||||
#### Données Privées (via `pcd_commitment`)
|
||||
- **Chiffrées** et accessibles uniquement aux membres autorisés
|
||||
- **Commitment** : Hash des données privées (`pcd_commitment[field]`)
|
||||
- **Clés de déchiffrement** : Stockées dans `state.keys[field]` pour chaque membre autorisé
|
||||
- **Exemple** : Secrets, clés privées, données sensibles
|
||||
|
||||
#### Rôles (`roles`)
|
||||
- **Définition des permissions** par rôle
|
||||
- **Validation rules** : Quorum et champs accessibles par rôle
|
||||
- **Membres** : Liste des IDs de pairing process pour chaque rôle
|
||||
|
||||
#### Métadonnées d'État
|
||||
- `state_id` : Identifiant unique de l'état
|
||||
- `validation_tokens` : Tokens nécessaires pour la validation
|
||||
- `validation_result` : Résultat de la validation
|
||||
|
||||
### 3. Rôles et Permissions (`RoleDefinition`)
|
||||
|
||||
Un rôle définit qui peut accéder à quels champs et comment :
|
||||
|
||||
```typescript
|
||||
interface RoleDefinition {
|
||||
members: string[]; // IDs de pairing process (identifiants des membres)
|
||||
validation_rules: ValidationRule[]; // Règles de validation
|
||||
}
|
||||
|
||||
interface ValidationRule {
|
||||
quorum: number; // Quorum requis (ex: 1.0 = tous, 0.5 = 50%)
|
||||
fields: string[]; // Champs accessibles pour ce rôle
|
||||
}
|
||||
```
|
||||
|
||||
**Exemples de rôles** :
|
||||
- **Administrateur** : Quorum 1.0, accès à tous les champs
|
||||
- **Membre** : Quorum 0.5, accès aux champs non critiques
|
||||
- **Lecteur** : Quorum 0, accès en lecture seule aux champs publics
|
||||
|
||||
### 4. Membres
|
||||
|
||||
Les membres sont identifiés par leur **pairing process ID** (l'identité numérique vérifiable créée lors du pairing).
|
||||
|
||||
- Un membre peut participer à plusieurs processus
|
||||
- Un processus peut avoir plusieurs membres
|
||||
- Les adresses SP (Silent Payment) sont associées aux membres pour la communication
|
||||
|
||||
## Cycle de Vie d'un Processus
|
||||
|
||||
### Phase 1 : Création
|
||||
|
||||
```typescript
|
||||
createProcess(
|
||||
privateData: Record<string, any>, // Données privées initiales
|
||||
publicData: Record<string, any>, // Données publiques initiales
|
||||
roles: Record<string, RoleDefinition> // Définition des rôles
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Étapes** :
|
||||
1. Encodage des données (JSON/Binary)
|
||||
2. Création du processus via SDK (WebAssembly)
|
||||
3. Génération du premier état (state 0)
|
||||
4. Établissement des connexions avec les membres
|
||||
|
||||
### Phase 2 : Mise à Jour
|
||||
|
||||
```typescript
|
||||
updateProcess(
|
||||
process: Process,
|
||||
privateData: Record<string, any>, // Nouvelles données privées
|
||||
publicData: Record<string, any>, // Nouvelles données publiques
|
||||
roles: Record<string, RoleDefinition> | null // Nouveaux rôles (optionnel)
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Logique de classification des champs** :
|
||||
|
||||
Le système détermine automatiquement si un champ est public ou privé :
|
||||
|
||||
1. **Champ existant dans `public_data`** → Reste public
|
||||
2. **Champ nouveau dans `privateFields`** → Privé
|
||||
3. **Champ existant dans `pcd_commitment`** → Reste privé
|
||||
4. **Sinon** → Nouveau champ public
|
||||
|
||||
```typescript
|
||||
// Logique dans handleUpdateProcess (router.ts:811-846)
|
||||
for (const field of Object.keys(newData)) {
|
||||
// 1. Vérifier si c'est déjà public
|
||||
if (lastState.public_data[field]) {
|
||||
publicData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Vérifier si c'est un nouveau champ privé
|
||||
if (privateFields.includes(field)) {
|
||||
privateData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Vérifier si c'était privé dans un état précédent
|
||||
for (let i = lastStateIndex; i >= 0; i--) {
|
||||
if (process.states[i].pcd_commitment[field]) {
|
||||
privateData[field] = newData[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sinon, c'est un nouveau champ public
|
||||
if (!privateData[field]) {
|
||||
publicData[field] = newData[field];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3 : Synchronisation (PRD Update)
|
||||
|
||||
**PRD** = Private Data Relay
|
||||
|
||||
```typescript
|
||||
createPrdUpdate(
|
||||
processId: string,
|
||||
stateId: string
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Objectif** :
|
||||
- Synchroniser les **clés de déchiffrement** des données privées avec tous les membres autorisés
|
||||
- Distribuer les données privées aux membres qui ont les permissions
|
||||
- Mettre à jour les `state.keys[field]` pour chaque membre autorisé
|
||||
|
||||
**Processus** :
|
||||
1. Création d'un message de mise à jour PRD
|
||||
2. Transmission via les relais aux membres autorisés
|
||||
3. Chaque membre reçoit les clés pour les champs auxquels il a accès
|
||||
|
||||
### Phase 4 : Validation (Approbation)
|
||||
|
||||
```typescript
|
||||
approveChange(
|
||||
processId: string,
|
||||
stateId: string
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Objectif** :
|
||||
- Valider un état du processus selon le **quorum requis**
|
||||
- S'assurer que suffisamment de membres ont approuvé le changement
|
||||
- Marquer l'état comme validé
|
||||
|
||||
**Quorum** :
|
||||
- Si quorum = 1.0 → Tous les membres du rôle doivent approuver
|
||||
- Si quorum = 0.5 → 50% des membres doivent approuver
|
||||
- Si quorum = 0 → Auto-approbation (pas de validation requise)
|
||||
|
||||
### Phase 5 : Commit sur Blockchain
|
||||
|
||||
Une fois validé, l'état est **committé sur la blockchain** :
|
||||
|
||||
- Création d'une transaction Bitcoin commitant l'état
|
||||
- Le `pcd_commitment` est inclut dans la transaction
|
||||
- L'état devient **immuable** et vérifiable
|
||||
|
||||
### Phase 6 : Accès aux Données
|
||||
|
||||
#### Données Publiques
|
||||
|
||||
Accessibles directement depuis `state.public_data` après décodage :
|
||||
|
||||
```typescript
|
||||
const publicData = service.getPublicData(process);
|
||||
const decodedValue = service.decodeValue(publicData['fieldName']);
|
||||
```
|
||||
|
||||
#### Données Privées
|
||||
|
||||
Nécessitent :
|
||||
1. **Permission** : Vérifier que le membre a accès au champ
|
||||
2. **Clé de déchiffrement** : Récupérer `state.keys[field]`
|
||||
3. **Commitment** : Vérifier `state.pcd_commitment[field]`
|
||||
4. **Déchiffrement** : Décrypter avec la clé
|
||||
|
||||
```typescript
|
||||
async decryptAttribute(
|
||||
processId: string,
|
||||
state: ProcessState,
|
||||
attribute: string
|
||||
): Promise<any | null>
|
||||
```
|
||||
|
||||
**Vérification des permissions** :
|
||||
```typescript
|
||||
// Vérifier si le membre a accès au champ
|
||||
for (const role of Object.values(state.roles)) {
|
||||
for (const rule of Object.values(role.validation_rules)) {
|
||||
if (rule.fields.includes(attribute)) {
|
||||
if (role.members.includes(pairingProcessId)) {
|
||||
// Le membre a accès
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Si la clé est manquante, le système demande automatiquement aux autres membres via `requestDataFromPeers()`.
|
||||
|
||||
## Flux Complet d'un Update
|
||||
|
||||
```
|
||||
1. Mise à jour demandée
|
||||
↓
|
||||
2. Classification automatique des champs (public/privé)
|
||||
↓
|
||||
3. updateProcess() → Création d'un nouvel état
|
||||
↓
|
||||
4. createPrdUpdate() → Synchronisation des clés privées
|
||||
↓
|
||||
5. approveChange() → Validation selon quorum
|
||||
↓
|
||||
6. Commit sur blockchain → État immuable
|
||||
↓
|
||||
7. Accès aux données via getPublicData() / decryptAttribute()
|
||||
```
|
||||
|
||||
## Exemples d'Utilisation
|
||||
|
||||
### Exemple 1 : Pairing (Identité Multi-Appareils)
|
||||
|
||||
```typescript
|
||||
// Création d'un processus de pairing
|
||||
const pairingProcess = await service.createPairingProcess(
|
||||
'', // memberPublicName (vide pour pairing)
|
||||
[creatorAddress] // pairedAddresses (liste des appareils)
|
||||
);
|
||||
|
||||
// Données publiques : Liste des adresses appairées
|
||||
// Données privées : Secrets Silent Payment partagés
|
||||
// Rôles : Tous les appareils ont le même niveau d'accès (quorum 1.0)
|
||||
```
|
||||
|
||||
### Exemple 2 : Contrat de Partage avec Niveaux d'Accès
|
||||
|
||||
```typescript
|
||||
// Création d'un contrat avec différents niveaux
|
||||
const contract = await service.createProcess(
|
||||
{
|
||||
secretKey: '...', // Privé : Clé secrète
|
||||
internalNotes: '...' // Privé : Notes internes
|
||||
},
|
||||
{
|
||||
contractName: 'Mon Contrat', // Public : Nom
|
||||
description: '...' // Public : Description
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
members: [adminPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['secretKey', 'internalNotes', 'contractName'] }
|
||||
]
|
||||
},
|
||||
member: {
|
||||
members: [memberPairingId1, memberPairingId2],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['contractName', 'description'] } // Lecture seule des publics
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Exemple 3 : Vote Décisionnel
|
||||
|
||||
```typescript
|
||||
const voteProcess = await service.createProcess(
|
||||
{
|
||||
votes: {} // Privé : Votes individuels
|
||||
},
|
||||
{
|
||||
proposal: 'Proposition...', // Public : Proposition
|
||||
result: null // Public : Résultat
|
||||
},
|
||||
{
|
||||
voter: {
|
||||
members: [...voterIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Points Importants
|
||||
|
||||
### 1. Immutabilité
|
||||
|
||||
Une fois qu'un état est **committé**, il devient immuable. Les nouveaux états ajoutent des modifications mais ne modifient jamais les états précédents.
|
||||
|
||||
### 2. Portage des Données Publiques
|
||||
|
||||
Les données publiques sont **automatiquement portées** dans chaque nouvel état. Pas besoin de les réenvoyer à chaque update.
|
||||
|
||||
### 3. Synchronisation Automatique
|
||||
|
||||
Le système gère automatiquement la **distribution des clés privées** aux membres autorisés via les relais.
|
||||
|
||||
### 4. Quorum Flexible
|
||||
|
||||
Le système supporte différents niveaux de quorum selon les besoins :
|
||||
- **Sécurisé** : Quorum 1.0 (tous doivent approuver)
|
||||
- **Démocratique** : Quorum 0.5 (majorité)
|
||||
- **Auto** : Quorum 0 (auto-approbation)
|
||||
|
||||
### 5. Extensibilité
|
||||
|
||||
Ce système peut être utilisé pour **n'importe quel type de contrat** :
|
||||
- Gestion documentaire
|
||||
- Votes décisionnels
|
||||
- Partage de fichiers
|
||||
- Contrats intelligents décentralisés
|
||||
- etc.
|
||||
|
||||
## Méthodes Utilitaires
|
||||
|
||||
### Récupération des Processus
|
||||
|
||||
```typescript
|
||||
// Récupérer un processus spécifique
|
||||
getProcess(processId: string): Promise<Process | null>
|
||||
|
||||
// Récupérer tous les processus
|
||||
getProcesses(): Promise<Record<string, Process>>
|
||||
|
||||
// Récupérer mes processus (où je suis membre)
|
||||
getMyProcesses(): Promise<string[] | null>
|
||||
```
|
||||
|
||||
### État Commité
|
||||
|
||||
```typescript
|
||||
// Récupérer le dernier état commité
|
||||
getLastCommitedState(process: Process): ProcessState | null
|
||||
|
||||
// Récupérer l'index du dernier état commité
|
||||
getLastCommitedStateIndex(process: Process): number | null
|
||||
```
|
||||
|
||||
**Important** : Les états non commités sont des "pending states" qui attendent validation.
|
||||
|
||||
### Rôles et Membres
|
||||
|
||||
```typescript
|
||||
// Récupérer les rôles d'un processus (depuis le dernier état commité)
|
||||
getRoles(process: Process): Record<string, RoleDefinition> | null
|
||||
|
||||
// Vérifier si je suis membre d'un processus
|
||||
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean
|
||||
|
||||
// Vérifier si un membre spécifique fait partie des rôles
|
||||
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean
|
||||
|
||||
// Récupérer tous les membres connus
|
||||
getAllMembers(): Record<string, Member>
|
||||
```
|
||||
|
||||
### Données Publiques
|
||||
|
||||
```typescript
|
||||
// Récupérer les données publiques (depuis le dernier état commité)
|
||||
getPublicData(process: Process): Record<string, any> | null
|
||||
|
||||
// Décoder une valeur encodée
|
||||
decodeValue(value: number[]): any | null
|
||||
```
|
||||
|
||||
### Données Privées
|
||||
|
||||
```typescript
|
||||
// Déchiffrer un attribut privé
|
||||
async decryptAttribute(
|
||||
processId: string,
|
||||
state: ProcessState,
|
||||
attribute: string
|
||||
): Promise<any | null>
|
||||
```
|
||||
|
||||
Cette méthode :
|
||||
1. Vérifie les permissions (rôles)
|
||||
2. Récupère la clé de déchiffrement (`state.keys[attribute]`)
|
||||
3. Demande aux autres membres si la clé est manquante
|
||||
4. Déchiffre la donnée
|
||||
|
||||
## Gestion du Cache et Synchronisation
|
||||
|
||||
### Cache des Processus
|
||||
|
||||
Les processus sont mis en cache localement pour améliorer les performances :
|
||||
|
||||
```typescript
|
||||
processesCache: Record<string, Process>
|
||||
```
|
||||
|
||||
### Synchronisation avec les Relais
|
||||
|
||||
Les relais synchronisent :
|
||||
- Les nouveaux processus
|
||||
- Les mises à jour d'états
|
||||
- Les clés privées (PRD updates)
|
||||
- Les validations
|
||||
|
||||
### Connexion entre Membres
|
||||
|
||||
Avant de créer ou mettre à jour un processus, le système établit des **connexions** (secrets partagés) entre tous les membres :
|
||||
|
||||
```typescript
|
||||
checkConnections(process: Process, stateId?: string): Promise<void>
|
||||
```
|
||||
|
||||
Cela crée des secrets Silent Payment entre les membres pour permettre la communication chiffrée.
|
||||
|
||||
## Avantages du Système
|
||||
|
||||
1. **Décentralisé** : Pas de tiers de confiance, tout sur la blockchain
|
||||
2. **Vérifiable** : Chaque état est commité et vérifiable
|
||||
3. **Flexible** : Permissions granulaires par champ et par rôle
|
||||
4. **Sécurisé** : Chiffrement des données privées, distribution via relais
|
||||
5. **Générique** : Réutilisable pour n'importe quel type de contrat
|
||||
6. **Sans tiers** : Les utilisateurs contrôlent leurs propres processus
|
||||
|
||||
## Cas d'Usage Avancés
|
||||
|
||||
### Gestion Documentaire Collaborative
|
||||
|
||||
```typescript
|
||||
const documentProcess = await service.createProcess(
|
||||
{
|
||||
documentContent: encryptedContent, // Privé : Contenu chiffré
|
||||
versionHistory: [] // Privé : Historique des versions
|
||||
},
|
||||
{
|
||||
documentTitle: 'Document Important', // Public : Titre
|
||||
lastModified: timestamp, // Public : Dernière modification
|
||||
author: authorAddress // Public : Auteur
|
||||
},
|
||||
{
|
||||
owner: {
|
||||
members: [ownerPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['documentContent', 'documentTitle'] }
|
||||
]
|
||||
},
|
||||
editor: {
|
||||
members: [...editorPairingIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['documentContent'] } // 50% des éditeurs doivent valider
|
||||
]
|
||||
},
|
||||
viewer: {
|
||||
members: [...viewerPairingIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0, fields: ['documentTitle'] } // Lecture seule, pas de validation
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Système de Votation
|
||||
|
||||
```typescript
|
||||
const votingProcess = await service.createProcess(
|
||||
{
|
||||
votes: {}, // Privé : Votes individuels
|
||||
voterIds: [] // Privé : Liste des votants
|
||||
},
|
||||
{
|
||||
question: 'Question...', // Public : Question
|
||||
options: ['A', 'B', 'C'], // Public : Options
|
||||
deadline: timestamp, // Public : Deadline
|
||||
result: null // Public : Résultat (mis à jour après)
|
||||
},
|
||||
{
|
||||
voter: {
|
||||
members: [...allVoterIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
|
||||
]
|
||||
},
|
||||
organizer: {
|
||||
members: [organizerPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['question', 'options', 'deadline', 'result'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Contrat Intelligent Décentralisé
|
||||
|
||||
Le système peut implémenter n'importe quel type de contrat intelligent décentralisé avec :
|
||||
- Conditions de validation personnalisées (quorum)
|
||||
- Permissions granulaires par champ
|
||||
- Audit trail complet (historique des états)
|
||||
- Vérifiabilité sur la blockchain
|
||||
123
eslint.config.js
Normal file
123
eslint.config.js
Normal file
@ -0,0 +1,123 @@
|
||||
import js from '@eslint/js';
|
||||
import typescript from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json'
|
||||
},
|
||||
globals: {
|
||||
'console': 'readonly',
|
||||
'window': 'readonly',
|
||||
'document': 'readonly',
|
||||
'navigator': 'readonly',
|
||||
'crypto': 'readonly',
|
||||
'setTimeout': 'readonly',
|
||||
'clearTimeout': 'readonly',
|
||||
'setInterval': 'readonly',
|
||||
'clearInterval': 'readonly',
|
||||
'alert': 'readonly',
|
||||
'confirm': 'readonly',
|
||||
'prompt': 'readonly',
|
||||
'fetch': 'readonly',
|
||||
'localStorage': 'readonly',
|
||||
'sessionStorage': 'readonly',
|
||||
'indexedDB': 'readonly',
|
||||
'IDBDatabase': 'readonly',
|
||||
'IDBTransaction': 'readonly',
|
||||
'IDBObjectStore': 'readonly',
|
||||
'IDBRequest': 'readonly',
|
||||
'customElements': 'readonly',
|
||||
'requestAnimationFrame': 'readonly',
|
||||
'cancelAnimationFrame': 'readonly',
|
||||
'performance': 'readonly',
|
||||
'WebAssembly': 'readonly',
|
||||
'btoa': 'readonly',
|
||||
'atob': 'readonly',
|
||||
'self': 'readonly',
|
||||
'SharedWorker': 'readonly',
|
||||
'Worker': 'readonly',
|
||||
'caches': 'readonly'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescript
|
||||
},
|
||||
rules: {
|
||||
// Qualité du code - Règles plus permissives pour commencer
|
||||
'complexity': ['warn', 15],
|
||||
'max-lines': ['warn', 500],
|
||||
'max-lines-per-function': ['warn', 100],
|
||||
'max-params': ['warn', 6],
|
||||
'max-depth': ['warn', 6],
|
||||
|
||||
// TypeScript spécifique - Plus permissif
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', {
|
||||
'argsIgnorePattern': '^_',
|
||||
'varsIgnorePattern': '^_',
|
||||
'ignoreRestSiblings': true
|
||||
}],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
|
||||
// Bonnes pratiques - Plus permissif
|
||||
'no-console': 'off', // Permettre console pour le debug
|
||||
'no-debugger': 'error',
|
||||
'no-alert': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'no-var': 'error',
|
||||
'eqeqeq': 'warn',
|
||||
'curly': 'warn',
|
||||
|
||||
// Sécurité
|
||||
'no-eval': 'error',
|
||||
'no-implied-eval': 'error',
|
||||
'no-new-func': 'error',
|
||||
|
||||
// Performance - Plus permissif
|
||||
'no-loop-func': 'warn',
|
||||
'no-await-in-loop': 'off' // Permettre await dans les boucles pour l'instant
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.worker.ts', '**/*.worker.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
'self': 'readonly',
|
||||
'postMessage': 'readonly',
|
||||
'onmessage': 'readonly',
|
||||
'importScripts': 'readonly',
|
||||
'btoa': 'readonly',
|
||||
'atob': 'readonly',
|
||||
'crypto': 'readonly',
|
||||
'console': 'readonly'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'dist/',
|
||||
'node_modules/',
|
||||
'*.js',
|
||||
'!eslint.config.js',
|
||||
'pkg/',
|
||||
'vite.config.ts',
|
||||
'test-browser/',
|
||||
'logs/',
|
||||
'coverage/',
|
||||
'.nyc_output/',
|
||||
'**/*.test.ts',
|
||||
'**/*.spec.ts'
|
||||
]
|
||||
}
|
||||
];
|
||||
150
examples/README.md
Normal file
150
examples/README.md
Normal file
@ -0,0 +1,150 @@
|
||||
# 4NK Pairing Integration Example
|
||||
|
||||
This example demonstrates how to integrate the 4NK pairing system into an external website using an iframe with channel_message communication.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ External Website (Parent) │
|
||||
│ ├── Header with site branding │
|
||||
│ ├── Main content area │
|
||||
│ └── Iframe (4NK App) │
|
||||
│ ├── No header (removed) │
|
||||
│ ├── Menu buttons in content │
|
||||
│ ├── Pairing interface │
|
||||
│ └── Communication with parent │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### External Site (Parent)
|
||||
- **Header**: Site branding and navigation
|
||||
- **Iframe Container**: Hosts the 4NK application
|
||||
- **Status Panel**: Shows communication status
|
||||
- **Log System**: Displays real-time communication
|
||||
- **Controls**: Test communication and refresh
|
||||
|
||||
### 4NK Application (Iframe)
|
||||
- **No Header**: Clean interface without site header
|
||||
- **Integrated Menu**: Menu buttons within content area
|
||||
- **Pairing System**: 4-word authentication system
|
||||
- **Communication**: Bidirectional message passing
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
### Messages from Parent to Iframe
|
||||
- `TEST_MESSAGE`: Test communication
|
||||
- `PAIRING_4WORDS_CREATE`: Request pairing creation
|
||||
- `PAIRING_4WORDS_JOIN`: Request pairing join with words
|
||||
|
||||
### Messages from Iframe to Parent
|
||||
- `IFRAME_READY`: Iframe initialization complete
|
||||
- `MENU_NAVIGATION`: Menu button clicked
|
||||
- `PAIRING_4WORDS_WORDS_GENERATED`: 4 words generated
|
||||
- `PAIRING_4WORDS_STATUS_UPDATE`: Status update
|
||||
- `PAIRING_4WORDS_SUCCESS`: Pairing successful
|
||||
- `PAIRING_4WORDS_ERROR`: Pairing error
|
||||
- `TEST_RESPONSE`: Response to test message
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Start the 4NK application**:
|
||||
```bash
|
||||
cd /home/ank/dev/ihm_client_dev3
|
||||
npm run start
|
||||
```
|
||||
|
||||
2. **Open the external site**:
|
||||
```bash
|
||||
# Open examples/external-site.html in a browser
|
||||
# Or serve it via a web server
|
||||
```
|
||||
|
||||
3. **Test the integration**:
|
||||
- The iframe loads the 4NK application
|
||||
- Use the "Send Test Message" button to test communication
|
||||
- Click menu buttons to see navigation messages
|
||||
- Use the pairing interface to test 4-word authentication
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Origin Verification**: In production, verify `event.origin` in message handlers
|
||||
- **Sandbox Attributes**: Iframe uses `sandbox` for security
|
||||
- **CSP Headers**: Consider Content Security Policy headers
|
||||
- **HTTPS**: Use HTTPS in production for secure communication
|
||||
|
||||
## Customization
|
||||
|
||||
### Styling the Iframe
|
||||
```css
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Messages
|
||||
```javascript
|
||||
// Send custom message to iframe
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'CUSTOM_ACTION',
|
||||
data: { parameter: 'value' }
|
||||
}, 'http://localhost:3004');
|
||||
```
|
||||
|
||||
### Handling Custom Events
|
||||
```javascript
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.origin !== 'http://localhost:3004') return;
|
||||
|
||||
const { type, data } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case 'CUSTOM_EVENT':
|
||||
// Handle custom event
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Iframe not loading**: Check CORS settings and iframe src URL
|
||||
2. **Messages not received**: Verify origin checking and message format
|
||||
3. **Styling issues**: Check iframe container dimensions and CSS
|
||||
4. **Communication errors**: Check browser console for error messages
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by adding to the iframe:
|
||||
```javascript
|
||||
window.DEBUG_IFRAME = true;
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
1. **Update Origins**: Change localhost URLs to production domains
|
||||
2. **Security Headers**: Add appropriate CSP and security headers
|
||||
3. **Error Handling**: Implement proper error handling and fallbacks
|
||||
4. **Monitoring**: Add logging and monitoring for communication events
|
||||
5. **Testing**: Test across different browsers and devices
|
||||
|
||||
## API Reference
|
||||
|
||||
### Parent Window API
|
||||
- `sendTestMessage()`: Send test message to iframe
|
||||
- `clearLog()`: Clear communication log
|
||||
- `refreshIframe()`: Refresh iframe content
|
||||
|
||||
### Iframe API
|
||||
- `initIframeCommunication()`: Initialize communication
|
||||
- `initContentMenu()`: Initialize menu buttons
|
||||
- `createPairingViaIframe()`: Create pairing process
|
||||
- `joinPairingViaIframe(words)`: Join pairing with words
|
||||
327
examples/external-site.html
Normal file
327
examples/external-site.html
Normal file
@ -0,0 +1,327 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>External Site - 4NK Integration Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.integration-section {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.integration-section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.integration-section p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.status-panel h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: #007bff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.log-entry.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🏢 External Business Site</h1>
|
||||
<p>Integrated 4NK Pairing System - Secure Device Authentication</p>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="integration-section">
|
||||
<h2>🔐 4NK Pairing Integration</h2>
|
||||
<p>
|
||||
This external site demonstrates how to integrate the 4NK pairing system
|
||||
using an iframe with channel_message communication. The iframe contains
|
||||
the 4NK application without header, and all menu options are integrated
|
||||
as buttons within the content.
|
||||
</p>
|
||||
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
id="4nk-iframe"
|
||||
src="http://localhost:3004"
|
||||
title="4NK Pairing System"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div class="status-panel">
|
||||
<h3>📊 Integration Status</h3>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Iframe Status:</span>
|
||||
<span class="status-value" id="iframe-status">Loading...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Communication:</span>
|
||||
<span class="status-value" id="communication-status">Waiting...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Last Message:</span>
|
||||
<span class="status-value" id="last-message">None</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" onclick="sendTestMessage()">📤 Send Test Message</button>
|
||||
<button class="btn secondary" onclick="clearLog()">🗑️ Clear Log</button>
|
||||
<button class="btn secondary" onclick="refreshIframe()">🔄 Refresh Iframe</button>
|
||||
</div>
|
||||
|
||||
<div class="log-container" id="log-container">
|
||||
<div class="log-entry info">🚀 External site loaded</div>
|
||||
<div class="log-entry info">📡 Waiting for iframe communication...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let messageCount = 0;
|
||||
|
||||
// Listen for messages from the iframe
|
||||
window.addEventListener('message', function(event) {
|
||||
// Security check - in production, verify event.origin
|
||||
if (event.origin !== 'http://localhost:3004') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, data } = event.data;
|
||||
messageCount++;
|
||||
|
||||
logMessage(`📨 Received: ${type}`, 'info');
|
||||
updateStatus('communication-status', 'Active');
|
||||
updateStatus('last-message', `${type} (${messageCount})`);
|
||||
|
||||
// Handle different message types
|
||||
switch (type) {
|
||||
case 'IFRAME_READY':
|
||||
logMessage('✅ 4NK iframe is ready', 'success');
|
||||
updateStatus('iframe-status', 'Ready');
|
||||
break;
|
||||
|
||||
case 'MENU_NAVIGATION':
|
||||
logMessage(`🧭 Menu navigation: ${data.page}`, 'info');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_WORDS_GENERATED':
|
||||
logMessage(`🔐 4 words generated: ${data.words}`, 'success');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_STATUS_UPDATE':
|
||||
logMessage(`📊 Status update: ${data.status}`, 'info');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_SUCCESS':
|
||||
logMessage(`✅ Pairing successful: ${data.message}`, 'success');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_ERROR':
|
||||
logMessage(`❌ Pairing error: ${data.error}`, 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
logMessage(`❓ Unknown message type: ${type}`, 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
function logMessage(message, type = 'info') {
|
||||
const logContainer = document.getElementById('log-container');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function sendTestMessage() {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'TEST_MESSAGE',
|
||||
data: { message: 'Hello from external site!' }
|
||||
}, 'http://localhost:3004');
|
||||
logMessage('📤 Sent test message to iframe', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
const logContainer = document.getElementById('log-container');
|
||||
logContainer.innerHTML = '<div class="log-entry info">🗑️ Log cleared</div>';
|
||||
}
|
||||
|
||||
function refreshIframe() {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
iframe.src = iframe.src;
|
||||
logMessage('🔄 Iframe refreshed', 'info');
|
||||
updateStatus('iframe-status', 'Refreshing...');
|
||||
}
|
||||
|
||||
// Initialize
|
||||
logMessage('🌐 External site initialized', 'success');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
326
examples/test-integration.html
Normal file
326
examples/test-integration.html
Normal file
@ -0,0 +1,326 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>4NK Integration Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.test-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-entry.info { color: #17a2b8; }
|
||||
.log-entry.success { color: #28a745; }
|
||||
.log-entry.error { color: #dc3545; }
|
||||
.log-entry.warning { color: #ffc107; }
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>🧪 4NK Integration Test</h1>
|
||||
<p>Test de l'intégration iframe avec communication channel_message</p>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📱 Interface 4NK (Iframe)</h3>
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
id="4nk-iframe"
|
||||
src="http://localhost:3004"
|
||||
title="4NK Pairing System"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>🎮 Contrôles de Test</h3>
|
||||
<div class="test-controls">
|
||||
<button class="btn" onclick="sendTestMessage()">📤 Test Message</button>
|
||||
<button class="btn" onclick="testCreatePairing()">🔐 Test Create Pairing</button>
|
||||
<button class="btn" onclick="testJoinPairing()">🔗 Test Join Pairing</button>
|
||||
<button class="btn secondary" onclick="clearLog()">🗑️ Clear Log</button>
|
||||
<button class="btn secondary" onclick="refreshIframe()">🔄 Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📊 Status</h3>
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<div class="status-label">Iframe Status</div>
|
||||
<div class="status-value" id="iframe-status">Loading...</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-label">Communication</div>
|
||||
<div class="status-value" id="communication-status">Waiting...</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-label">Messages Received</div>
|
||||
<div class="status-value" id="message-count">0</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="status-label">Last Message</div>
|
||||
<div class="status-value" id="last-message">None</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📝 Communication Log</h3>
|
||||
<div class="log-container" id="log-container">
|
||||
<div class="log-entry info">🚀 Test page loaded</div>
|
||||
<div class="log-entry info">📡 Waiting for iframe communication...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let messageCount = 0;
|
||||
let iframeReady = false;
|
||||
|
||||
// Listen for messages from the iframe
|
||||
window.addEventListener('message', function(event) {
|
||||
// Security check - in production, verify event.origin
|
||||
if (event.origin !== 'http://localhost:3004') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, data } = event.data;
|
||||
messageCount++;
|
||||
|
||||
logMessage(`📨 Received: ${type}`, 'info');
|
||||
updateStatus('communication-status', 'Active');
|
||||
updateStatus('message-count', messageCount.toString());
|
||||
updateStatus('last-message', `${type} (${messageCount})`);
|
||||
|
||||
// Handle different message types
|
||||
switch (type) {
|
||||
case 'IFRAME_READY':
|
||||
logMessage('✅ 4NK iframe is ready', 'success');
|
||||
updateStatus('iframe-status', 'Ready');
|
||||
iframeReady = true;
|
||||
break;
|
||||
|
||||
case 'MENU_NAVIGATION':
|
||||
logMessage(`🧭 Menu navigation: ${data.page}`, 'info');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_WORDS_GENERATED':
|
||||
logMessage(`🔐 4 words generated: ${data.words}`, 'success');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_STATUS_UPDATE':
|
||||
logMessage(`📊 Status update: ${data.status}`, 'info');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_SUCCESS':
|
||||
logMessage(`✅ Pairing successful: ${data.message}`, 'success');
|
||||
break;
|
||||
|
||||
case 'PAIRING_4WORDS_ERROR':
|
||||
logMessage(`❌ Pairing error: ${data.error}`, 'error');
|
||||
break;
|
||||
|
||||
case 'TEST_RESPONSE':
|
||||
logMessage(`🧪 Test response: ${data.response}`, 'success');
|
||||
break;
|
||||
|
||||
default:
|
||||
logMessage(`❓ Unknown message type: ${type}`, 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
function logMessage(message, type = 'info') {
|
||||
const logContainer = document.getElementById('log-container');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry ${type}`;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function sendTestMessage() {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'TEST_MESSAGE',
|
||||
data: { message: 'Hello from test page!' }
|
||||
}, 'http://localhost:3004');
|
||||
logMessage('📤 Sent test message to iframe', 'info');
|
||||
} else {
|
||||
logMessage('❌ Iframe not ready', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testCreatePairing() {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'PAIRING_4WORDS_CREATE',
|
||||
data: {}
|
||||
}, 'http://localhost:3004');
|
||||
logMessage('🔐 Sent create pairing request', 'info');
|
||||
} else {
|
||||
logMessage('❌ Iframe not ready', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testJoinPairing() {
|
||||
const words = prompt('Enter 4 words to test join pairing:');
|
||||
if (words) {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'PAIRING_4WORDS_JOIN',
|
||||
data: { words: words }
|
||||
}, 'http://localhost:3004');
|
||||
logMessage(`🔗 Sent join pairing request with words: ${words}`, 'info');
|
||||
} else {
|
||||
logMessage('❌ Iframe not ready', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
const logContainer = document.getElementById('log-container');
|
||||
logContainer.innerHTML = '<div class="log-entry info">🗑️ Log cleared</div>';
|
||||
}
|
||||
|
||||
function refreshIframe() {
|
||||
const iframe = document.getElementById('4nk-iframe');
|
||||
iframe.src = iframe.src;
|
||||
logMessage('🔄 Iframe refreshed', 'info');
|
||||
updateStatus('iframe-status', 'Refreshing...');
|
||||
iframeReady = false;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
logMessage('🌐 Test page initialized', 'success');
|
||||
|
||||
// Auto-test after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (iframeReady) {
|
||||
logMessage('🧪 Auto-testing communication...', 'info');
|
||||
sendTestMessage();
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
148
fix-logs.js
Normal file
148
fix-logs.js
Normal file
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script pour corriger automatiquement tous les logs console.* en secureLogger
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Fichiers à corriger
|
||||
const filesToFix = [
|
||||
'src/pages/home/home.ts',
|
||||
'src/pages/pairing/pairing.ts',
|
||||
'src/pages/wallet-setup/wallet-setup.ts',
|
||||
'src/pages/security-setup/security-setup.ts',
|
||||
'src/pages/birthday-setup/birthday-setup.ts',
|
||||
'src/pages/block-sync/block-sync.ts',
|
||||
'src/utils/sp-address.utils.ts',
|
||||
'src/router.ts',
|
||||
'src/websockets.ts'
|
||||
];
|
||||
|
||||
// Fonction pour déterminer le niveau de log
|
||||
function determineLogLevel(message) {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
if (lowerMessage.includes('error') || lowerMessage.includes('failed') || lowerMessage.includes('❌')) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('warn') || lowerMessage.includes('⚠️') || lowerMessage.includes('skipping')) {
|
||||
return 'warn';
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('debug') || lowerMessage.includes('🔍') || lowerMessage.includes('checking')) {
|
||||
return 'debug';
|
||||
}
|
||||
|
||||
return 'info';
|
||||
}
|
||||
|
||||
// Fonction pour déterminer le contexte
|
||||
function determineContext(filePath, message) {
|
||||
const fileName = path.basename(filePath, '.ts');
|
||||
|
||||
if (fileName.includes('service')) return 'Service';
|
||||
if (fileName.includes('home')) return 'HomePage';
|
||||
if (fileName.includes('pairing')) return 'PairingPage';
|
||||
if (fileName.includes('wallet')) return 'WalletSetup';
|
||||
if (fileName.includes('security')) return 'SecuritySetup';
|
||||
if (fileName.includes('birthday')) return 'BirthdaySetup';
|
||||
if (fileName.includes('block-sync')) return 'BlockSync';
|
||||
if (fileName.includes('router')) return 'Router';
|
||||
if (fileName.includes('websocket')) return 'WebSocket';
|
||||
if (fileName.includes('sp-address')) return 'SPAddressUtils';
|
||||
|
||||
return 'Application';
|
||||
}
|
||||
|
||||
// Fonction pour corriger un fichier
|
||||
function fixFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(`⚠️ Fichier non trouvé: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let modified = false;
|
||||
|
||||
// Ajouter l'import secureLogger si pas déjà présent
|
||||
if (!content.includes('import { secureLogger }')) {
|
||||
const importMatch = content.match(/import.*from.*['"][^'"]+['"];?\s*\n/);
|
||||
if (importMatch) {
|
||||
const importIndex = content.lastIndexOf(importMatch[0]) + importMatch[0].length;
|
||||
content = content.slice(0, importIndex) +
|
||||
`import { secureLogger } from '../services/secure-logger';\n` +
|
||||
content.slice(importIndex);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remplacer console.log par secureLogger
|
||||
content = content.replace(
|
||||
/console\.log\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
||||
(match, message) => {
|
||||
const level = determineLogLevel(message);
|
||||
const context = determineContext(filePath, message);
|
||||
modified = true;
|
||||
return `secureLogger.${level}('${message}', { component: '${context}' })`;
|
||||
}
|
||||
);
|
||||
|
||||
// Remplacer console.warn par secureLogger.warn
|
||||
content = content.replace(
|
||||
/console\.warn\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
||||
(match, message) => {
|
||||
const context = determineContext(filePath, message);
|
||||
modified = true;
|
||||
return `secureLogger.warn('${message}', { component: '${context}' })`;
|
||||
}
|
||||
);
|
||||
|
||||
// Remplacer console.error par secureLogger.error
|
||||
content = content.replace(
|
||||
/console\.error\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
||||
(match, message) => {
|
||||
const context = determineContext(filePath, message);
|
||||
modified = true;
|
||||
return `secureLogger.error('${message}', { component: '${context}' })`;
|
||||
}
|
||||
);
|
||||
|
||||
// Remplacer console.info par secureLogger.info
|
||||
content = content.replace(
|
||||
/console\.info\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
||||
(match, message) => {
|
||||
const context = determineContext(filePath, message);
|
||||
modified = true;
|
||||
return `secureLogger.info('${message}', { component: '${context}' })`;
|
||||
}
|
||||
);
|
||||
|
||||
// Remplacer console.debug par secureLogger.debug
|
||||
content = content.replace(
|
||||
/console\.debug\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
||||
(match, message) => {
|
||||
const context = determineContext(filePath, message);
|
||||
modified = true;
|
||||
return `secureLogger.debug('${message}', { component: '${context}' })`;
|
||||
}
|
||||
);
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✅ Corrigé: ${filePath}`);
|
||||
} else {
|
||||
console.log(`⏭️ Aucune modification: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter les corrections
|
||||
console.log('🔧 Correction des logs console.* en secureLogger...\n');
|
||||
|
||||
filesToFix.forEach(file => {
|
||||
fixFile(file);
|
||||
});
|
||||
|
||||
console.log('\n✅ Correction terminée !');
|
||||
@ -7,11 +7,11 @@
|
||||
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="./style/4nk.css">
|
||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<title>4NK Application</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-container"></div>
|
||||
<div id="containerId" class="container">
|
||||
<!-- 4NK Web5 Solution -->
|
||||
</div>
|
||||
|
||||
13
lint-all.sh
Executable file
13
lint-all.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script pour linter tout le projet et corriger automatiquement les erreurs
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔍 Running ESLint with --fix option..."
|
||||
npm run lint
|
||||
|
||||
echo ""
|
||||
echo "✅ Linting completed with auto-fix!"
|
||||
|
||||
|
||||
48
nginx.dev.conf
Normal file
48
nginx.dev.conf
Normal file
@ -0,0 +1,48 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Redirection des requêtes HTTP vers Vite
|
||||
location / {
|
||||
proxy_pass http://localhost:3004;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
proxy_pass http://localhost:8090;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
location /storage/ {
|
||||
rewrite ^/storage(/.*)$ $1 break;
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8091;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
|
||||
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
|
||||
}
|
||||
}
|
||||
7185
package-lock.json
generated
7185
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@ -2,44 +2,53 @@
|
||||
"name": "sdk_client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
|
||||
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev3/pkg ../sdk_client --target bundler --dev",
|
||||
"start": "vite --host 0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
|
||||
"deploy:front": "./scripts/deploy_front.sh",
|
||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
|
||||
"build:dist": "tsc -p tsconfig.build.json",
|
||||
"lint": "eslint src/ --fix",
|
||||
"lint:check": "eslint src/",
|
||||
"type-check": "tsc --noEmit",
|
||||
"quality": "npm run prettify",
|
||||
"quality:strict": "npm run type-check && npm run lint:check && npm run prettify",
|
||||
"quality:fix": "npm run lint && npm run prettify",
|
||||
"analyze": "npm run build && npx bundle-analyzer dist/assets/*.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:ci": "jest --ci --coverage --watchAll=false"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^9.38.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.2"
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/elements": "^19.0.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"axios": "^1.7.8",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"jose": "^6.0.11",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"sweetalert2": "^11.14.5",
|
||||
"vite-plugin-copy": "^0.1.6",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-wasm": "^3.3.0"
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
27
public/favicon.svg
Normal file
27
public/favicon.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<defs>
|
||||
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3a506b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#2c3e50;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Bouclier principal -->
|
||||
<path d="M16 2L6 6v10c0 8 10 12 10 12s10-4 10-12V6L16 2z"
|
||||
fill="url(#shieldGradient)"
|
||||
stroke="#1a252f"
|
||||
stroke-width="0.5"/>
|
||||
|
||||
<!-- Symbole de sécurité au centre -->
|
||||
<path d="M12 16l3 3 6-6"
|
||||
stroke="#ffffff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"/>
|
||||
|
||||
<!-- Points de sécurité -->
|
||||
<circle cx="16" cy="8" r="1" fill="#ffffff" opacity="0.8"/>
|
||||
<circle cx="20" cy="12" r="0.8" fill="#ffffff" opacity="0.6"/>
|
||||
<circle cx="12" cy="12" r="0.8" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 959 B |
1413
public/style/4nk.css
1413
public/style/4nk.css
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,597 +1,522 @@
|
||||
/* Styles de base */
|
||||
:root {
|
||||
--primary-color: #3A506B;
|
||||
/* Bleu métallique */
|
||||
--secondary-color: #B0BEC5;
|
||||
/* Gris acier */
|
||||
--accent-color: #D68C45;
|
||||
/* Cuivre */
|
||||
}
|
||||
|
||||
/* Chat page base */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: #f3f5f9;
|
||||
color: var(--color-text-primary);
|
||||
font-family: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
/* 4NK NAVBAR */
|
||||
|
||||
.brand-logo {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #37474F;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
/* Icônes de la barre de navigation */
|
||||
.nav-right-icons {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(232, 238, 244, 0.9));
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.14);
|
||||
z-index: 25;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
cursor: pointer;
|
||||
.brand-logo {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.nav-right-icons {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.notification-container {
|
||||
position: relative;
|
||||
/* Conserve la position pour le notification-board */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
/* Position absolue pour le placer par rapport au container */
|
||||
top: 40px;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
width: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
/* Scroll si les notifications dépassent la taille */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
/* Définit la priorité d'affichage au-dessus des autres éléments */
|
||||
display: none;
|
||||
/* Par défaut, la notification est masquée */
|
||||
}
|
||||
|
||||
.notification-item{
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-base);
|
||||
}
|
||||
|
||||
.notification-bell:hover,
|
||||
.burger-menu:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: 35px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
display: none;
|
||||
/* S'affiche seulement lorsqu'il y a des notifications */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Par défaut, le menu est masqué */
|
||||
#menu {
|
||||
display: none;
|
||||
/* Menu caché par défaut */
|
||||
transition: display 0.3s ease-in-out;
|
||||
.notification-badge.is-visible {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.burger-menu {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Icône burger */
|
||||
#burger-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
top: calc(100% + 12px);
|
||||
right: 0;
|
||||
width: min(280px, 90vw);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
padding: 10px 0;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Ajustement pour la barre de navigation fixe */
|
||||
.container {
|
||||
.notification-board.is-visible {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 90vh;
|
||||
margin-top: 9vh;
|
||||
margin-left: -1%;
|
||||
text-align: left;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.notification-board .notification-item,
|
||||
.notification-board .notification-element {
|
||||
padding: 10px 16px;
|
||||
color: var(--color-text-primary);
|
||||
transition: background var(--transition-base);
|
||||
}
|
||||
|
||||
/* Liste des groupes */
|
||||
.notification-board .notification-item:hover,
|
||||
.notification-board .notification-element:hover {
|
||||
background: rgba(58, 80, 107, 0.1);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
margin-top: 90px;
|
||||
display: grid;
|
||||
grid-template-columns: clamp(220px, 22%, 280px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
padding: 24px 32px 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.group-list {
|
||||
width: 25%;
|
||||
background-color: #1f2c3d;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #1f2c3d, #1b2735);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 22px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
height: calc(100vh - 136px);
|
||||
overflow-y: auto;
|
||||
border-right: 2px solid #2c3e50;
|
||||
flex-shrink: 0;
|
||||
padding-right: 10px;
|
||||
height: 91vh;
|
||||
}
|
||||
|
||||
.group-list ul {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-right: 10px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.group-list li {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #273646;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px 16px;
|
||||
transition: transform var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.group-list li:hover {
|
||||
background-color: #34495e;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
.group-list li:hover,
|
||||
.group-list li.active {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 14px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.group-list .member-container {
|
||||
.member-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.group-list .member-container button {
|
||||
margin-left: 40px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
border-radius: 50px;
|
||||
.member-container button {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: -25px;
|
||||
top: -16px;
|
||||
right: -16px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-base);
|
||||
}
|
||||
|
||||
.group-list .member-container button:hover {
|
||||
background: var(--accent-color)
|
||||
.member-container button:hover {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
|
||||
/* Zone de chat */
|
||||
.chat-area {
|
||||
/* Chat area */
|
||||
.chat-area,
|
||||
.signature-area {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 136px);
|
||||
}
|
||||
|
||||
/* En-tête du chat */
|
||||
.chat-header {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px 10px 0 0;
|
||||
.chat-header,
|
||||
.signature-header {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px 18px;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: rgba(58, 80, 107, 0.04);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
}
|
||||
.message-container .message {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-container .message.user {
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 70%;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background:var(--secondary-color);
|
||||
margin: 2px 0;
|
||||
max-width: 68%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.12);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Messages de l'utilisateur */
|
||||
.message.user {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
background: linear-gradient(135deg, #2196f3, #1363b5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.7em;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
margin-left: 0px;
|
||||
margin-top: 5px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
|
||||
/* Amélioration de l'esthétique des messages */
|
||||
/* .message.user:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -10px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: #3498db;
|
||||
} */
|
||||
|
||||
/* Zone de saisie */
|
||||
.input-area {
|
||||
padding: 10px;
|
||||
background-color: #bdc3c7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
/* Alignement vertical */
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.input-area input[type="text"] {
|
||||
.input-area input[type='text'] {
|
||||
flex: 1;
|
||||
/* Prend l'espace restant */
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.input-area .attachment-icon {
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(58, 80, 107, 0.12);
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
background-color: #2980b9;
|
||||
color: white;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, #2980b9, #1f608d);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
.input-area button:hover {
|
||||
background-color: #1f608d;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 22px rgba(41, 128, 185, 0.35);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
margin: 20px 0px;
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 10px 20px;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
margin-right: 5px;
|
||||
border-radius: 10px;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px rgba(58, 80, 107, 0.25);
|
||||
}
|
||||
|
||||
/* Signature */
|
||||
.signature-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
transition: all 1s ease 0.1s;
|
||||
visibility: visible;
|
||||
gap: 18px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.signature-area.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.signature-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-left: 4%;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.signature-content {
|
||||
padding: 10px;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
background: rgba(58, 80, 107, 0.08);
|
||||
color: var(--color-text-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.signature-description {
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
.signature-description,
|
||||
.signature-documents {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.signature-description li {
|
||||
margin: 1% 0% 1% 0%;
|
||||
list-style: none;
|
||||
padding: 2%;
|
||||
border-radius: 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--secondary-color);
|
||||
width: 20%;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 2%;
|
||||
overflow: auto;
|
||||
transition: transform var(--transition-base), background var(--transition-base);
|
||||
}
|
||||
|
||||
.signature-description li .member-list {
|
||||
margin-left: -30%;
|
||||
.signature-description li:hover {
|
||||
background: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.signature-description li .member-list li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-description li .member-list li:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.signature-documents {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signature-documents-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 15%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#request-document-button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
.new-request-btn,
|
||||
#request-document-button,
|
||||
.sign-button {
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
background: linear-gradient(135deg, var(--color-success), #2e7d32);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-left: 5%;
|
||||
font-weight: bold;
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
#request-document-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
.new-request-btn:hover,
|
||||
#request-document-button:hover,
|
||||
.sign-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 24px rgba(76, 175, 80, 0.32);
|
||||
}
|
||||
|
||||
#close-signature {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 2%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: -3%;
|
||||
margin-top: -5%;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#close-signature:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* REQUEST MODAL */
|
||||
.request-modal {
|
||||
/* Modals */
|
||||
.modal,
|
||||
.notifications-modal,
|
||||
.qr-modal,
|
||||
.request-modal,
|
||||
.modal-document,
|
||||
.pairing-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--secondary-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.modal-members {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-members ul li{
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
background: var(--background-color-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.remove-file {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.remove-file:hover {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
#message-input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 60;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.modal.is-visible,
|
||||
.notifications-modal.is-visible,
|
||||
.qr-modal.is-visible,
|
||||
.request-modal.is-visible,
|
||||
.modal-document.is-visible,
|
||||
.pairing-modal.is-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content,
|
||||
.notifications-content,
|
||||
.qr-modal-content,
|
||||
.request-modal .modal-content,
|
||||
.modal-document .modal-content,
|
||||
.pairing-modal-content {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
width: min(480px, 100%);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-modal,
|
||||
.close-button,
|
||||
.close-qr-modal,
|
||||
.close-signature,
|
||||
.close-contract-popup {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1.4rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-modal:hover,
|
||||
.close-button:hover,
|
||||
.close-qr-modal:hover,
|
||||
.close-signature:hover,
|
||||
.close-contract-popup:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.modal-footer,
|
||||
.button-group,
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 768px) {
|
||||
@media (max-width: 1024px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 18px 18px 36px;
|
||||
}
|
||||
|
||||
.group-list {
|
||||
display: none;
|
||||
/* Masquer la liste des groupes sur les petits écrans */
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
@media (max-width: 768px) {
|
||||
.nav-wrapper {
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--primary-color);
|
||||
border-radius: 5px;
|
||||
.nav-right-icons {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--secondary-color);
|
||||
border-radius: 5px;
|
||||
.container {
|
||||
padding: 16px 14px 32px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color);
|
||||
.chat-area,
|
||||
.signature-area {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
0
screenlog.0
Normal file
0
screenlog.0
Normal file
68
scripts/deploy_front.sh
Executable file
68
scripts/deploy_front.sh
Executable file
@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOG_DIR="$PROJECT_ROOT/logs"
|
||||
PID_FILE="$LOG_DIR/ihm_client_dev3.front.pid"
|
||||
LOG_FILE="$LOG_DIR/ihm_client_dev3.front.log"
|
||||
PORT=3004
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
echo "[deploy] Ensuring nothing listens on :$PORT..."
|
||||
if ss -ltnp | grep -q ":$PORT"; then
|
||||
# Extract PID(s) for the port
|
||||
PIDS=$(ss -ltnp | awk -v p=":$PORT" '$0 ~ p {print $NF}' | sed -E 's/.*pid=([0-9]+).*/\1/' | sort -u)
|
||||
for PID in $PIDS; do
|
||||
if [[ "$PID" =~ ^[0-9]+$ ]]; then
|
||||
echo "[deploy] Killing PID $PID on port $PORT"
|
||||
kill -TERM "$PID" || true
|
||||
# Wait up to 10s for process to exit
|
||||
for i in {1..10}; do
|
||||
if ! ps -p "$PID" >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if ps -p "$PID" >/dev/null 2>&1; then
|
||||
echo "[deploy] Force killing PID $PID"
|
||||
kill -KILL "$PID" || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "[deploy] Cleaning Vite caches and previous dist..."
|
||||
rm -rf "$PROJECT_ROOT/node_modules/.vite" "$PROJECT_ROOT/.vite" "$PROJECT_ROOT/dist"
|
||||
|
||||
echo "[deploy] Building production bundle..."
|
||||
cd "$PROJECT_ROOT"
|
||||
npm run build
|
||||
|
||||
echo "[deploy] Starting Vite dev server on :$PORT (non-bloquant) ..."
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
# Clean stale PID file if any
|
||||
OLD_PID=$(cat "$PID_FILE" || true)
|
||||
if [[ -n "${OLD_PID}" ]] && ps -p "$OLD_PID" >/dev/null 2>&1; then
|
||||
echo "[deploy] Previous PID $OLD_PID still running, terminating"
|
||||
kill -TERM "$OLD_PID" || true
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
nohup npm run start >"$LOG_FILE" 2>&1 &
|
||||
NEW_PID=$!
|
||||
echo "$NEW_PID" > "$PID_FILE"
|
||||
|
||||
echo "[deploy] Launched PID $NEW_PID. Tail logs: tail -f $LOG_FILE"
|
||||
|
||||
echo "[deploy] Verifying port $PORT availability..."
|
||||
for i in {1..10}; do
|
||||
if ss -ltnp | grep -q ":$PORT"; then
|
||||
echo "[deploy] OK: port $PORT is listening."
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[deploy] ERROR: port $PORT not listening after start. Check $LOG_FILE"
|
||||
exit 1
|
||||
1401
src/4nk.css
1401
src/4nk.css
File diff suppressed because it is too large
Load Diff
74
src/components/account-nav/account-nav.css
Normal file
74
src/components/account-nav/account-nav.css
Normal file
@ -0,0 +1,74 @@
|
||||
.account-nav {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nav-btn.disconnect-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.nav-btn.disconnect-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.nav-btn.delete-btn {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.nav-btn.delete-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.account-nav {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
17
src/components/account-nav/account-nav.html
Normal file
17
src/components/account-nav/account-nav.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="account-nav">
|
||||
<div class="nav-actions">
|
||||
<button class="nav-btn" onclick="importJSON()" title="Import backup">📥 Import</button>
|
||||
<button class="nav-btn" onclick="createBackUp()" title="Export backup">📤 Export</button>
|
||||
<button class="nav-btn" onclick="navigate('chat')" title="Chat">💬 Chat</button>
|
||||
<button class="nav-btn" onclick="navigate('signature')" title="Signatures">
|
||||
✍️ Signatures
|
||||
</button>
|
||||
<button class="nav-btn" onclick="navigate('process')" title="Process">⚙️ Process</button>
|
||||
<button class="nav-btn disconnect-btn" onclick="disconnect()" title="Disconnect">
|
||||
🚪 Disconnect
|
||||
</button>
|
||||
<button class="nav-btn delete-btn" onclick="deleteAccount()" title="Delete account">
|
||||
🗑️ Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
845
src/components/device-management/device-management.ts
Normal file
845
src/components/device-management/device-management.ts
Normal file
@ -0,0 +1,845 @@
|
||||
import Services from '../../services/service';
|
||||
import { addressToWords } from '../../utils/sp-address.utils';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
|
||||
// Global function declarations
|
||||
declare global {
|
||||
interface Window {
|
||||
importJSON: () => Promise<void>;
|
||||
createBackUp: () => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceManagementComponent extends HTMLElement {
|
||||
private service: Services | null = null;
|
||||
private currentDeviceWords: string = '';
|
||||
private pairedDevices: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.service = await Services.getInstance();
|
||||
await this.loadDeviceData();
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
this.injectAccountNav();
|
||||
}
|
||||
|
||||
async loadDeviceData() {
|
||||
if (!this.service) {return;}
|
||||
|
||||
try {
|
||||
// Get current device address and generate 4 words
|
||||
const currentAddress = await this.service.getDeviceAddress();
|
||||
if (currentAddress) {
|
||||
this.currentDeviceWords = await addressToWords(currentAddress);
|
||||
}
|
||||
|
||||
// Get paired devices from the pairing process
|
||||
const pairingProcessId = this.service.getPairingProcessId();
|
||||
if (pairingProcessId) {
|
||||
const process = await this.service.getProcess(pairingProcessId);
|
||||
if (process && process.states && process.states.length > 0) {
|
||||
const lastState = process.states[process.states.length - 1];
|
||||
const publicData = lastState.public_data;
|
||||
if (publicData && publicData['pairedAddresses']) {
|
||||
this.pairedDevices = this.service.decodeValue(publicData['pairedAddresses']) || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
secureLogger.error('Error loading device data', error as Error, { component: 'DeviceManagement' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot!.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.device-management {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #3a506b;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.contract-description {
|
||||
background: rgba(58, 80, 107, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid #3a506b;
|
||||
}
|
||||
|
||||
.contract-description p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #3a506b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.contract-description ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.contract-description li {
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.current-device {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border-left: 4px solid #3a506b;
|
||||
}
|
||||
|
||||
.current-device h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #3a506b;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.words-display {
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #3a506b;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
word-spacing: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #3a506b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #2c3e50;
|
||||
}
|
||||
|
||||
.paired-devices {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.paired-devices h3 {
|
||||
color: #3a506b;
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.device-address {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.add-device {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.add-device h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #3a506b;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.words-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.words-input:focus {
|
||||
outline: none;
|
||||
border-color: #3a506b;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a506b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.delete-account-btn {
|
||||
background: #f44336 !important;
|
||||
color: white !important;
|
||||
font-weight: bold;
|
||||
border: 2px solid #d32f2f;
|
||||
}
|
||||
|
||||
.delete-account-btn:hover {
|
||||
background: #d32f2f !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.import-export {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.import-export .btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.import-export .btn-danger {
|
||||
flex: 1.2;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.import-export .btn-critical {
|
||||
flex: 1.3;
|
||||
min-width: 180px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: 2px solid #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.import-export .btn-critical:hover {
|
||||
background: #c82333;
|
||||
border-color: #c82333;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="device-management">
|
||||
<div class="header">
|
||||
<h1>🔐 Contrat de Pairing</h1>
|
||||
<p>Gestion sécurisée de vos devices avec authentification 4 mots</p>
|
||||
<div class="contract-description">
|
||||
<p><strong>📋 Description du contrat :</strong></p>
|
||||
<ul>
|
||||
<li>🔐 <strong>Sécurité :</strong> Chaque device est authentifié par 4 mots uniques</li>
|
||||
<li>🔗 <strong>Pairing :</strong> Connexion sécurisée entre devices approuvés</li>
|
||||
<li>🛡️ <strong>Protection :</strong> Au moins 1 device doit toujours rester actif</li>
|
||||
<li>🔄 <strong>Gestion :</strong> Ajout/suppression de devices en temps réel</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="import-export">
|
||||
<button class="btn btn-secondary" id="importBtn">📥 Importer</button>
|
||||
<button class="btn btn-secondary" id="exportBtn">📤 Exporter</button>
|
||||
<button class="btn btn-critical" id="criticalExportBtn">🚨 Export Critique (Clé Privée)</button>
|
||||
<button class="btn btn-danger" id="deleteAccountBtn">🗑️ Supprimer le Compte</button>
|
||||
</div>
|
||||
|
||||
<div class="current-device">
|
||||
<h3>📱 Device Actuel</h3>
|
||||
<p>Vos 4 mots d'authentification :</p>
|
||||
<div class="words-display" id="currentWords">${this.currentDeviceWords}</div>
|
||||
<button class="copy-btn" id="copyCurrentWords">📋 Copier</button>
|
||||
</div>
|
||||
|
||||
<div class="paired-devices">
|
||||
<h3>🔗 Devices Appairés (${this.pairedDevices.length})</h3>
|
||||
<ul class="device-list" id="deviceList">
|
||||
${this.pairedDevices
|
||||
.map(
|
||||
(address, index) => `
|
||||
<li class="device-item">
|
||||
<div class="device-info">
|
||||
<strong>Device ${index + 1}</strong>
|
||||
<div class="device-address">${address}</div>
|
||||
</div>
|
||||
${
|
||||
this.pairedDevices.length > 1
|
||||
? `
|
||||
<button class="remove-btn" data-address="${address}">🗑️ Supprimer</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="add-device">
|
||||
<h3>➕ Ajouter un Device</h3>
|
||||
<div class="input-group">
|
||||
<label for="newDeviceWords">4 mots du nouveau device :</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newDeviceWords"
|
||||
class="words-input"
|
||||
placeholder="Entrez les 4 mots (ex: abandon ability able about)"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div class="input-hint">Séparez les mots par des espaces</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="addDeviceBtn" disabled>➕ Ajouter Device</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" id="saveChangesBtn" disabled>💾 Sauvegarder</button>
|
||||
<button class="btn btn-secondary" id="cancelChangesBtn" disabled>❌ Annuler</button>
|
||||
</div>
|
||||
|
||||
<div id="statusMessage"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Copy current words
|
||||
this.shadowRoot!.getElementById('copyCurrentWords')?.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(this.currentDeviceWords);
|
||||
this.showStatus('4 mots copiés dans le presse-papiers !', 'success');
|
||||
});
|
||||
|
||||
// Import/Export buttons
|
||||
this.shadowRoot!.getElementById('importBtn')?.addEventListener('click', () => {
|
||||
this.importAccount();
|
||||
});
|
||||
|
||||
this.shadowRoot!.getElementById('exportBtn')?.addEventListener('click', () => {
|
||||
this.exportAccount();
|
||||
});
|
||||
|
||||
// Critical export button
|
||||
this.shadowRoot!.getElementById('criticalExportBtn')?.addEventListener('click', () => {
|
||||
this.criticalExport();
|
||||
});
|
||||
|
||||
// Delete account button
|
||||
this.shadowRoot!.getElementById('deleteAccountBtn')?.addEventListener('click', () => {
|
||||
this.deleteAccount();
|
||||
});
|
||||
|
||||
// Add device input validation
|
||||
const wordsInput = this.shadowRoot!.getElementById('newDeviceWords') as HTMLInputElement;
|
||||
const addBtn = this.shadowRoot!.getElementById('addDeviceBtn') as HTMLButtonElement;
|
||||
|
||||
wordsInput?.addEventListener('input', () => {
|
||||
const words = wordsInput.value.trim();
|
||||
const isValid = this.validateWords(words);
|
||||
addBtn.disabled = !isValid;
|
||||
|
||||
if (words && !isValid) {
|
||||
wordsInput.style.borderColor = '#f44336';
|
||||
} else {
|
||||
wordsInput.style.borderColor = '#e0e0e0';
|
||||
}
|
||||
});
|
||||
|
||||
// Add device button
|
||||
addBtn?.addEventListener('click', () => {
|
||||
this.addDevice();
|
||||
});
|
||||
|
||||
// Save/Cancel buttons
|
||||
this.shadowRoot!.getElementById('saveChangesBtn')?.addEventListener('click', () => {
|
||||
this.saveChanges();
|
||||
});
|
||||
|
||||
this.shadowRoot!.getElementById('cancelChangesBtn')?.addEventListener('click', () => {
|
||||
this.cancelChanges();
|
||||
});
|
||||
|
||||
// Remove device buttons (delegated event listener)
|
||||
this.shadowRoot!.addEventListener('click', e => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('remove-btn')) {
|
||||
const address = target.getAttribute('data-address');
|
||||
if (address) {
|
||||
this.removeDevice(address);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateWords(words: string): boolean {
|
||||
const wordArray = words.trim().split(/\s+/);
|
||||
return wordArray.length === 4 && wordArray.every(word => word.length > 0);
|
||||
}
|
||||
|
||||
async addDevice() {
|
||||
const wordsInput = this.shadowRoot!.getElementById('newDeviceWords') as HTMLInputElement;
|
||||
const words = wordsInput.value.trim();
|
||||
|
||||
if (!this.validateWords(words)) {
|
||||
this.showStatus(
|
||||
'❌ Format invalide. Entrez exactement 4 mots séparés par des espaces.',
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert words back to address (this would need to be implemented)
|
||||
// For now, we'll simulate adding a device
|
||||
const newAddress = `tsp1${Math.random().toString(36).substr(2, 9)}...`;
|
||||
this.pairedDevices.push(newAddress);
|
||||
|
||||
this.showStatus(`✅ Device ajouté avec succès !`, 'success');
|
||||
this.updateUI();
|
||||
this.enableSaveButton();
|
||||
|
||||
// Clear input
|
||||
wordsInput.value = '';
|
||||
wordsInput.style.borderColor = '#e0e0e0';
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de l'ajout du device: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
removeDevice(address: string) {
|
||||
if (this.pairedDevices.length <= 1) {
|
||||
this.showStatus(
|
||||
'❌ Impossible de supprimer le dernier device. Il doit en rester au moins un.',
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pairedDevices = this.pairedDevices.filter(addr => addr !== address);
|
||||
this.updateUI();
|
||||
this.enableSaveButton();
|
||||
this.showStatus('✅ Device supprimé de la liste', 'success');
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
const deviceList = this.shadowRoot!.getElementById('deviceList');
|
||||
if (deviceList) {
|
||||
deviceList.innerHTML = this.pairedDevices
|
||||
.map(
|
||||
(address, index) => `
|
||||
<li class="device-item">
|
||||
<div class="device-info">
|
||||
<strong>Device ${index + 1}</strong>
|
||||
<div class="device-address">${address}</div>
|
||||
</div>
|
||||
${
|
||||
this.pairedDevices.length > 1
|
||||
? `
|
||||
<button class="remove-btn" data-address="${address}">🗑️ Supprimer</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
enableSaveButton() {
|
||||
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
|
||||
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
|
||||
|
||||
if (saveBtn) {saveBtn.disabled = false;}
|
||||
if (cancelBtn) {cancelBtn.disabled = false;}
|
||||
}
|
||||
|
||||
async saveChanges() {
|
||||
if (!this.service) {return;}
|
||||
|
||||
try {
|
||||
// Update the pairing process with new devices
|
||||
const pairingProcessId = this.service.getPairingProcessId();
|
||||
if (pairingProcessId) {
|
||||
// This would need to be implemented to update the process
|
||||
this.showStatus('✅ Modifications sauvegardées !', 'success');
|
||||
this.disableSaveButtons();
|
||||
}
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de la sauvegarde: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
cancelChanges() {
|
||||
// Reload original data
|
||||
this.loadDeviceData();
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
this.disableSaveButtons();
|
||||
this.showStatus('❌ Modifications annulées', 'success');
|
||||
}
|
||||
|
||||
disableSaveButtons() {
|
||||
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
|
||||
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
|
||||
|
||||
if (saveBtn) {saveBtn.disabled = true;}
|
||||
if (cancelBtn) {cancelBtn.disabled = true;}
|
||||
}
|
||||
|
||||
async importAccount() {
|
||||
try {
|
||||
// Create file input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async e => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const _data = JSON.parse(text);
|
||||
// Data parsed but not used yet (for future use)
|
||||
|
||||
// Import the account data
|
||||
if (window.importJSON) {
|
||||
await window.importJSON();
|
||||
this.showStatus('✅ Compte importé avec succès !', 'success');
|
||||
// Reload the page to apply changes
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showStatus("❌ Fonction d'import non disponible", 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de l'import: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de l'import: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async exportAccount() {
|
||||
try {
|
||||
if (window.createBackUp) {
|
||||
await window.createBackUp();
|
||||
this.showStatus('✅ Compte exporté avec succès !', 'success');
|
||||
} else {
|
||||
this.showStatus("❌ Fonction d'export non disponible", 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de l'export: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async criticalExport() {
|
||||
// Triple confirmation for critical export
|
||||
const confirm1 = confirm(
|
||||
'🚨 EXPORT CRITIQUE: Cette action va exposer votre CLÉ PRIVÉE.\n\nCette clé permet de signer des transactions sans interaction sur le 2ème device.\n\nÊtes-vous sûr de vouloir continuer ?'
|
||||
);
|
||||
if (!confirm1) {return;}
|
||||
|
||||
const confirm2 = confirm(
|
||||
'⚠️ SÉCURITÉ: Votre clé privée sera visible en clair.\n\nAssurez-vous que personne ne peut voir votre écran.\n\nContinuer ?'
|
||||
);
|
||||
if (!confirm2) {return;}
|
||||
|
||||
const confirm3 = prompt(
|
||||
'🔐 DERNIÈRE CONFIRMATION: Cette clé privée donne un accès TOTAL à votre compte.\n\nTapez "EXPORTER" pour confirmer:'
|
||||
);
|
||||
if (confirm3 !== 'EXPORTER') {
|
||||
alert('❌ Export critique annulé');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the device's private key
|
||||
// @ts-ignore - deviceRaw is guaranteed to be non-null after the check below
|
||||
const deviceRaw = await this.service.getDeviceFromDatabase();
|
||||
if (!deviceRaw?.sp_wallet) {
|
||||
throw new Error('Device ou clé privée non trouvée');
|
||||
}
|
||||
// TypeScript assertion: deviceRaw is guaranteed to be non-null after the check
|
||||
const device = deviceRaw!;
|
||||
|
||||
// Create critical export data
|
||||
const criticalData = {
|
||||
type: 'CRITICAL_EXPORT',
|
||||
timestamp: new Date().toISOString(),
|
||||
device_address: device.sp_wallet.address,
|
||||
private_key: device.sp_wallet.private_key,
|
||||
pairing_commitment: device.pairing_process_commitment,
|
||||
warning:
|
||||
'ATTENTION: Cette clé privée donne un accès total au compte. Gardez-la SECRÈTE et SÉCURISÉE.',
|
||||
instructions: [
|
||||
'1. Sauvegardez cette clé dans un endroit sûr',
|
||||
'2. Ne la partagez JAMAIS avec qui que ce soit',
|
||||
'3. Utilisez-la uniquement pour signer des transactions critiques',
|
||||
'4. En cas de compromission, changez immédiatement votre compte',
|
||||
],
|
||||
};
|
||||
|
||||
// Create and download the file
|
||||
const blob = new Blob([JSON.stringify(criticalData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `critical-export-${Date.now()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showStatus('🚨 Export critique généré - Clé privée exposée !', 'error');
|
||||
|
||||
// Show additional warning
|
||||
setTimeout(() => {
|
||||
alert(
|
||||
'🚨 EXPORT CRITIQUE TERMINÉ\n\nVotre clé privée a été exportée.\n\n⚠️ GARDEZ CE FICHIER SÉCURISÉ !'
|
||||
);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.showStatus(`❌ Erreur lors de l'export critique: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
// First confirmation
|
||||
if (
|
||||
!confirm(
|
||||
'⚠️ Êtes-vous sûr de vouloir supprimer complètement votre compte ?\n\nCette action est IRRÉVERSIBLE et supprimera :\n• Tous vos processus\n• Toutes vos données\n• Votre wallet\n• Votre historique\n\nTapez "SUPPRIMER" pour confirmer.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation with text input
|
||||
const confirmation = prompt('Tapez "SUPPRIMER" pour confirmer la suppression :');
|
||||
if (confirmation !== 'SUPPRIMER') {
|
||||
this.showStatus(
|
||||
'❌ Suppression annulée. Le texte de confirmation ne correspond pas.',
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.service) {
|
||||
this.showStatus('❌ Service non disponible', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading status
|
||||
this.showStatus('🗑️ Suppression du compte en cours...', 'success');
|
||||
|
||||
// Delete the account
|
||||
await this.service.deleteAccount();
|
||||
|
||||
// Show success message
|
||||
this.showStatus('✅ Compte supprimé avec succès ! Redirection en cours...', 'success');
|
||||
|
||||
// Reload the page to restart the application
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
secureLogger.error('Erreur lors de la suppression du compte', error as Error, { component: 'DeviceManagement' });
|
||||
this.showStatus(`❌ Erreur lors de la suppression du compte: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showStatus(message: string, type: 'success' | 'error') {
|
||||
const statusDiv = this.shadowRoot!.getElementById('statusMessage');
|
||||
if (statusDiv) {
|
||||
statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
statusDiv.innerHTML = '';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async injectAccountNav() {
|
||||
try {
|
||||
// Load account navigation HTML
|
||||
const navHtml = await fetch('/src/components/account-nav/account-nav.html').then(res =>
|
||||
res.text()
|
||||
);
|
||||
|
||||
// Create a container for the navigation
|
||||
const navContainer = document.createElement('div');
|
||||
navContainer.innerHTML = navHtml;
|
||||
navContainer.className = 'account-nav-container';
|
||||
|
||||
// Add CSS styles
|
||||
const style = document.createElement('style');
|
||||
const cssResponse = await fetch('/src/components/account-nav/account-nav.css');
|
||||
const cssText = await cssResponse.text();
|
||||
style.textContent = cssText;
|
||||
navContainer.appendChild(style);
|
||||
|
||||
// Add to document body
|
||||
document.body.appendChild(navContainer);
|
||||
|
||||
secureLogger.info('Account navigation injected', { component: 'DeviceManagement' });
|
||||
} catch (error) {
|
||||
secureLogger.error('Error injecting account navigation', error as Error, { component: 'DeviceManagement' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('device-management', DeviceManagementComponent);
|
||||
@ -1,36 +0,0 @@
|
||||
<div class="nav-wrapper">
|
||||
<div id="profile-header-container"></div>
|
||||
<div class="brand-logo">4NK</div>
|
||||
<div class="nav-right-icons">
|
||||
<div class="notification-container">
|
||||
<div class="bell-icon">
|
||||
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path
|
||||
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="notification-badge"></div>
|
||||
<div id="notification-board" class="notification-board">
|
||||
<div class="no-notification">No notifications available</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="burger-menu">
|
||||
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
|
||||
</svg>
|
||||
|
||||
<div class="menu-content" id="menu">
|
||||
<!-- <a onclick="unpair()">Revoke</a> -->
|
||||
<a onclick="importJSON()">Import</a>
|
||||
<a onclick="createBackUp()">Export</a>
|
||||
<a onclick="navigate('account')">Account</a>
|
||||
<a onclick="navigate('chat')">Chat</a>
|
||||
<a onclick="navigate('signature')">Signatures</a>
|
||||
<a onclick="navigate('process')">Process</a>
|
||||
<a onclick="disconnect()">Disconnect</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,220 +0,0 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
import { INotification } from '../../models/notification.model';
|
||||
import { currentRoute, navigate } from '../../router';
|
||||
import Services from '../../services/service';
|
||||
import { BackUp } from '~/models/backup.model';
|
||||
|
||||
let notifications = [];
|
||||
|
||||
export async function unpair() {
|
||||
const service = await Services.getInstance();
|
||||
await service.unpairDevice();
|
||||
navigate('home');
|
||||
}
|
||||
|
||||
(window as any).unpair = unpair;
|
||||
|
||||
function toggleMenu() {
|
||||
const menu = document.getElementById('menu');
|
||||
if (menu) {
|
||||
if (menu.style.display === 'block') {
|
||||
menu.style.display = 'none';
|
||||
} else {
|
||||
menu.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
(window as any).toggleMenu = toggleMenu;
|
||||
|
||||
async function getNotifications() {
|
||||
const service = await Services.getInstance();
|
||||
notifications = service.getNotifications();
|
||||
return notifications;
|
||||
}
|
||||
function openCloseNotifications() {
|
||||
const notifications = document.querySelector('.notification-board') as HTMLDivElement;
|
||||
notifications.style.display = notifications?.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
(window as any).openCloseNotifications = openCloseNotifications;
|
||||
|
||||
export async function initHeader() {
|
||||
if (currentRoute === 'account') {
|
||||
// Charger le profile-header
|
||||
const profileContainer = document.getElementById('profile-header-container');
|
||||
if (profileContainer) {
|
||||
const profileHeaderHtml = await fetch('/src/components/profile-header/profile-header.html').then((res) => res.text());
|
||||
profileContainer.innerHTML = profileHeaderHtml;
|
||||
|
||||
// Initialiser les données du profil
|
||||
loadUserProfile();
|
||||
}
|
||||
}
|
||||
if (currentRoute === 'home') {
|
||||
hideSomeFunctionnalities();
|
||||
} else {
|
||||
fetchNotifications();
|
||||
setInterval(fetchNotifications, 2 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function hideSomeFunctionnalities() {
|
||||
const bell = document.querySelector('.bell-icon') as HTMLDivElement;
|
||||
if (bell) bell.style.display = 'none';
|
||||
const notifBadge = document.querySelector('.notification-badge') as HTMLDivElement;
|
||||
if (notifBadge) notifBadge.style.display = 'none';
|
||||
const actions = document.querySelectorAll('.menu-content a') as NodeListOf<HTMLAnchorElement>;
|
||||
const excludedActions = ['Import', 'Export'];
|
||||
for (const action of actions) {
|
||||
if (!excludedActions.includes(action.innerHTML)) {
|
||||
action.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setNotification(notifications: any[]): Promise<void> {
|
||||
const badge = document.querySelector('.notification-badge') as HTMLDivElement;
|
||||
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
|
||||
if (notifications?.length) {
|
||||
badge.innerText = notifications.length.toString();
|
||||
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
|
||||
notificationBoard.querySelectorAll('.notification-element')?.forEach((elem) => elem.remove());
|
||||
noNotifications.style.display = 'none';
|
||||
for (const notif of notifications) {
|
||||
const notifElement = document.createElement('div');
|
||||
notifElement.className = 'notification-element';
|
||||
notifElement.setAttribute('notif-id', notif.processId);
|
||||
notifElement.innerHTML = `
|
||||
<div>Validation required : </div>
|
||||
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
|
||||
`;
|
||||
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
|
||||
notificationBoard.appendChild(notifElement);
|
||||
notifElement.addEventListener('click', async () => {
|
||||
const modalService = await ModalService.getInstance();
|
||||
modalService.injectValidationModal(notif);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
noNotifications.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
const service = await Services.getInstance();
|
||||
const data = service.getNotifications();
|
||||
setNotification(data);
|
||||
}
|
||||
|
||||
async function loadUserProfile() {
|
||||
// Charger les données du profil depuis le localStorage
|
||||
const userName = localStorage.getItem('userName');
|
||||
const userLastName = localStorage.getItem('userLastName');
|
||||
const userAvatar = localStorage.getItem('userAvatar') || 'https://via.placeholder.com/150';
|
||||
const userBanner = localStorage.getItem('userBanner') || 'https://via.placeholder.com/800x200';
|
||||
|
||||
// Mettre à jour les éléments du DOM
|
||||
const nameElement = document.querySelector('.user-name');
|
||||
const lastNameElement = document.querySelector('.user-lastname');
|
||||
const avatarElement = document.querySelector('.avatar');
|
||||
const bannerElement = document.querySelector('.banner-image');
|
||||
|
||||
if (nameElement) nameElement.textContent = userName;
|
||||
if (lastNameElement) lastNameElement.textContent = userLastName;
|
||||
if (avatarElement) (avatarElement as HTMLImageElement).src = userAvatar;
|
||||
if (bannerElement) (bannerElement as HTMLImageElement).src = userBanner;
|
||||
}
|
||||
|
||||
async function importJSON() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content: BackUp = JSON.parse(e.target?.result as string);
|
||||
const service = await Services.getInstance();
|
||||
await service.importJSON(content);
|
||||
alert('Import réussi');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
alert("Erreur lors de l'import: " + error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
(window as any).importJSON = importJSON;
|
||||
|
||||
async function createBackUp() {
|
||||
const service = await Services.getInstance();
|
||||
const backUp = await service.createBackUp();
|
||||
if (!backUp) {
|
||||
console.error("No device to backup");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const backUpJson = JSON.stringify(backUp, null, 2)
|
||||
const blob = new Blob([backUpJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '4nk-backup.json';
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log('Backup successfully prepared for download');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).createBackUp = createBackUp;
|
||||
|
||||
async function disconnect() {
|
||||
console.log('Disconnecting...');
|
||||
try {
|
||||
localStorage.clear();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('4nk');
|
||||
request.onsuccess = () => {
|
||||
console.log('IndexedDB deleted successfully');
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onblocked = () => {
|
||||
console.log('Database deletion was blocked');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map(registration => registration.unregister()));
|
||||
console.log('Service worker unregistered');
|
||||
|
||||
navigate('home');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.origin;
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect:', error);
|
||||
// force reload
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).disconnect = disconnect;
|
||||
143
src/components/iframe-pairing/iframe-pairing.ts
Normal file
143
src/components/iframe-pairing/iframe-pairing.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { MessageType } from '../../models/process.model';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
|
||||
export class IframePairingComponent {
|
||||
private iframe: HTMLIFrameElement | null = null;
|
||||
private isReady = false;
|
||||
private messageId = 0;
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// Listen for messages from iframe
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
}
|
||||
|
||||
private handleMessage(event: MessageEvent) {
|
||||
const { type, data } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case 'IFRAME_READY':
|
||||
secureLogger.info('Iframe pairing service is ready', { component: 'IframePairingComponent' });
|
||||
this.isReady = true;
|
||||
break;
|
||||
case MessageType.PAIRING_4WORDS_WORDS_GENERATED:
|
||||
this.onWordsGenerated(data);
|
||||
break;
|
||||
case MessageType.PAIRING_4WORDS_STATUS_UPDATE:
|
||||
this.onStatusUpdate(data);
|
||||
break;
|
||||
case MessageType.PAIRING_4WORDS_SUCCESS:
|
||||
this.onPairingSuccess(data);
|
||||
break;
|
||||
case MessageType.PAIRING_4WORDS_ERROR:
|
||||
this.onPairingError(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public createHiddenIframe(): void {
|
||||
if (this.iframe) {
|
||||
return; // Already created
|
||||
}
|
||||
|
||||
// Create hidden iframe
|
||||
this.iframe = document.createElement('iframe');
|
||||
this.iframe.src = '/src/pages/iframe-pairing.html';
|
||||
this.iframe.style.cssText = `
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.iframe);
|
||||
secureLogger.debug('Hidden iframe created for pairing', { component: 'IframePairingComponent' });
|
||||
}
|
||||
|
||||
public async createPairing(): Promise<void> {
|
||||
if (!this.isReady) {
|
||||
throw new Error('Iframe pairing service not ready');
|
||||
}
|
||||
|
||||
const messageId = ++this.messageId;
|
||||
this.iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: MessageType.PAIRING_4WORDS_CREATE,
|
||||
data: {},
|
||||
messageId,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
public async joinPairing(words: string): Promise<void> {
|
||||
if (!this.isReady) {
|
||||
throw new Error('Iframe pairing service not ready');
|
||||
}
|
||||
|
||||
const messageId = ++this.messageId;
|
||||
this.iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: MessageType.PAIRING_4WORDS_JOIN,
|
||||
data: { words },
|
||||
messageId,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
private onWordsGenerated(data: any) {
|
||||
secureLogger.info('4 words generated', { component: 'IframePairingComponent', hasWords: !!data.words });
|
||||
// Emit custom event for the parent application
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('pairing-words-generated', {
|
||||
detail: { words: data.words },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onStatusUpdate(data: any) {
|
||||
secureLogger.debug('Pairing status update', { component: 'IframePairingComponent', status: data.status });
|
||||
// Emit custom event for the parent application
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('pairing-status-update', {
|
||||
detail: { status: data.status, type: data.type },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onPairingSuccess(data: any) {
|
||||
secureLogger.info('Pairing successful', { component: 'IframePairingComponent', message: data.message });
|
||||
// Emit custom event for the parent application
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('pairing-success', {
|
||||
detail: { message: data.message },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private onPairingError(data: any) {
|
||||
secureLogger.error('Pairing error', { component: 'IframePairingComponent', error: data.error });
|
||||
// Emit custom event for the parent application
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('pairing-error', {
|
||||
detail: { error: data.error },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.iframe) {
|
||||
this.iframe.remove();
|
||||
this.iframe = null;
|
||||
}
|
||||
this.isReady = false;
|
||||
}
|
||||
}
|
||||
@ -9,5 +9,7 @@ export async function closeLoginModal() {
|
||||
router.closeLoginModal();
|
||||
}
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
window.confirmLogin = confirmLogin;
|
||||
window.closeLoginModal = closeLoginModal;
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
<div id="modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to pair device?<br />
|
||||
Attempting to pair device with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
with device with address <br />
|
||||
<strong>{{device2}}</strong>
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,13 +0,0 @@
|
||||
import ModalService from '../../services/modal.service';
|
||||
|
||||
const modalService = await ModalService.getInstance();
|
||||
export async function confirm() {
|
||||
modalService.confirmPairing();
|
||||
}
|
||||
|
||||
export async function closeConfirmationModal() {
|
||||
modalService.closeConfirmationModal();
|
||||
}
|
||||
|
||||
(window as any).confirm = confirm;
|
||||
(window as any).closeConfirmationModal = closeConfirmationModal;
|
||||
@ -1,14 +0,0 @@
|
||||
<div id="creation-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to create a 4NK member?<br />
|
||||
Attempting to create a member with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,8 +0,0 @@
|
||||
<div id="waiting-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Waiting for Device 2...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,73 +0,0 @@
|
||||
import QrScanner from 'qr-scanner';
|
||||
import Services from '../../services/service';
|
||||
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
|
||||
|
||||
export default class QrScannerComponent extends HTMLElement {
|
||||
videoElement: any;
|
||||
wrapper: any;
|
||||
qrScanner: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.style.position = 'relative';
|
||||
this.wrapper.style.width = '150px';
|
||||
this.wrapper.style.height = '150px';
|
||||
|
||||
this.videoElement = document.createElement('video');
|
||||
this.videoElement.style.width = '100%';
|
||||
document.body?.append(this.wrapper);
|
||||
this.wrapper.prepend(this.videoElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.initializeScanner();
|
||||
}
|
||||
|
||||
async initializeScanner() {
|
||||
if (!this.videoElement) {
|
||||
console.error('Video element not found!');
|
||||
return;
|
||||
}
|
||||
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
|
||||
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
|
||||
highlightScanRegion: true,
|
||||
highlightCodeOutline: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await QrScanner.hasCamera();
|
||||
this.qrScanner.start();
|
||||
this.videoElement.style = 'height: 200px; width: 200px';
|
||||
this.shadowRoot?.appendChild(this.wrapper);
|
||||
} catch (e) {
|
||||
console.error('No camera found or error starting the QR scanner', e);
|
||||
}
|
||||
}
|
||||
|
||||
async onQrCodeScanned(result: any) {
|
||||
console.log(`QR Code detected:`, result);
|
||||
const data = result.data;
|
||||
const scannedUrl = new URL(data);
|
||||
|
||||
// Extract the 'sp_address' parameter
|
||||
const spAddress = scannedUrl.searchParams.get('sp_address');
|
||||
if (spAddress) {
|
||||
// Call the sendPairingTx function with the extracted sp_address
|
||||
try {
|
||||
await prepareAndSendPairingTx(spAddress);
|
||||
} catch (e) {
|
||||
console.error('Failed to pair:', e);
|
||||
}
|
||||
}
|
||||
this.qrScanner.stop(); // if you want to stop scanning after one code is detected
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.qrScanner) {
|
||||
this.qrScanner.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('qr-scanner', QrScannerComponent);
|
||||
264
src/components/secure-credentials/secure-credentials.css
Normal file
264
src/components/secure-credentials/secure-credentials.css
Normal file
@ -0,0 +1,264 @@
|
||||
.secure-credentials-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.credentials-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.credentials-header h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.credentials-description {
|
||||
color: #6c757d;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.credentials-section {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.credentials-section h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.3rem;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.password-strength.weak {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.password-strength.medium {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.password-strength.strong {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background-color: #138496;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.credentials-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #6c757d;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.credentials-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.credentials-messages {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.security-indicator {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.security-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.security-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.security-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.secure-credentials-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.credentials-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.credentials-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
108
src/components/secure-credentials/secure-credentials.html
Normal file
108
src/components/secure-credentials/secure-credentials.html
Normal file
@ -0,0 +1,108 @@
|
||||
<div class="secure-credentials-container">
|
||||
<div class="credentials-header">
|
||||
<h2>🔐 Credentials Sécurisés</h2>
|
||||
<p class="credentials-description">
|
||||
Gestion sécurisée des clés de spend et de scan avec PBKDF2 et credentials du navigateur
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="credentials-actions">
|
||||
<!-- Création de credentials -->
|
||||
<div id="create-credentials-section" class="credentials-section">
|
||||
<h3>Créer de nouveaux credentials</h3>
|
||||
<form id="create-credentials-form">
|
||||
<div class="form-group">
|
||||
<label for="password">Mot de passe sécurisé :</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Entrez un mot de passe fort"
|
||||
required
|
||||
minlength="8"
|
||||
/>
|
||||
<div id="password-strength" class="password-strength"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirmer le mot de passe :</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm-password"
|
||||
name="confirm-password"
|
||||
placeholder="Confirmez le mot de passe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="create-credentials-btn" class="btn btn-primary">
|
||||
🔐 Créer les credentials sécurisés
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Accès aux credentials existants -->
|
||||
<div id="access-credentials-section" class="credentials-section" style="display: none;">
|
||||
<h3>Accéder aux credentials existants</h3>
|
||||
<form id="access-credentials-form">
|
||||
<div class="form-group">
|
||||
<label for="access-password">Mot de passe :</label>
|
||||
<input
|
||||
type="password"
|
||||
id="access-password"
|
||||
name="access-password"
|
||||
placeholder="Entrez votre mot de passe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="access-credentials-btn" class="btn btn-secondary">
|
||||
🔓 Accéder aux credentials
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Gestion des credentials -->
|
||||
<div id="manage-credentials-section" class="credentials-section" style="display: none;">
|
||||
<h3>Gestion des credentials</h3>
|
||||
<div class="credentials-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Status :</span>
|
||||
<span id="credentials-status" class="value">Chargement...</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Clé de spend :</span>
|
||||
<span id="spend-key-status" class="value">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Clé de scan :</span>
|
||||
<span id="scan-key-status" class="value">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Créé le :</span>
|
||||
<span id="credentials-timestamp" class="value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credentials-actions">
|
||||
<button id="refresh-credentials-btn" class="btn btn-info">
|
||||
🔄 Actualiser
|
||||
</button>
|
||||
<button id="delete-credentials-btn" class="btn btn-danger">
|
||||
🗑️ Supprimer les credentials
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages de statut -->
|
||||
<div id="credentials-messages" class="credentials-messages"></div>
|
||||
|
||||
<!-- Indicateur de sécurité -->
|
||||
<div class="security-indicator">
|
||||
<div class="security-badge">
|
||||
<span class="security-icon">🛡️</span>
|
||||
<span class="security-text">Credentials sécurisés avec PBKDF2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
392
src/components/secure-credentials/secure-credentials.ts
Normal file
392
src/components/secure-credentials/secure-credentials.ts
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* SecureCredentialsComponent - Composant pour la gestion des credentials sécurisés
|
||||
* Interface utilisateur pour la gestion des clés de spend et de scan avec PBKDF2
|
||||
*/
|
||||
import { SecureCredentialsService } from '../../services/secure-credentials.service';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
import { eventBus } from '../../services/event-bus';
|
||||
|
||||
export class SecureCredentialsComponent {
|
||||
private container: HTMLElement | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le composant
|
||||
*/
|
||||
private async init(): Promise<void> {
|
||||
try {
|
||||
this.container = document.getElementById('secure-credentials-container');
|
||||
if (!this.container) {
|
||||
throw new Error('Secure credentials container not found');
|
||||
}
|
||||
|
||||
await this.loadHTML();
|
||||
await this.loadCSS();
|
||||
this.attachEventListeners();
|
||||
await this.updateUI();
|
||||
|
||||
this.isInitialized = true;
|
||||
|
||||
secureLogger.info('SecureCredentialsComponent initialized', {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'init',
|
||||
});
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to initialize SecureCredentialsComponent', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'init',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le HTML du composant
|
||||
*/
|
||||
private async loadHTML(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/src/components/secure-credentials/secure-credentials.html');
|
||||
const html = await response.text();
|
||||
this.container!.innerHTML = html;
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to load secure credentials HTML', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'loadHTML',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le CSS du composant
|
||||
*/
|
||||
private async loadCSS(): Promise<void> {
|
||||
try {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/src/components/secure-credentials/secure-credentials.css';
|
||||
document.head.appendChild(link);
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to load secure credentials CSS', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'loadCSS',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache les écouteurs d'événements
|
||||
*/
|
||||
private attachEventListeners(): void {
|
||||
// Formulaire de création de credentials
|
||||
const createForm = document.getElementById('create-credentials-form') as HTMLFormElement;
|
||||
if (createForm) {
|
||||
createForm.addEventListener('submit', this.handleCreateCredentials.bind(this));
|
||||
}
|
||||
|
||||
// Formulaire d'accès aux credentials
|
||||
const accessForm = document.getElementById('access-credentials-form') as HTMLFormElement;
|
||||
if (accessForm) {
|
||||
accessForm.addEventListener('submit', this.handleAccessCredentials.bind(this));
|
||||
}
|
||||
|
||||
// Validation du mot de passe en temps réel
|
||||
const passwordInput = document.getElementById('password') as HTMLInputElement;
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('input', this.handlePasswordInput.bind(this));
|
||||
}
|
||||
|
||||
// Boutons d'action
|
||||
const refreshBtn = document.getElementById('refresh-credentials-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', this.handleRefreshCredentials.bind(this));
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('delete-credentials-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', this.handleDeleteCredentials.bind(this));
|
||||
}
|
||||
|
||||
// Écouter les événements du service
|
||||
eventBus.on('credentials:created', this.handleCredentialsCreated.bind(this));
|
||||
eventBus.on('credentials:deleted', this.handleCredentialsDeleted.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la création de credentials
|
||||
*/
|
||||
private async handleCreateCredentials(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirm-password') as string;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
this.showMessage('Les mots de passe ne correspondent pas', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showMessage('Création des credentials en cours...', 'info');
|
||||
|
||||
// Générer les credentials
|
||||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||||
const credentials = await secureCredentialsService.generateSecureCredentials(password);
|
||||
|
||||
// Stocker les credentials
|
||||
await secureCredentialsService.storeCredentials(credentials, password);
|
||||
|
||||
this.showMessage('Credentials créés et stockés avec succès !', 'success');
|
||||
await this.updateUI();
|
||||
|
||||
// Émettre l'événement
|
||||
eventBus.emit('credentials:created', { credentials });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
||||
this.showMessage(`Erreur lors de la création des credentials: ${errorMessage}`, 'error');
|
||||
|
||||
secureLogger.error('Failed to create credentials', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'handleCreateCredentials',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'accès aux credentials
|
||||
*/
|
||||
private async handleAccessCredentials(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const password = formData.get('access-password') as string;
|
||||
|
||||
try {
|
||||
this.showMessage('Récupération des credentials...', 'info');
|
||||
|
||||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||||
const credentials = await secureCredentialsService.retrieveCredentials(password);
|
||||
|
||||
if (credentials) {
|
||||
this.showMessage('Credentials récupérés avec succès !', 'success');
|
||||
await this.updateCredentialsInfo(credentials);
|
||||
await this.updateUI();
|
||||
} else {
|
||||
this.showMessage('Aucun credential trouvé ou mot de passe incorrect', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
||||
this.showMessage(`Erreur lors de la récupération des credentials: ${errorMessage}`, 'error');
|
||||
|
||||
secureLogger.error('Failed to access credentials', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'handleAccessCredentials',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la validation du mot de passe en temps réel
|
||||
*/
|
||||
private handlePasswordInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const password = input.value;
|
||||
|
||||
const strengthDiv = document.getElementById('password-strength');
|
||||
|
||||
if (strengthDiv) {
|
||||
strengthDiv.className = 'password-strength';
|
||||
|
||||
if (password.length === 0) {
|
||||
strengthDiv.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple password strength check
|
||||
const score =
|
||||
password.length >= 12 ? (password.length >= 16 ? 5 : 4) : password.length >= 8 ? 3 : 2;
|
||||
|
||||
if (score < 3) {
|
||||
strengthDiv.className += ' weak';
|
||||
strengthDiv.textContent = 'Mot de passe faible';
|
||||
} else if (score < 5) {
|
||||
strengthDiv.className += ' medium';
|
||||
strengthDiv.textContent = 'Mot de passe moyen';
|
||||
} else {
|
||||
strengthDiv.className += ' strong';
|
||||
strengthDiv.textContent = 'Mot de passe fort';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'actualisation des credentials
|
||||
*/
|
||||
private async handleRefreshCredentials(): Promise<void> {
|
||||
try {
|
||||
await this.updateUI();
|
||||
this.showMessage('Credentials actualisés', 'success');
|
||||
} catch {
|
||||
this.showMessage("Erreur lors de l'actualisation", 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la suppression des credentials
|
||||
*/
|
||||
private async handleDeleteCredentials(): Promise<void> {
|
||||
if (
|
||||
!confirm(
|
||||
'Êtes-vous sûr de vouloir supprimer tous les credentials ? Cette action est irréversible.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement credentials deletion
|
||||
secureLogger.warn('Credentials deletion requested but not implemented', {
|
||||
component: 'SecureCredentials',
|
||||
});
|
||||
this.showMessage('Suppression des credentials non implémentée', 'warning');
|
||||
await this.updateUI();
|
||||
|
||||
// Émettre l'événement
|
||||
eventBus.emit('credentials:deleted');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
||||
this.showMessage(`Erreur lors de la suppression: ${errorMessage}`, 'error');
|
||||
|
||||
secureLogger.error('Failed to delete credentials', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'handleDeleteCredentials',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour l'interface utilisateur
|
||||
*/
|
||||
private async updateUI(): Promise<void> {
|
||||
try {
|
||||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||||
const hasCredentials = await secureCredentialsService.hasCredentials();
|
||||
|
||||
const createSection = document.getElementById('create-credentials-section');
|
||||
const accessSection = document.getElementById('access-credentials-section');
|
||||
const manageSection = document.getElementById('manage-credentials-section');
|
||||
|
||||
if (hasCredentials) {
|
||||
createSection!.style.display = 'none';
|
||||
accessSection!.style.display = 'block';
|
||||
manageSection!.style.display = 'block';
|
||||
} else {
|
||||
createSection!.style.display = 'block';
|
||||
accessSection!.style.display = 'none';
|
||||
manageSection!.style.display = 'none';
|
||||
}
|
||||
|
||||
// Mettre à jour le statut
|
||||
const statusElement = document.getElementById('credentials-status');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = hasCredentials ? 'Disponibles' : 'Non disponibles';
|
||||
statusElement.className = hasCredentials ? 'value success' : 'value error';
|
||||
}
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to update UI', error as Error, {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'updateUI',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les informations des credentials
|
||||
*/
|
||||
private async updateCredentialsInfo(credentials: any): Promise<void> {
|
||||
const spendKeyStatus = document.getElementById('spend-key-status');
|
||||
const scanKeyStatus = document.getElementById('scan-key-status');
|
||||
const timestampElement = document.getElementById('credentials-timestamp');
|
||||
|
||||
if (spendKeyStatus) {
|
||||
spendKeyStatus.textContent = credentials.spendKey ? 'Disponible' : 'Non disponible';
|
||||
spendKeyStatus.className = credentials.spendKey ? 'value success' : 'value error';
|
||||
}
|
||||
|
||||
if (scanKeyStatus) {
|
||||
scanKeyStatus.textContent = credentials.scanKey ? 'Disponible' : 'Non disponible';
|
||||
scanKeyStatus.className = credentials.scanKey ? 'value success' : 'value error';
|
||||
}
|
||||
|
||||
if (timestampElement && credentials.timestamp) {
|
||||
const date = new Date(credentials.timestamp);
|
||||
timestampElement.textContent = date.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message à l'utilisateur
|
||||
*/
|
||||
private showMessage(message: string, type: 'success' | 'error' | 'warning' | 'info'): void {
|
||||
const messagesContainer = document.getElementById('credentials-messages');
|
||||
if (!messagesContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}`;
|
||||
messageDiv.textContent = message;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
|
||||
// Supprimer le message après 5 secondes
|
||||
setTimeout(() => {
|
||||
if (messageDiv.parentNode) {
|
||||
messageDiv.parentNode.removeChild(messageDiv);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'événement de création de credentials
|
||||
*/
|
||||
private handleCredentialsCreated(_data: any): void {
|
||||
secureLogger.info('Credentials created', { component: 'SecureCredentials' });
|
||||
this.showMessage('Credentials créés avec succès !', 'success');
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'événement de suppression de credentials
|
||||
*/
|
||||
private handleCredentialsDeleted(): void {
|
||||
this.showMessage('Credentials supprimés', 'info');
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Détruit le composant
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.isInitialized) {
|
||||
// Nettoyer les écouteurs d'événements
|
||||
eventBus.off('credentials:created', this.handleCredentialsCreated.bind(this));
|
||||
eventBus.off('credentials:deleted', this.handleCredentialsDeleted.bind(this));
|
||||
|
||||
this.isInitialized = false;
|
||||
|
||||
secureLogger.info('SecureCredentialsComponent destroyed', {
|
||||
component: 'SecureCredentialsComponent',
|
||||
operation: 'destroy',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export du composant
|
||||
export default SecureCredentialsComponent;
|
||||
297
src/components/security-mode-selector/security-mode-selector.css
Normal file
297
src/components/security-mode-selector/security-mode-selector.css
Normal file
@ -0,0 +1,297 @@
|
||||
.security-mode-selector {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.security-mode-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.security-mode-header h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.security-mode-header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.security-options {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.security-option {
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.security-option:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.security-option.selected {
|
||||
border-color: #27ae60;
|
||||
background: #f8fff8;
|
||||
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15);
|
||||
}
|
||||
|
||||
.option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.security-level {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.security-level.high {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.security-level.medium {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.security-level.low {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.security-level.critical {
|
||||
background: #f5c6cb;
|
||||
color: #721c24;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.option-description {
|
||||
color: #5a6c7d;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.option-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
background: #e8f5e8;
|
||||
color: #2d5a2d;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-warnings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #ffeaa7;
|
||||
color: #d63031;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.warning.critical {
|
||||
background: #fab1a0;
|
||||
color: #d63031;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.security-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary, .btn-danger {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #229954;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.warning-actions {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #fff3cd;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.warning-actions label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.warning-actions input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.security-mode-selector {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.security-options {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.security-option {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.option-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.security-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
<div id="security-mode-selector" class="security-mode-selector">
|
||||
<div class="security-mode-header">
|
||||
<h2>🔐 Mode de Sécurisation</h2>
|
||||
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
|
||||
</div>
|
||||
|
||||
<div class="security-options">
|
||||
<!-- Proton Pass -->
|
||||
<div class="security-option" data-mode="proton-pass">
|
||||
<div class="option-header">
|
||||
<div class="option-icon">🔒</div>
|
||||
<div class="option-title">Proton Pass</div>
|
||||
<div class="security-level high">Sécurisé</div>
|
||||
</div>
|
||||
<div class="option-description">
|
||||
Utilise Proton Pass pour l'authentification biométrique et la gestion des clés
|
||||
</div>
|
||||
<div class="option-features">
|
||||
<span class="feature">✅ Authentification biométrique</span>
|
||||
<span class="feature">✅ Chiffrement end-to-end</span>
|
||||
<span class="feature">✅ Synchronisation sécurisée</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OS Authenticator -->
|
||||
<div class="security-option" data-mode="os">
|
||||
<div class="option-header">
|
||||
<div class="option-icon">🖥️</div>
|
||||
<div class="option-title">Authentificateur OS</div>
|
||||
<div class="security-level high">Sécurisé</div>
|
||||
</div>
|
||||
<div class="option-description">
|
||||
Utilise l'authentificateur intégré de votre système d'exploitation
|
||||
</div>
|
||||
<div class="option-features">
|
||||
<span class="feature">✅ Windows Hello / Touch ID / Face ID</span>
|
||||
<span class="feature">✅ Chiffrement matériel</span>
|
||||
<span class="feature">✅ Protection par mot de passe</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application 2FA -->
|
||||
<div class="security-option" data-mode="2fa">
|
||||
<div class="option-header">
|
||||
<div class="option-icon">📱</div>
|
||||
<div class="option-title">Application 2FA</div>
|
||||
<div class="security-level low">⚠️ Non sécurisé</div>
|
||||
</div>
|
||||
<div class="option-description">
|
||||
Stockage en clair avec authentification par application 2FA
|
||||
</div>
|
||||
<div class="option-warnings">
|
||||
<span class="warning">⚠️ Clés stockées en clair</span>
|
||||
<span class="warning">⚠️ Risque de compromission</span>
|
||||
<span class="warning">⚠️ Non recommandé pour des données sensibles</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aucune sécurité -->
|
||||
<div class="security-option" data-mode="none">
|
||||
<div class="option-header">
|
||||
<div class="option-icon">🚨</div>
|
||||
<div class="option-title">Aucune Sécurité</div>
|
||||
<div class="security-level critical">DANGEREUX</div>
|
||||
</div>
|
||||
<div class="option-description">
|
||||
Stockage en clair sans aucune protection
|
||||
</div>
|
||||
<div class="option-warnings">
|
||||
<span class="warning critical">🚨 Clés stockées en clair</span>
|
||||
<span class="warning critical">🚨 Accès non protégé</span>
|
||||
<span class="warning critical">🚨 RISQUE ÉLEVÉ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="security-actions">
|
||||
<button id="confirm-security-mode" class="btn-primary" disabled>
|
||||
Confirmer le Mode de Sécurisation
|
||||
</button>
|
||||
<button id="cancel-security-mode" class="btn-secondary">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmation pour les modes non sécurisés -->
|
||||
<div id="security-warning-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="warning-content">
|
||||
<!-- Contenu généré dynamiquement -->
|
||||
</div>
|
||||
<div class="warning-actions">
|
||||
<label>
|
||||
<input type="checkbox" id="understand-risks">
|
||||
Je comprends les risques et souhaite continuer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="confirm-risky-mode" class="btn-danger" disabled>
|
||||
Continuer Malgré les Risques
|
||||
</button>
|
||||
<button id="cancel-risky-mode" class="btn-secondary">
|
||||
Choisir un Autre Mode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
369
src/components/security-mode-selector/security-mode-selector.ts
Normal file
369
src/components/security-mode-selector/security-mode-selector.ts
Normal file
@ -0,0 +1,369 @@
|
||||
/**
|
||||
* SecurityModeSelector - Composant de sélection du mode de sécurisation
|
||||
* Permet à l'utilisateur de choisir comment sécuriser ses clés privées
|
||||
*/
|
||||
|
||||
export type SecurityMode = 'proton-pass' | 'os' | '2fa' | 'none';
|
||||
|
||||
export interface SecurityModeConfig {
|
||||
mode: SecurityMode;
|
||||
name: string;
|
||||
description: string;
|
||||
securityLevel: 'high' | 'medium' | 'low' | 'critical';
|
||||
requiresConfirmation: boolean;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export class SecurityModeSelector {
|
||||
private container: HTMLElement;
|
||||
private selectedMode: SecurityMode | null = null;
|
||||
private onModeSelected: (mode: SecurityMode) => void;
|
||||
private onCancel: () => void;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
onModeSelected: (mode: SecurityMode) => void,
|
||||
onCancel: () => void
|
||||
) {
|
||||
this.container = container;
|
||||
this.onModeSelected = onModeSelected;
|
||||
this.onCancel = onCancel;
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
this.container.innerHTML = `
|
||||
<div class="security-mode-selector">
|
||||
<div class="security-mode-header">
|
||||
<h2>🔐 Mode de Sécurisation</h2>
|
||||
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
|
||||
</div>
|
||||
|
||||
<div class="security-options">
|
||||
${this.getSecurityOptionsHTML()}
|
||||
</div>
|
||||
|
||||
<div class="security-actions">
|
||||
<button id="confirm-security-mode" class="btn-primary" disabled>
|
||||
Confirmer le Mode de Sécurisation
|
||||
</button>
|
||||
<button id="cancel-security-mode" class="btn-secondary">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.getWarningModalHTML()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getSecurityOptionsHTML(): string {
|
||||
const options = this.getSecurityModes();
|
||||
|
||||
return options.map(option => `
|
||||
<div class="security-option" data-mode="${option.mode}">
|
||||
<div class="option-header">
|
||||
<div class="option-icon">${this.getModeIcon(option.mode)}</div>
|
||||
<div class="option-title">${option.name}</div>
|
||||
<div class="security-level ${option.securityLevel}">
|
||||
${this.getSecurityLevelText(option.securityLevel)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-description">${option.description}</div>
|
||||
${this.getModeFeaturesHTML(option)}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
private getModeFeaturesHTML(option: SecurityModeConfig): string {
|
||||
if (option.securityLevel === 'low' || option.securityLevel === 'critical') {
|
||||
return `
|
||||
<div class="option-warnings">
|
||||
${option.warnings.map(warning => `
|
||||
<span class="warning ${option.securityLevel === 'critical' ? 'critical' : ''}">
|
||||
${warning}
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="option-features">
|
||||
${this.getModeFeatures(option.mode).map(feature => `
|
||||
<span class="feature">${feature}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private getWarningModalHTML(): string {
|
||||
return `
|
||||
<div id="security-warning-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="warning-content">
|
||||
<!-- Contenu généré dynamiquement -->
|
||||
</div>
|
||||
<div class="warning-actions">
|
||||
<label>
|
||||
<input type="checkbox" id="understand-risks">
|
||||
Je comprends les risques et souhaite continuer
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="confirm-risky-mode" class="btn-danger" disabled>
|
||||
Continuer Malgré les Risques
|
||||
</button>
|
||||
<button id="cancel-risky-mode" class="btn-secondary">
|
||||
Choisir un Autre Mode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getSecurityModes(): SecurityModeConfig[] {
|
||||
return [
|
||||
{
|
||||
mode: 'proton-pass',
|
||||
name: 'Proton Pass',
|
||||
description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
|
||||
securityLevel: 'high',
|
||||
requiresConfirmation: false,
|
||||
warnings: []
|
||||
},
|
||||
{
|
||||
mode: 'os',
|
||||
name: 'Authentificateur OS',
|
||||
description: 'Utilise l\'authentificateur intégré de votre système d\'exploitation',
|
||||
securityLevel: 'high',
|
||||
requiresConfirmation: false,
|
||||
warnings: []
|
||||
},
|
||||
{
|
||||
mode: '2fa',
|
||||
name: 'Application 2FA',
|
||||
description: 'Stockage en clair avec authentification par application 2FA',
|
||||
securityLevel: 'low',
|
||||
requiresConfirmation: true,
|
||||
warnings: [
|
||||
'⚠️ Clés stockées en clair',
|
||||
'⚠️ Risque de compromission',
|
||||
'⚠️ Non recommandé pour des données sensibles'
|
||||
]
|
||||
},
|
||||
{
|
||||
mode: 'none',
|
||||
name: 'Aucune Sécurité',
|
||||
description: 'Stockage en clair sans aucune protection',
|
||||
securityLevel: 'critical',
|
||||
requiresConfirmation: true,
|
||||
warnings: [
|
||||
'🚨 Clés stockées en clair',
|
||||
'🚨 Accès non protégé',
|
||||
'🚨 RISQUE ÉLEVÉ'
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private getModeIcon(mode: SecurityMode): string {
|
||||
const icons = {
|
||||
'proton-pass': '🔒',
|
||||
'os': '🖥️',
|
||||
'browser': '🌐',
|
||||
'2fa': '📱',
|
||||
'none': '🚨'
|
||||
};
|
||||
return icons[mode];
|
||||
}
|
||||
|
||||
private getSecurityLevelText(level: string): string {
|
||||
const texts = {
|
||||
'high': 'Sécurisé',
|
||||
'medium': 'Moyennement sécurisé',
|
||||
'low': '⚠️ Non sécurisé',
|
||||
'critical': 'DANGEREUX'
|
||||
};
|
||||
return texts[level as keyof typeof texts];
|
||||
}
|
||||
|
||||
private getModeFeatures(mode: SecurityMode): string[] {
|
||||
const features = {
|
||||
'proton-pass': [
|
||||
'✅ Authentification biométrique',
|
||||
'✅ Chiffrement end-to-end',
|
||||
'✅ Synchronisation sécurisée'
|
||||
],
|
||||
'os': [
|
||||
'✅ Windows Hello / Touch ID / Face ID',
|
||||
'✅ Chiffrement matériel',
|
||||
'✅ Protection par mot de passe'
|
||||
],
|
||||
'2fa': [],
|
||||
'none': []
|
||||
};
|
||||
return features[mode];
|
||||
}
|
||||
|
||||
private attachEventListeners(): void {
|
||||
// Sélection d'un mode
|
||||
this.container.addEventListener('click', (e) => {
|
||||
const option = (e.target as HTMLElement).closest('.security-option');
|
||||
if (option) {
|
||||
this.selectMode((option as HTMLElement).dataset.mode as SecurityMode);
|
||||
}
|
||||
});
|
||||
|
||||
// Confirmation du mode
|
||||
this.container.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).id === 'confirm-security-mode') {
|
||||
this.confirmSelection();
|
||||
}
|
||||
});
|
||||
|
||||
// Annulation
|
||||
this.container.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).id === 'cancel-security-mode') {
|
||||
this.onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion de la modal d'avertissement
|
||||
this.attachWarningModalListeners();
|
||||
}
|
||||
|
||||
private attachWarningModalListeners(): void {
|
||||
// Checkbox de compréhension des risques
|
||||
this.container.addEventListener('change', (e) => {
|
||||
if ((e.target as HTMLElement).id === 'understand-risks') {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
const confirmBtn = this.container.querySelector('#confirm-risky-mode') as HTMLButtonElement;
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Confirmation du mode risqué
|
||||
this.container.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).id === 'confirm-risky-mode') {
|
||||
this.hideWarningModal();
|
||||
this.onModeSelected(this.selectedMode!);
|
||||
}
|
||||
});
|
||||
|
||||
// Annulation du mode risqué
|
||||
this.container.addEventListener('click', (e) => {
|
||||
if ((e.target as HTMLElement).id === 'cancel-risky-mode') {
|
||||
this.hideWarningModal();
|
||||
this.clearSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private selectMode(mode: SecurityMode): void {
|
||||
// Désélectionner tous les modes
|
||||
this.container.querySelectorAll('.security-option').forEach(option => {
|
||||
option.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Sélectionner le nouveau mode
|
||||
const selectedOption = this.container.querySelector(`[data-mode="${mode}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('selected');
|
||||
this.selectedMode = mode;
|
||||
this.updateConfirmButton();
|
||||
}
|
||||
}
|
||||
|
||||
private updateConfirmButton(): void {
|
||||
const confirmBtn = this.container.querySelector('#confirm-security-mode') as HTMLButtonElement;
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = !this.selectedMode;
|
||||
}
|
||||
}
|
||||
|
||||
private confirmSelection(): void {
|
||||
if (!this.selectedMode) {return;}
|
||||
|
||||
const modeConfig = this.getSecurityModes().find(m => m.mode === this.selectedMode);
|
||||
|
||||
if (modeConfig?.requiresConfirmation) {
|
||||
this.showWarningModal(modeConfig);
|
||||
} else {
|
||||
this.onModeSelected(this.selectedMode);
|
||||
}
|
||||
}
|
||||
|
||||
private showWarningModal(modeConfig: SecurityModeConfig): void {
|
||||
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
|
||||
const warningContent = this.container.querySelector('#warning-content') as HTMLElement;
|
||||
|
||||
if (modal && warningContent) {
|
||||
warningContent.innerHTML = `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
|
||||
<p>${modeConfig.description}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
|
||||
<h5 style="color: #721c24; margin-top: 0;">⚠️ Risques identifiés :</h5>
|
||||
<ul style="color: #721c24; margin-bottom: 0;">
|
||||
${modeConfig.warnings.map(warning => `<li>${warning}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-top: 15px;">
|
||||
<p style="color: #856404; margin: 0;">
|
||||
<strong>Recommandation :</strong>
|
||||
${modeConfig.securityLevel === 'low'
|
||||
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
|
||||
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
private hideWarningModal(): void {
|
||||
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private clearSelection(): void {
|
||||
this.selectedMode = null;
|
||||
this.container.querySelectorAll('.security-option').forEach(option => {
|
||||
option.classList.remove('selected');
|
||||
});
|
||||
this.updateConfirmButton();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this.container.style.display = 'block';
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
<div id="validation-modal" class="validation-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Validate Process {{processId}}</div>
|
||||
<div class="validation-box">
|
||||
|
||||
</div>
|
||||
<div class="validation-box"></div>
|
||||
<div class="modal-action">
|
||||
<button onclick="validate()">Validate</button>
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
import ModalService from '../../services/modal.service';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
|
||||
async function validate() {
|
||||
console.log('==> VALIDATE');
|
||||
secureLogger.debug('Validation modal triggered', { component: 'ValidationModal' });
|
||||
const modalservice = await ModalService.getInstance();
|
||||
modalservice.closeValidationModal();
|
||||
}
|
||||
|
||||
export async function initValidationModal(processDiffs: any) {
|
||||
console.log("🚀 ~ initValidationModal ~ processDiffs:", processDiffs)
|
||||
secureLogger.debug('Initializing validation modal', { component: 'ValidationModal', data: processDiffs });
|
||||
for (const diff of processDiffs.diffs) {
|
||||
let diffs = ''
|
||||
let diffs = '';
|
||||
for (const value of diff) {
|
||||
diffs += `
|
||||
<div class="radio-buttons">
|
||||
@ -30,7 +31,7 @@ for(const diff of processDiffs.diffs) {
|
||||
<pre>+${value.new_value}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
|
||||
const state = `
|
||||
@ -40,15 +41,15 @@ for(const diff of processDiffs.diffs) {
|
||||
${diffs}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const box = document.querySelector('.validation-box')
|
||||
if(box) box.innerHTML += state
|
||||
`;
|
||||
const box = document.querySelector('.validation-box');
|
||||
if (box) {box.innerHTML += state;}
|
||||
}
|
||||
document.querySelectorAll('.expansion-panel-header').forEach((header) => {
|
||||
document.querySelectorAll('.expansion-panel-header').forEach(header => {
|
||||
header.addEventListener('click', function (event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const body = target.nextElementSibling as HTMLElement;
|
||||
if (body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
|
||||
if (body?.style) {body.style.display = body.style.display === 'block' ? 'none' : 'block';}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
10
src/decs.d.ts
vendored
10
src/decs.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
declare class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor();
|
||||
connectedCallback(): void;
|
||||
fetchData(): Promise<void>;
|
||||
set callback(fn: any);
|
||||
get callback(): any;
|
||||
render(): void;
|
||||
}
|
||||
export { AccountComponent };
|
||||
42
src/index.ts
42
src/index.ts
@ -1,39 +1,3 @@
|
||||
// import Services from './services/service';
|
||||
|
||||
// document.addEventListener('DOMContentLoaded', async () => {
|
||||
// try {
|
||||
|
||||
// const services = await Services.getInstance();
|
||||
// setTimeout( async () => {
|
||||
// let device = await services.getDevice()
|
||||
// console.log("🚀 ~ setTimeout ~ device:", device)
|
||||
|
||||
// if(!device) {
|
||||
// device = await services.createNewDevice();
|
||||
// } else {
|
||||
// await services.restoreDevice(device)
|
||||
// }
|
||||
// await services.restoreProcesses();
|
||||
// await services.restoreMessages();
|
||||
|
||||
// const amount = await services.getAmount();
|
||||
|
||||
// if (amount === 0n) {
|
||||
// const faucetMsg = await services.createFaucetMessage();
|
||||
// await services.sendFaucetMessage(faucetMsg);
|
||||
// }
|
||||
// if (services.isPaired()) { await services.injectProcessListPage() }
|
||||
// else {
|
||||
// const queryString = window.location.search;
|
||||
// const urlParams = new URLSearchParams(queryString)
|
||||
// const pairingAddress = urlParams.get('sp_address')
|
||||
|
||||
// if(pairingAddress) {
|
||||
// setTimeout(async () => await services.sendPairingTx(pairingAddress), 2000)
|
||||
// }
|
||||
// }
|
||||
// }, 500);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// });
|
||||
export { default as Services } from './services/service';
|
||||
export { default as Database } from './services/database.service';
|
||||
export { MessageType } from './models/process.model';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentSignature } from '~/models/signature.models';
|
||||
import { DocumentSignature } from '../models/signature.models';
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
|
||||
31
src/main.ts
31
src/main.ts
@ -1,30 +1 @@
|
||||
import { SignatureComponent } from './pages/signature/signature-component';
|
||||
import { SignatureElement } from './pages/signature/signature';
|
||||
import { ChatComponent } from './pages/chat/chat-component';
|
||||
import { ChatElement } from './pages/chat/chat';
|
||||
import { AccountComponent } from './pages/account/account-component';
|
||||
import { AccountElement } from './pages/account/account';
|
||||
|
||||
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'signature-component': SignatureComponent;
|
||||
'signature-element': SignatureElement;
|
||||
'chat-component': ChatComponent;
|
||||
'chat-element': ChatElement;
|
||||
'account-component': AccountComponent;
|
||||
'account-element': AccountElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration pour le mode indépendant
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
||||
// Initialiser les composants si nécessaire
|
||||
customElements.define('signature-component', SignatureComponent);
|
||||
customElements.define('signature-element', SignatureElement);
|
||||
customElements.define('chat-component', ChatComponent);
|
||||
customElements.define('chat-element', ChatElement);
|
||||
customElements.define('account-component', AccountComponent);
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
||||
// Main entry point - no custom elements needed for current implementation
|
||||
|
||||
@ -1,272 +0,0 @@
|
||||
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
pairing: 'pairingRows',
|
||||
wallet: 'walletRows',
|
||||
process: 'processRows',
|
||||
data: 'dataRows',
|
||||
};
|
||||
|
||||
// Initialiser le stockage des lignes par défaut dans le localStorage
|
||||
export const defaultRows = [
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
|
||||
column2: '🎊😑🎄😩',
|
||||
column3: 'Laptop',
|
||||
},
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
|
||||
column2: '🎏🎕😧🌥',
|
||||
column3: 'Phone',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockNotifications: { [key: string]: Notification[] } = {};
|
||||
|
||||
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
|
||||
|
||||
export const mockDataRows = [
|
||||
{
|
||||
column1: 'User Project',
|
||||
column2: 'private',
|
||||
column3: 'User',
|
||||
column4: '6 months',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #123',
|
||||
processName: 'User Process',
|
||||
zone: 'A',
|
||||
},
|
||||
{
|
||||
column1: 'Process Project',
|
||||
column2: 'private',
|
||||
column3: 'Process',
|
||||
column4: '1 year',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #456',
|
||||
processName: 'Process Management',
|
||||
zone: 'B',
|
||||
},
|
||||
{
|
||||
column1: 'Member Project',
|
||||
column2: 'private',
|
||||
column3: 'Member',
|
||||
column4: '3 months',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #789',
|
||||
processName: 'Member Process',
|
||||
zone: 'C',
|
||||
},
|
||||
{
|
||||
column1: 'Peer Project',
|
||||
column2: 'public',
|
||||
column3: 'Peer',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #101',
|
||||
processName: 'Peer Process',
|
||||
zone: 'D',
|
||||
},
|
||||
{
|
||||
column1: 'Payment Project',
|
||||
column2: 'confidential',
|
||||
column3: 'Payment',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #102',
|
||||
processName: 'Payment Process',
|
||||
zone: 'E',
|
||||
},
|
||||
{
|
||||
column1: 'Deposit Project',
|
||||
column2: 'private',
|
||||
column3: 'Deposit',
|
||||
column4: '6 months',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #103',
|
||||
processName: 'Deposit Process',
|
||||
zone: 'F',
|
||||
},
|
||||
{
|
||||
column1: 'Artefact Project',
|
||||
column2: 'public',
|
||||
column3: 'Artefact',
|
||||
column4: '1 year',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #104',
|
||||
processName: 'Artefact Process',
|
||||
zone: 'G',
|
||||
},
|
||||
{
|
||||
column1: 'Resolve Project',
|
||||
column2: 'private',
|
||||
column3: 'Resolve',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #105',
|
||||
processName: 'Resolve Process',
|
||||
zone: 'H',
|
||||
},
|
||||
{
|
||||
column1: 'Backup Project',
|
||||
column2: 'public',
|
||||
column3: 'Backup',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #106',
|
||||
processName: 'Backup Process',
|
||||
zone: 'I',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockProcessRows = [
|
||||
{
|
||||
process: 'User Project',
|
||||
role: 'User',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
|
||||
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Member Project',
|
||||
role: 'Member',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
|
||||
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
|
||||
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Peer Project',
|
||||
role: 'Peer',
|
||||
notification: {
|
||||
unread: 2,
|
||||
total: 4,
|
||||
messages: [
|
||||
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
|
||||
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Deposit Project',
|
||||
role: 'Deposit',
|
||||
notification: {
|
||||
unread: 1,
|
||||
total: 10,
|
||||
messages: [
|
||||
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
|
||||
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Artefact Project',
|
||||
role: 'Artefact',
|
||||
notification: {
|
||||
unread: 0,
|
||||
total: 3,
|
||||
messages: [
|
||||
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
|
||||
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Resolve Project',
|
||||
role: 'Resolve',
|
||||
notification: {
|
||||
unread: 5,
|
||||
total: 12,
|
||||
messages: [
|
||||
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
|
||||
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockContracts = {
|
||||
'Contract #123': {
|
||||
title: 'User Project Agreement',
|
||||
date: '2024-01-15',
|
||||
parties: ['Company XYZ', 'User Team'],
|
||||
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
|
||||
content: 'This agreement establishes the terms and conditions for user project management.',
|
||||
},
|
||||
'Contract #456': {
|
||||
title: 'Process Management Contract',
|
||||
date: '2024-02-01',
|
||||
parties: ['Company XYZ', 'Process Team'],
|
||||
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
|
||||
content: 'This contract defines the process management standards and procedures.',
|
||||
},
|
||||
'Contract #789': {
|
||||
title: 'Member Access Agreement',
|
||||
date: '2024-03-15',
|
||||
parties: ['Company XYZ', 'Member Team'],
|
||||
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
|
||||
content: 'This agreement outlines the terms for member access and privileges.',
|
||||
},
|
||||
'Contract #101': {
|
||||
title: 'Peer Collaboration Agreement',
|
||||
date: '2024-04-01',
|
||||
parties: ['Company XYZ', 'Peer Network'],
|
||||
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
|
||||
content: 'This contract establishes peer collaboration and networking guidelines.',
|
||||
},
|
||||
'Contract #102': {
|
||||
title: 'Payment Processing Agreement',
|
||||
date: '2024-05-01',
|
||||
parties: ['Company XYZ', 'Payment Team'],
|
||||
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
|
||||
content: 'This agreement defines payment processing terms and conditions.',
|
||||
},
|
||||
'Contract #103': {
|
||||
title: 'Deposit Management Contract',
|
||||
date: '2024-06-01',
|
||||
parties: ['Company XYZ', 'Deposit Team'],
|
||||
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
|
||||
content: 'This contract outlines deposit management procedures and security measures.',
|
||||
},
|
||||
'Contract #104': {
|
||||
title: 'Artefact Handling Agreement',
|
||||
date: '2024-07-01',
|
||||
parties: ['Company XYZ', 'Artefact Team'],
|
||||
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
|
||||
content: 'This agreement establishes artefact handling and preservation guidelines.',
|
||||
},
|
||||
'Contract #105': {
|
||||
title: 'Resolution Protocol Agreement',
|
||||
date: '2024-08-01',
|
||||
parties: ['Company XYZ', 'Resolution Team'],
|
||||
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
|
||||
content: 'This contract defines the resolution process and protocol standards.',
|
||||
},
|
||||
'Contract #106': {
|
||||
title: 'Backup Service Agreement',
|
||||
date: '2024-09-01',
|
||||
parties: ['Company XYZ', 'Backup Team'],
|
||||
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
|
||||
content: 'This agreement outlines backup service terms and recovery procedures.',
|
||||
},
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
export interface Row {
|
||||
column1: string;
|
||||
column2: string;
|
||||
column3: string;
|
||||
}
|
||||
|
||||
// Types supplémentaires nécessaires
|
||||
export interface Contract {
|
||||
title: string;
|
||||
date: string;
|
||||
parties: string[];
|
||||
terms: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface WalletRow {
|
||||
column1: string; // Label
|
||||
column2: string; // Wallet
|
||||
column3: string; // Type
|
||||
}
|
||||
|
||||
export interface DataRow {
|
||||
column1: string; // Name
|
||||
column2: string; // Visibility
|
||||
column3: string; // Role
|
||||
column4: string; // Duration
|
||||
column5: string; // Legal
|
||||
column6: string; // Contract
|
||||
processName: string;
|
||||
zone: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
message: string;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
// Déplacer l'interface en dehors de la classe, au début du fichier
|
||||
export interface NotificationMessage {
|
||||
id: number;
|
||||
read: boolean;
|
||||
date: string;
|
||||
message: string;
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 🚀 ',
|
||||
roles: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 1, name: 'Member 1' },
|
||||
{ id: 2, name: 'Member 2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Role 2',
|
||||
members: [
|
||||
{ id: 3, name: 'Member 3' },
|
||||
{ id: 4, name: 'Member 4' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group ₿',
|
||||
roles: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 5, name: 'Member 5' },
|
||||
{ id: 6, name: 'Member 6' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 🪙',
|
||||
roles: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 7, name: 'Member 7' },
|
||||
{ id: 8, name: 'Member 8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,64 +0,0 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,471 +0,0 @@
|
||||
// Définir les rôles autorisés
|
||||
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
const VISIBILITY_LEVELS = {
|
||||
PUBLIC: 'public',
|
||||
CONFIDENTIAL: 'confidential',
|
||||
PRIVATE: 'private',
|
||||
};
|
||||
|
||||
const DOCUMENT_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
PENDING: 'pending',
|
||||
IN_REVIEW: 'in_review',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
EXPIRED: 'expired',
|
||||
};
|
||||
|
||||
// Fonction pour créer un rôle
|
||||
function createRole(name, members) {
|
||||
if (!VALID_ROLES.includes(name)) {
|
||||
throw new Error(`Role "${name}" is not valid.`);
|
||||
}
|
||||
return { name, members };
|
||||
}
|
||||
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Processus 1',
|
||||
description: 'Description du processus 1',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 101,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: 'Urgency A',
|
||||
description: "Document vierge pour le plan d'urgence A",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
name: 'Urgency B',
|
||||
description: "Document vierge pour le plan d'urgence B",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
name: 'Urgency C',
|
||||
description: "Document vierge pour le plan d'urgence C",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
name: 'Document à signer',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'User',
|
||||
members: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Document User A',
|
||||
description: 'Description du document User A.',
|
||||
visibility: 'public',
|
||||
createdAt: '2024-01-01',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 1, name: 'Alice' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 2, name: 'Bob' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Document User B',
|
||||
description: 'Document vierge pour le rôle User',
|
||||
visibility: 'confidential',
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Document User C',
|
||||
description: 'Document vierge pour validation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Document User D',
|
||||
description: 'Document vierge pour approbation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Process',
|
||||
members: [
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Document Process A',
|
||||
description: 'Description du document Process A.',
|
||||
visibility: 'confidential',
|
||||
createdAt: '2024-01-10',
|
||||
deadline: '2024-03-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-12',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Document Process B',
|
||||
description: 'Document vierge pour processus interne',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Document Process C',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Process D',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Process E',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Backup',
|
||||
members: [
|
||||
{ id: 15, name: 'Oscar' },
|
||||
{ id: 16, name: 'Patricia' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Backup A',
|
||||
description: 'Document vierge pour sauvegarde',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Processus 2',
|
||||
description: 'Description du processus 2',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 201,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 203,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 204,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 205,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Artefact',
|
||||
members: [
|
||||
{ id: 17, name: 'Quinn' },
|
||||
{ id: 18, name: 'Rachel' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Artefact A',
|
||||
description: 'Document vierge pour artefact',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Document Artefact B',
|
||||
description: 'Document vierge pour validation artefact',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Resolve',
|
||||
members: [
|
||||
{ id: 19, name: 'Sam' },
|
||||
{ id: 20, name: 'Tom' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 14,
|
||||
name: 'Document Resolve A',
|
||||
description: 'Document vierge pour résolution',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Processus 3',
|
||||
description: 'Description du processus 3',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 301,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Deposit',
|
||||
members: [
|
||||
{ id: 21, name: 'Uma' },
|
||||
{ id: 22, name: 'Victor' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 15,
|
||||
name: 'Document Deposit A',
|
||||
description: 'Document vierge pour dépôt',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Document Deposit B',
|
||||
description: 'Document vierge pour validation dépôt',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Payment',
|
||||
members: [
|
||||
{ id: 23, name: 'Walter' },
|
||||
{ id: 24, name: 'Xena' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 17,
|
||||
name: 'Document Payment B',
|
||||
description: 'Document vierge pour paiement',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Document Payment C',
|
||||
description: 'Document vierge pour validation paiement',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,105 +0,0 @@
|
||||
export const membersMock = [
|
||||
// Processus 1
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice',
|
||||
avatar: 'A',
|
||||
email: 'alice@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob',
|
||||
avatar: 'B',
|
||||
email: 'bob@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Charlie',
|
||||
avatar: 'C',
|
||||
email: 'charlie@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David',
|
||||
avatar: 'D',
|
||||
email: 'david@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Oscar',
|
||||
avatar: 'O',
|
||||
email: 'oscar@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Patricia',
|
||||
avatar: 'P',
|
||||
email: 'patricia@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
|
||||
// Processus 2
|
||||
{
|
||||
id: 17,
|
||||
name: 'Quinn',
|
||||
avatar: 'Q',
|
||||
email: 'quinn@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Rachel',
|
||||
avatar: 'R',
|
||||
email: 'rachel@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Sam',
|
||||
avatar: 'S',
|
||||
email: 'sam@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Tom',
|
||||
avatar: 'T',
|
||||
email: 'tom@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
|
||||
// Processus 3
|
||||
{
|
||||
id: 21,
|
||||
name: 'Uma',
|
||||
avatar: 'U',
|
||||
email: 'uma@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Victor',
|
||||
avatar: 'V',
|
||||
email: 'victor@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Walter',
|
||||
avatar: 'W',
|
||||
email: 'walter@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Xena',
|
||||
avatar: 'X',
|
||||
email: 'xena@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
];
|
||||
@ -1,64 +0,0 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,7 +1,7 @@
|
||||
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
||||
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
|
||||
|
||||
export interface BackUp {
|
||||
device: Device,
|
||||
secrets: SecretsStore,
|
||||
processes: Record<string, Process>,
|
||||
device: Device;
|
||||
secrets: SecretsStore;
|
||||
processes: Record<string, Process>;
|
||||
}
|
||||
|
||||
@ -21,3 +21,59 @@ export interface INotification {
|
||||
sendToNotificationPage?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum MessageType {
|
||||
// Establish connection and keep alive
|
||||
LISTENING = 'LISTENING',
|
||||
REQUEST_LINK = 'REQUEST_LINK',
|
||||
LINK_ACCEPTED = 'LINK_ACCEPTED',
|
||||
CREATE_PAIRING = 'CREATE_PAIRING',
|
||||
PAIRING_CREATED = 'PAIRING_CREATED',
|
||||
SUCCESS = 'SUCCESS',
|
||||
ERROR = 'ERROR',
|
||||
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
|
||||
RENEW_TOKEN = 'RENEW_TOKEN',
|
||||
// Get various information
|
||||
GET_PAIRING_ID = 'GET_PAIRING_ID',
|
||||
GET_PROCESSES = 'GET_PROCESSES',
|
||||
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
|
||||
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
|
||||
RETRIEVE_DATA = 'RETRIEVE_DATA',
|
||||
DATA_RETRIEVED = 'DATA_RETRIEVED',
|
||||
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
|
||||
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
|
||||
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
|
||||
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
|
||||
// Processes
|
||||
CREATE_PROCESS = 'CREATE_PROCESS',
|
||||
PROCESS_CREATED = 'PROCESS_CREATED',
|
||||
CREATE_CONVERSATION = 'CREATE_CONVERSATION',
|
||||
CONVERSATION_CREATED = 'CONVERSATION_CREATED',
|
||||
UPDATE_PROCESS = 'UPDATE_PROCESS',
|
||||
PROCESS_UPDATED = 'PROCESS_UPDATED',
|
||||
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
|
||||
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
|
||||
VALIDATE_STATE = 'VALIDATE_STATE',
|
||||
STATE_VALIDATED = 'STATE_VALIDATED',
|
||||
// Hash and merkle proof
|
||||
HASH_VALUE = 'HASH_VALUE',
|
||||
VALUE_HASHED = 'VALUE_HASHED',
|
||||
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
|
||||
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
|
||||
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
|
||||
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
|
||||
// Account management
|
||||
ADD_DEVICE = 'ADD_DEVICE',
|
||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
||||
// Private key access notifications
|
||||
PRIVATE_KEY_ACCESSED = 'PRIVATE_KEY_ACCESSED',
|
||||
// 4 words pairing via iframe
|
||||
PAIRING_4WORDS_CREATE = 'PAIRING_4WORDS_CREATE',
|
||||
PAIRING_4WORDS_JOIN = 'PAIRING_4WORDS_JOIN',
|
||||
PAIRING_4WORDS_WORDS_GENERATED = 'PAIRING_4WORDS_WORDS_GENERATED',
|
||||
PAIRING_4WORDS_STATUS_UPDATE = 'PAIRING_4WORDS_STATUS_UPDATE',
|
||||
PAIRING_4WORDS_SUCCESS = 'PAIRING_4WORDS_SUCCESS',
|
||||
PAIRING_4WORDS_ERROR = 'PAIRING_4WORDS_ERROR',
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
import { AccountElement } from './account';
|
||||
import accountCss from '../../../public/style/account.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
accountElement: AccountElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
this.fetchData();
|
||||
|
||||
if (!customElements.get('account-element')) {
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
||||
const data = await (window as any).myService?.getProcesses();
|
||||
} else {
|
||||
const service = await Services.getInstance();
|
||||
const data = await service.getProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = accountCss;
|
||||
|
||||
const accountElement = document.createElement('account-element');
|
||||
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(accountElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AccountComponent };
|
||||
customElements.define('account-component', AccountComponent);
|
||||
@ -1,10 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Account</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: Arial, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.account-container {
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.account-header {
|
||||
background: linear-gradient(135deg, #3a506b 0%, #2c3e50 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.account-header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.account-content {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<account-component></account-component>
|
||||
<script type="module" src="./account.ts"></script>
|
||||
<div class="account-container">
|
||||
<div class="account-header">
|
||||
<h1>🔐 Mon Compte</h1>
|
||||
<p>Gestion sécurisée de vos devices et contrats de pairing</p>
|
||||
</div>
|
||||
<div class="account-content">
|
||||
<device-management></device-management>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/components/device-management/device-management.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
95
src/pages/birthday-setup/birthday-setup.html
Normal file
95
src/pages/birthday-setup/birthday-setup.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configuration de la Date Anniversaire - 4NK</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status.loading {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #667eea;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎂 Configuration de la Date Anniversaire</h1>
|
||||
<p class="subtitle">Mise à jour de la date anniversaire et scan des blocs</p>
|
||||
|
||||
<div class="status loading" id="status">
|
||||
🔄 Connexion aux relais...
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./birthday-setup.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
241
src/pages/birthday-setup/birthday-setup.ts
Normal file
241
src/pages/birthday-setup/birthday-setup.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Page de configuration de la date anniversaire
|
||||
* Mise à jour de la date anniversaire et scan des blocs
|
||||
*/
|
||||
|
||||
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
|
||||
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
let isInitializing = false;
|
||||
|
||||
// Type definition for update functions - parameters are template names
|
||||
/* eslint-disable no-unused-vars */
|
||||
interface UpdateFunctions {
|
||||
updateStatus: (message: string, type: 'loading' | 'success' | 'error') => void;
|
||||
updateProgress: (percent: number) => void;
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
async function initializeServices() {
|
||||
secureLogger.info('🔄 Importing services...', { component: 'BirthdaySetup' });
|
||||
const serviceModule = await import('../../services/service');
|
||||
secureLogger.debug(`Service module imported: ${Object.keys(serviceModule)}`, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
|
||||
const Services = serviceModule.default;
|
||||
if (!Services) {
|
||||
throw new Error('Services class not found in default export');
|
||||
}
|
||||
|
||||
try {
|
||||
const services = await Services.getInstance();
|
||||
secureLogger.info('✅ Services instance obtained successfully', {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
return services;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('Out of memory') || errorMessage.includes('insufficient memory')) {
|
||||
secureLogger.error('🚫 Memory error detected', { component: 'BirthdaySetup' });
|
||||
throw new Error(
|
||||
'WebAssembly initialization failed due to insufficient memory. Please refresh the page.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToSetup(page: string, updateFunctions: UpdateFunctions): void {
|
||||
secureLogger.warn(`Redirecting to ${page}...`, { component: 'BirthdaySetup' });
|
||||
updateFunctions.updateStatus(`⚠️ Redirection vers ${page}...`, 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = `/src/pages/${page}/${page}.html`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function verifyPrerequisites(updateFunctions: UpdateFunctions) {
|
||||
const pbkdf2KeyResult = await checkPBKDF2Key();
|
||||
if (!pbkdf2KeyResult) {
|
||||
redirectToSetup('security-setup', updateFunctions);
|
||||
return null;
|
||||
}
|
||||
|
||||
const wallet = await checkWalletWithRetries();
|
||||
if (!wallet) {
|
||||
redirectToSetup('wallet-setup', updateFunctions);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wallet.sp_wallet?.birthday === undefined) {
|
||||
throw new Error('Wallet found but missing required data (sp_wallet or birthday)');
|
||||
}
|
||||
|
||||
secureLogger.info(`Wallet found in database with birthday: ${wallet.sp_wallet.birthday}`, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
|
||||
return { wallet, pbkdf2KeyResult };
|
||||
}
|
||||
|
||||
async function waitForBlockHeight(services: any, updateFunctions: UpdateFunctions) {
|
||||
updateFunctions.updateStatus('⏳ Attente de la synchronisation avec le réseau...', 'loading');
|
||||
updateFunctions.updateProgress(40);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for block height from handshake'));
|
||||
}, 15000);
|
||||
|
||||
const checkBlockHeight = () => {
|
||||
const blockHeight = services.getCurrentBlockHeight();
|
||||
if (blockHeight !== -1 && blockHeight > 0) {
|
||||
secureLogger.info(`✅ Block height set from handshake: ${blockHeight}`, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkBlockHeight, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkBlockHeight();
|
||||
});
|
||||
|
||||
const currentBlockHeight = services.getCurrentBlockHeight();
|
||||
if (currentBlockHeight !== -1 && currentBlockHeight > 0) {
|
||||
secureLogger.info(`Relays connected successfully, chain_tip: ${currentBlockHeight}`, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
} else {
|
||||
throw new Error('Handshake not received or chain_tip not set');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateBirthday(services: any, updateFunctions: UpdateFunctions) {
|
||||
updateFunctions.updateStatus('🎂 Mise à jour de la date anniversaire...', 'loading');
|
||||
updateFunctions.updateProgress(60);
|
||||
|
||||
secureLogger.info('🔄 Calling updateDeviceBlockHeight()...', { component: 'BirthdaySetup' });
|
||||
await services.updateDeviceBlockHeight();
|
||||
secureLogger.info('✅ updateDeviceBlockHeight() completed successfully', {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyBirthdayUpdate(services: any, updateFunctions: UpdateFunctions) {
|
||||
updateFunctions.updateStatus('🔍 Vérification de la mise à jour...', 'loading');
|
||||
updateFunctions.updateProgress(70);
|
||||
|
||||
secureLogger.info('🔄 Verifying birthday update...', { component: 'BirthdaySetup' });
|
||||
const updatedWallet = await services.getDeviceFromDatabase();
|
||||
|
||||
if (updatedWallet?.sp_wallet?.birthday && updatedWallet.sp_wallet.birthday > 0) {
|
||||
secureLogger.info(`Birthday updated successfully: ${updatedWallet.sp_wallet.birthday}`, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
} else {
|
||||
secureLogger.error('Birthday update verification failed', new Error('Verification failed'), {
|
||||
component: 'BirthdaySetup',
|
||||
data: {
|
||||
birthday: updatedWallet?.sp_wallet?.birthday,
|
||||
hasSpWallet: !!updatedWallet?.sp_wallet,
|
||||
},
|
||||
});
|
||||
throw new Error(
|
||||
`Birthday update verification failed: expected birthday > 0, got ${updatedWallet?.sp_wallet?.birthday}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToBlockSync(updateFunctions: UpdateFunctions) {
|
||||
updateFunctions.updateStatus('🔄 Redirection vers la synchronisation des blocs...', 'loading');
|
||||
updateFunctions.updateProgress(100);
|
||||
|
||||
secureLogger.info('🎉 Birthday setup completed successfully - redirecting to block sync', {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
secureLogger.info('🔄 Executing redirect now...', { component: 'BirthdaySetup' });
|
||||
window.location.href = '/src/pages/block-sync/block-sync.html';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (isInitializing) {
|
||||
secureLogger.warn('⚠️ Birthday setup page already initializing, skipping...', {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isInitializing = true;
|
||||
secureLogger.info('🎂 Birthday setup page loaded', { component: 'BirthdaySetup' });
|
||||
secureLogger.debug(`Current URL: ${window.location.href}`, { component: 'BirthdaySetup' });
|
||||
secureLogger.debug(`Referrer: ${document.referrer}`, { component: 'BirthdaySetup' });
|
||||
|
||||
const status = document.getElementById('status') as HTMLDivElement;
|
||||
const progressBar = document.getElementById('progressBar') as HTMLDivElement;
|
||||
|
||||
let lastStatusMessage = '';
|
||||
let lastStatusType: 'loading' | 'success' | 'error' | null = null;
|
||||
|
||||
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
|
||||
if (lastStatusMessage === message && lastStatusType === type) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastStatusMessage = message;
|
||||
lastStatusType = type;
|
||||
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
status.setAttribute('data-status', type);
|
||||
}
|
||||
|
||||
function updateProgress(percent: number) {
|
||||
progressBar.style.width = `${percent}%`;
|
||||
}
|
||||
|
||||
const updateFunctions: UpdateFunctions = { updateStatus, updateProgress };
|
||||
|
||||
try {
|
||||
updateStatus('🌐 Connexion aux relais...', 'loading');
|
||||
updateProgress(20);
|
||||
|
||||
let services;
|
||||
try {
|
||||
services = await initializeServices();
|
||||
} catch (error) {
|
||||
secureLogger.error('Services not available', error as Error, { component: 'BirthdaySetup' });
|
||||
updateStatus('❌ Erreur: Services non disponibles', 'error');
|
||||
throw error;
|
||||
}
|
||||
|
||||
updateStatus('🔍 Vérification des prérequis...', 'loading');
|
||||
updateProgress(20);
|
||||
|
||||
const prerequisites = await verifyPrerequisites(updateFunctions);
|
||||
if (!prerequisites) {
|
||||
return; // Already redirected
|
||||
}
|
||||
|
||||
secureLogger.info('✅ All prerequisites verified', { component: 'BirthdaySetup' });
|
||||
|
||||
await services.connectAllRelays();
|
||||
await waitForBlockHeight(services, updateFunctions);
|
||||
await updateBirthday(services, updateFunctions);
|
||||
await verifyBirthdayUpdate(services, updateFunctions);
|
||||
redirectToBlockSync(updateFunctions);
|
||||
} catch (error) {
|
||||
secureLogger.error('Error during birthday setup', error as Error, {
|
||||
component: 'BirthdaySetup',
|
||||
});
|
||||
updateStatus('❌ Erreur lors de la configuration de la date anniversaire', 'error');
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
|
||||
});
|
||||
172
src/pages/block-sync/block-sync.html
Normal file
172
src/pages/block-sync/block-sync.html
Normal file
@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Synchronisation des Blocs - 4NK</title>
|
||||
<link rel="stylesheet" href="../../4nk.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1rem;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status.loading {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #667eea;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.sync-details {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sync-details h3 {
|
||||
margin-top: 0;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sync-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.sync-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sync-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sync-status.pending {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.sync-status.completed {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.sync-status.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔄 Synchronisation des Blocs</h1>
|
||||
<p class="subtitle">Synchronisation avec le réseau Bitcoin pour récupérer l'historique des transactions</p>
|
||||
|
||||
<div class="status loading" id="status">
|
||||
🔄 Initialisation de la synchronisation...
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
|
||||
<div class="sync-details">
|
||||
<h3>📊 Détails de la synchronisation</h3>
|
||||
<div class="sync-item">
|
||||
<span>Hauteur de bloc actuelle:</span>
|
||||
<span id="currentBlock" class="sync-status pending">En attente...</span>
|
||||
</div>
|
||||
<div class="sync-item">
|
||||
<span>Date anniversaire:</span>
|
||||
<span id="birthday" class="sync-status pending">En attente...</span>
|
||||
</div>
|
||||
<div class="sync-item">
|
||||
<span>Blocs à scanner:</span>
|
||||
<span id="blocksToScan" class="sync-status pending">En attente...</span>
|
||||
</div>
|
||||
<div class="sync-item">
|
||||
<span>Blocs scannés:</span>
|
||||
<span id="blocksScanned" class="sync-status pending">0</span>
|
||||
</div>
|
||||
<div class="sync-item">
|
||||
<span>Transactions trouvées:</span>
|
||||
<span id="transactionsFound" class="sync-status pending">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script type="module" src="./block-sync.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
276
src/pages/block-sync/block-sync.ts
Normal file
276
src/pages/block-sync/block-sync.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
import Services from '../../services/service';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
secureLogger.info('🔄 Block sync page loaded', { component: 'BlockSync' });
|
||||
|
||||
const status = document.getElementById('status') as HTMLElement;
|
||||
const progressBar = document.getElementById('progressBar') as HTMLElement;
|
||||
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
|
||||
|
||||
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
|
||||
if (status) {
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(percentage: number) {
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSyncItem(
|
||||
elementId: string,
|
||||
value: string,
|
||||
status: 'pending' | 'completed' | 'error' = 'pending'
|
||||
) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
element.className = `sync-status ${status}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion du bouton continuer (définie avant le try pour être toujours disponible)
|
||||
if (continueBtn) {
|
||||
continueBtn.addEventListener('click', async () => {
|
||||
secureLogger.info('🔗 Redirecting to pairing page...', { component: 'BlockSync' });
|
||||
window.location.href = '/src/pages/pairing/pairing.html';
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifier les prérequis
|
||||
secureLogger.debug('🔍 Verifying prerequisites...', { component: 'BlockSync' });
|
||||
updateStatus('🔍 Vérification des prérequis...', 'loading');
|
||||
|
||||
const pbkdf2KeyResult = await checkPBKDF2Key();
|
||||
if (!pbkdf2KeyResult) {
|
||||
secureLogger.warn('⚠️ PBKDF2 key not found, redirecting to security-setup...', {
|
||||
component: 'BlockSync',
|
||||
});
|
||||
updateStatus('⚠️ Redirection vers la configuration de sécurité...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const wallet = await checkWalletWithRetries();
|
||||
if (!wallet) {
|
||||
secureLogger.warn('⚠️ Wallet not found, redirecting to wallet-setup...', {
|
||||
component: 'BlockSync',
|
||||
});
|
||||
updateStatus('⚠️ Redirection vers la configuration du wallet...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wallet.sp_wallet?.birthday || wallet.sp_wallet.birthday === 0) {
|
||||
secureLogger.warn('⚠️ Birthday not configured, redirecting to birthday-setup...', {
|
||||
component: 'BlockSync',
|
||||
});
|
||||
updateStatus('⚠️ Redirection vers la configuration de la date anniversaire...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
secureLogger.info('✅ All prerequisites verified for block sync', { component: 'BlockSync' });
|
||||
updateStatus('✅ Prerequisites verified', 'success');
|
||||
|
||||
// Initialiser les services
|
||||
secureLogger.info('🔄 Waiting for services to be ready...', { component: 'BlockSync' });
|
||||
updateStatus('🔄 Initialisation des services...', 'loading');
|
||||
|
||||
let services: Services;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
secureLogger.info(
|
||||
'🔄 Attempting to get services (attempt ${attempts + 1}/${maxAttempts})...',
|
||||
{ component: 'BlockSync' }
|
||||
);
|
||||
services = await Services.getInstance();
|
||||
secureLogger.info('✅ Services initialized successfully', { component: 'BlockSync' });
|
||||
break;
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
secureLogger.warn(
|
||||
`Services initialization failed (attempt ${attempts}/${maxAttempts})`,
|
||||
error as Error,
|
||||
{ component: 'BlockSync' }
|
||||
);
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new Error('Failed to initialize services after maximum attempts');
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (!services!) {
|
||||
throw new Error('Services not initialized');
|
||||
}
|
||||
|
||||
// Vérifier si le wallet est déjà synchronisé
|
||||
const currentBlockHeight = services.getCurrentBlockHeight();
|
||||
if (currentBlockHeight === -1) {
|
||||
secureLogger.warn('⚠️ Block height not available, connecting to relays...', {
|
||||
component: 'BlockSync',
|
||||
});
|
||||
updateStatus('⚠️ Connexion aux relays...', 'loading');
|
||||
|
||||
// Attendre que les services se connectent aux relays
|
||||
await services.connectAllRelays();
|
||||
|
||||
// Attendre que la hauteur de bloc soit définie
|
||||
await services.waitForBlockHeight();
|
||||
}
|
||||
|
||||
const finalBlockHeight = services.getCurrentBlockHeight();
|
||||
const birthday = wallet.sp_wallet.birthday;
|
||||
const lastScan = wallet.sp_wallet.last_scan || 0;
|
||||
const toScan = Math.max(0, finalBlockHeight - lastScan);
|
||||
|
||||
secureLogger.info(
|
||||
'📊 Sync info: current=${finalBlockHeight}, birthday=${birthday}, lastScan=${lastScan}, toScan=${toScan}',
|
||||
{ component: 'BlockSync' }
|
||||
);
|
||||
|
||||
// Mettre à jour l'interface
|
||||
updateSyncItem('currentBlock', finalBlockHeight.toString(), 'completed');
|
||||
updateSyncItem('birthday', birthday.toString(), 'completed');
|
||||
updateSyncItem('lastScan', lastScan.toString(), 'completed');
|
||||
|
||||
if (toScan === 0) {
|
||||
secureLogger.info('✅ Wallet already synchronized', { component: 'BlockSync' });
|
||||
updateStatus('✅ Wallet déjà synchronisé', 'success');
|
||||
updateSyncItem('blocksToScan', '0', 'completed');
|
||||
updateSyncItem('blocksScanned', '0', 'completed');
|
||||
updateSyncItem('transactionsFound', '0', 'completed');
|
||||
|
||||
// Activer le bouton et rediriger automatiquement
|
||||
if (continueBtn) {
|
||||
continueBtn.disabled = false;
|
||||
continueBtn.textContent = 'Aller au pairing';
|
||||
}
|
||||
|
||||
// Auto-redirection après 3 secondes
|
||||
setTimeout(() => {
|
||||
secureLogger.info('🔗 Auto-redirecting to pairing page...', { component: 'BlockSync' });
|
||||
window.location.href = '/src/pages/pairing/pairing.html';
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher la barre de progression
|
||||
updateProgress(0);
|
||||
updateStatus('🔄 Synchronisation en cours...', 'loading');
|
||||
updateSyncItem('blocksToScan', toScan.toString(), 'pending');
|
||||
|
||||
// Intercepter les messages de progression du scan
|
||||
|
||||
// Fonction pour intercepter les messages de progression
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = (...args: any[]) => {
|
||||
const message = args.join(' ');
|
||||
const containsProgress = message.includes('Scan progress:');
|
||||
|
||||
secureLogger.debug('SDK console output intercepted', {
|
||||
component: 'BlockSync',
|
||||
containsProgress
|
||||
});
|
||||
|
||||
if (containsProgress) {
|
||||
// Extraire les informations de progression
|
||||
const progressMatch = message.match(/Scan progress: (\d+)\/(\d+) \((\d+)%\)/);
|
||||
if (progressMatch) {
|
||||
const currentBlock = parseInt(progressMatch[1]);
|
||||
const totalBlocks = parseInt(progressMatch[2]);
|
||||
const percentage = parseInt(progressMatch[3]);
|
||||
|
||||
// Mettre à jour l'interface avec les détails de progression
|
||||
updateStatus(
|
||||
`🔍 Synchronisation des blocs: ${currentBlock}/${totalBlocks} (${percentage}%)`,
|
||||
'loading'
|
||||
);
|
||||
updateProgress(percentage);
|
||||
|
||||
// Mettre à jour les éléments de synchronisation
|
||||
updateSyncItem('blocksScanned', currentBlock.toString(), 'pending');
|
||||
updateSyncItem('blocksToScan', (totalBlocks - currentBlock).toString(), 'pending');
|
||||
|
||||
secureLogger.debug('SDK progress update parsed', {
|
||||
component: 'BlockSync',
|
||||
currentBlock,
|
||||
totalBlocks,
|
||||
percentage
|
||||
});
|
||||
}
|
||||
}
|
||||
// Appeler la fonction console.log originale
|
||||
originalConsoleLog.apply(console, args);
|
||||
};
|
||||
|
||||
try {
|
||||
// Effectuer la synchronisation
|
||||
await services.updateDeviceBlockHeight();
|
||||
secureLogger.info('✅ Block scan completed successfully', { component: 'BlockSync' });
|
||||
|
||||
// Restaurer la fonction console.log originale
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
updateStatus('✅ Synchronisation terminée', 'success');
|
||||
updateProgress(100);
|
||||
updateSyncItem('blocksScanned', toScan.toString(), 'completed');
|
||||
updateSyncItem('blocksToScan', '0', 'completed');
|
||||
updateSyncItem('transactionsFound', '0', 'completed');
|
||||
|
||||
// Activer le bouton et rediriger automatiquement
|
||||
if (continueBtn) {
|
||||
continueBtn.disabled = false;
|
||||
continueBtn.textContent = 'Aller au pairing';
|
||||
}
|
||||
|
||||
// Auto-redirection après 3 secondes
|
||||
setTimeout(() => {
|
||||
secureLogger.info('🔗 Auto-redirecting to pairing page...', { component: 'BlockSync' });
|
||||
window.location.href = '/src/pages/pairing/pairing.html';
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
// Restaurer la fonction console.log originale en cas d'erreur
|
||||
console.log = originalConsoleLog;
|
||||
secureLogger.error('Error during block scan', error as Error, { component: 'BlockSync' });
|
||||
updateStatus(`❌ Erreur lors de la synchronisation: ${(error as Error).message}`, 'error');
|
||||
updateSyncItem('blocksToScan', 'Erreur', 'error');
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
secureLogger.error('Error in block sync page', error as Error, { component: 'BlockSync' });
|
||||
updateStatus(`❌ Erreur: ${(error as Error).message}`, 'error');
|
||||
|
||||
// Rediriger vers la page appropriée selon l'erreur
|
||||
const errorMessage = (error as Error).message;
|
||||
if (errorMessage.includes('PBKDF2') || errorMessage.includes('security')) {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 2000);
|
||||
} else if (errorMessage.includes('wallet') || errorMessage.includes('device')) {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
|
||||
}, 2000);
|
||||
} else if (errorMessage.includes('birthday')) {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,49 +0,0 @@
|
||||
import { ChatElement } from './chat';
|
||||
import chatCss from '../../../public/style/chat.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class ChatComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
chatElement: ChatElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
|
||||
if (!customElements.get('chat-element')) {
|
||||
customElements.define('chat-element', ChatElement);
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
// Créer l'élément chat-element
|
||||
const chatElement = document.createElement('chat-element');
|
||||
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
|
||||
this.shadowRoot.appendChild(chatElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatComponent };
|
||||
customElements.define('chat-component', ChatComponent);
|
||||
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Chat</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<chat-component></chat-component>
|
||||
<script type="module" src="./chat.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import loginHtml from './home.html?raw';
|
||||
import loginScript from './home.ts?raw';
|
||||
import loginCss from '../../4nk.css?raw';
|
||||
import { initHomePage } from './home';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
|
||||
export class LoginComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
@ -11,7 +12,7 @@ export class LoginComponent extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK LOGIN PAGE');
|
||||
secureLogger.info('Login component connected', { component: 'LoginComponent' });
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initHomePage();
|
||||
@ -22,7 +23,7 @@ export class LoginComponent extends HTMLElement {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
secureLogger.error('Callback is not a function', { component: 'LoginComponent' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +33,7 @@ export class LoginComponent extends HTMLElement {
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
{this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${loginCss}
|
||||
</style>${loginHtml}
|
||||
@ -40,7 +41,7 @@ export class LoginComponent extends HTMLElement {
|
||||
${loginScript}
|
||||
</scipt>
|
||||
|
||||
`;
|
||||
`;}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,42 +1,22 @@
|
||||
<div class="title-container">
|
||||
<h1>Create Account / New Session</h1>
|
||||
<div class="pairing-container">
|
||||
<!-- Main Pairing Interface -->
|
||||
<div id="main-pairing" class="card pairing-card">
|
||||
<div class="card-header">
|
||||
<h2>🔐 4NK Pairing</h2>
|
||||
<p class="card-description">Secure device pairing with WebAuthn authentication</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="tab1">Create an account</div>
|
||||
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-container">
|
||||
<div id="tab1" class="card tab-content active">
|
||||
<div class="card-description">Create an account :</div>
|
||||
<div class="pairing-request"></div>
|
||||
<!-- <div class="card-image qr-code">
|
||||
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
|
||||
</div> -->
|
||||
<button id="createButton" class="create-btn"></button>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div id="tab2" class="card tab-content">
|
||||
<div class="card-description">Add a device for an existing member :</div>
|
||||
<div class="card-image camera-card">
|
||||
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
|
||||
<button id="scan-btn" onclick="scanDevice()">Scan</button>
|
||||
<div class="qr-code-scanner">
|
||||
<div id="qr-reader" style="width: 200px; display: contents"></div>
|
||||
<div id="qr-reader-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Or</p>
|
||||
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
|
||||
<div id="emoji-display-2"></div> -->
|
||||
<div class="card-description">Chose a member :</div>
|
||||
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
|
||||
<!-- Options -->
|
||||
</select>
|
||||
|
||||
<button id="okButton" style="display: none">OK</button>
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" id="main-status">
|
||||
<!-- Content will be set by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-actions">
|
||||
<button id="deleteAccountButton" class="danger-btn">🗑️ Delete Account</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
72
src/pages/home/iframe-home.html
Normal file
72
src/pages/home/iframe-home.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!-- Menu buttons for iframe integration -->
|
||||
<div class="content-menu">
|
||||
<button class="menu-btn active" data-page="home">🏠 Home</button>
|
||||
<button class="menu-btn" data-page="account">👤 Account</button>
|
||||
<button class="menu-btn" data-page="settings">⚙️ Settings</button>
|
||||
<button class="menu-btn" data-page="help">❓ Help</button>
|
||||
</div>
|
||||
|
||||
<div class="pairing-container">
|
||||
<!-- Creator Flow -->
|
||||
<div id="creator-flow" class="card pairing-card" style="display: none">
|
||||
<div class="card-header">
|
||||
<h2>🔐 Create New Pairing</h2>
|
||||
</div>
|
||||
|
||||
<div class="pairing-request"></div>
|
||||
|
||||
<div class="words-display-container">
|
||||
<div class="words-label">Share these 4 words with the other device:</div>
|
||||
<div class="words-content" id="creator-words"></div>
|
||||
<button class="copy-btn" id="copyWordsBtn">📋 Copy Words</button>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" id="creator-status">
|
||||
<div class="spinner"></div>
|
||||
<span>Creating pairing process...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="createButton" class="primary-btn">Create Pairing</button>
|
||||
</div>
|
||||
|
||||
<!-- Joiner Flow -->
|
||||
<div id="joiner-flow" class="card pairing-card" style="display: none">
|
||||
<div class="card-header">
|
||||
<h2>🔗 Join Existing Pairing</h2>
|
||||
<p class="card-description">Enter the 4 words from the creator device</p>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<input
|
||||
type="text"
|
||||
id="wordsInput"
|
||||
placeholder="Enter 4 words (e.g., abandon ability able about)"
|
||||
class="words-input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div class="input-hint">Separate words with spaces</div>
|
||||
</div>
|
||||
|
||||
<div class="words-display" id="words-display-2"></div>
|
||||
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" id="joiner-status">
|
||||
<span>Ready to join</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="joinButton" class="primary-btn" disabled>Join Pairing</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-flow" class="card pairing-card">
|
||||
<div class="loading-container">
|
||||
<div class="spinner large"></div>
|
||||
<h2>Initializing...</h2>
|
||||
<p>Setting up secure pairing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
77
src/pages/iframe-pairing.html
Normal file
77
src/pages/iframe-pairing.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>4NK Pairing - Hidden</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hidden-container {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hidden-container">
|
||||
<!-- This iframe is completely hidden and only used for pairing communication -->
|
||||
<div id="pairing-status">Ready</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { MessageType } from '../models/process.model';
|
||||
import IframePairingService from '../services/iframe-pairing.service';
|
||||
import { secureLogger } from '../services/secure-logger';
|
||||
|
||||
// Initialize the iframe pairing service
|
||||
const pairingService = IframePairingService.getInstance();
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', event => {
|
||||
const { type, data } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case MessageType.PAIRING_4WORDS_CREATE:
|
||||
secureLogger.info('Parent requested pairing creation', {
|
||||
component: 'IframePairingPage'
|
||||
});
|
||||
pairingService.createPairing();
|
||||
break;
|
||||
case MessageType.PAIRING_4WORDS_JOIN:
|
||||
secureLogger.info('Parent requested pairing join', {
|
||||
component: 'IframePairingPage',
|
||||
hasWords: Boolean(data?.words)
|
||||
});
|
||||
pairingService.joinPairing(data.words);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Notify parent that iframe is ready
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'IFRAME_READY',
|
||||
data: { service: 'pairing' },
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
secureLogger.info('Hidden iframe pairing service ready', {
|
||||
component: 'IframePairingPage'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
186
src/pages/pairing/pairing.html
Normal file
186
src/pages/pairing/pairing.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pairing - 4NK</title>
|
||||
<link rel="stylesheet" href="../../4nk.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1rem;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status.loading {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* Styles pour le contenu de pairing */
|
||||
.pairing-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #666;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-indicator.loading {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-indicator.success {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.account-actions {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.danger-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.pairing-request {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.debug-info h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.debug-info pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Pairing</h1>
|
||||
<p class="subtitle">Appairage sécurisé des appareils</p>
|
||||
|
||||
<div class="status loading" id="status">
|
||||
🔄 Initialisation en cours...
|
||||
</div>
|
||||
|
||||
<div class="pairing-container" id="pairingContainer">
|
||||
<!-- Le contenu de pairing sera injecté ici -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./pairing.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
180
src/pages/pairing/pairing.ts
Normal file
180
src/pages/pairing/pairing.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import { DeviceReaderService } from '../../services/device-reader.service';
|
||||
import { secureLogger } from '../../services/secure-logger';
|
||||
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
|
||||
|
||||
// Extend WindowEventMap to include custom events
|
||||
declare global {
|
||||
interface WindowEventMap {
|
||||
'pairing-words-generated': CustomEvent;
|
||||
'pairing-success': CustomEvent;
|
||||
'pairing-error': CustomEvent;
|
||||
}
|
||||
}
|
||||
|
||||
let isInitializing = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (isInitializing) {
|
||||
secureLogger.warn('⚠️ Pairing page already initializing, skipping...', { component: 'PairingPage' });
|
||||
return;
|
||||
}
|
||||
|
||||
isInitializing = true;
|
||||
secureLogger.info('🔐 Pairing page loaded', { component: 'PairingPage' });
|
||||
|
||||
const status = document.getElementById('status') as HTMLElement;
|
||||
|
||||
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
|
||||
if (status) {
|
||||
if (type === 'error') {
|
||||
status.innerHTML = `
|
||||
<div class="error-container">
|
||||
<div class="error-icon">❌</div>
|
||||
<div class="error-content">
|
||||
<div class="error-title">Error</div>
|
||||
<div class="error-message">${message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'success') {
|
||||
status.innerHTML = `
|
||||
<div class="success-container">
|
||||
<div class="success-icon">✅</div>
|
||||
<div class="success-content">
|
||||
<div class="success-title">Success</div>
|
||||
<div class="success-message">${message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
status.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-message">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
status.className = `status ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les prérequis en base de données
|
||||
secureLogger.debug('🔍 Verifying prerequisites...', { component: 'PairingPage' });
|
||||
updateStatus('🔍 Vérification des prérequis...', 'loading');
|
||||
|
||||
try {
|
||||
secureLogger.info('🔧 Getting device reader service...', { component: 'PairingPage' });
|
||||
// const deviceReader = DeviceReaderService.getInstance(); // Not used yet
|
||||
|
||||
// Vérifier que le PBKDF2 key existe d'abord
|
||||
const pbkdf2KeyResult = await checkPBKDF2Key();
|
||||
if (!pbkdf2KeyResult) {
|
||||
secureLogger.warn('⚠️ PBKDF2 key not found, redirecting to security-setup...', { component: 'PairingPage' });
|
||||
updateStatus('⚠️ Redirection vers la configuration de sécurité...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le wallet existe en base
|
||||
const wallet = await checkWalletWithRetries();
|
||||
if (!wallet) {
|
||||
secureLogger.warn('⚠️ Wallet still not found after retries, redirecting to wallet-setup...', { component: 'PairingPage' });
|
||||
updateStatus('⚠️ Redirection vers la configuration du wallet...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que le wallet contient bien les données attendues
|
||||
if (wallet.sp_wallet?.birthday !== undefined) {
|
||||
secureLogger.info(`Wallet found in database with birthday: ${wallet.sp_wallet.birthday}`, { component: 'Pairing' });
|
||||
} else {
|
||||
throw new Error('Wallet found but missing required data (sp_wallet or birthday)');
|
||||
}
|
||||
|
||||
// Vérifier que le birthday est configuré (> 0)
|
||||
if (!wallet.sp_wallet.birthday || wallet.sp_wallet.birthday === 0) {
|
||||
secureLogger.warn('⚠️ Birthday not configured, redirecting to birthday-setup...', { component: 'PairingPage' });
|
||||
updateStatus('⚠️ Redirection vers la configuration de la date anniversaire...', 'loading');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
secureLogger.info('✅ All prerequisites verified for pairing page', { component: 'PairingPage' });
|
||||
|
||||
// Charger le contenu de pairing depuis home.html
|
||||
updateStatus('🔄 Initialisation du pairing...', 'loading');
|
||||
|
||||
// Injecter le contenu de pairing dans la zone de contenu
|
||||
const pairingContainer = document.getElementById('pairingContainer');
|
||||
if (pairingContainer) {
|
||||
// Créer un contenu HTML complet pour la page de pairing
|
||||
const pairingContent = `
|
||||
<div class="pairing-container">
|
||||
<!-- Main Pairing Interface -->
|
||||
<div id="main-pairing" class="card pairing-card">
|
||||
<div class="card-header">
|
||||
<h2>🔐 4NK Pairing</h2>
|
||||
<p class="card-description">Secure device pairing with WebAuthn authentication</p>
|
||||
</div>
|
||||
|
||||
<div class="pairing-request"></div>
|
||||
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" id="main-status">
|
||||
<!-- Content will be set by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-actions">
|
||||
<button id="deleteAccountButton" class="danger-btn">🗑️ Delete Account</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
pairingContainer.innerHTML = pairingContent;
|
||||
}
|
||||
|
||||
// Importer et initialiser la logique de pairing depuis home.ts
|
||||
const { initHomePage } = await import('../home/home');
|
||||
await initHomePage();
|
||||
|
||||
updateStatus('✅ Prêt pour le pairing', 'success');
|
||||
setTimeout(() => {
|
||||
if (status) {
|
||||
status.style.display = 'none';
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
secureLogger.info('✅ Pairing page initialization completed', { component: 'PairingPage' });
|
||||
} catch (error) {
|
||||
secureLogger.error('Error initializing pairing page', error as Error, { component: 'Pairing' });
|
||||
updateStatus(`❌ Erreur: ${(error as Error).message}`, 'error');
|
||||
|
||||
// Si l'erreur est liée aux prérequis, rediriger vers la page appropriée
|
||||
const errorMessage = (error as Error).message;
|
||||
if (errorMessage.includes('PBKDF2') || errorMessage.includes('security')) {
|
||||
secureLogger.error('⚠️ Security error detected, redirecting to security-setup...', { component: 'PairingPage' });
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 2000);
|
||||
} else if (errorMessage.includes('wallet') || errorMessage.includes('device')) {
|
||||
secureLogger.error('⚠️ Wallet error detected, redirecting to wallet-setup...', { component: 'PairingPage' });
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
|
||||
}, 2000);
|
||||
} else if (errorMessage.includes('birthday')) {
|
||||
secureLogger.error('⚠️ Birthday error detected, redirecting to birthday-setup...', { component: 'PairingPage' });
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
}, 2000);
|
||||
}
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
});
|
||||
@ -1,51 +0,0 @@
|
||||
import processHtml from './process-element.html?raw';
|
||||
import processScript from './process-element.ts?raw';
|
||||
import processCss from '../../4nk.css?raw';
|
||||
import { initProcessElement } from './process-element';
|
||||
|
||||
export class ProcessListComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
id: string = '';
|
||||
zone: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK PROCESS LIST PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initProcessElement(this.id, this.zone);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${processCss}
|
||||
</style>${processHtml}
|
||||
<script type="module">
|
||||
${processScript}
|
||||
</scipt>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('process-4nk-component')) {
|
||||
customElements.define('process-4nk-component', ProcessListComponent);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<div class="title-container">
|
||||
<h1>Process {{processTitle}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="process-container"></div>
|
||||
@ -1,50 +0,0 @@
|
||||
import { interpolate } from '../../utils/html.utils';
|
||||
import Services from '../../services/service';
|
||||
import { Process } from 'pkg/sdk_client';
|
||||
import { getCorrectDOM } from '~/utils/document.utils';
|
||||
|
||||
let currentPageStyle: HTMLStyleElement | null = null;
|
||||
|
||||
export async function initProcessElement(id: string, zone: string) {
|
||||
const processes = await getProcesses();
|
||||
const container = getCorrectDOM('process-4nk-component');
|
||||
// const currentProcess = processes.find((process) => process[0] === id)[1];
|
||||
// const currentProcess = {title: 'Hello', html: '', css: ''};
|
||||
// await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' });
|
||||
// const wrapper = document.querySelector('.process-container');
|
||||
// if (wrapper) {
|
||||
// wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' });
|
||||
// injectCss(currentProcess.css);
|
||||
// }
|
||||
}
|
||||
|
||||
async function loadPage(data?: any) {
|
||||
const content = document.getElementById('containerId');
|
||||
if (content && data) {
|
||||
if (data) {
|
||||
content.innerHTML = interpolate(content.innerHTML, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function injectCss(cssContent: string) {
|
||||
removeCss(); // Ensure that the previous CSS is removed
|
||||
|
||||
currentPageStyle = document.createElement('style');
|
||||
currentPageStyle.type = 'text/css';
|
||||
currentPageStyle.appendChild(document.createTextNode(cssContent));
|
||||
document.head.appendChild(currentPageStyle);
|
||||
}
|
||||
|
||||
function removeCss() {
|
||||
if (currentPageStyle) {
|
||||
document.head.removeChild(currentPageStyle);
|
||||
currentPageStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getProcesses(): Promise<Record<string, Process>> {
|
||||
const service = await Services.getInstance();
|
||||
const processes = await service.getProcesses();
|
||||
return processes;
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import processHtml from './process.html?raw';
|
||||
import processScript from './process.ts?raw';
|
||||
import processCss from '../../4nk.css?raw';
|
||||
import { init } from './process';
|
||||
|
||||
export class ProcessListComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK PROCESS LIST PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${processCss}
|
||||
</style>${processHtml}
|
||||
<script type="module">
|
||||
${processScript}
|
||||
</scipt>
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('process-list-4nk-component')) {
|
||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<div class="title-container">
|
||||
<h1>Process Selection</h1>
|
||||
</div>
|
||||
|
||||
<div class="process-container">
|
||||
<div class="process-card">
|
||||
<div class="process-card-description">
|
||||
<div class="input-container">
|
||||
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
|
||||
<label for="autocomplete" class="input-label">Filter processes :</label>
|
||||
<div class="selected-processes"></div>
|
||||
</div>
|
||||
<div class="process-card-content"></div>
|
||||
</div>
|
||||
<div class="process-card-action">
|
||||
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,520 +0,0 @@
|
||||
import { addSubscription } from '../../utils/subscription.utils';
|
||||
import Services from '../../services/service';
|
||||
import { getCorrectDOM } from '~/utils/html.utils';
|
||||
import { Process } from 'pkg/sdk_client';
|
||||
import chatStyle from '../../../public/style/chat.css?inline';
|
||||
import { Database } from '../../services/database.service';
|
||||
|
||||
// Initialize function, create initial tokens with itens that are already selected by the user
|
||||
export async function init() {
|
||||
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const element = container.querySelector('select') as HTMLSelectElement;
|
||||
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
|
||||
const wrapper = document.createElement('div');
|
||||
if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper);
|
||||
wrapper.classList.add('multi-select-component');
|
||||
wrapper.classList.add('input-field');
|
||||
|
||||
// Create elements of search
|
||||
const search_div = document.createElement('div');
|
||||
search_div.classList.add('search-container');
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('selected-input');
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('tabindex', '0');
|
||||
if (input) {
|
||||
addSubscription(input, 'keyup', inputChange);
|
||||
addSubscription(input, 'keydown', deletePressed);
|
||||
addSubscription(input, 'click', openOptions);
|
||||
}
|
||||
|
||||
const dropdown_icon = document.createElement('a');
|
||||
dropdown_icon.classList.add('dropdown-icon');
|
||||
|
||||
if (dropdown_icon) addSubscription(dropdown_icon, 'click', clickDropdown);
|
||||
const autocomplete_list = document.createElement('ul');
|
||||
autocomplete_list.classList.add('autocomplete-list');
|
||||
search_div.appendChild(input);
|
||||
search_div.appendChild(autocomplete_list);
|
||||
search_div.appendChild(dropdown_icon);
|
||||
|
||||
// set the wrapper as child (instead of the element)
|
||||
element.parentNode?.replaceChild(wrapper, element);
|
||||
// set element as child of wrapper
|
||||
wrapper.appendChild(element);
|
||||
wrapper.appendChild(search_div);
|
||||
|
||||
addPlaceholder(wrapper);
|
||||
}
|
||||
|
||||
function removePlaceholder(wrapper: HTMLElement) {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
input_search?.removeAttribute('placeholder');
|
||||
}
|
||||
|
||||
function addPlaceholder(wrapper: HTMLElement) {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const tokens = wrapper.querySelectorAll('.selected-wrapper');
|
||||
if (!tokens.length && !(document.activeElement === input_search)) input_search?.setAttribute('placeholder', '---------');
|
||||
}
|
||||
|
||||
// Listener of user search
|
||||
function inputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const wrapper = target?.parentNode?.parentNode;
|
||||
const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
|
||||
const input_val = target?.value;
|
||||
|
||||
if (input_val) {
|
||||
dropdown?.classList.add('active');
|
||||
populateAutocompleteList(select, input_val.trim());
|
||||
} else {
|
||||
dropdown?.classList.remove('active');
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for clicks on the wrapper, if click happens focus on the input
|
||||
function clickOnWrapper(e: Event) {
|
||||
const wrapper = e.target as HTMLElement;
|
||||
if (wrapper.tagName == 'DIV') {
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const dropdown = wrapper.querySelector('.dropdown-icon');
|
||||
if (!dropdown?.classList.contains('active')) {
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
(input_search as HTMLInputElement)?.focus();
|
||||
removePlaceholder(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
function openOptions(e: Event) {
|
||||
const input_search = e.target as HTMLElement;
|
||||
const wrapper = input_search?.parentElement?.parentElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
if (!dropdown?.classList.contains('active')) {
|
||||
const event = new Event('click');
|
||||
dropdown?.dispatchEvent(event);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Function that create a token inside of a wrapper with the given value
|
||||
function createToken(wrapper: HTMLElement, value: any) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const search = wrapper.querySelector('.search-container');
|
||||
const inputInderline = container.querySelector('.selected-processes');
|
||||
// Create token wrapper
|
||||
const token = document.createElement('div');
|
||||
token.classList.add('selected-wrapper');
|
||||
const token_span = document.createElement('span');
|
||||
token_span.classList.add('selected-label');
|
||||
token_span.innerText = value;
|
||||
const close = document.createElement('a');
|
||||
close.classList.add('selected-close');
|
||||
close.setAttribute('tabindex', '-1');
|
||||
close.setAttribute('data-option', value);
|
||||
close.setAttribute('data-hits', '0');
|
||||
close.innerText = 'x';
|
||||
if (close) addSubscription(close, 'click', removeToken);
|
||||
token.appendChild(token_span);
|
||||
token.appendChild(close);
|
||||
inputInderline?.appendChild(token);
|
||||
}
|
||||
|
||||
// Listen for clicks in the dropdown option
|
||||
function clickDropdown(e: Event) {
|
||||
const dropdown = e.target as HTMLElement;
|
||||
const wrapper = dropdown?.parentNode?.parentNode;
|
||||
const input_search = wrapper?.querySelector('.selected-input') as HTMLInputElement;
|
||||
const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
dropdown.classList.toggle('active');
|
||||
|
||||
if (dropdown.classList.contains('active')) {
|
||||
removePlaceholder(wrapper as HTMLElement);
|
||||
input_search?.focus();
|
||||
|
||||
if (!input_search?.value) {
|
||||
populateAutocompleteList(select, '', true);
|
||||
} else {
|
||||
populateAutocompleteList(select, input_search.value);
|
||||
}
|
||||
} else {
|
||||
clearAutocompleteList(select);
|
||||
addPlaceholder(wrapper as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears the results of the autocomplete list
|
||||
function clearAutocompleteList(select: HTMLSelectElement) {
|
||||
const wrapper = select.parentNode;
|
||||
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
}
|
||||
|
||||
async function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
|
||||
const { autocomplete_options } = getOptions(select);
|
||||
|
||||
let options_to_show = [];
|
||||
|
||||
const service = await Services.getInstance();
|
||||
const mineArray: string[] = await service.getMyProcesses();
|
||||
const allProcesses = await service.getProcesses();
|
||||
const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
|
||||
|
||||
const wrapper = select.parentNode;
|
||||
const input_search = wrapper?.querySelector('.search-container');
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
|
||||
const addProcessToList = (processId:string, isMine: boolean) => {
|
||||
const li = document.createElement('li');
|
||||
li.innerText = processId;
|
||||
li.setAttribute("data-value", processId);
|
||||
|
||||
if (isMine) {
|
||||
li.classList.add("my-process");
|
||||
li.style.cssText = `color: var(--accent-color)`;
|
||||
}
|
||||
|
||||
if (li) addSubscription(li, 'click', selectOption);
|
||||
autocomplete_list?.appendChild(li);
|
||||
};
|
||||
|
||||
mineArray.forEach(processId => addProcessToList(processId, true));
|
||||
allArray.forEach(processId => addProcessToList(processId, false));
|
||||
|
||||
if (mineArray.length === 0 && allArray.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('not-cursor');
|
||||
li.innerText = 'No options found';
|
||||
autocomplete_list?.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
// Listener to autocomplete results when clicked set the selected property in the select option
|
||||
function selectOption(e: any) {
|
||||
console.log('🎯 Click event:', e);
|
||||
console.log('🎯 Target value:', e.target.dataset.value);
|
||||
|
||||
const wrapper = e.target.parentNode.parentNode.parentNode;
|
||||
const select = wrapper.querySelector('select');
|
||||
const input_search = wrapper.querySelector('.selected-input');
|
||||
const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`);
|
||||
|
||||
console.log('🎯 Selected option:', option);
|
||||
console.log('🎯 Process ID:', option?.getAttribute('data-process-id'));
|
||||
|
||||
if (e.target.dataset.value.includes('messaging')) {
|
||||
const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]);
|
||||
const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`);
|
||||
|
||||
console.log('🚀 Dispatching newMessagingProcess event:', {
|
||||
processId,
|
||||
processName: `Messaging Process ${processId}`
|
||||
});
|
||||
|
||||
// Dispatch l'événement avant la navigation
|
||||
document.dispatchEvent(new CustomEvent('newMessagingProcess', {
|
||||
detail: {
|
||||
processId: processId,
|
||||
processName: `Messaging Process ${processId}`
|
||||
}
|
||||
}));
|
||||
|
||||
// Navigation vers le chat
|
||||
const navigateEvent = new CustomEvent('navigate', {
|
||||
detail: {
|
||||
page: 'chat',
|
||||
processId: processId || ''
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(navigateEvent);
|
||||
return;
|
||||
}
|
||||
option.setAttribute('selected', '');
|
||||
createToken(wrapper, e.target.dataset.value);
|
||||
if (input_search.value) {
|
||||
input_search.value = '';
|
||||
}
|
||||
|
||||
showSelectedProcess(e.target.dataset.value);
|
||||
|
||||
input_search.focus();
|
||||
|
||||
e.target.remove();
|
||||
const autocomplete_list = wrapper.querySelector('.autocomplete-list');
|
||||
|
||||
if (!autocomplete_list.children.length) {
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('not-cursor');
|
||||
li.innerText = 'No options found';
|
||||
autocomplete_list.appendChild(li);
|
||||
}
|
||||
|
||||
const event = new Event('keyup');
|
||||
input_search.dispatchEvent(event);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// function that returns a list with the autcomplete list of matches
|
||||
function autocomplete(query: string, options: any) {
|
||||
// No query passed, just return entire list
|
||||
if (!query) {
|
||||
return options;
|
||||
}
|
||||
let options_return = [];
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (query.toLowerCase() === options[i].slice(0, query.length).toLowerCase()) {
|
||||
options_return.push(options[i]);
|
||||
}
|
||||
}
|
||||
return options_return;
|
||||
}
|
||||
|
||||
// Returns the options that are selected by the user and the ones that are not
|
||||
function getOptions(select: HTMLSelectElement) {
|
||||
// Select all the options available
|
||||
const all_options = Array.from(select.querySelectorAll('option')).map((el) => el.value);
|
||||
|
||||
// Get the options that are selected from the user
|
||||
const options_selected = Array.from(select.querySelectorAll('option:checked')).map((el: any) => el.value);
|
||||
|
||||
// Create an autocomplete options array with the options that are not selected by the user
|
||||
const autocomplete_options: any[] = [];
|
||||
all_options.forEach((option) => {
|
||||
if (!options_selected.includes(option)) {
|
||||
autocomplete_options.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
autocomplete_options.sort();
|
||||
|
||||
return {
|
||||
options_selected,
|
||||
autocomplete_options,
|
||||
};
|
||||
}
|
||||
|
||||
// Listener for when the user wants to remove a given token.
|
||||
function removeToken(e: Event) {
|
||||
// Get the value to remove
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const value_to_remove = target.dataset.option;
|
||||
const wrapper = target.parentNode?.parentNode?.parentNode;
|
||||
const input_search = wrapper?.querySelector('.selected-input');
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
// Get the options in the select to be unselected
|
||||
const option_to_unselect = wrapper?.querySelector(`select option[value="${value_to_remove}"]`);
|
||||
option_to_unselect?.removeAttribute('selected');
|
||||
// Remove token attribute
|
||||
(target.parentNode as any)?.remove();
|
||||
dropdown?.classList.remove('active');
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
const process = container.querySelector('#' + target.dataset.option);
|
||||
process?.remove();
|
||||
}
|
||||
|
||||
// Listen for 2 sequence of hits on the delete key, if this happens delete the last token if exist
|
||||
function deletePressed(e: Event) {
|
||||
const input_search = e.target as HTMLInputElement;
|
||||
const wrapper = input_search?.parentNode?.parentNode;
|
||||
const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
|
||||
const tokens = wrapper?.querySelectorAll('.selected-wrapper');
|
||||
|
||||
if (tokens?.length) {
|
||||
const last_token_x = tokens[tokens.length - 1].querySelector('a');
|
||||
let hits = +(last_token_x?.dataset?.hits || 0);
|
||||
|
||||
if (key == 8 || key == 46) {
|
||||
if (!input_search.value) {
|
||||
if (hits > 1) {
|
||||
// Trigger delete event
|
||||
const event = new Event('click');
|
||||
last_token_x?.dispatchEvent(event);
|
||||
} else {
|
||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dismiss on outside click
|
||||
addSubscription(document, 'click', () => {
|
||||
// get select that has the options available
|
||||
const select = document.querySelectorAll('[data-multi-select-plugin]');
|
||||
for (let i = 0; i < select.length; i++) {
|
||||
if (event) {
|
||||
var isClickInside = select[i].parentElement?.parentElement?.contains(event.target as Node);
|
||||
|
||||
if (!isClickInside) {
|
||||
const wrapper = select[i].parentElement?.parentElement;
|
||||
const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
//the click was outside the specifiedElement, do something
|
||||
dropdown?.classList.remove('active');
|
||||
if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
addPlaceholder(wrapper as HTMLElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function showSelectedProcess(elem: MouseEvent) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
if (elem) {
|
||||
const cardContent = container.querySelector('.process-card-content');
|
||||
|
||||
const processes = await getProcesses();
|
||||
const process = processes.find((process: any) => process[1].title === elem);
|
||||
if (process) {
|
||||
const processDiv = document.createElement('div');
|
||||
processDiv.className = 'process';
|
||||
processDiv.id = process[0];
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.className = 'process-title';
|
||||
titleDiv.innerHTML = `${process[1].title} : ${process[1].description}`;
|
||||
processDiv.appendChild(titleDiv);
|
||||
for (const zone of process.zones) {
|
||||
const zoneElement = document.createElement('div');
|
||||
zoneElement.className = 'process-element';
|
||||
const zoneId = process[1].title + '-' + zone.id;
|
||||
zoneElement.setAttribute('zone-id', zoneId);
|
||||
zoneElement.setAttribute('process-title', process[1].title);
|
||||
zoneElement.setAttribute('process-id', `${process[0]}_${zone.id}`);
|
||||
zoneElement.innerHTML = `${zone.title}: ${zone.description}`;
|
||||
addSubscription(zoneElement, 'click', select);
|
||||
processDiv.appendChild(zoneElement);
|
||||
}
|
||||
if (cardContent) cardContent.appendChild(processDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function select(event: Event) {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
const target = event.target as HTMLElement;
|
||||
const oldSelectedProcess = container.querySelector('.selected-process-zone');
|
||||
oldSelectedProcess?.classList.remove('selected-process-zone');
|
||||
if (target) {
|
||||
target.classList.add('selected-process-zone');
|
||||
}
|
||||
const name = target.getAttribute('zone-id');
|
||||
console.log('🚀 ~ select ~ name:', name);
|
||||
}
|
||||
|
||||
function goToProcessPage() {
|
||||
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
const target = container.querySelector('.selected-process-zone');
|
||||
console.log('🚀 ~ goToProcessPage ~ event:', target);
|
||||
if (target) {
|
||||
const process = target?.getAttribute('process-id');
|
||||
|
||||
console.log('=======================> going to process page', process);
|
||||
// navigate('process-element/' + process);
|
||||
document.querySelector('process-list-4nk-component')?.dispatchEvent(
|
||||
new CustomEvent('processSelected', {
|
||||
detail: {
|
||||
process: process,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).goToProcessPage = goToProcessPage;
|
||||
|
||||
async function createMessagingProcess(): Promise<void> {
|
||||
console.log('Creating messaging process');
|
||||
const service = await Services.getInstance();
|
||||
const otherMembers = [
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqd7snxfh44am8f7a3x36znkh4v0dcagcgakfux488ghsg0tny7degq4gd9q4n4us0cyp82643f2p4jgcmtwknadqwl3waf9zrynl6n7lug5tg73a",
|
||||
"tsp1qqvd8pak9fyz55rxqj90wxazqzwupf2egderc96cn84h3l84z8an9vql85scudrmwvsnltfuy9ungg7pxnhys2ft5wnf2gyr3n4ukvezygswesjuc"
|
||||
]
|
||||
},
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqgl5vawdey6wnnn2sfydcejsr06uzwsjlfa6p6yr8u4mkqwezsnvyqlazuqmxhxd8crk5eq3wfvdwv4k3tn68mkj2nj72jj39d2ngauu4unfx0q7",
|
||||
"tsp1qqthmj56gj8vvkjzwhcmswftlrf6ye7ukpks2wra92jkehqzrvx7m2q570q5vv6zj6dnxvussx2h8arvrcfwz9sp5hpdzrfugmmzz90pmnganxk28"
|
||||
]
|
||||
},
|
||||
{
|
||||
sp_addresses: [
|
||||
"tsp1qqwjtxr9jye7d40qxrsmd6h02egdwel6mfnujxzskgxvxphfya4e6qqjq4tsdmfdmtnmccz08ut24q8y58qqh4lwl3w8pvh86shlmavrt0u3smhv2",
|
||||
"tsp1qqwn7tf8q2jhmfh8757xze53vg2zc6x5u6f26h3wyty9mklvcy0wnvqhhr4zppm5uyyte4y86kljvh8r0tsmkmszqqwa3ecf2lxcs7q07d56p8sz5"
|
||||
]
|
||||
}
|
||||
];
|
||||
await service.checkConnections(otherMembers);
|
||||
const relayAddress = service.getAllRelays().pop();
|
||||
if (!relayAddress) {
|
||||
throw new Error('Empty relay address list');
|
||||
}
|
||||
const feeRate = 1;
|
||||
setTimeout(async () => {
|
||||
const createProcessReturn = await service.createMessagingProcess(otherMembers, relayAddress.spAddress, feeRate);
|
||||
const updatedProcess = createProcessReturn.updated_process.current_process;
|
||||
if (!updatedProcess) {
|
||||
console.error('Failed to retrieved new messaging process');
|
||||
return;
|
||||
}
|
||||
const processId = updatedProcess.states[0].commited_in;
|
||||
const stateId = updatedProcess.states[0].state_id;
|
||||
await service.handleApiReturn(createProcessReturn);
|
||||
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
|
||||
await service.handleApiReturn(createPrdReturn);
|
||||
const approveChangeReturn = await service.approveChange(processId, stateId);
|
||||
await service.handleApiReturn(approveChangeReturn);
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function getDescription(processId: string, process: Process): Promise<string | null> {
|
||||
const service = await Services.getInstance();
|
||||
// Get the `commited_in` value of the last state and remove it from the array
|
||||
const currentCommitedIn = process.states.pop()?.commited_in;
|
||||
|
||||
if (currentCommitedIn === undefined) {
|
||||
return null; // No states available
|
||||
}
|
||||
|
||||
// Find the last state where `commited_in` is different
|
||||
let lastDifferentState = process.states.findLast(
|
||||
state => state.commited_in !== currentCommitedIn
|
||||
);
|
||||
|
||||
if (!lastDifferentState) {
|
||||
// It means that we only have one state that is not commited yet, that can happen with process we just created
|
||||
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
|
||||
lastDifferentState = process.states.pop();
|
||||
}
|
||||
|
||||
// Take the description out of the state, if any
|
||||
const description = lastDifferentState!.pcd_commitment['description'];
|
||||
if (description) {
|
||||
const userDiff = await service.getDiffByValue(description);
|
||||
if (userDiff) {
|
||||
console.log("Successfully retrieved userDiff:", userDiff);
|
||||
return userDiff.new_value;
|
||||
} else {
|
||||
console.log("Failed to retrieve a non-null userDiff.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
150
src/pages/security-setup/security-setup.html
Normal file
150
src/pages/security-setup/security-setup.html
Normal file
@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configuration de la Sécurité - 4NK</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.security-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.security-option {
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.security-option:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f2ff;
|
||||
}
|
||||
|
||||
.security-option.selected {
|
||||
border-color: #667eea;
|
||||
background: #f0f2ff;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.continue-btn {
|
||||
width: 100%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.continue-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.continue-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Configuration de la Sécurité</h1>
|
||||
<p class="subtitle">Choisissez votre mode de sécurité pour protéger vos clés</p>
|
||||
|
||||
<div class="security-options">
|
||||
<div class="security-option" data-mode="proton-pass">
|
||||
<div class="option-title">🛡️ Clé de sécurité</div>
|
||||
<div class="option-description">Sécurité maximale avec votre gestionnaire de clés de sécurité</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="os">
|
||||
<div class="option-title">🔒 Système d'exploitation</div>
|
||||
<div class="option-description">Utilise l'authentification biométrique de votre système</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="otp">
|
||||
<div class="option-title">🔐 OTP (code à usage unique)</div>
|
||||
<div class="option-description">Code OTP généré par Proton Pass, Google Authenticator, etc.</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="password">
|
||||
<div class="option-title">🔑 Mot de passe</div>
|
||||
<div class="option-description">Chiffrement par mot de passe (non récupérable si oublié)</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="none">
|
||||
<div class="option-title">⚠️ Aucune protection</div>
|
||||
<div class="option-description">Stockage en clair (non recommandé)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warning" id="warning" style="display: none;">
|
||||
⚠️ <strong>Attention :</strong> Ce mode de sécurité n'est pas recommandé. Vos clés seront stockées en clair.
|
||||
</div>
|
||||
|
||||
<button class="continue-btn" id="continueBtn" disabled>Continuer</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./security-setup.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user