Compare commits
271 Commits
create-acc
...
int-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92473bb1c3 | ||
|
|
59f184eaea | ||
|
|
1bc594fa90 | ||
|
|
81e52c1da4 | ||
|
|
6a9bee3c99 | ||
|
|
67fc87314e | ||
|
|
c1e3a5a588 | ||
|
|
72173caeed | ||
|
|
34d57c95f7 | ||
|
|
8c83757025 | ||
|
|
662b04bfdd | ||
|
|
1542f0fe98 | ||
|
|
9db61039c6 | ||
|
|
f7b30cb495 | ||
|
|
2379af9505 | ||
|
|
18c61fd5e0 | ||
|
|
b7df1bc58e | ||
|
|
4aa19b0b2a | ||
|
|
fd351374fa | ||
|
|
8bba3e5437 | ||
|
|
e341c45639 | ||
|
|
30a3960b2e | ||
|
|
ae58d95e39 | ||
|
|
2bfee359e0 | ||
|
|
4c3328b3db | ||
|
|
e41d17f7f4 | ||
|
|
09c8493aeb | ||
|
|
cb6b4a02da | ||
|
|
1756278094 | ||
|
|
48ea3614b8 | ||
|
|
2d0bbc425c | ||
|
|
0a033dd636 | ||
|
|
c4d35331bd | ||
|
|
85c4106ae6 | ||
|
|
0b57f6c79c | ||
|
|
333101b3f4 | ||
|
|
a302149ea8 | ||
|
|
537e627ebc | ||
|
|
5e3f050132 | ||
|
|
3e53ed70d1 | ||
|
|
b9f435c2bf | ||
|
|
4f7d51e9b2 | ||
|
|
a6f54dc8f5 | ||
|
|
a40416a248 | ||
|
|
c21eb9170a | ||
|
|
d47db27583 | ||
|
|
eb6f6be67b | ||
|
|
e9f6af41a1 | ||
|
|
1c20450b05 | ||
|
|
e76f2e1db9 | ||
|
|
0cf587fa6e | ||
|
|
01b1b50d6b | ||
|
|
5da295be1a | ||
|
|
a4dd8c57f2 | ||
|
|
653b86fbc9 | ||
|
|
70f3a30f64 | ||
|
|
2dda47534b | ||
|
|
01ad5a3816 | ||
|
|
5884e8d845 | ||
|
|
fb54522bce | ||
|
|
59ad93516c | ||
|
|
6ef5186407 | ||
|
|
c89d956821 | ||
|
|
7a52bb2ee7 | ||
|
|
903ad3849d | ||
|
|
17e2cd282e | ||
|
|
e6569cf908 | ||
|
|
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 |
2
.ci-trigger
Normal file
2
.ci-trigger
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# CI Trigger
|
||||||
|
# CI Trigger Sun Sep 21 19:57:46 UTC 2025
|
||||||
10
.cursorignore
Normal file
10
.cursorignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Cursor ignore file for ihm_client
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
.DS_Store
|
||||||
|
coverage/
|
||||||
|
build/
|
||||||
|
pkg/
|
||||||
|
pkg2/
|
||||||
165
.cursorrules
Normal file
165
.cursorrules
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
# Règles globales Cursor pour les projets
|
||||||
|
|
||||||
|
## Principes généraux
|
||||||
|
- Lire impérativement le fichier `.cursorrules` au démarrage de chaque session.
|
||||||
|
- Lire tous les fichiers du dossier `docs/`, le code et les paramètres avant de commencer.
|
||||||
|
- Poser des questions et proposer des améliorations si nécessaire.
|
||||||
|
- Ajouter les leçons apprises à ce fichier `.cursorrules`.
|
||||||
|
- Écrire des documents complets et exhaustifs.
|
||||||
|
- Respecter strictement les règles de lint du Markdown.
|
||||||
|
- Préférer toujours un shell **bash** à PowerShell.
|
||||||
|
- Fermer et relancer le terminal avant chaque utilisation.
|
||||||
|
- Si le terminal est interrompu, analyser la commande précédente (interruption probablement volontaire).
|
||||||
|
- Exécuter automatiquement les étapes de résolution de problème.
|
||||||
|
- Expliquer les commandes complexes avant de les lancer.
|
||||||
|
- Compiler régulièrement et corriger toutes les erreurs avant de passer à l’étape suivante.
|
||||||
|
- Tester, documenter, compiler, aligner tag git, changelog et version avant déploiement et push.
|
||||||
|
- Utiliser `docx2txt` pour lire les fichiers `.docx`.
|
||||||
|
- Ajouter automatiquement les dépendances et rechercher systématiquement les dernières versions.
|
||||||
|
- Faire des commandes simples et claires en plusieurs étapes.
|
||||||
|
- Vérifie toujours tes hypothèses avant de commencer.
|
||||||
|
- N'oublie jamais qu'après la correction d'un problème, il faut corriger toutes les erreurs qui peuvent en découler.
|
||||||
|
|
||||||
|
## Organisation des fichiers et répertoires
|
||||||
|
- Scripts regroupés dans `scripts/`
|
||||||
|
- Configurations regroupées dans `confs/`
|
||||||
|
- Journaux regroupés dans `logs/`
|
||||||
|
- Répertoires obligatoires :
|
||||||
|
- `docs/` : documentation de toute fonctionnalité ajoutée, modifiée, supprimée ou découverte.
|
||||||
|
- `tests/` : tests liés à toute fonctionnalité ajoutée, modifiée, supprimée ou découverte.
|
||||||
|
- Remplacer les résumés (`RESUME`) par des mises à jour dans `docs/`.
|
||||||
|
|
||||||
|
## Configuration critique des services
|
||||||
|
- Mempool du réseau signet :
|
||||||
|
`https://mempool2.4nkweb.com/fr/docs/api/rest`
|
||||||
|
|
||||||
|
## Développement et sécurité
|
||||||
|
- Ne jamais committer de clés privées ou secrets.
|
||||||
|
- Utiliser des variables d’environnement pour les données sensibles.
|
||||||
|
- Définir correctement les dépendances Docker avec healthchecks.
|
||||||
|
- Utiliser les URLs de service Docker Compose (`http://service_name:port`).
|
||||||
|
- Documenter toutes les modifications importantes dans `docs/`.
|
||||||
|
- Externaliser au maximum les variables d’environnement.
|
||||||
|
- Toujours utiliser une clé SSH pour cloner les dépôts.
|
||||||
|
- Monter en version les dépôts au début du travail.
|
||||||
|
- Pousser les tags docker `int-dev` via la CI sur `git.4nkweb.com`.
|
||||||
|
- Corriger systématiquement les problèmes, même mineurs, sans contournement.
|
||||||
|
|
||||||
|
## Scripts (règles critiques)
|
||||||
|
- Vérifier l’existence d’un script dans `scripts/` avant toute action.
|
||||||
|
- Utiliser les scripts existants plutôt que des commandes directes.
|
||||||
|
- Ne jamais créer plusieurs versions ou noms de scripts.
|
||||||
|
- Améliorer l’existant au lieu de créer des variantes (`startup-v2.sh`, etc.).
|
||||||
|
|
||||||
|
## Images Docker (règles critiques)
|
||||||
|
- Ajouter systématiquement `apt update && apt upgrade` dans les Dockerfiles.
|
||||||
|
- Installer en arrière-plan dans les images Docker :
|
||||||
|
`curl, git, sed, awk, nc, wget, jq, telnet, tee, wscat, ping, npm (dernière version)`
|
||||||
|
- Appliquer à tous les Dockerfiles et `docker-compose.yml`.
|
||||||
|
- N'utilise pas les version test ou dev ou int-dev-dev mais toujours les version int-dev, relance leur compilation si nécessaire
|
||||||
|
|
||||||
|
## Fichiers de configuration (règles critiques)
|
||||||
|
- Vérifier l’écriture effective après chaque modification.
|
||||||
|
- Fichiers concernés : `nginx.conf`, `bitcoin.conf`, `package.json`, `Cargo.toml`.
|
||||||
|
- Utiliser `cat`, `jq` ou vérificateurs de syntaxe.
|
||||||
|
|
||||||
|
## Connexion au réseau Bitcoin signet
|
||||||
|
Commande unique et obligatoire :
|
||||||
|
```bash
|
||||||
|
docker exec bitcoin-signet bitcoin-cli -signet -rpccookiefile=/home/bitcoin/.bitcoin/signet/.cookie getblockchaininfo
|
||||||
|
````
|
||||||
|
|
||||||
|
## Connexion au relay/faucet bootstrap
|
||||||
|
|
||||||
|
* Test via WSS : `wss://dev3.4nkweb.com/ws/`
|
||||||
|
* Envoi Faucet, réponse attendue avec `NewTx` (tx hex et tweak\_data).
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
* Automatiser dans le code toute solution validée.
|
||||||
|
* Pérenniser les retours d’expérience dans code et paramètres.
|
||||||
|
* Compléter les tests pour éviter les régressions.
|
||||||
|
|
||||||
|
## Nginx
|
||||||
|
|
||||||
|
* Tous les fichiers dans `conf/ngnix` doivent être mappés avec ceux du serveur.
|
||||||
|
|
||||||
|
## Minage (règles critiques)
|
||||||
|
|
||||||
|
* Toujours valider les adresses utilisées (adresses TSP non reconnues).
|
||||||
|
* Utiliser uniquement des adresses Bitcoin valides (bech32m).
|
||||||
|
* Vérifier que le minage génère des blocs avec transactions, pas uniquement coinbase.
|
||||||
|
* Surveiller les logs du minage pour détecter les erreurs d’adresse.
|
||||||
|
* Vérifier la propagation via le mempool externe.
|
||||||
|
|
||||||
|
## Mempool externe
|
||||||
|
|
||||||
|
* Utiliser `https://mempool2.4nkweb.com` pour vérifier les transactions.
|
||||||
|
* Vérifier la synchronisation entre réseau local et externe.
|
||||||
|
|
||||||
|
## Données et modèles
|
||||||
|
|
||||||
|
* Utiliser les fichiers CSV comme base des modèles de données.
|
||||||
|
* Être attentif aux en-têtes multi-lignes.
|
||||||
|
* Confirmer la structure comprise et demander définition de toutes les colonnes.
|
||||||
|
* Corriger automatiquement incohérences de type.
|
||||||
|
|
||||||
|
## Implémentation et architecture
|
||||||
|
|
||||||
|
* Code splitting avec `React.lazy` et `Suspense`.
|
||||||
|
* Centraliser l’état avec Redux ou Context API.
|
||||||
|
* Créer une couche d’abstraction pour les services de données.
|
||||||
|
* Aller systématiquement au bout d’une implémentation.
|
||||||
|
|
||||||
|
## Préparation open source
|
||||||
|
|
||||||
|
Chaque projet doit être prêt pour un dépôt sur `git.4nkweb.com` :
|
||||||
|
|
||||||
|
* Inclure : `LICENSE` (MIT, Apache 2.0 ou GPL), `CONTRIBUTING.md`, `CHANGELOG.md`, `CODE_OF_CONDUCT.md`.
|
||||||
|
* Aligner documentation et tests avec `4NK_node`.
|
||||||
|
|
||||||
|
## Versioning et documentation
|
||||||
|
|
||||||
|
* Mettre à jour documentation et tests systématiquement.
|
||||||
|
* Gérer versioning avec changelog.
|
||||||
|
* Demander validation avant tag.
|
||||||
|
* Documenter les hypothèses testées dans un REX technique.
|
||||||
|
* Tester avant tout commit.
|
||||||
|
* Tester les buildsavant tout tag.
|
||||||
|
|
||||||
|
## Bonnes pratiques de confidentialité et sécurité
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
- Ne jamais stocker de secrets (clés, tokens, mots de passe) dans les Dockerfiles ou docker-compose.yml.
|
||||||
|
- Utiliser des fichiers `.env` sécurisés (non commités avec copie en .env.example) pour toutes les variables sensibles.
|
||||||
|
- Ne pas exécuter de conteneurs avec l’utilisateur root, privilégier un utilisateur dédié.
|
||||||
|
- Limiter les capacités des conteneurs (option `--cap-drop`) pour réduire la surface d’attaque.
|
||||||
|
- Scanner régulièrement les images Docker avec un outil de sécurité (ex : Trivy, Clair).
|
||||||
|
- Mettre à jour en continu les images de base afin d’éliminer les vulnérabilités.
|
||||||
|
- Ne jamais exposer de ports inutiles.
|
||||||
|
- Restreindre les volumes montés au strict nécessaire.
|
||||||
|
- Utiliser des réseaux Docker internes pour la communication inter-containers.
|
||||||
|
- Vérifier et tenir à jour les .dockerignore.
|
||||||
|
|
||||||
|
### Git
|
||||||
|
- Ne jamais committer de secrets, clés ou identifiants (même temporairement).
|
||||||
|
- Configurer des hooks Git (pre-commit) pour détecter automatiquement les secrets et les failles.
|
||||||
|
- Vérifier l’historique (`git log`, `git filter-repo`) pour s’assurer qu’aucune information sensible n’a été poussée.
|
||||||
|
- Signer les commits avec GPG pour garantir l’authenticité.
|
||||||
|
- Utiliser systématiquement SSH pour les connexions à distance.
|
||||||
|
- Restreindre les accès aux dépôts (principes du moindre privilège).
|
||||||
|
- Documenter les changements sensibles dans `CHANGELOG.md`.
|
||||||
|
- Ne jamais pousser directement sur `main` ou `master`, toujours passer par des branches de feature ou PR.
|
||||||
|
- Vérifier et tenir à jour les .gitignore.
|
||||||
|
- Vérifier et tenir à jour les .gitkeep.
|
||||||
|
- Vérifier et tenir à jour les .gitattributes.
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
- Toujours ouvrir une session en commençant par relire le fichier `.cursorrules`.
|
||||||
|
- Vérifier que Cursor ne propose pas de commit contenant des secrets ou fichiers sensibles.
|
||||||
|
- Ne pas exécuter dans Cursor de commandes non comprises ou copiées sans vérification.
|
||||||
|
- Préférer l’utilisation de scripts audités dans `scripts/` plutôt que des commandes directes dans Cursor.
|
||||||
|
- Fermer et relancer Cursor régulièrement pour éviter des contextes persistants non désirés.
|
||||||
|
- Ne jamais partager le contenu du terminal ou des fichiers sensibles via Cursor en dehors du périmètre du projet.
|
||||||
|
- Vérifier et tenir à jour les .cursorrules.
|
||||||
|
- Vérifier et tenir à jour les .cursorignore.
|
||||||
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.env*
|
||||||
3
.env
3
.env
@ -1,3 +0,0 @@
|
|||||||
# .env
|
|
||||||
VITE_API_URL=https://api.example.com
|
|
||||||
VITE_API_KEY=your_api_key
|
|
||||||
25
.env.exemple
Normal file
25
.env.exemple
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
BOOTSTRAPURL=wss://dev3.4nkweb.com/ws/
|
||||||
|
|
||||||
|
# WS
|
||||||
|
# RELAY_URLS=wss://demo.4nkweb.com/ws
|
||||||
|
RELAY_URLS=ws://sdk_relay:8090,wss://dev3.4nkweb.com/ws/
|
||||||
|
|
||||||
|
# SIGNER_WS_URL=ws://dev4.4nkweb.com/signer/
|
||||||
|
SIGNER_WS_URL=ws://dev3.4nkweb.com
|
||||||
|
SIGNER_BASE_URL=https://dev3.4nkweb.com
|
||||||
|
|
||||||
|
core_url="http://bitcoin:38332"
|
||||||
|
ws_url="0.0.0.0:8090"
|
||||||
|
wallet_name="default"
|
||||||
|
network="signet"
|
||||||
|
blindbit_url="http://blindbit:8000"
|
||||||
|
zmq_url="tcp://bitcoin:29000"
|
||||||
|
storage="https://dev4.4nkweb.com/storage"
|
||||||
|
data_dir="/home/bitcoin/.4nk"
|
||||||
|
bitcoin_data_dir="/home/bitcoin/.bitcoin"
|
||||||
|
|
||||||
|
|
||||||
|
# ===================== /!\ donnée sensible =======================
|
||||||
|
|
||||||
|
SIGNER_API_KEY=your-api-key-change-this
|
||||||
|
VITE_JWT_SECRET_KEY=52b3d77617bb00982dfee15b08effd52cfe5b2e69b2f61cc4848cfe1e98c0bc9
|
||||||
69
.gitea/workflows/build-int-dev.yml
Normal file
69
.gitea/workflows/build-int-dev.yml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: Build and Push Docker image (int-dev)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- int-dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.4nkweb.com
|
||||||
|
IMAGE_NAMESPACE: 4nk
|
||||||
|
IMAGE_NAME: ihm_client
|
||||||
|
DOCKER_TAG: int-dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.USER }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup SSH agent for git clone
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: |
|
||||||
|
${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Verify WebAssembly files
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Verify that the pre-built WASM files are present
|
||||||
|
echo "Verifying WebAssembly files..."
|
||||||
|
ls -la pkg/
|
||||||
|
file pkg/sdk_client_bg.wasm
|
||||||
|
echo "WebAssembly files verified successfully"
|
||||||
|
|
||||||
|
- name: Extract docker tag from commit message (optional)
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
MSG=$(git log -1 --pretty=%B | tr -d '\r')
|
||||||
|
if echo "$MSG" | grep -q "ci: docker_tag="; then
|
||||||
|
T=$(echo "$MSG" | sed -nE 's/.*ci: docker_tag=([^ ]+).*/\1/p' | tr -d '\n')
|
||||||
|
if [ -n "$T" ]; then
|
||||||
|
echo "tag=$T" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag || env.DOCKER_TAG }}
|
||||||
|
ssh: default
|
||||||
39
.gitignore
vendored
39
.gitignore
vendored
@ -1,7 +1,36 @@
|
|||||||
target/
|
# Secrets et fichiers sensibles
|
||||||
pkg/
|
.env
|
||||||
Cargo.lock
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.exemple
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Node.js
|
||||||
node_modules/
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build
|
||||||
dist/
|
dist/
|
||||||
.vscode
|
build/
|
||||||
public/ssl/
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
*.tmp
|
||||||
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Documentation WebSocket et architecture
|
||||||
|
- **Documentation WebSocket** : Ajout de `docs/WEBSOCKET_CONNECTION.md` avec analyse complète de l'architecture
|
||||||
|
- **Architecture de l'iframe** : Documentation de la logique de fonctionnement (init, services, WebSocket)
|
||||||
|
- **Configuration WebSocket** : Documentation des variables d'environnement et connexions
|
||||||
|
- **Gestion des messages** : Documentation du handshake et mise à jour des adresses relay
|
||||||
|
- **Communication parent** : Documentation de l'écoute des messages et gestion des erreurs
|
||||||
|
- **Problème persistant** : 502 Bad Gateway - Nginx ne transmet pas les headers WebSocket
|
||||||
|
|
||||||
|
## [1.0.0]
|
||||||
|
### Version initiale
|
||||||
|
- Interface utilisateur pour LeCoffre
|
||||||
|
- Intégration WebSocket avec les relays
|
||||||
|
- Gestion des processus et authentification
|
||||||
|
- Communication avec le parent via postMessage
|
||||||
49
Dockerfile
49
Dockerfile
@ -1,13 +1,44 @@
|
|||||||
FROM node:20
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
ENV TZ=Europe/Paris
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
||||||
|
|
||||||
# use this user because he have uid et gid 1000 like theradia
|
|
||||||
USER node
|
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
# Installation des dépendances minimales nécessaires
|
||||||
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]
|
RUN apt-get update && apt-get upgrade -y && \
|
||||||
|
apt-get install -y --fix-missing \
|
||||||
|
ca-certificates curl jq git \
|
||||||
|
net-tools iputils-ping dnsutils \
|
||||||
|
netcat-openbsd telnet procps && \
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# Installation de Node.js
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||||
|
apt-get install -y nodejs && \
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# Création d'un utilisateur non-root
|
||||||
|
RUN useradd -m -u 1000 appuser && \
|
||||||
|
mkdir -p /app && chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Copy project files
|
||||||
|
COPY . .
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Ensure pkg directory exists and has correct permissions
|
||||||
|
RUN mkdir -p pkg && chmod -R 755 pkg
|
||||||
|
|
||||||
|
# Verify pkg files are present
|
||||||
|
RUN ls -la pkg/
|
||||||
|
|
||||||
|
# Copy the provided prebuilt WASM package (ESM)
|
||||||
|
# The directory pkg is provided in the build context
|
||||||
|
# and already contains sdk_client.js (ES module) and wasm
|
||||||
|
# so no compilation is required here.
|
||||||
|
|
||||||
|
# Installation des dépendances Node.js
|
||||||
|
USER appuser
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 3003
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
|||||||
39
docs/ANALYSE.md
Normal file
39
docs/ANALYSE.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
## Analyse détaillée
|
||||||
|
|
||||||
|
### Périmètre
|
||||||
|
|
||||||
|
Client front (Vite) intégrant un package WASM pré‑construit `pkg/` et Nginx pour le dev.
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
|
||||||
|
- **Outillage**: Vite 5, TypeScript 5
|
||||||
|
- **WASM**: paquet `sdk_client` précompilé (copié dans `pkg/`)
|
||||||
|
- **UI/Libs**: axios, QR, SweetAlert2, plugins Vite (React/Vue activables)
|
||||||
|
- **Serveur**: Nginx en dev via `start-dev.sh`
|
||||||
|
|
||||||
|
### Build et exécution
|
||||||
|
|
||||||
|
- Scripts: `build_wasm`, `start` (Vite host 0.0.0.0), `build`, `deploy`.
|
||||||
|
- Dockerfile: Node 20‑alpine, installe `git` et `nginx`, `npm install`, copie `nginx.dev.conf`, script de démarrage.
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
|
- 3003 (exposition dev), 80 via Nginx.
|
||||||
|
|
||||||
|
### Risques et points d’attention
|
||||||
|
|
||||||
|
- Coexistence double serveur (Vite + Nginx) en dev: veiller au routage, CORS et proxys.
|
||||||
|
- Paquet WASM précompilé: vérifier cohérence de version avec `sdk_client`.
|
||||||
|
- Absence de tests automatiques; ajouter stratégie `tests/` (unit/integration).
|
||||||
|
|
||||||
|
### Actions proposées
|
||||||
|
|
||||||
|
- Documenter matrice compatibilité `pkg/` ↔ `sdk_client` (source, commit/tag, date).
|
||||||
|
- Ajouter lints/tests en CI; unifier serveur dev (proxy Nginx vers Vite ou inverse).
|
||||||
|
- Paramétrer variables d’env front (URLs relais, API) et fournir `.env.example`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
23
docs/ARCHITECTURE.md
Normal file
23
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Architecture - IHM Client
|
||||||
|
|
||||||
|
## Composants
|
||||||
|
- Frontend embarqué en iframe dans `lecoffre-front`.
|
||||||
|
- Dialogue avec `sdk_signer` et `sdk_relay` via WebSocket.
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
- `sdk_signer` via `VITE_SIGNER_URL`.
|
||||||
|
- `sdk_relay` via `VITE_WS_URL`.
|
||||||
|
- Backend `lecoffre-back-mini` via `VITE_API_BASE_URL`.
|
||||||
|
|
||||||
|
## Réseau et ports
|
||||||
|
- Exposé derrière Nginx via `https://dev4.4nkweb.com/`.
|
||||||
|
|
||||||
|
## Variables d’environnement (centralisées)
|
||||||
|
- Chargement depuis `lecoffre_node/.env.master`.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
- Logs → Promtail → Loki → Grafana (Frontend Services).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Code splitting (`React.lazy`, `Suspense`).
|
||||||
|
- Pas de `.env` local, configuration via Docker Compose.
|
||||||
40
docs/CORRECTIONS_APPLIQUEES.md
Normal file
40
docs/CORRECTIONS_APPLIQUEES.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Corrections Appliquées - IHM Client
|
||||||
|
|
||||||
|
## Date: 20 Septembre 2025
|
||||||
|
|
||||||
|
### 🔧 Corrections Majeures
|
||||||
|
|
||||||
|
#### 1. Problème de Configuration WebSocket
|
||||||
|
**Problème:** L'iframe était bloquée sur "Chargement de l'authentification..." car elle tentait de se connecter à une URL WebSocket inaccessible.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Correction de `VITE_BOOTSTRAPURL` pour pointer vers le bootstrap externe
|
||||||
|
- Configuration des `RELAY_URLS` pour utiliser le relais local et externe
|
||||||
|
- Mise à jour des variables d'environnement
|
||||||
|
|
||||||
|
**Fichiers modifiés:**
|
||||||
|
- `.env` - Configuration WebSocket corrigée
|
||||||
|
- `docker-compose.yml` - Variables d'environnement mises à jour
|
||||||
|
|
||||||
|
#### 2. Configuration des URLs
|
||||||
|
**Variables d'environnement:**
|
||||||
|
```env
|
||||||
|
BOOTSTRAPURL=wss://dev3.4nkweb.com/ws/
|
||||||
|
RELAY_URLS=ws://sdk_relay:8090,wss://dev3.4nkweb.com/ws/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Installation des Outils
|
||||||
|
**Ajouté dans le Dockerfile:**
|
||||||
|
- `curl`, `git`, `wget`, `jq`, `telnet`, `npm`, `wscat`
|
||||||
|
- Outils de diagnostic et de connectivité
|
||||||
|
|
||||||
|
### 📊 État Actuel
|
||||||
|
- **Statut:** ✅ Healthy
|
||||||
|
- **Connectivité:** Bootstrap et relais configurés
|
||||||
|
- **URLs:** Correctement mappées
|
||||||
|
- **Logs:** Optimisés
|
||||||
|
|
||||||
|
### 🔄 Prochaines Étapes
|
||||||
|
- Tests de connectivité WebSocket
|
||||||
|
- Monitoring des performances
|
||||||
|
- Optimisations supplémentaires
|
||||||
23
docs/DEPLOIEMENT.md
Normal file
23
docs/DEPLOIEMENT.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Déploiement - IHM Client
|
||||||
|
|
||||||
|
## Préparation
|
||||||
|
- Branche `int-dev` sur tous les dépôts.
|
||||||
|
- Variables dans `lecoffre_node/.env.master` (pas de `.env` local).
|
||||||
|
- Ne pas utiliser `docker compose up -d`.
|
||||||
|
|
||||||
|
## Déploiement (orchestrateur)
|
||||||
|
```bash
|
||||||
|
cd /home/debian/4NK_env/lecoffre_node
|
||||||
|
./scripts/start.sh | cat
|
||||||
|
./scripts/validate-deployment.sh | cat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vérifications
|
||||||
|
- `https://dev4.4nkweb.com/` (iframe OK)
|
||||||
|
- WS `wss://dev4.4nkweb.com/ws/`
|
||||||
|
- `./scripts/monitor-progress.sh | cat`
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
- Pousser sur `int-dev` sans déclencher de CI tant que non nécessaire.
|
||||||
|
- Config centralisée uniquement.
|
||||||
|
- Logs via Promtail → Loki → Grafana.
|
||||||
10
docs/FLUX.md
Normal file
10
docs/FLUX.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Description des Flux - IHM Client
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flux principaux
|
||||||
|
1. Auth notaire via front → IdNot → front → iframe IHM.
|
||||||
|
2. IHM ↔ Signer (opérations signées).
|
||||||
|
3. IHM ↔ Relay (WebSocket) pour évènements.
|
||||||
18
docs/FONCTIONNEL.md
Normal file
18
docs/FONCTIONNEL.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Description Fonctionnelle - IHM Client
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Fournir l’interface d’interaction utilisateur (iframe) pour les flux métiers et les opérations liées aux clés Bitcoin (Silent Payment).
|
||||||
|
|
||||||
|
## Parcours clés
|
||||||
|
- Authentification via redirection IdNot (depuis `lecoffre-front`).
|
||||||
|
- Connexion au `sdk_signer` pour opérations signées.
|
||||||
|
- Échanges temps réel via `sdk_relay` (WebSocket).
|
||||||
|
|
||||||
|
## Rôles
|
||||||
|
- Notaire: initie les dossiers, suit l’état.
|
||||||
|
- Client: accède aux dossiers, valide via SMS, téléverse des pièces.
|
||||||
|
|
||||||
|
## Résultats attendus
|
||||||
|
- Affichage fiable de l’iframe.
|
||||||
|
- Opérations signées validées.
|
||||||
|
- Erreurs affichées à l’utilisateur, logs collectés.
|
||||||
35
docs/INSTALLATION.md
Normal file
35
docs/INSTALLATION.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Installation - IHM Client
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Accès au dépôt `4NK_env` (branche `int-dev`).
|
||||||
|
- Docker/Compose installés.
|
||||||
|
- Variables centralisées dans `lecoffre_node/.env.master`.
|
||||||
|
|
||||||
|
## Récupération du code
|
||||||
|
```bash
|
||||||
|
cd /home/debian/4NK_env
|
||||||
|
# Assure-toi d'être sur la branche int-dev dans tous les dépôts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- Ne pas créer de `.env` local.
|
||||||
|
- Renseigner/valider `VITE_*` dans `lecoffre_node/.env.master`.
|
||||||
|
|
||||||
|
## Démarrage (via orchestrateur)
|
||||||
|
- Lancer via `lecoffre_node` (recommandé) :
|
||||||
|
```bash
|
||||||
|
cd /home/debian/4NK_env/lecoffre_node
|
||||||
|
./scripts/start.sh | cat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accès
|
||||||
|
- `https://dev4.4nkweb.com/` (intégré via Nginx).
|
||||||
|
|
||||||
|
## Vérifications
|
||||||
|
- Page statut: `https://dev4.4nkweb.com/status/`
|
||||||
|
- WebSocket: `wss://dev4.4nkweb.com/ws/`
|
||||||
|
- Logs Grafana.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Brancher IHM via iframe dans `lecoffre-front`.
|
||||||
|
- Ne pas déclencher de CI depuis ce dépôt; builds images depuis pipelines tag `int-dev` si nécessaire.
|
||||||
6
docs/QUALITE.md
Normal file
6
docs/QUALITE.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Qualité Logicielle - IHM Client
|
||||||
|
|
||||||
|
- Lint/format: respecter config projet.
|
||||||
|
- Tests: ajouter vérifs WS et intégration iframe.
|
||||||
|
- Performance: code splitting et lazy loading.
|
||||||
|
- Observabilité: logs structurés, erreurs gérées.
|
||||||
6
docs/SECURITE.md
Normal file
6
docs/SECURITE.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Sécurité - IHM Client
|
||||||
|
|
||||||
|
- Pas de secrets dans le code/dépôt.
|
||||||
|
- Variables via `.env.master` uniquement.
|
||||||
|
- CSP/headers via Nginx.
|
||||||
|
- WS sécurisé via `wss://`.
|
||||||
22
docs/TECHNIQUE.md
Normal file
22
docs/TECHNIQUE.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Description Technique - IHM Client
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
- Node.js 20, Vite/React.
|
||||||
|
- Code splitting (`React.lazy`, `Suspense`).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- Variables `VITE_*` via `lecoffre_node/.env.master`.
|
||||||
|
- Aucune lecture de `.env` local.
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
- WebSocket `VITE_WS_URL` (relay).
|
||||||
|
- REST `VITE_API_BASE_URL` (backend).
|
||||||
|
- `VITE_SIGNER_URL` (signer).
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
- Aucune clé en dépôt.
|
||||||
|
- Headers sécurisés via Nginx.
|
||||||
|
|
||||||
|
## Observabilité
|
||||||
|
- Logs Promtail → Loki.
|
||||||
|
- Dashboards Grafana.
|
||||||
6
docs/TODO.md
Normal file
6
docs/TODO.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# TODO - IHM Client
|
||||||
|
|
||||||
|
- Vérifier intégration iframe avec `lecoffre-front`.
|
||||||
|
- Tester WS `wss://dev4.4nkweb.com/ws/`.
|
||||||
|
- Vérifier configuration `VITE_*` via `.env.master`.
|
||||||
|
- Ajouter dashboards Grafana si manquants.
|
||||||
124
docs/WEBSOCKET_CONNECTION.md
Normal file
124
docs/WEBSOCKET_CONNECTION.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Connexion WebSocket - ihm_client
|
||||||
|
|
||||||
|
## Architecture de l'iframe
|
||||||
|
|
||||||
|
### Structure de fonctionnement
|
||||||
|
L'iframe `ihm_client` suit une architecture modulaire :
|
||||||
|
|
||||||
|
1. **Initialisation** (`router.ts`) :
|
||||||
|
- `init()` initialise les services
|
||||||
|
- `Services.getInstance()` crée l'instance singleton
|
||||||
|
- `connectAllRelays()` établit les connexions WebSocket
|
||||||
|
|
||||||
|
2. **Services** (`services/service.ts`) :
|
||||||
|
- Gestion des connexions WebSocket
|
||||||
|
- Communication avec les relays
|
||||||
|
- Gestion des messages et handshakes
|
||||||
|
|
||||||
|
3. **WebSocket** (`websockets.ts`) :
|
||||||
|
- API WebSocket native
|
||||||
|
- Gestion des événements (open, message, error, close)
|
||||||
|
|
||||||
|
## Configuration WebSocket
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
```bash
|
||||||
|
VITE_BOOTSTRAPURL=wss://dev4.4nkweb.com/ws/
|
||||||
|
RELAY_URLS=wss://dev4.4nkweb.com/ws/,wss://dev3.4nkweb.com/ws/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connexion aux relays
|
||||||
|
```typescript
|
||||||
|
// Dans service.ts
|
||||||
|
const BOOTSTRAPURL = [import.meta.env.VITE_BOOTSTRAPURL || `wss://${BASEURL}/ws/`];
|
||||||
|
|
||||||
|
// Connexion à tous les relays
|
||||||
|
await services.connectAllRelays();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des messages
|
||||||
|
|
||||||
|
### Handshake
|
||||||
|
```typescript
|
||||||
|
public async handleHandshakeMsg(url: string, parsedMsg: any) {
|
||||||
|
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
|
||||||
|
this.updateRelay(url, handshakeMsg.sp_address);
|
||||||
|
this.currentBlockHeight = handshakeMsg.chain_tip;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mise à jour des adresses relay
|
||||||
|
```typescript
|
||||||
|
public updateRelay(wsurl: string, spAddress: string): void {
|
||||||
|
this.relayAddresses[wsurl] = spAddress;
|
||||||
|
console.log(`Updated: ${wsurl} -> ${spAddress}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Communication avec le parent
|
||||||
|
|
||||||
|
### Écoute des messages
|
||||||
|
```typescript
|
||||||
|
// Dans router.ts
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
// L'iframe écoute les messages du parent
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestion des erreurs
|
||||||
|
```typescript
|
||||||
|
// Détection des fonds insuffisants
|
||||||
|
if (insufficientFunds) {
|
||||||
|
await this.triggerAutomaticFundsTransfer();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Problèmes résolus
|
||||||
|
|
||||||
|
### 1. Configuration WebSocket incorrecte
|
||||||
|
**Problème :** L'iframe utilisait `ws://sdk_relay:8090/` au lieu de `wss://dev4.4nkweb.com/ws/`.
|
||||||
|
|
||||||
|
**Solution :** Correction des variables d'environnement et redémarrage du container.
|
||||||
|
|
||||||
|
### 2. Mixed Content errors
|
||||||
|
**Problème :** Tentative de connexion WS depuis une page HTTPS.
|
||||||
|
|
||||||
|
**Solution :** Utilisation de WSS (WebSocket Secure) pour toutes les connexions.
|
||||||
|
|
||||||
|
### 3. Headers WebSocket manquants
|
||||||
|
**Problème :** Nginx ne transmettait pas les headers WebSocket.
|
||||||
|
|
||||||
|
**Solution :** Configuration Nginx avec headers WebSocket explicites.
|
||||||
|
|
||||||
|
## Problème persistant
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
**Statut :** ⚠️ Problème persistant
|
||||||
|
- L'iframe reçoit 502 Bad Gateway lors de la connexion WebSocket
|
||||||
|
- Nginx ne transmet pas les headers WebSocket vers le relay
|
||||||
|
- Le relay rejette les connexions sans headers
|
||||||
|
|
||||||
|
**Investigation :** La configuration Nginx semble correcte mais les headers ne sont pas transmis.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Test de connexion WebSocket
|
||||||
|
```bash
|
||||||
|
# Depuis l'iframe
|
||||||
|
wget -O- https://dev4.4nkweb.com/ws/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat actuel :** 502 Bad Gateway
|
||||||
|
|
||||||
|
### Test avec headers WebSocket
|
||||||
|
```bash
|
||||||
|
curl -v -H "Upgrade: websocket" \
|
||||||
|
-H "Connection: upgrade" \
|
||||||
|
-H "Sec-WebSocket-Version: 13" \
|
||||||
|
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||||
|
https://dev4.4nkweb.com/ws/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date de mise à jour
|
||||||
|
2025-01-20 - Architecture de l'iframe analysée et problèmes de connexion WebSocket identifiés
|
||||||
31
docs/analyse.md
Normal file
31
docs/analyse.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
### Objet
|
||||||
|
Analyse synthétique de `ihm_client` (iframe chargée par `lecoffre-front`).
|
||||||
|
|
||||||
|
### Stack et build
|
||||||
|
- **Outil**: Vite
|
||||||
|
- **Langage**: TypeScript + HTML templates
|
||||||
|
- **Cible**: `index.html` + `src/main.ts` (SPA montée en iframe)
|
||||||
|
- **Serveur dev**: `nginx.dev.conf` et script `start-dev.sh`
|
||||||
|
|
||||||
|
### Arborescence notable
|
||||||
|
- `src/components`: header, modales (confirmation/creation/waiting), login-modal, qrcode-scanner
|
||||||
|
- `src/pages`: home, chat, account, process, signature (+ variantes)
|
||||||
|
- `src/services`: database, storage, token, modal, service générique
|
||||||
|
- `src/utils`: documents, HTML helpers, notifications store, subscriptions utils
|
||||||
|
- `src/websockets.ts`: temps-réel côté iframe
|
||||||
|
|
||||||
|
### Intégrations et communication
|
||||||
|
- **Token/Session**: `src/services/token.ts`
|
||||||
|
- **Stockage**: `src/services/storage.service.ts`
|
||||||
|
- **Base de données**: `src/services/database.service.ts` (cache/worker)
|
||||||
|
- **Workers**: `service-workers/` (cache/database)
|
||||||
|
- **Échanges avec parent**: via postMessage (cf. utils/services) et WebSockets
|
||||||
|
|
||||||
|
### Points d’attention
|
||||||
|
- Sécurité iframe (sandbox, `postMessage` sécurisé par origine)
|
||||||
|
- Gestion des tokens (renouvellement, stockage, effacement)
|
||||||
|
- Cohérence de version avec `lecoffre-front` (API bus/messages)
|
||||||
|
|
||||||
|
### Déploiement
|
||||||
|
- **Dockerfile**: fourni
|
||||||
|
- **Nginx**: `nginx.dev.conf` pour dev local
|
||||||
14
index.html
14
index.html
@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="./style/4nk.css">
|
<link rel="stylesheet" href="./style/4nk.css">
|
||||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
<script src="https://unpkg.com/html5-qrcode"></script>
|
||||||
<title>4NK Application</title>
|
<title>4NK Application</title>
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header-container"></div>
|
<div id="header-container"></div>
|
||||||
@ -17,9 +18,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- <script type="module" src="/src/index.ts"></script> -->
|
<!-- <script type="module" src="/src/index.ts"></script> -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { init } from '/src/router.ts';
|
// Charge le module WASM (ESM auto-start, pas d'export default)
|
||||||
|
import './pkg/sdk_client.js';
|
||||||
|
|
||||||
|
// Initialise l'application
|
||||||
|
import { init as initRouter } from '/src/router.ts';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await init();
|
try {
|
||||||
|
await initRouter();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize application:', error);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
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:3003;
|
||||||
|
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://dev4.4nkweb.com: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3833
package-lock.json
generated
3833
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,11 +5,12 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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/pkg ../sdk_client --target bundler --dev",
|
||||||
"start": "vite --host 0.0.0.0",
|
"start": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
|
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
|
||||||
|
"build:dist": "tsc -p tsconfig.build.json"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -30,11 +31,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/elements": "^19.0.1",
|
"@angular/elements": "^19.0.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
|
"jose": "^6.0.11",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"sweetalert2": "^11.14.5",
|
"sweetalert2": "^11.14.5",
|
||||||
|
|||||||
1
pkg/.gitignore
vendored
Normal file
1
pkg/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
17
pkg/package.json
Normal file
17
pkg/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "sdk_client",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"files": [
|
||||||
|
"sdk_client_bg.wasm",
|
||||||
|
"sdk_client.js",
|
||||||
|
"sdk_client_bg.js",
|
||||||
|
"sdk_client.d.ts"
|
||||||
|
],
|
||||||
|
"main": "sdk_client.js",
|
||||||
|
"types": "sdk_client.d.ts",
|
||||||
|
"sideEffects": [
|
||||||
|
"./sdk_client.js",
|
||||||
|
"./snippets/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
249
pkg/sdk_client.d.ts
vendored
Normal file
249
pkg/sdk_client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export function setup(): void;
|
||||||
|
export function get_address(): string;
|
||||||
|
export function get_member(): Member;
|
||||||
|
export function restore_device(device: any): void;
|
||||||
|
export function create_device_from_sp_wallet(sp_wallet: string): string;
|
||||||
|
export function create_new_device(birthday: number, network_str: string): string;
|
||||||
|
export function scan_blocks(tip_height: number, blindbit_url: string): Promise<void>;
|
||||||
|
export function is_paired(): boolean;
|
||||||
|
export function pair_device(process_id: string, sp_addresses: string[]): void;
|
||||||
|
export function unpair_device(): void;
|
||||||
|
export function dump_wallet(): string;
|
||||||
|
export function reset_shared_secrets(): void;
|
||||||
|
export function set_shared_secrets(secrets: string): void;
|
||||||
|
export function get_pairing_process_id(): string;
|
||||||
|
export function dump_device(): Device;
|
||||||
|
export function dump_neutered_device(): Device;
|
||||||
|
export function reset_device(): void;
|
||||||
|
export function get_txid(transaction: string): string;
|
||||||
|
export function get_prevouts(transaction: string): string[];
|
||||||
|
export function get_opreturn(transaction: string): string;
|
||||||
|
export function process_commit_new_state(process: Process, state_id: string, new_tip: string): Process;
|
||||||
|
export function parse_new_tx(new_tx_msg: string, block_height: number, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function parse_cipher(cipher_msg: string, members_list: OutPointMemberMap, processes: OutPointProcessMap): ApiReturn;
|
||||||
|
export function get_outputs(): any;
|
||||||
|
export function get_available_amount(): bigint;
|
||||||
|
/**
|
||||||
|
* We send a transaction that pays at least one output to each address
|
||||||
|
* The goal can be to establish a shared_secret to be used as an encryption key for further communication
|
||||||
|
* or if the recipient is a relay it can be the init transaction for a new process
|
||||||
|
*/
|
||||||
|
export function create_transaction(addresses: string[], fee_rate: number): ApiReturn;
|
||||||
|
export function sign_transaction(partial_tx: TsUnsignedTransaction): ApiReturn;
|
||||||
|
export function create_new_process(private_data: Pcd, roles: Roles, public_data: Pcd, relay_address: string, fee_rate: number, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function update_process(process: Process, new_attributes: Pcd, roles: Roles, new_public_data: Pcd, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function request_data(process_id: string, state_ids_str: string[], roles: any, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function create_update_message(process: Process, state_id: string, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function validate_state(process: Process, state_id: string, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function refuse_state(process: Process, state_id: string, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function evaluate_state(process: Process, state_id: string, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function create_response_prd(process: Process, state_id: string, members_list: OutPointMemberMap): ApiReturn;
|
||||||
|
export function create_faucet_msg(): string;
|
||||||
|
export function get_storages(process_outpoint: string): string[];
|
||||||
|
export function is_child_role(parent_roles: string, child_roles: string): void;
|
||||||
|
export function decrypt_data(key: Uint8Array, data: Uint8Array): Uint8Array;
|
||||||
|
export function encode_binary(data: any): Pcd;
|
||||||
|
export function encode_json(json_data: any): Pcd;
|
||||||
|
export function decode_value(value: Uint8Array): any;
|
||||||
|
export function hash_value(value: any, commited_in: string, label: string): string;
|
||||||
|
/**
|
||||||
|
* Generate a merkle proof for a specific attribute in a process state.
|
||||||
|
*
|
||||||
|
* This function creates a merkle proof that proves the existence of a specific attribute
|
||||||
|
* in a given state of a process. The proof can be used to verify that the attribute
|
||||||
|
* was indeed part of the state without revealing the entire state.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `process_state` - The process state object as a JavaScript value
|
||||||
|
* * `attribute_name` - The name of the attribute to generate a proof for
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* A MerkleProofResult object containing:
|
||||||
|
* * `proof` - The merkle proof as a hex string
|
||||||
|
* * `root` - The merkle root (state_id) as a hex string
|
||||||
|
* * `attribute` - The attribute name that was proven
|
||||||
|
* * `attribute_index` - The index of the attribute in the merkle tree
|
||||||
|
* * `total_leaves_count` - The total number of leaves in the merkle tree
|
||||||
|
*
|
||||||
|
* # Errors
|
||||||
|
* * "Failed to deserialize process state" - If the process state cannot be deserialized from JsValue
|
||||||
|
* * "Attribute not found in state" - If the attribute doesn't exist in the state
|
||||||
|
*/
|
||||||
|
export function get_merkle_proof(process_state: any, attribute_name: string): MerkleProofResult;
|
||||||
|
/**
|
||||||
|
* Validate a merkle proof for a specific attribute.
|
||||||
|
*
|
||||||
|
* This function verifies that a merkle proof is valid and proves the existence
|
||||||
|
* of a specific attribute in a given state. It checks that the proof correctly
|
||||||
|
* leads to the claimed root when combined with the attribute hash.
|
||||||
|
*
|
||||||
|
* # Arguments
|
||||||
|
* * `proof_result` - a JsValue expected to contain a MerkleProofResult with the proof and metadata
|
||||||
|
* * `hash` - The hash of the attribute data as a hex string (the leaf value)
|
||||||
|
*
|
||||||
|
* # Returns
|
||||||
|
* A boolean indicating whether the proof is valid
|
||||||
|
*
|
||||||
|
* # Errors
|
||||||
|
* * "serde_wasm_bindgen deserialization error" - If the proof is not a valid MerkleProofResult
|
||||||
|
* * "Invalid proof format" - If the proof cannot be parsed
|
||||||
|
* * "Invalid hash format" - If the hash is not a valid 32-byte hex string
|
||||||
|
* * "Invalid root format" - If the root is not a valid 32-byte hex string
|
||||||
|
*/
|
||||||
|
export function validate_merkle_proof(proof_result: any, hash: string): boolean;
|
||||||
|
export type DiffStatus = "None" | "Rejected" | "Validated";
|
||||||
|
|
||||||
|
export interface UserDiff {
|
||||||
|
process_id: string;
|
||||||
|
state_id: string;
|
||||||
|
value_commitment: string;
|
||||||
|
field: string;
|
||||||
|
roles: Roles;
|
||||||
|
description: string | null;
|
||||||
|
notify_user: boolean;
|
||||||
|
need_validation: boolean;
|
||||||
|
validation_status: DiffStatus;
|
||||||
|
storages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatedProcess {
|
||||||
|
process_id: OutPoint;
|
||||||
|
current_process: Process;
|
||||||
|
diffs: UserDiff[];
|
||||||
|
encrypted_data: Record<string, string>;
|
||||||
|
validated_state: number[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiReturn {
|
||||||
|
secrets: SecretsStore | null;
|
||||||
|
updated_process: UpdatedProcess | null;
|
||||||
|
new_tx_to_send: NewTxMessage | null;
|
||||||
|
ciphers_to_send: string[];
|
||||||
|
commit_to_send: CommitMessage | null;
|
||||||
|
push_to_storage: string[];
|
||||||
|
partial_tx: TsUnsignedTransaction | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface encryptWithNewKeyResult {
|
||||||
|
cipher: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerkleProofResult {
|
||||||
|
proof: string;
|
||||||
|
root: string;
|
||||||
|
attribute: string;
|
||||||
|
attribute_index: number;
|
||||||
|
total_leaves_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Prd {
|
||||||
|
prd_type: PrdType;
|
||||||
|
process_id: OutPoint;
|
||||||
|
sender: OutPoint | null;
|
||||||
|
keys: Record<string, number[]>;
|
||||||
|
pcd_commitments: PcdCommitments;
|
||||||
|
validation_tokens: Proof[];
|
||||||
|
roles: Roles;
|
||||||
|
public_data: Pcd;
|
||||||
|
payload: string;
|
||||||
|
proof: Proof | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PrdType = "None" | "Connect" | "Message" | "Update" | "List" | "Response" | "Confirm" | "TxProposal" | "Request";
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
sp_wallet: SpWallet;
|
||||||
|
pairing_process_commitment: OutPoint | null;
|
||||||
|
paired_member: Member;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutPointProcessMap = Record<OutPoint, Process>;
|
||||||
|
|
||||||
|
export type OutPointMemberMap = Record<OutPoint, Member>;
|
||||||
|
|
||||||
|
export interface HandshakeMessage {
|
||||||
|
sp_address: string;
|
||||||
|
peers_list: OutPointMemberMap;
|
||||||
|
processes_list: OutPointProcessMap;
|
||||||
|
chain_tip: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewTxMessage {
|
||||||
|
transaction: string;
|
||||||
|
tweak_data: string | null;
|
||||||
|
error: AnkError | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FaucetMessage {
|
||||||
|
sp_address: string;
|
||||||
|
commitment: string;
|
||||||
|
error: AnkError | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message sent to the server to commit some state in a transaction
|
||||||
|
* Client must first send a commit message with empty validation_tokens
|
||||||
|
* Relay will ignore a commit message for an update he\'s not aware of that also bears validation_tokens
|
||||||
|
*/
|
||||||
|
export interface CommitMessage {
|
||||||
|
process_id: OutPoint;
|
||||||
|
pcd_commitment: PcdCommitments;
|
||||||
|
roles: Roles;
|
||||||
|
public_data: Pcd;
|
||||||
|
validation_tokens: Proof[];
|
||||||
|
error: AnkError | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnkFlag = "NewTx" | "Faucet" | "Cipher" | "Commit" | "Handshake" | "Sync" | "Unknown";
|
||||||
|
|
||||||
|
export type TsUnsignedTransaction = SilentPaymentUnsignedTransaction;
|
||||||
|
|
||||||
|
export interface SecretsStore {
|
||||||
|
shared_secrets: Record<SilentPaymentAddress, AnkSharedSecretHash>;
|
||||||
|
unconfirmed_secrets: AnkSharedSecretHash[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A process is basically a succession of states
|
||||||
|
* The latest state MUST be an empty state with only the commited_in field set at the last unspent outpoint
|
||||||
|
* Commiting this last empty state in a transaction is called obliterating a process, basically terminating it
|
||||||
|
*/
|
||||||
|
export interface Process {
|
||||||
|
states: ProcessState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessState {
|
||||||
|
commited_in: OutPoint;
|
||||||
|
pcd_commitment: Record<string, string>;
|
||||||
|
state_id: string;
|
||||||
|
keys: Record<string, string>;
|
||||||
|
validation_tokens: Proof[];
|
||||||
|
public_data: Pcd;
|
||||||
|
roles: Record<string, RoleDefinition>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Roles = Record<string, RoleDefinition>;
|
||||||
|
|
||||||
|
export interface RoleDefinition {
|
||||||
|
members: OutPoint[];
|
||||||
|
validation_rules: ValidationRule[];
|
||||||
|
storages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationRule {
|
||||||
|
quorum: number;
|
||||||
|
fields: string[];
|
||||||
|
min_sig_member: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PcdCommitments = Record<string, string>;
|
||||||
|
|
||||||
|
export type Pcd = Record<string, number[]>;
|
||||||
|
|
||||||
|
export interface Member {
|
||||||
|
sp_addresses: string[];
|
||||||
|
}
|
||||||
|
|
||||||
5
pkg/sdk_client.js
Normal file
5
pkg/sdk_client.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as wasm from "./sdk_client_bg.wasm";
|
||||||
|
export * from "./sdk_client_bg.js";
|
||||||
|
import { __wbg_set_wasm } from "./sdk_client_bg.js";
|
||||||
|
__wbg_set_wasm(wasm);
|
||||||
|
wasm.__wbindgen_start();
|
||||||
1691
pkg/sdk_client_bg.js
Normal file
1691
pkg/sdk_client_bg.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
pkg/sdk_client_bg.wasm
Normal file
BIN
pkg/sdk_client_bg.wasm
Normal file
Binary file not shown.
73
pkg/sdk_client_bg.wasm.d.ts
vendored
Normal file
73
pkg/sdk_client_bg.wasm.d.ts
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const setup: () => void;
|
||||||
|
export const get_address: () => [number, number, number, number];
|
||||||
|
export const get_member: () => [number, number, number];
|
||||||
|
export const restore_device: (a: any) => [number, number];
|
||||||
|
export const create_device_from_sp_wallet: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const create_new_device: (a: number, b: number, c: number) => [number, number, number, number];
|
||||||
|
export const scan_blocks: (a: number, b: number, c: number) => any;
|
||||||
|
export const is_paired: () => [number, number, number];
|
||||||
|
export const pair_device: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
|
export const unpair_device: () => [number, number];
|
||||||
|
export const dump_wallet: () => [number, number, number, number];
|
||||||
|
export const reset_shared_secrets: () => [number, number];
|
||||||
|
export const set_shared_secrets: (a: number, b: number) => [number, number];
|
||||||
|
export const get_pairing_process_id: () => [number, number, number, number];
|
||||||
|
export const dump_device: () => [number, number, number];
|
||||||
|
export const dump_neutered_device: () => [number, number, number];
|
||||||
|
export const reset_device: () => [number, number];
|
||||||
|
export const get_txid: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const get_prevouts: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const get_opreturn: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const process_commit_new_state: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||||
|
export const parse_new_tx: (a: number, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const parse_cipher: (a: number, b: number, c: any, d: any) => [number, number, number];
|
||||||
|
export const get_outputs: () => [number, number, number];
|
||||||
|
export const get_available_amount: () => [bigint, number, number];
|
||||||
|
export const create_transaction: (a: number, b: number, c: number) => [number, number, number];
|
||||||
|
export const sign_transaction: (a: any) => [number, number, number];
|
||||||
|
export const create_new_process: (a: any, b: any, c: any, d: number, e: number, f: number, g: any) => [number, number, number];
|
||||||
|
export const update_process: (a: any, b: any, c: any, d: any, e: any) => [number, number, number];
|
||||||
|
export const request_data: (a: number, b: number, c: number, d: number, e: any, f: any) => [number, number, number];
|
||||||
|
export const create_update_message: (a: any, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const validate_state: (a: any, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const refuse_state: (a: any, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const evaluate_state: (a: any, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const create_response_prd: (a: any, b: number, c: number, d: any) => [number, number, number];
|
||||||
|
export const create_faucet_msg: () => [number, number, number, number];
|
||||||
|
export const get_storages: (a: number, b: number) => [number, number, number, number];
|
||||||
|
export const is_child_role: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
|
export const decrypt_data: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||||
|
export const encode_binary: (a: any) => [number, number, number];
|
||||||
|
export const encode_json: (a: any) => [number, number, number];
|
||||||
|
export const decode_value: (a: number, b: number) => [number, number, number];
|
||||||
|
export const hash_value: (a: any, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||||
|
export const get_merkle_proof: (a: any, b: number, c: number) => [number, number, number];
|
||||||
|
export const validate_merkle_proof: (a: any, b: number, c: number) => [number, number, number];
|
||||||
|
export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
|
||||||
|
export const rust_zstd_wasm_shim_malloc: (a: number) => number;
|
||||||
|
export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
||||||
|
export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
|
||||||
|
export const rust_zstd_wasm_shim_free: (a: number) => void;
|
||||||
|
export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
|
||||||
|
export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
|
||||||
|
export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
|
||||||
|
export const rustsecp256k1_v0_9_2_context_create: (a: number) => number;
|
||||||
|
export const rustsecp256k1_v0_9_2_context_destroy: (a: number) => void;
|
||||||
|
export const rustsecp256k1_v0_9_2_default_illegal_callback_fn: (a: number, b: number) => void;
|
||||||
|
export const rustsecp256k1_v0_9_2_default_error_callback_fn: (a: number, b: number) => void;
|
||||||
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
export const __wbindgen_exn_store: (a: number) => void;
|
||||||
|
export const __externref_table_alloc: () => number;
|
||||||
|
export const __wbindgen_export_4: WebAssembly.Table;
|
||||||
|
export const __wbindgen_export_5: WebAssembly.Table;
|
||||||
|
export const __externref_table_dealloc: (a: number) => void;
|
||||||
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||||
|
export const wasm_bindgen__convert__closures_____invoke__hc142e2252e76ee8b: (a: number, b: number) => void;
|
||||||
|
export const closure681_externref_shim: (a: number, b: number, c: any) => void;
|
||||||
|
export const closure1281_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||||
|
export const __wbindgen_start: () => void;
|
||||||
111
public/authorized-client.html
Normal file
111
public/authorized-client.html
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Redirection en cours…</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 2rem; color: #0a0a0a; }
|
||||||
|
.box { max-width: 720px; margin: 10vh auto; padding: 1.5rem; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||||
|
.muted { color: #6b7280; font-size: .95rem; }
|
||||||
|
.error { color: #b91c1c; }
|
||||||
|
.ok { color: #065f46; }
|
||||||
|
code { background: #f3f4f6; padding: .2rem .35rem; border-radius: 6px; }
|
||||||
|
a { color: #006BE0; text-decoration: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="box">
|
||||||
|
<h1>Connexion IdNot</h1>
|
||||||
|
<p class="muted" id="status">Traitement du code d'autorisation…</p>
|
||||||
|
<pre class="muted" id="details" style="white-space: pre-wrap"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function getQueryParam(name) {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
return params.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
|
// Domaine implicite: dev4.4nkweb.com (hébergement de cette page)
|
||||||
|
document.cookie = name + '=' + encodeURIComponent(value) + '; Path=/; Expires=' + expires + '; SameSite=None; Secure';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const code = getQueryParam('code');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const detailsEl = document.getElementById('details');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
statusEl.textContent = 'Aucun code reçu dans la redirection IdNot.';
|
||||||
|
statusEl.className = 'error';
|
||||||
|
detailsEl.textContent = 'Paramètre attendu: ?code=…\nRetour à l\'espace: https://dev4.4nkweb.com/lecoffre/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch('https://dev4.4nkweb.com/api/v1/idnot/auth', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Request-ID': 'bridge_' + Math.random().toString(36).slice(2)
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code })
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await resp.text();
|
||||||
|
let data;
|
||||||
|
try { data = JSON.parse(text); } catch (_) { data = null; }
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
statusEl.textContent = 'Connexion refusée (' + resp.status + ').';
|
||||||
|
statusEl.className = 'error';
|
||||||
|
detailsEl.textContent = (data && data.error && data.error.message) ? data.error.message : text;
|
||||||
|
// Redirige néanmoins vers le front avec état d\'erreur afin d\'afficher un message utilisateur.
|
||||||
|
setTimeout(function(){ location.replace('https://dev4.4nkweb.com/lecoffre/authorized-bridge#error=' + encodeURIComponent(String(resp.status))); }, 600);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attendu: { idNotUser, authToken }
|
||||||
|
if (!data || !data.authToken) {
|
||||||
|
statusEl.textContent = 'Réponse invalide du serveur.';
|
||||||
|
statusEl.className = 'error';
|
||||||
|
detailsEl.textContent = text;
|
||||||
|
setTimeout(function(){ location.replace('https://dev4.4nkweb.com/lecoffre/authorized-bridge#error=invalid_response'); }, 800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stocker le jeton pour le domaine dev4 (utilisé par le front)
|
||||||
|
setCookie('leCoffreAccessToken', data.authToken, 1);
|
||||||
|
|
||||||
|
statusEl.textContent = 'Connexion réussie, redirection…';
|
||||||
|
statusEl.className = 'ok';
|
||||||
|
detailsEl.textContent = '';
|
||||||
|
|
||||||
|
// Redirection vers le front avec token en hash en sauvegarde
|
||||||
|
location.replace('https://dev4.4nkweb.com/lecoffre/authorized-bridge#token=' + encodeURIComponent(data.authToken));
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.textContent = 'Erreur réseau lors de la connexion.';
|
||||||
|
statusEl.className = 'error';
|
||||||
|
detailsEl.textContent = String(e && e.message || e);
|
||||||
|
setTimeout(function(){ location.replace('https://dev4.4nkweb.com/lecoffre/authorized-bridge#error=network'); }, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -77,6 +77,99 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Confirmation Modal Styles */
|
||||||
|
#confirmation-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-confirmation {
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-confirmation h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-confirmation p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-confirmation h3 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-confirmation p {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nav-wrapper {
|
.nav-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@ -427,24 +427,43 @@ body {
|
|||||||
|
|
||||||
/* Style pour la modal de confirmation */
|
/* Style pour la modal de confirmation */
|
||||||
.confirm-delete-modal {
|
.confirm-delete-modal {
|
||||||
display: none;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translate(-50%, -50%);
|
width: 100%;
|
||||||
background: white;
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-content {
|
||||||
|
background-color: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 1100;
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-delete-content h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-content p {
|
||||||
|
margin: 15px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-delete-buttons {
|
.confirm-delete-buttons {
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-delete-buttons button {
|
.confirm-delete-buttons button {
|
||||||
@ -452,25 +471,27 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-btn {
|
.confirm-delete-buttons .confirm-btn {
|
||||||
background-color: #dc3545;
|
background-color: #dc3545;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.confirm-delete-buttons .confirm-btn:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-buttons .cancel-btn {
|
||||||
background-color: #6c757d;
|
background-color: #6c757d;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-account-section {
|
.confirm-delete-buttons .cancel-btn:hover {
|
||||||
border-top: 1px solid #eee;
|
background-color: #5a6268;
|
||||||
padding-top: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*-------------------------------------- Export--------------------------------------*/
|
/*-------------------------------------- Export--------------------------------------*/
|
||||||
.export-section {
|
.export-section {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
@ -1423,3 +1444,64 @@ body {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pairing-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pairing-modal-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pairing-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
background-color: #ccc;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|||||||
BIN
sdk_client-0.1.3.tgz
Normal file
BIN
sdk_client-0.1.3.tgz
Normal file
Binary file not shown.
@ -9,7 +9,7 @@ let notifications = [];
|
|||||||
export async function unpair() {
|
export async function unpair() {
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
await service.unpairDevice();
|
await service.unpairDevice();
|
||||||
navigate('home');
|
await navigate('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
(window as any).unpair = unpair;
|
(window as any).unpair = unpair;
|
||||||
@ -28,7 +28,7 @@ function toggleMenu() {
|
|||||||
|
|
||||||
async function getNotifications() {
|
async function getNotifications() {
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
notifications = service.getNotifications();
|
notifications = service.getNotifications() || [];
|
||||||
return notifications;
|
return notifications;
|
||||||
}
|
}
|
||||||
function openCloseNotifications() {
|
function openCloseNotifications() {
|
||||||
@ -102,8 +102,8 @@ async function setNotification(notifications: any[]): Promise<void> {
|
|||||||
|
|
||||||
async function fetchNotifications() {
|
async function fetchNotifications() {
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const data = service.getNotifications();
|
const data = service.getNotifications() || [];
|
||||||
setNotification(data);
|
await setNotification(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUserProfile() {
|
async function loadUserProfile() {
|
||||||
@ -204,7 +204,7 @@ async function disconnect() {
|
|||||||
await Promise.all(registrations.map(registration => registration.unregister()));
|
await Promise.all(registrations.map(registration => registration.unregister()));
|
||||||
console.log('Service worker unregistered');
|
console.log('Service worker unregistered');
|
||||||
|
|
||||||
navigate('home');
|
await navigate('home');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = window.location.origin;
|
window.location.href = window.location.origin;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import ModalService from '../../services/modal.service';
|
import ModalService from '../../services/modal.service';
|
||||||
|
|
||||||
const modalService = await ModalService.getInstance();
|
const modalService = await ModalService.getInstance();
|
||||||
export async function confirm() {
|
// export async function confirm() {
|
||||||
modalService.confirmPairing();
|
// modalService.confirmPairing();
|
||||||
}
|
// }
|
||||||
|
|
||||||
export async function closeConfirmationModal() {
|
export async function closeConfirmationModal() {
|
||||||
modalService.closeConfirmationModal();
|
modalService.closeConfirmationModal();
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export default class QrScannerComponent extends HTMLElement {
|
|||||||
if (spAddress) {
|
if (spAddress) {
|
||||||
// Call the sendPairingTx function with the extracted sp_address
|
// Call the sendPairingTx function with the extracted sp_address
|
||||||
try {
|
try {
|
||||||
await prepareAndSendPairingTx(spAddress);
|
await prepareAndSendPairingTx();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to pair:', e);
|
console.error('Failed to pair:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
<div id="validation-rule-modal" style="
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
">
|
||||||
|
<h2 style="font-size: 1.2rem; font-weight: bold; margin-bottom: 1rem;">
|
||||||
|
Add Validation Rule
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<label style="display: block; margin-bottom: 0.5rem;">
|
||||||
|
Quorum:
|
||||||
|
<input id="vr-quorum" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style="display: block; margin-bottom: 0.5rem;">
|
||||||
|
Min Sig Member:
|
||||||
|
<input id="vr-minsig" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style="display: block; margin-bottom: 1rem;">
|
||||||
|
Fields (comma-separated):
|
||||||
|
<input id="vr-fields" type="text" placeholder="e.g. field1, field2" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
|
||||||
|
<button id="vr-cancel" style="padding: 0.5rem 1rem;">Cancel</button>
|
||||||
|
<button id="vr-submit" style="padding: 0.5rem 1rem; background-color: #4f46e5; color: white; border: none; border-radius: 0.375rem;">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
export interface ValidationRule {
|
||||||
|
quorum: number;
|
||||||
|
fields: string[];
|
||||||
|
min_sig_member: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and injects the modal HTML into the document if not already loaded.
|
||||||
|
*/
|
||||||
|
export async function loadValidationRuleModal(templatePath: string = '/src/components/validation-rule-modal/validation-rule-modal.html') {
|
||||||
|
if (document.getElementById('validation-rule-modal')) return;
|
||||||
|
|
||||||
|
const res = await fetch(templatePath);
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = html;
|
||||||
|
|
||||||
|
const modal = tempDiv.querySelector('#validation-rule-modal');
|
||||||
|
if (!modal) {
|
||||||
|
throw new Error('Modal HTML missing #validation-rule-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the modal and lets the user input a ValidationRule.
|
||||||
|
* Calls the callback with the constructed rule on submit.
|
||||||
|
*/
|
||||||
|
export function showValidationRuleModal(onSubmit: (rule: ValidationRule) => void) {
|
||||||
|
const modal = document.getElementById('validation-rule-modal')!;
|
||||||
|
const quorumInput = document.getElementById('vr-quorum') as HTMLInputElement;
|
||||||
|
const minsigInput = document.getElementById('vr-minsig') as HTMLInputElement;
|
||||||
|
const fieldsInput = document.getElementById('vr-fields') as HTMLInputElement;
|
||||||
|
|
||||||
|
const cancelBtn = document.getElementById('vr-cancel')!;
|
||||||
|
const submitBtn = document.getElementById('vr-submit')!;
|
||||||
|
|
||||||
|
// Reset values
|
||||||
|
quorumInput.value = '';
|
||||||
|
minsigInput.value = '';
|
||||||
|
fieldsInput.value = '';
|
||||||
|
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
cancelBtn.onclick = () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
submitBtn.onclick = () => {
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
quorum: parseInt(quorumInput.value),
|
||||||
|
min_sig_member: parseInt(minsigInput.value),
|
||||||
|
fields: fieldsInput.value.split(',').map(f => f.trim()).filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.style.display = 'none';
|
||||||
|
onSubmit(rule);
|
||||||
|
};
|
||||||
|
}
|
||||||
42
src/index.ts
42
src/index.ts
@ -1,39 +1,3 @@
|
|||||||
// import Services from './services/service';
|
export { default as Services } from './services/service';
|
||||||
|
export { default as Database } from './services/database.service';
|
||||||
// document.addEventListener('DOMContentLoaded', async () => {
|
export { MessageType } from './models/process.model';
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|||||||
14
src/main.ts
14
src/main.ts
@ -1,18 +1,18 @@
|
|||||||
import { SignatureComponent } from './pages/signature/signature-component';
|
import { SignatureComponent } from './pages/signature/signature-component';
|
||||||
import { SignatureElement } from './pages/signature/signature';
|
import { SignatureElement } from './pages/signature/signature';
|
||||||
import { ChatComponent } from './pages/chat/chat-component';
|
/*import { ChatComponent } from './pages/chat/chat-component';
|
||||||
import { ChatElement } from './pages/chat/chat';
|
import { ChatElement } from './pages/chat/chat';*/
|
||||||
import { AccountComponent } from './pages/account/account-component';
|
import { AccountComponent } from './pages/account/account-component';
|
||||||
import { AccountElement } from './pages/account/account';
|
import { AccountElement } from './pages/account/account';
|
||||||
|
|
||||||
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
|
export { SignatureComponent, SignatureElement, AccountComponent, AccountElement };
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'signature-component': SignatureComponent;
|
'signature-component': SignatureComponent;
|
||||||
'signature-element': SignatureElement;
|
'signature-element': SignatureElement;
|
||||||
'chat-component': ChatComponent;
|
/*'chat-component': ChatComponent;
|
||||||
'chat-element': ChatElement;
|
'chat-element': ChatElement;*/
|
||||||
'account-component': AccountComponent;
|
'account-component': AccountComponent;
|
||||||
'account-element': AccountElement;
|
'account-element': AccountElement;
|
||||||
}
|
}
|
||||||
@ -23,8 +23,8 @@ if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
|||||||
// Initialiser les composants si nécessaire
|
// Initialiser les composants si nécessaire
|
||||||
customElements.define('signature-component', SignatureComponent);
|
customElements.define('signature-component', SignatureComponent);
|
||||||
customElements.define('signature-element', SignatureElement);
|
customElements.define('signature-element', SignatureElement);
|
||||||
customElements.define('chat-component', ChatComponent);
|
/*customElements.define('chat-component', ChatComponent);
|
||||||
customElements.define('chat-element', ChatElement);
|
customElements.define('chat-element', ChatElement);*/
|
||||||
customElements.define('account-component', AccountComponent);
|
customElements.define('account-component', AccountComponent);
|
||||||
customElements.define('account-element', AccountElement);
|
customElements.define('account-element', AccountElement);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
import { Device, Process, SecretsStore } from ".././pkg/sdk_client.js";
|
||||||
|
|
||||||
export interface BackUp {
|
export interface BackUp {
|
||||||
device: Device,
|
device: Device,
|
||||||
|
|||||||
@ -21,3 +21,45 @@ export interface INotification {
|
|||||||
sendToNotificationPage?: boolean;
|
sendToNotificationPage?: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|||||||
@ -20,23 +20,25 @@ declare global {
|
|||||||
updateNavbarBanner: (bannerUrl: string) => void;
|
updateNavbarBanner: (bannerUrl: string) => void;
|
||||||
saveBannerToLocalStorage: (bannerUrl: string) => void;
|
saveBannerToLocalStorage: (bannerUrl: string) => void;
|
||||||
loadSavedBanner: () => void;
|
loadSavedBanner: () => void;
|
||||||
cancelAddRow: () => void;
|
cancelAddRowPairing: () => void;
|
||||||
saveName: (cell: HTMLElement, input: HTMLInputElement) => void;
|
saveName: (cell: HTMLElement, input: HTMLInputElement) => void;
|
||||||
showProcessNotifications: (processName: string) => void;
|
showProcessNotifications: (processName: string) => void;
|
||||||
handleLogout: () => void;
|
handleLogout: () => void;
|
||||||
initializeEventListeners: () => void;
|
initializeEventListeners: () => void;
|
||||||
showProcess: () => void;
|
showProcess: () => void;
|
||||||
|
showProcessCreation: () => void;
|
||||||
|
showDocumentValidation: () => void;
|
||||||
updateNavbarName: (name: string) => void;
|
updateNavbarName: (name: string) => void;
|
||||||
updateNavbarLastName: (lastName: string) => void;
|
updateNavbarLastName: (lastName: string) => void;
|
||||||
showAlert: (title: string, text?: string, icon?: string) => void;
|
showAlert: (title: string, text?: string, icon?: string) => void;
|
||||||
addRow: () => void;
|
addRowPairing: () => void;
|
||||||
confirmRow: () => void;
|
confirmRowPairing: () => void;
|
||||||
cancelRow: () => void;
|
cancelRowPairing: () => void;
|
||||||
deleteRow: (button: HTMLButtonElement) => void;
|
deleteRowPairing: (button: HTMLButtonElement) => void;
|
||||||
generateRecoveryWords: () => string[];
|
generateRecoveryWords: () => string[];
|
||||||
exportUserData: () => void;
|
exportUserData: () => void;
|
||||||
updateActionButtons: () => void;
|
updateActionButtons: () => void;
|
||||||
showQRCodeModal: (address: string) => void;
|
showQRCodeModal: (pairingId: string) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,11 +49,31 @@ import { addressToEmoji } from '../../utils/sp-address.utils';
|
|||||||
import { getCorrectDOM } from '../../utils/document.utils';
|
import { getCorrectDOM } from '../../utils/document.utils';
|
||||||
import accountStyle from '../../../public/style/account.css?inline';
|
import accountStyle from '../../../public/style/account.css?inline';
|
||||||
import Services from '../../services/service';
|
import Services from '../../services/service';
|
||||||
|
import { getProcessCreation } from './process-creation';
|
||||||
|
import { getDocumentValidation } from './document-validation';
|
||||||
|
import { createProcessTab } from './process';
|
||||||
|
|
||||||
let isAddingRow = false;
|
let isAddingRow = false;
|
||||||
let currentRow: HTMLTableRowElement | null = null;
|
let currentRow: HTMLTableRowElement | null = null;
|
||||||
let currentMode: keyof typeof STORAGE_KEYS = 'pairing';
|
let currentMode: keyof typeof STORAGE_KEYS = 'pairing';
|
||||||
|
|
||||||
|
interface Process {
|
||||||
|
states: Array<{
|
||||||
|
committed_in: string;
|
||||||
|
keys: {};
|
||||||
|
pcd_commitment: {
|
||||||
|
counter: string;
|
||||||
|
};
|
||||||
|
public_data: {
|
||||||
|
memberPublicName?: string;
|
||||||
|
};
|
||||||
|
roles: {
|
||||||
|
pairing?: {};
|
||||||
|
};
|
||||||
|
state_id: string;
|
||||||
|
validation_tokens: Array<any>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
class AccountElement extends HTMLElement {
|
class AccountElement extends HTMLElement {
|
||||||
private dom: Node;
|
private dom: Node;
|
||||||
@ -141,7 +163,7 @@ class AccountElement extends HTMLElement {
|
|||||||
<!-- User Info Section -->
|
<!-- User Info Section -->
|
||||||
<div class="popup-info">
|
<div class="popup-info">
|
||||||
<p><strong>Name:</strong> <span class="editable" id="popup-name"></span></p>
|
<p><strong>Name:</strong> <span class="editable" id="popup-name"></span></p>
|
||||||
<p><strong>Last Name:</strong> <span class="editable" id="popup-lastname"></span></p>
|
<!--<p><strong>Last Name:</strong> <span class="editable" id="popup-lastname"></span></p>-->
|
||||||
<p><strong>Address:</strong> 🏠 🌍 🗽🎊😩-🎊😑🎄😩</p>
|
<p><strong>Address:</strong> 🏠 🌍 🗽🎊😩-🎊😑🎄😩</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -154,18 +176,22 @@ class AccountElement extends HTMLElement {
|
|||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="parameter-list-ul" onclick="window.showPairing()">Pairing 🔗</ul>
|
<ul class="parameter-list-ul" onclick="window.showPairing()">Pairing 🔗</ul>
|
||||||
<ul class="parameter-list-ul" onclick="window.showWallet()">Wallet 👛</ul>
|
<!-- <ul class="parameter-list-ul" onclick="window.showWallet()">Wallet 👛</ul> -->
|
||||||
<ul class="parameter-list-ul" onclick="window.showProcess()">Process ⚙️</ul>
|
<ul class="parameter-list-ul" onclick="window.showProcess()">Process ⚙️</ul>
|
||||||
<ul class="parameter-list-ul" onclick="window.showData()">Data 💾</ul>
|
<ul class="parameter-list-ul" onclick="window.showProcessCreation()">Process Creation</ul>
|
||||||
|
<ul class="parameter-list-ul" onclick="window.showDocumentValidation()">Document Validation</ul>
|
||||||
|
<!-- <ul class="parameter-list-ul" onclick="window.showData()">Data 💾</ul> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Parameter Area -->
|
<!-- Parameter Area -->
|
||||||
<div class="parameter-area">
|
<div class="parameter-area">
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div id="pairing-content"></div>
|
<div id="pairing-content"></div>
|
||||||
<div id="wallet-content"></div>
|
<!-- <div id="wallet-content"></div> -->
|
||||||
<div id="process-content"></div>
|
<div id="process-content"></div>
|
||||||
<div id="data-content"></div>
|
<div id="process-creation-content"></div>
|
||||||
|
<div id="document-validation-content"></div>
|
||||||
|
<!-- <div id="data-content"></div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -175,6 +201,8 @@ class AccountElement extends HTMLElement {
|
|||||||
window.showPairing = () => this.showPairing();
|
window.showPairing = () => this.showPairing();
|
||||||
window.showWallet = () => this.showWallet();
|
window.showWallet = () => this.showWallet();
|
||||||
window.showProcess = () => this.showProcess();
|
window.showProcess = () => this.showProcess();
|
||||||
|
window.showProcessCreation = () => this.showProcessCreation();
|
||||||
|
window.showDocumentValidation = () => this.showDocumentValidation();
|
||||||
window.showData = () => this.showData();
|
window.showData = () => this.showData();
|
||||||
window.addWalletRow = () => this.addWalletRow();
|
window.addWalletRow = () => this.addWalletRow();
|
||||||
window.confirmWalletRow = () => this.confirmWalletRow();
|
window.confirmWalletRow = () => this.confirmWalletRow();
|
||||||
@ -184,10 +212,10 @@ class AccountElement extends HTMLElement {
|
|||||||
window.handleLogout = () => this.handleLogout();
|
window.handleLogout = () => this.handleLogout();
|
||||||
window.confirmDeleteAccount = () => this.confirmDeleteAccount();
|
window.confirmDeleteAccount = () => this.confirmDeleteAccount();
|
||||||
window.showContractPopup = (contractId: string) => this.showContractPopup(contractId);
|
window.showContractPopup = (contractId: string) => this.showContractPopup(contractId);
|
||||||
window.addRow = () => this.addRow();
|
window.addRowPairing = () => this.addRowPairing();
|
||||||
window.deleteRow = (button: HTMLButtonElement) => this.deleteRow(button);
|
window.deleteRowPairing = (button: HTMLButtonElement) => this.deleteRowPairing(button);
|
||||||
window.confirmRow = () => this.confirmRow();
|
window.confirmRowPairing = () => this.confirmRowPairing();
|
||||||
window.cancelRow = () => this.cancelRow();
|
window.cancelRowPairing = () => this.cancelRowPairing();
|
||||||
window.updateNavbarBanner = (bannerUrl: string) => this.updateNavbarBanner(bannerUrl);
|
window.updateNavbarBanner = (bannerUrl: string) => this.updateNavbarBanner(bannerUrl);
|
||||||
window.saveBannerToLocalStorage = (bannerUrl: string) => this.saveBannerToLocalStorage(bannerUrl);
|
window.saveBannerToLocalStorage = (bannerUrl: string) => this.saveBannerToLocalStorage(bannerUrl);
|
||||||
window.loadSavedBanner = () => this.loadSavedBanner();
|
window.loadSavedBanner = () => this.loadSavedBanner();
|
||||||
@ -199,7 +227,7 @@ class AccountElement extends HTMLElement {
|
|||||||
window.updateActionButtons = () => this.updateActionButtons();
|
window.updateActionButtons = () => this.updateActionButtons();
|
||||||
window.openAvatarPopup = () => this.openAvatarPopup();
|
window.openAvatarPopup = () => this.openAvatarPopup();
|
||||||
window.closeAvatarPopup = () => this.closeAvatarPopup();
|
window.closeAvatarPopup = () => this.closeAvatarPopup();
|
||||||
window.showQRCodeModal = (address: string) => this.showQRCodeModal(address);
|
window.showQRCodeModal = (pairingId: string) => this.showQRCodeModal(pairingId);
|
||||||
|
|
||||||
if (!localStorage.getItem('rows')) {
|
if (!localStorage.getItem('rows')) {
|
||||||
localStorage.setItem('rows', JSON.stringify(defaultRows));
|
localStorage.setItem('rows', JSON.stringify(defaultRows));
|
||||||
@ -488,7 +516,7 @@ private getConfirmFunction(): string {
|
|||||||
case 'process':
|
case 'process':
|
||||||
return 'window.confirmProcessRow()';
|
return 'window.confirmProcessRow()';
|
||||||
default:
|
default:
|
||||||
return 'window.confirmRow()';
|
return 'window.confirmRowPairing()';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,50 +527,94 @@ private getCancelFunction(): string {
|
|||||||
case 'process':
|
case 'process':
|
||||||
return 'window.cancelProcessRow()';
|
return 'window.cancelProcessRow()';
|
||||||
default:
|
default:
|
||||||
return 'window.cancelRow()';
|
return 'window.cancelRowPairing()';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonctions de gestion des tableaux
|
// Fonctions de gestion des tableaux
|
||||||
private addRow(): void {
|
private async addRowPairing(): Promise<void> {
|
||||||
if (isAddingRow) return;
|
if (isAddingRow) return;
|
||||||
|
|
||||||
isAddingRow = true;
|
isAddingRow = true;
|
||||||
const table = this.shadowRoot?.querySelector<HTMLTableElement>('#pairing-table tbody');
|
|
||||||
if (!table) return;
|
|
||||||
|
|
||||||
currentRow = table.insertRow();
|
// Créer la popup
|
||||||
const cells = ['SP Address', 'Device Name', 'SP Emojis'];
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'pairing-modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="pairing-modal-content">
|
||||||
|
<h3>Add New Device</h3>
|
||||||
|
<div class="pairing-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sp-address">SP Address</label>
|
||||||
|
<input type="text" id="sp-address" class="edit-input" placeholder="Enter SP Address">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-name">Device Name</label>
|
||||||
|
<input type="text" id="device-name" class="edit-input" placeholder="Enter Device Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sp-emojis">SP Emojis</label>
|
||||||
|
<input type="text" id="sp-emojis" class="edit-input" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="confirm-button">Confirm</button>
|
||||||
|
<button class="cancel-button">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
cells.forEach((_, index) => {
|
this.shadowRoot?.appendChild(modal);
|
||||||
const cell = currentRow!.insertCell();
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'text';
|
|
||||||
input.className = 'edit-input';
|
|
||||||
|
|
||||||
// Ajouter un événement pour mettre à jour automatiquement les emojis
|
// Ajouter les event listeners
|
||||||
if (index === 0) {
|
const spAddressInput = modal.querySelector('#sp-address') as HTMLInputElement;
|
||||||
input.addEventListener('input', async (e) => {
|
const spEmojisInput = modal.querySelector('#sp-emojis') as HTMLInputElement;
|
||||||
const addressInput = e.target as HTMLInputElement;
|
const deviceNameInput = modal.querySelector('#device-name') as HTMLInputElement;
|
||||||
const emojiCell = currentRow!.cells[2];
|
const confirmButton = modal.querySelector('.confirm-button');
|
||||||
const emojis = await addressToEmoji(addressInput.value);
|
const cancelButton = modal.querySelector('.cancel-button');
|
||||||
if (emojiCell.querySelector('input')) {
|
|
||||||
(emojiCell.querySelector('input') as HTMLInputElement).value = emojis;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === 2) {
|
// Mettre à jour les emojis automatiquement
|
||||||
input.readOnly = true;
|
spAddressInput?.addEventListener('input', async () => {
|
||||||
}
|
const emojis = await addressToEmoji(spAddressInput.value);
|
||||||
|
if (spEmojisInput) spEmojisInput.value = emojis;
|
||||||
cell.appendChild(input);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteCell = currentRow.insertCell();
|
// Gérer la confirmation
|
||||||
deleteCell.style.width = '40px';
|
confirmButton?.addEventListener('click', () => {
|
||||||
|
const spAddress = spAddressInput?.value.trim();
|
||||||
|
const deviceName = deviceNameInput?.value.trim();
|
||||||
|
const spEmojis = spEmojisInput?.value.trim();
|
||||||
|
|
||||||
this.updateActionButtons();
|
if (!spAddress || !deviceName) {
|
||||||
|
this.showAlert('Please fill in all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (spAddress.length !== 118) {
|
||||||
|
// this.showAlert('SP Address must be exactly 118 characters long');
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
|
||||||
|
const newRow: Row = {
|
||||||
|
column1: spAddress,
|
||||||
|
column2: deviceName,
|
||||||
|
column3: spEmojis || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const storageKey = STORAGE_KEYS[currentMode];
|
||||||
|
const rows: Row[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
||||||
|
rows.push(newRow);
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(rows));
|
||||||
|
|
||||||
|
this.updateTableContent(rows);
|
||||||
|
modal.remove();
|
||||||
|
isAddingRow = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer l'annulation
|
||||||
|
cancelButton?.addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
isAddingRow = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonctions de mise à jour de l'interface
|
// Fonctions de mise à jour de l'interface
|
||||||
@ -552,7 +624,6 @@ private updateTableContent(rows: Row[]): void {
|
|||||||
|
|
||||||
tbody.innerHTML = rows.map(row => `
|
tbody.innerHTML = rows.map(row => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${row.column1}</td>
|
|
||||||
<td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td>
|
<td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td>
|
||||||
<td>${row.column3}</td>
|
<td>${row.column3}</td>
|
||||||
<td>
|
<td>
|
||||||
@ -563,7 +634,7 @@ private updateTableContent(rows: Row[]): void {
|
|||||||
onclick="window.showQRCodeModal('${encodeURIComponent(row.column1)}')">
|
onclick="window.showQRCodeModal('${encodeURIComponent(row.column1)}')">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="delete-button" onclick="window.deleteRow(this)">
|
<button class="delete-button" onclick="window.deleteRowPairing(this)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red">
|
||||||
<path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
|
<path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -575,7 +646,7 @@ private updateTableContent(rows: Row[]): void {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private confirmRow(): void {
|
private confirmRowPairing(): void {
|
||||||
if (!currentRow) return;
|
if (!currentRow) return;
|
||||||
|
|
||||||
const inputs = currentRow.getElementsByTagName('input');
|
const inputs = currentRow.getElementsByTagName('input');
|
||||||
@ -611,7 +682,7 @@ private confirmRow(): void {
|
|||||||
this.updateTableContent(rows);
|
this.updateTableContent(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelRow(): void {
|
private cancelRowPairing(): void {
|
||||||
if (!currentRow) return;
|
if (!currentRow) return;
|
||||||
|
|
||||||
currentRow.remove();
|
currentRow.remove();
|
||||||
@ -626,11 +697,11 @@ private resetButtonContainer(): void {
|
|||||||
if (!buttonContainer) return;
|
if (!buttonContainer) return;
|
||||||
|
|
||||||
buttonContainer.innerHTML = `
|
buttonContainer.innerHTML = `
|
||||||
<button class="add-row-button button-style" onclick="window.addRow()">Add a line</button>
|
<button class="add-row-button button-style" onclick="window.addRowPairing()">Add a line</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteRow(button: HTMLButtonElement): void {
|
private deleteRowPairing(button: HTMLButtonElement): void {
|
||||||
const row = button.closest('tr');
|
const row = button.closest('tr');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
|
|
||||||
@ -643,21 +714,53 @@ private deleteRow(button: HTMLButtonElement): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = Array.from(table.children).indexOf(row);
|
// Créer la modal de confirmation
|
||||||
row.style.transition = 'opacity 0.3s, transform 0.3s';
|
const modal = document.createElement('div');
|
||||||
row.style.opacity = '0';
|
modal.className = 'confirm-delete-modal';
|
||||||
row.style.transform = 'translateX(-100%)';
|
modal.innerHTML = `
|
||||||
|
<div class="confirm-delete-content">
|
||||||
|
<h3>Confirm Deletion</h3>
|
||||||
|
<p>Are you sure you want to delete this device?</p>
|
||||||
|
<div class="confirm-delete-buttons">
|
||||||
|
<button class="cancel-btn">Cancel</button>
|
||||||
|
<button class="confirm-btn">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
setTimeout(() => {
|
this.shadowRoot?.appendChild(modal);
|
||||||
row.remove();
|
|
||||||
|
|
||||||
|
// Gérer les boutons de la modal
|
||||||
|
const confirmBtn = modal.querySelector('.confirm-btn');
|
||||||
|
const cancelBtn = modal.querySelector('.cancel-btn');
|
||||||
|
|
||||||
|
confirmBtn?.addEventListener('click', () => {
|
||||||
|
// Calculer l'index AVANT de supprimer la ligne du DOM
|
||||||
|
const index = Array.from(table.children).indexOf(row);
|
||||||
const storageKey = STORAGE_KEYS[currentMode];
|
const storageKey = STORAGE_KEYS[currentMode];
|
||||||
const rows = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
const rows = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
||||||
|
|
||||||
|
// Supprimer du localStorage
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
rows.splice(index, 1);
|
rows.splice(index, 1);
|
||||||
localStorage.setItem(storageKey, JSON.stringify(rows));
|
localStorage.setItem(storageKey, JSON.stringify(rows));
|
||||||
}
|
}
|
||||||
}, 300);
|
|
||||||
|
// Animation et suppression du DOM
|
||||||
|
row.style.transition = 'opacity 0.3s, transform 0.3s';
|
||||||
|
row.style.opacity = '0';
|
||||||
|
row.style.transform = 'translateX(-100%)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
row.remove();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelBtn?.addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private editDeviceName(cell: HTMLTableCellElement): void {
|
private editDeviceName(cell: HTMLTableCellElement): void {
|
||||||
@ -682,7 +785,7 @@ private editDeviceName(cell: HTMLTableCellElement): void {
|
|||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): void {
|
private async finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): Promise<void> {
|
||||||
const newValue = input.value.trim();
|
const newValue = input.value.trim();
|
||||||
if (newValue === '') {
|
if (newValue === '') {
|
||||||
cell.textContent = cell.getAttribute('data-original-value') || '';
|
cell.textContent = cell.getAttribute('data-original-value') || '';
|
||||||
@ -690,24 +793,25 @@ private finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = cell.closest('tr');
|
try {
|
||||||
if (!row) return;
|
const service = await Services.getInstance();
|
||||||
|
const pairingProcessId = service.getPairingProcessId();
|
||||||
|
const process = await service.getProcess(pairingProcessId);
|
||||||
|
|
||||||
const table = row.closest('tbody');
|
// Mettre à jour le nom via le service
|
||||||
if (!table) return;
|
await service.updateMemberPublicName(process, newValue);
|
||||||
|
|
||||||
const index = Array.from(table.children).indexOf(row);
|
// Mettre à jour l'interface
|
||||||
const storageKey = STORAGE_KEYS[currentMode];
|
cell.textContent = newValue;
|
||||||
const rows: Row[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
cell.classList.remove('editing');
|
||||||
|
} catch (error) {
|
||||||
if (rows[index]) {
|
console.error('Failed to update name:', error);
|
||||||
rows[index].column2 = newValue;
|
// Restaurer l'ancienne valeur en cas d'erreur
|
||||||
localStorage.setItem(storageKey, JSON.stringify(rows));
|
cell.textContent = cell.getAttribute('data-original-value') || '';
|
||||||
|
cell.classList.remove('editing');
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.textContent = newValue;
|
|
||||||
cell.classList.remove('editing');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour gérer le téléchargement de l'avatar
|
// Fonction pour gérer le téléchargement de l'avatar
|
||||||
private handleAvatarUpload(event: Event): void {
|
private handleAvatarUpload(event: Event): void {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@ -729,63 +833,62 @@ private handleAvatarUpload(event: Event): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async showProcessCreation(): Promise<void> {
|
||||||
private showProcess(): void {
|
|
||||||
//console.log("showProcess called");
|
|
||||||
currentMode = 'process';
|
|
||||||
this.hideAllContent();
|
this.hideAllContent();
|
||||||
|
const container = this.shadowRoot?.getElementById('process-creation-content');
|
||||||
const headerTitle = this.shadowRoot?.getElementById('header-title');
|
if (container) {
|
||||||
if (headerTitle) headerTitle.textContent = 'Process';
|
getProcessCreation(container);
|
||||||
|
|
||||||
const processContent = this.shadowRoot?.getElementById('process-content');
|
|
||||||
if (processContent) {
|
|
||||||
processContent.style.display = 'block';
|
|
||||||
processContent.innerHTML = `
|
|
||||||
<div class="parameter-header" id="parameter-header">Process</div>
|
|
||||||
<div class="table-container">
|
|
||||||
<table class="parameter-table" id="process-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Process Name</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Notifications</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
||||||
this.updateProcessTableContent(mockProcessRows);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction utilitaire pour mettre à jour le contenu du tableau Process
|
private async showDocumentValidation(): Promise<void> {
|
||||||
private updateProcessTableContent(rows: any[]): void {
|
this.hideAllContent();
|
||||||
const tbody = this.shadowRoot?.querySelector('#process-table tbody');
|
const container = this.shadowRoot?.getElementById('document-validation-content');
|
||||||
if (!tbody) return;
|
if (container) {
|
||||||
|
getDocumentValidation(container);
|
||||||
tbody.innerHTML = rows.map(row => `
|
}
|
||||||
<tr>
|
|
||||||
<td>${row.process}</td>
|
|
||||||
<td>${row.role}</td>
|
|
||||||
<td>
|
|
||||||
<div class="notification-container" onclick="window.showProcessNotifications('${row.process}')">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16">
|
|
||||||
<path d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416H416c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3H160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="notification-count" data-process="${row.process}">
|
|
||||||
${row.notification?.messages?.filter((m: any) => !m.read).length || 0}/${row.notification?.messages?.length || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async showProcess(): Promise<void> {
|
||||||
|
this.hideAllContent();
|
||||||
|
const container = this.shadowRoot?.getElementById('process-content');
|
||||||
|
if (container) {
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
const myProcesses = await service.getMyProcesses();
|
||||||
|
if (myProcesses && myProcesses.length != 0) {
|
||||||
|
const myProcessesDataUnfiltered: { name: string, publicData: Record<string, any> }[] = await Promise.all(myProcesses.map(async processId => {
|
||||||
|
const process = await service.getProcess(processId);
|
||||||
|
const lastState = service.getLastCommitedState(process);
|
||||||
|
if (!lastState) {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
publicData: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const description = await service.decryptAttribute(processId, lastState, 'description');
|
||||||
|
const name = description ? description : 'N/A';
|
||||||
|
const publicData = await service.getPublicData(process);
|
||||||
|
if (!publicData) {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
publicData: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
publicData: publicData
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
const myProcessesData = myProcessesDataUnfiltered.filter(
|
||||||
|
(p) => p.name !== '' && Object.keys(p.publicData).length != 0
|
||||||
|
);
|
||||||
|
|
||||||
|
createProcessTab(container, myProcessesData);
|
||||||
|
} else {
|
||||||
|
createProcessTab(container, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private showProcessNotifications(processName: string): void {
|
private showProcessNotifications(processName: string): void {
|
||||||
const process = mockProcessRows.find(p => p.process === processName);
|
const process = mockProcessRows.find(p => p.process === processName);
|
||||||
@ -852,9 +955,11 @@ private handleLogout(): void {
|
|||||||
|
|
||||||
|
|
||||||
// Fonctions de gestion des contrats
|
// Fonctions de gestion des contrats
|
||||||
private showContractPopup(contractId: string) {
|
private showContractPopup(contractId: string, event?: Event) {
|
||||||
// Empêcher la navigation par défaut
|
if (event) {
|
||||||
event?.preventDefault();
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the contract exists in mockContracts
|
// Check if the contract exists in mockContracts
|
||||||
const contract = mockContracts[contractId as keyof typeof mockContracts];
|
const contract = mockContracts[contractId as keyof typeof mockContracts];
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
@ -862,7 +967,6 @@ private showContractPopup(contractId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer la popup
|
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.className = 'contract-popup-overlay';
|
popup.className = 'contract-popup-overlay';
|
||||||
popup.innerHTML = `
|
popup.innerHTML = `
|
||||||
@ -881,10 +985,8 @@ private showContractPopup(contractId: string) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Ajouter la popup au body
|
|
||||||
this.shadowRoot?.appendChild(popup);
|
this.shadowRoot?.appendChild(popup);
|
||||||
|
|
||||||
// Gérer la fermeture
|
|
||||||
const closeBtn = popup.querySelector('.close-contract-popup');
|
const closeBtn = popup.querySelector('.close-contract-popup');
|
||||||
const closePopup = () => popup.remove();
|
const closePopup = () => popup.remove();
|
||||||
|
|
||||||
@ -896,7 +998,7 @@ private showContractPopup(contractId: string) {
|
|||||||
|
|
||||||
// Fonction utilitaire pour cacher tous les contenus
|
// Fonction utilitaire pour cacher tous les contenus
|
||||||
private hideAllContent(): void {
|
private hideAllContent(): void {
|
||||||
const contents = ['pairing-content', 'wallet-content', 'process-content', 'data-content'];
|
const contents = ['pairing-content', 'wallet-content', 'process-content', 'process-creation-content', 'data-content', 'document-validation-content'];
|
||||||
contents.forEach(id => {
|
contents.forEach(id => {
|
||||||
const element = this.shadowRoot?.getElementById(id);
|
const element = this.shadowRoot?.getElementById(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
@ -930,7 +1032,6 @@ private async showPairing(): Promise<void> {
|
|||||||
<table class="parameter-table" id="pairing-table">
|
<table class="parameter-table" id="pairing-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>SP Address</th>
|
|
||||||
<th>Device Name</th>
|
<th>Device Name</th>
|
||||||
<th>SP Emojis</th>
|
<th>SP Emojis</th>
|
||||||
<th>QR Code</th>
|
<th>QR Code</th>
|
||||||
@ -940,7 +1041,7 @@ private async showPairing(): Promise<void> {
|
|||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<button class="add-row-button button-style" onclick="window.addRow()">Add a line</button>
|
<button class="add-row-button button-style" onclick="window.addRowPairing()">Add a device</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -960,8 +1061,7 @@ private async showPairing(): Promise<void> {
|
|||||||
const pairingProcess = await service.getProcess(pairingProcessId);
|
const pairingProcess = await service.getProcess(pairingProcessId);
|
||||||
console.log('Pairing Process:', pairingProcess);
|
console.log('Pairing Process:', pairingProcess);
|
||||||
|
|
||||||
const userName = pairingProcess?.states?.[0]?.metadata?.userName
|
const userName = pairingProcess?.states?.[0]?.public_data?.memberPublicName
|
||||||
|| pairingProcess?.states?.[0]?.metadata?.name
|
|
||||||
|| localStorage.getItem('userName')
|
|| localStorage.getItem('userName')
|
||||||
|
|
||||||
console.log('Username found:', userName);
|
console.log('Username found:', userName);
|
||||||
@ -1209,10 +1309,10 @@ private openAvatarPopup(): void {
|
|||||||
<strong>Name:</strong>
|
<strong>Name:</strong>
|
||||||
<input type="text" id="userName" value="${savedName}" class="editable">
|
<input type="text" id="userName" value="${savedName}" class="editable">
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<!--<div class="info-row">
|
||||||
<strong>Last Name:</strong>
|
<strong>Last Name:</strong>
|
||||||
<input type="text" id="userLastName" value="${savedLastName}" class="editable">
|
<input type="text" id="userLastName" value="${savedLastName}" class="editable">
|
||||||
</div>
|
</div>-->
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<strong>Address:</strong>
|
<strong>Address:</strong>
|
||||||
<span>${savedAddress}</span>
|
<span>${savedAddress}</span>
|
||||||
@ -1460,16 +1560,16 @@ private initializeEventListeners() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private showQRCodeModal(address: string): void {
|
private showQRCodeModal(pairingId: string): void {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'qr-modal';
|
modal.className = 'qr-modal';
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="qr-modal-content">
|
<div class="qr-modal-content">
|
||||||
<span class="close-qr-modal">×</span>
|
<span class="close-qr-modal">×</span>
|
||||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${address}"
|
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${pairingId}"
|
||||||
alt="QR Code Large"
|
alt="QR Code Large"
|
||||||
class="qr-code-large">
|
class="qr-code-large">
|
||||||
<div class="qr-address">${decodeURIComponent(address)}</div>
|
<div class="qr-address">${decodeURIComponent(pairingId)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
321
src/pages/account/document-validation.ts
Normal file
321
src/pages/account/document-validation.ts
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import { ProcessState } from '.././pkg/sdk_client.js';
|
||||||
|
import Services from '../../services/service';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
file: File | null;
|
||||||
|
fileHash: string | null;
|
||||||
|
certificate: ProcessState | null;
|
||||||
|
commitmentHashes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vin {
|
||||||
|
txid: string; // The txid of the previous transaction (being spent)
|
||||||
|
vout: number; // The output index in the previous tx
|
||||||
|
prevout: {
|
||||||
|
scriptpubkey: string;
|
||||||
|
scriptpubkey_asm: string;
|
||||||
|
scriptpubkey_type: string;
|
||||||
|
scriptpubkey_address: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
scriptsig: string;
|
||||||
|
scriptsig_asm: string;
|
||||||
|
witness: string[];
|
||||||
|
is_coinbase: boolean;
|
||||||
|
sequence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionInfo {
|
||||||
|
txid: string;
|
||||||
|
version: number;
|
||||||
|
locktime: number;
|
||||||
|
vin: Vin[];
|
||||||
|
vout: any[];
|
||||||
|
size: number;
|
||||||
|
weight: number;
|
||||||
|
fee: number;
|
||||||
|
status: {
|
||||||
|
confirmed: boolean;
|
||||||
|
block_height: number;
|
||||||
|
block_hash: string;
|
||||||
|
block_time: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocumentValidation(container: HTMLElement) {
|
||||||
|
const state: State = {
|
||||||
|
file: null,
|
||||||
|
fileHash: null,
|
||||||
|
certificate: null,
|
||||||
|
commitmentHashes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
gap: 2rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function createDropButton(
|
||||||
|
label: string,
|
||||||
|
onDrop: (file: File, updateVisuals: (file: File) => void) => void,
|
||||||
|
accept: string = '*/*'
|
||||||
|
): HTMLElement {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.style.cssText = `
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
border: 2px dashed #888;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #f8f8f8;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.textContent = label;
|
||||||
|
|
||||||
|
const filename = document.createElement('div');
|
||||||
|
filename.style.cssText = `
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: #444;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
wrapper.appendChild(filename);
|
||||||
|
|
||||||
|
const updateVisuals = (file: File) => {
|
||||||
|
wrapper.style.borderColor = 'green';
|
||||||
|
wrapper.style.background = '#e6ffed';
|
||||||
|
filename.textContent = file.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Hidden file input ===
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = accept;
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
fileInput.onchange = () => {
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
onDrop(file, updateVisuals);
|
||||||
|
fileInput.value = ''; // reset so same file can be re-selected
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Handle drag-and-drop ===
|
||||||
|
wrapper.ondragover = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
wrapper.style.background = '#e0e0e0';
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.ondragleave = () => {
|
||||||
|
wrapper.style.background = '#f8f8f8';
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.ondrop = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
wrapper.style.background = '#f8f8f8';
|
||||||
|
|
||||||
|
const file = e.dataTransfer?.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
onDrop(file, updateVisuals);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Handle click to open file manager ===
|
||||||
|
wrapper.onclick = () => {
|
||||||
|
fileInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileDropButton = createDropButton('Drop file', async (file, updateVisuals) => {
|
||||||
|
try {
|
||||||
|
state.file = file;
|
||||||
|
updateVisuals(file);
|
||||||
|
console.log('Loaded file:', state.file);
|
||||||
|
checkReady();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to drop the file.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const certDropButton = createDropButton('Drop certificate', async (file, updateVisuals) => {
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (
|
||||||
|
typeof json === 'object' &&
|
||||||
|
json !== null &&
|
||||||
|
typeof json.pcd_commitment === 'object' &&
|
||||||
|
typeof json.state_id === 'string'
|
||||||
|
) {
|
||||||
|
state.certificate = json as ProcessState;
|
||||||
|
|
||||||
|
state.commitmentHashes = Object.values(json.pcd_commitment).map((h: string) =>
|
||||||
|
h.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
updateVisuals(file);
|
||||||
|
console.log('Loaded certificate, extracted hashes:', state.commitmentHashes);
|
||||||
|
checkReady();
|
||||||
|
} else {
|
||||||
|
alert('Invalid certificate structure.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to parse certificate JSON.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonRow = document.createElement('div');
|
||||||
|
buttonRow.style.display = 'flex';
|
||||||
|
buttonRow.style.gap = '2rem';
|
||||||
|
buttonRow.appendChild(fileDropButton);
|
||||||
|
buttonRow.appendChild(certDropButton);
|
||||||
|
|
||||||
|
container.appendChild(buttonRow);
|
||||||
|
|
||||||
|
async function checkReady() {
|
||||||
|
if (state.file && state.certificate && state.commitmentHashes.length > 0) {
|
||||||
|
// We take the commited_in and all pcd_commitment keys to reconstruct all the possible hash
|
||||||
|
const fileBlob = {
|
||||||
|
type: state.file.type,
|
||||||
|
data: new Uint8Array(await state.file.arrayBuffer())
|
||||||
|
};
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
const commitedIn = state.certificate.commited_in;
|
||||||
|
if (!commitedIn) return;
|
||||||
|
const [prevTxid, prevTxVout] = commitedIn.split(':');
|
||||||
|
const processId = state.certificate.process_id;
|
||||||
|
const stateId = state.certificate.state_id;
|
||||||
|
const process = await service.getProcess(processId);
|
||||||
|
if (!process) return;
|
||||||
|
|
||||||
|
// Get the transaction that comes right after the commited_in
|
||||||
|
const nextState = service.getNextStateAfterId(process, stateId);
|
||||||
|
|
||||||
|
if (!nextState) {
|
||||||
|
alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [outspentTxId, _] = nextState.commited_in.split(':');
|
||||||
|
console.log(outspentTxId);
|
||||||
|
|
||||||
|
// Check that the commitment transaction exists, and that it commits to the state id
|
||||||
|
|
||||||
|
const txInfo = await fetchTransaction(outspentTxId);
|
||||||
|
if (!txInfo) {
|
||||||
|
console.error(`Validation error: Can't fetch new state commitment transaction`);
|
||||||
|
alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must check that this transaction indeed spend the commited_in we have in the certificate
|
||||||
|
let found = false;
|
||||||
|
for (const vin of txInfo.vin) {
|
||||||
|
if (vin.txid === prevTxid) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
|
||||||
|
alert('❌ Validation failed: Unconsistent commitment transactions history.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set found back to false for next check
|
||||||
|
found = false;
|
||||||
|
|
||||||
|
// is the state_id commited in the transaction?
|
||||||
|
for (const vout of txInfo.vout) {
|
||||||
|
console.log(vout);
|
||||||
|
if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vout.scriptpubkey_asm) {
|
||||||
|
const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
|
||||||
|
if (hash) {
|
||||||
|
if (hash !== stateId) {
|
||||||
|
console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
|
||||||
|
alert('❌ Validation failed: Transaction does not commit to that state.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
alert('❌ Validation failed: Transaction does not contain data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set found back to false for next check
|
||||||
|
found = false;
|
||||||
|
|
||||||
|
for (const label of Object.keys(state.certificate.pcd_commitment)) {
|
||||||
|
// Compute the hash for this label
|
||||||
|
console.log(`Computing hash with label ${label}`)
|
||||||
|
const fileHex = service.getHashForFile(commitedIn, label, fileBlob);
|
||||||
|
console.log(`Found hash ${fileHex}`);
|
||||||
|
found = state.commitmentHashes.includes(fileHex);
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
alert('✅ Validation successful: file hash found in pcd_commitment.');
|
||||||
|
} else {
|
||||||
|
alert('❌ Validation failed: file hash NOT found in pcd_commitment.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTransaction(txid: string): Promise<TransactionInfo> {
|
||||||
|
const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outspend: TransactionInfo = await response.json();
|
||||||
|
return outspend;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHexFromScriptAsm(scriptAsm: string): string | null {
|
||||||
|
const parts = scriptAsm.trim().split(/\s+/);
|
||||||
|
const last = parts[parts.length - 1];
|
||||||
|
|
||||||
|
// Basic validation: must be 64-char hex (32 bytes)
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(last)) {
|
||||||
|
return last.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/pages/account/key-value-section.ts
Normal file
196
src/pages/account/key-value-section.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import { ValidationRule, RoleDefinition } from '.././pkg/sdk_client.js';
|
||||||
|
import { showValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
||||||
|
|
||||||
|
export function createKeyValueSection(title: string, id: string, isRoleSection = false) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.id = id;
|
||||||
|
section.style.cssText = 'margin-bottom: 2rem; background: #fff; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);';
|
||||||
|
|
||||||
|
const titleEl = document.createElement('h2');
|
||||||
|
titleEl.textContent = title;
|
||||||
|
titleEl.style.cssText = 'font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;';
|
||||||
|
section.appendChild(titleEl);
|
||||||
|
|
||||||
|
const rowContainer = document.createElement('div');
|
||||||
|
section.appendChild(rowContainer);
|
||||||
|
|
||||||
|
const addBtn = document.createElement('button');
|
||||||
|
addBtn.textContent = '+ Add Row';
|
||||||
|
addBtn.style.cssText = `
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #888;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
section.appendChild(addBtn);
|
||||||
|
|
||||||
|
const roleRowStates: {
|
||||||
|
roleNameInput: HTMLInputElement;
|
||||||
|
membersInput: HTMLInputElement;
|
||||||
|
storagesInput: HTMLInputElement;
|
||||||
|
validationRules: ValidationRule[];
|
||||||
|
}[] = [];
|
||||||
|
type fileBlob = {
|
||||||
|
type: string,
|
||||||
|
data: Uint8Array
|
||||||
|
};
|
||||||
|
const nonRoleRowStates: {
|
||||||
|
keyInput: HTMLInputElement,
|
||||||
|
valueInput: HTMLInputElement,
|
||||||
|
fileInput: HTMLInputElement,
|
||||||
|
fileBlob: fileBlob | null
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
const inputStyle = 'flex: 1; height: 2.5rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem;';
|
||||||
|
|
||||||
|
const createRow = () => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 0.5rem; align-items: center;';
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.textContent = '🗑️';
|
||||||
|
deleteBtn.style.cssText = 'background: none; border: none; font-size: 1.2rem; cursor: pointer;';
|
||||||
|
deleteBtn.onclick = () => {
|
||||||
|
row.remove();
|
||||||
|
updateDeleteButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isRoleSection) {
|
||||||
|
const roleName = document.createElement('input');
|
||||||
|
const members = document.createElement('input');
|
||||||
|
const storages = document.createElement('input');
|
||||||
|
|
||||||
|
roleName.placeholder = 'Role name';
|
||||||
|
members.placeholder = 'members';
|
||||||
|
storages.placeholder = 'storages';
|
||||||
|
[roleName, members, storages].forEach(input => {
|
||||||
|
input.type = 'text';
|
||||||
|
input.style.cssText = inputStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ruleButton = document.createElement('button');
|
||||||
|
ruleButton.textContent = 'Add Validation Rule';
|
||||||
|
ruleButton.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||||
|
|
||||||
|
const rules: ValidationRule[] = [];
|
||||||
|
ruleButton.onclick = () => {
|
||||||
|
showValidationRuleModal(rule => {
|
||||||
|
rules.push(rule);
|
||||||
|
ruleButton.textContent = `Rules (${rules.length})`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
row.appendChild(roleName);
|
||||||
|
row.appendChild(members);
|
||||||
|
row.appendChild(storages);
|
||||||
|
row.appendChild(ruleButton);
|
||||||
|
row.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
roleRowStates.push({ roleNameInput: roleName, membersInput: members, storagesInput: storages, validationRules: rules });
|
||||||
|
} else {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
fileInput.onchange = async () => {
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const uint8 = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
rowState.fileBlob = {
|
||||||
|
type: file.type,
|
||||||
|
data: uint8,
|
||||||
|
};
|
||||||
|
|
||||||
|
valueInput.value = `📄 ${file.name}`;
|
||||||
|
valueInput.disabled = true;
|
||||||
|
attachBtn.textContent = `📎 ${file.name}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachBtn = document.createElement('button');
|
||||||
|
attachBtn.textContent = '📎 Attach';
|
||||||
|
attachBtn.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||||
|
attachBtn.onclick = () => fileInput.click();
|
||||||
|
|
||||||
|
const keyInput = document.createElement('input');
|
||||||
|
const valueInput = document.createElement('input');
|
||||||
|
|
||||||
|
const rowState = {
|
||||||
|
keyInput,
|
||||||
|
valueInput,
|
||||||
|
fileInput,
|
||||||
|
fileBlob: null as fileBlob | null
|
||||||
|
};
|
||||||
|
nonRoleRowStates.push(rowState);
|
||||||
|
|
||||||
|
keyInput.placeholder = 'Key';
|
||||||
|
valueInput.placeholder = 'Value';
|
||||||
|
[keyInput, valueInput].forEach(input => {
|
||||||
|
input.type = 'text';
|
||||||
|
input.style.cssText = inputStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(keyInput);
|
||||||
|
row.appendChild(valueInput);
|
||||||
|
|
||||||
|
row.appendChild(attachBtn);
|
||||||
|
row.appendChild(fileInput);
|
||||||
|
|
||||||
|
row.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowContainer.appendChild(row);
|
||||||
|
updateDeleteButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeleteButtons = () => {
|
||||||
|
const rows = Array.from(rowContainer.children);
|
||||||
|
rows.forEach(row => {
|
||||||
|
const btn = row.querySelector('button:last-child') as HTMLButtonElement;
|
||||||
|
if (rows.length === 1) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.style.visibility = 'hidden';
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
createRow();
|
||||||
|
addBtn.addEventListener('click', createRow);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: section,
|
||||||
|
getData: () => {
|
||||||
|
if (isRoleSection) {
|
||||||
|
const data: Record<string, RoleDefinition> = {};
|
||||||
|
for (const row of roleRowStates) {
|
||||||
|
const key = row.roleNameInput.value.trim();
|
||||||
|
if (!key) continue;
|
||||||
|
data[key] = {
|
||||||
|
members: row.membersInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
||||||
|
storages: row.storagesInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
||||||
|
validation_rules: row.validationRules
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
const data: Record<string, string | fileBlob> = {};
|
||||||
|
for (const row of nonRoleRowStates) {
|
||||||
|
const key = row.keyInput.value.trim();
|
||||||
|
if (!key) continue;
|
||||||
|
if (row.fileBlob) {
|
||||||
|
data[key] = row.fileBlob;
|
||||||
|
} else {
|
||||||
|
data[key] = row.valueInput.value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
91
src/pages/account/process-creation.ts
Normal file
91
src/pages/account/process-creation.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { createKeyValueSection } from './key-value-section';
|
||||||
|
import { loadValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
||||||
|
import Services from '../../services/service';
|
||||||
|
import { RoleDefinition } from '.././pkg/sdk_client.js';
|
||||||
|
|
||||||
|
export async function getProcessCreation(container: HTMLElement) {
|
||||||
|
await loadValidationRuleModal();
|
||||||
|
|
||||||
|
container.style.display = 'block';
|
||||||
|
container.innerHTML = `<div class="parameter-header">Process Creation</div>`;
|
||||||
|
const privateSec = createKeyValueSection('Private Data', 'private-section');
|
||||||
|
const publicSec = createKeyValueSection('Public Data', 'public-section');
|
||||||
|
const rolesSec = createKeyValueSection('Roles', 'roles-section', true);
|
||||||
|
|
||||||
|
container.appendChild(privateSec.element);
|
||||||
|
container.appendChild(publicSec.element);
|
||||||
|
container.appendChild(rolesSec.element);
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = 'Create Process';
|
||||||
|
btn.style.cssText = `
|
||||||
|
display: block;
|
||||||
|
margin: 2rem auto 0;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #4f46e5;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
btn.onclick = async () => {
|
||||||
|
const privateData = privateSec.getData();
|
||||||
|
const publicData = publicSec.getData();
|
||||||
|
const roles = rolesSec.getData() as Record<string, RoleDefinition>;
|
||||||
|
|
||||||
|
console.log('Private:', privateData);
|
||||||
|
console.log('Public:', publicData);
|
||||||
|
console.log('Roles:', roles);
|
||||||
|
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
|
||||||
|
const createProcessResult = await service.createProcess(privateData, publicData, roles);
|
||||||
|
const processId = createProcessResult.updated_process!.process_id;
|
||||||
|
const stateId = createProcessResult.updated_process!.current_process.states[0].state_id;
|
||||||
|
await service.handleApiReturn(createProcessResult);
|
||||||
|
|
||||||
|
// Now we want to validate the update and register the first state of our new process
|
||||||
|
const updateProcessResult = await service.createPrdUpdate(processId, stateId);
|
||||||
|
await service.handleApiReturn(createProcessResult);
|
||||||
|
|
||||||
|
const approveChangeResult = await service.approveChange(processId, stateId);
|
||||||
|
await service.handleApiReturn(approveChangeResult);
|
||||||
|
if (approveChangeResult) {
|
||||||
|
const process = await service.getProcess(processId);
|
||||||
|
let newState = service.getStateFromId(process, stateId);
|
||||||
|
if (!newState) return;
|
||||||
|
for (const label of Object.keys(newState.keys)) {
|
||||||
|
const hash = newState.pcd_commitment[label];
|
||||||
|
const encryptedData = await service.getBlobFromDb(hash);
|
||||||
|
const filename = `${label}-${hash.slice(0,8)}.bin`;
|
||||||
|
|
||||||
|
const blob = new Blob([encryptedData], { type: "application/octet-stream" });
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.generateProcessPdf(processId, newState);
|
||||||
|
|
||||||
|
// Add processId to the state we export
|
||||||
|
newState['process_id'] = processId;
|
||||||
|
const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `process_${processId}_${stateId}.json`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url); // Clean up
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.appendChild(btn);
|
||||||
|
}
|
||||||
66
src/pages/account/process.ts
Normal file
66
src/pages/account/process.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
export function createProcessTab(container: HTMLElement, processes: { name: string, publicData: Record<string, any> }[]): HTMLElement {
|
||||||
|
container.id = 'process-tab';
|
||||||
|
container.style.display = 'block';
|
||||||
|
container.style.cssText = 'padding: 1.5rem;';
|
||||||
|
|
||||||
|
const title = document.createElement('h2');
|
||||||
|
title.textContent = 'Processes';
|
||||||
|
title.style.cssText = 'font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;';
|
||||||
|
container.appendChild(title);
|
||||||
|
|
||||||
|
processes.forEach(proc => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.style.cssText = 'margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; background: #fff;';
|
||||||
|
|
||||||
|
const nameEl = document.createElement('h3');
|
||||||
|
nameEl.textContent = proc.name;
|
||||||
|
nameEl.style.cssText = 'font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem;';
|
||||||
|
card.appendChild(nameEl);
|
||||||
|
|
||||||
|
const dataList = document.createElement('div');
|
||||||
|
for (const [key, value] of Object.entries(proc.publicData)) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.style.cssText = 'margin-bottom: 0.5rem;';
|
||||||
|
|
||||||
|
const label = document.createElement('strong');
|
||||||
|
label.textContent = key + ': ';
|
||||||
|
item.appendChild(label);
|
||||||
|
|
||||||
|
// Let's trim the quotes
|
||||||
|
const trimmed = value.replace(/^'|'$/g, '');
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(trimmed);
|
||||||
|
} catch (_) {
|
||||||
|
parsed = trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.textContent = '💾 Save as JSON';
|
||||||
|
saveBtn.style.cssText = 'margin-left: 0.5rem; padding: 0.25rem 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||||
|
saveBtn.onclick = () => {
|
||||||
|
const blob = new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${proc.name}_${key}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
item.appendChild(saveBtn);
|
||||||
|
} else {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = String(parsed);
|
||||||
|
item.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataList.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.appendChild(dataList);
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ChatElement } from './chat';
|
/*import { ChatElement } from './chat';
|
||||||
import chatCss from '../../../public/style/chat.css?raw';
|
import chatCss from '../../../public/style/chat.css?raw';
|
||||||
import Services from '../../services/service.js';
|
import Services from '../../services/service.js';
|
||||||
|
|
||||||
@ -46,4 +46,4 @@ class ChatComponent extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { ChatComponent };
|
export { ChatComponent };
|
||||||
customElements.define('chat-component', ChatComponent);
|
customElements.define('chat-component', ChatComponent);*/
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!--
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
declare global {
|
/*declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
loadMemberChat: (memberId: string | number) => void;
|
loadMemberChat: (memberId: string | number) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import { membersMock } from '../../mocks/mock-signature/membersMocks';
|
import { membersMock } from '../../mocks/mock-signature/membersMocks';
|
||||||
import { ApiReturn, Device, Member, Process, RoleDefinition } from '../../../pkg/sdk_client';
|
import { ApiReturn, Device, Member, Process, RoleDefinition } from '.././pkg/sdk_client.js';
|
||||||
import { getCorrectDOM } from '../../utils/document.utils';
|
import { getCorrectDOM } from '../../utils/document.utils';
|
||||||
import chatStyle from '../../../public/style/chat.css?inline';
|
import chatStyle from '../../../public/style/chat.css?inline';
|
||||||
import { addressToEmoji } from '../../utils/sp-address.utils';
|
import { addressToEmoji } from '../../utils/sp-address.utils';
|
||||||
@ -385,37 +385,38 @@ class ChatElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async lookForChildren(): Promise<string | null> {
|
// TODO rewrite that
|
||||||
// Filter processes for the children of current process
|
// private async lookForChildren(): Promise<string | null> {
|
||||||
const service = await Services.getInstance();
|
// // Filter processes for the children of current process
|
||||||
if (!this.selectedChatProcessId) {
|
// const service = await Services.getInstance();
|
||||||
console.error('No process id');
|
// if (!this.selectedChatProcessId) {
|
||||||
return null;
|
// console.error('No process id');
|
||||||
}
|
// return null;
|
||||||
const children: string[] = await service.getChildrenOfProcess(this.selectedChatProcessId);
|
// }
|
||||||
|
// const children: string[] = await service.getChildrenOfProcess(this.selectedChatProcessId);
|
||||||
|
|
||||||
const processRoles = this.processRoles;
|
// const processRoles = this.processRoles;
|
||||||
const selectedMember = this.selectedMember;
|
// const selectedMember = this.selectedMember;
|
||||||
for (const child of children) {
|
// for (const child of children) {
|
||||||
const roles = service.getRoles(JSON.parse(child));
|
// const roles = service.getRoles(JSON.parse(child));
|
||||||
// Check that we and the other members are in the role
|
// // Check that we and the other members are in the role
|
||||||
if (!service.isChildRole(processRoles, roles)) {
|
// if (!service.isChildRole(processRoles, roles)) {
|
||||||
console.error('Child process roles are not a subset of parent')
|
// console.error('Child process roles are not a subset of parent')
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
if (!service.rolesContainsMember(roles, selectedMember)) {
|
// if (!service.rolesContainsMember(roles, selectedMember)) {
|
||||||
console.error('Member is not part of the process');
|
// console.error('Member is not part of the process');
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
if (!service.rolesContainsUs(roles)) {
|
// if (!service.rolesContainsUs(roles)) {
|
||||||
console.error('We\'re not part of child process');
|
// console.error('We\'re not part of child process');
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
return child;
|
// return child;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async loadAllMembers() {
|
private async loadAllMembers() {
|
||||||
const groupList = this.shadowRoot?.querySelector('#group-list');
|
const groupList = this.shadowRoot?.querySelector('#group-list');
|
||||||
@ -689,18 +690,6 @@ class ChatElement extends HTMLElement {
|
|||||||
this.selectedChatProcessId = dmProcessId;
|
this.selectedChatProcessId = dmProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO
|
|
||||||
console.log("Je suis messagesProcess", messagesProcess);
|
|
||||||
// --- GET THE STATE ID ---
|
|
||||||
const messagesProcessStateId = messagesProcess?.states?.[0]?.state_id;
|
|
||||||
console.log("Je suis messagesProcessStateId", messagesProcessStateId);
|
|
||||||
|
|
||||||
// --- GET THE DIFF FROM THE STATE ID ---
|
|
||||||
if (messagesProcessStateId) {
|
|
||||||
const diffFromStateId = await this.getDiffByStateId(messagesProcessStateId);
|
|
||||||
console.log("Je suis diffFromStateId", diffFromStateId);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Récupérer les messages depuis les états du processus
|
// Récupérer les messages depuis les états du processus
|
||||||
const allMessages: any[] = [];
|
const allMessages: any[] = [];
|
||||||
|
|
||||||
@ -1072,7 +1061,7 @@ class ChatElement extends HTMLElement {
|
|||||||
await this.loadAllProcesses(processSet);
|
await this.loadAllProcesses(processSet);
|
||||||
break;
|
break;
|
||||||
case 'members':
|
case 'members':
|
||||||
await this.lookForMyDms():
|
await this.lookForMyDms();
|
||||||
await this.loadAllMembers();
|
await this.loadAllMembers();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1087,7 +1076,9 @@ class ChatElement extends HTMLElement {
|
|||||||
|
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const allProcesses: Record<string, Process> = await service.getProcesses();
|
const allProcesses: Record<string, Process> = await service.getProcesses();
|
||||||
|
console.log('All processes:', allProcesses);
|
||||||
const myProcesses: string[] = await service.getMyProcesses();
|
const myProcesses: string[] = await service.getMyProcesses();
|
||||||
|
console.log('My processes:', myProcesses);
|
||||||
|
|
||||||
const groupList = this.shadowRoot?.querySelector('#group-list');
|
const groupList = this.shadowRoot?.querySelector('#group-list');
|
||||||
if (!groupList) {
|
if (!groupList) {
|
||||||
@ -1116,7 +1107,7 @@ class ChatElement extends HTMLElement {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//trier les processus : ceux de l'utilisateur en premier
|
// Trier les processus : ceux de l'utilisateur en premier
|
||||||
const sortedEntries = Object.entries(allProcesses).sort(
|
const sortedEntries = Object.entries(allProcesses).sort(
|
||||||
([keyA], [keyB]) => {
|
([keyA], [keyB]) => {
|
||||||
const inSetA = myProcesses.includes(keyA);
|
const inSetA = myProcesses.includes(keyA);
|
||||||
@ -1743,6 +1734,4 @@ class ChatElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('chat-element', ChatElement);
|
customElements.define('chat-element', ChatElement);
|
||||||
export { ChatElement };
|
export { ChatElement };*/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import Routing from '../../services/modal.service';
|
import Routing from '../../services/modal.service';
|
||||||
import Services from '../../services/service';
|
import Services from '../../services/service';
|
||||||
import { addSubscription } from '../../utils/subscription.utils';
|
import { addSubscription } from '../../utils/subscription.utils';
|
||||||
import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';
|
import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji} from '../../utils/sp-address.utils';
|
||||||
import { getCorrectDOM } from '../../utils/html.utils';
|
import { getCorrectDOM } from '../../utils/html.utils';
|
||||||
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
|
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
|
||||||
|
import { navigate, registerAllListeners } from '../../router';
|
||||||
|
|
||||||
export { QrScannerComponent };
|
export { QrScannerComponent };
|
||||||
export async function initHomePage(): Promise<void> {
|
export async function initHomePage(): Promise<void> {
|
||||||
console.log('INIT-HOME');
|
console.log('INIT-HOME');
|
||||||
@ -21,7 +23,7 @@ export async function initHomePage(): Promise<void> {
|
|||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const spAddress = await service.getDeviceAddress();
|
const spAddress = await service.getDeviceAddress();
|
||||||
// generateQRCode(spAddress);
|
// generateQRCode(spAddress);
|
||||||
generateCreateBtn ();
|
generateCreateBtn();
|
||||||
displayEmojis(spAddress);
|
displayEmojis(spAddress);
|
||||||
|
|
||||||
// Add this line to populate the select when the page loads
|
// Add this line to populate the select when the page loads
|
||||||
@ -59,7 +61,7 @@ async function populateMemberSelect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const members = service.getAllMembersSorted();
|
const members = await service.getAllMembersSorted();
|
||||||
|
|
||||||
for (const [processId, member] of Object.entries(members)) {
|
for (const [processId, member] of Object.entries(members)) {
|
||||||
const process = await service.getProcess(processId);
|
const process = await service.getProcess(processId);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { interpolate } from '../../utils/html.utils';
|
import { interpolate } from '../../utils/html.utils';
|
||||||
import Services from '../../services/service';
|
import Services from '../../services/service';
|
||||||
import { Process } from 'pkg/sdk_client';
|
import { Process } from '.././pkg/sdk_client.js';
|
||||||
import { getCorrectDOM } from '~/utils/document.utils';
|
import { getCorrectDOM } from '~/utils/document.utils';
|
||||||
|
|
||||||
let currentPageStyle: HTMLStyleElement | null = null;
|
let currentPageStyle: HTMLStyleElement | null = null;
|
||||||
|
|||||||
@ -1,49 +1,49 @@
|
|||||||
import processHtml from './process.html?raw';
|
// import processHtml from './process.html?raw';
|
||||||
import processScript from './process.ts?raw';
|
// import processScript from './process.ts?raw';
|
||||||
import processCss from '../../4nk.css?raw';
|
// import processCss from '../../4nk.css?raw';
|
||||||
import { init } from './process';
|
// import { init } from './process';
|
||||||
|
|
||||||
export class ProcessListComponent extends HTMLElement {
|
// export class ProcessListComponent extends HTMLElement {
|
||||||
_callback: any;
|
// _callback: any;
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
this.attachShadow({ mode: 'open' });
|
// this.attachShadow({ mode: 'open' });
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
console.log('CALLBACK PROCESS LIST PAGE');
|
// console.log('CALLBACK PROCESS LIST PAGE');
|
||||||
this.render();
|
// this.render();
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
init();
|
// init();
|
||||||
}, 500);
|
// }, 500);
|
||||||
}
|
// }
|
||||||
|
|
||||||
set callback(fn) {
|
// set callback(fn) {
|
||||||
if (typeof fn === 'function') {
|
// if (typeof fn === 'function') {
|
||||||
this._callback = fn;
|
// this._callback = fn;
|
||||||
} else {
|
// } else {
|
||||||
console.error('Callback is not a function');
|
// console.error('Callback is not a function');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
get callback() {
|
// get callback() {
|
||||||
return this._callback;
|
// return this._callback;
|
||||||
}
|
// }
|
||||||
|
|
||||||
render() {
|
// render() {
|
||||||
if (this.shadowRoot)
|
// if (this.shadowRoot)
|
||||||
this.shadowRoot.innerHTML = `
|
// this.shadowRoot.innerHTML = `
|
||||||
<style>
|
// <style>
|
||||||
${processCss}
|
// ${processCss}
|
||||||
</style>${processHtml}
|
// </style>${processHtml}
|
||||||
<script type="module">
|
// <script type="module">
|
||||||
${processScript}
|
// ${processScript}
|
||||||
</scipt>
|
// </scipt>
|
||||||
|
|
||||||
`;
|
// `;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!customElements.get('process-list-4nk-component')) {
|
// if (!customElements.get('process-list-4nk-component')) {
|
||||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
// customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||||
}
|
// }
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<div class="title-container">
|
<!-- <div class="title-container">
|
||||||
<h1>Process Selection</h1>
|
<h1>Process Selection</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -16,4 +16,4 @@
|
|||||||
<a class="btn" onclick="goToProcessPage()">OK</a>
|
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
819
src/router.ts
819
src/router.ts
@ -1,13 +1,17 @@
|
|||||||
import '../public/style/4nk.css';
|
import '../public/style/4nk.css';
|
||||||
import { initHeader } from '../src/components/header/header';
|
import { initHeader } from '../src/components/header/header';
|
||||||
import { initChat } from '../src/pages/chat/chat';
|
/*import { initChat } from '../src/pages/chat/chat';*/
|
||||||
import Database from './services/database.service';
|
import Database from './services/database.service';
|
||||||
import Services from './services/service';
|
import Services from './services/service';
|
||||||
|
import TokenService from './services/token';
|
||||||
import { cleanSubscriptions } from './utils/subscription.utils';
|
import { cleanSubscriptions } from './utils/subscription.utils';
|
||||||
import { LoginComponent } from './pages/home/home-component';
|
import { LoginComponent } from './pages/home/home-component';
|
||||||
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
|
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
|
||||||
import ModalService from './services/modal.service';
|
import ModalService from './services/modal.service';
|
||||||
export { Services };
|
import { MessageType } from './models/process.model';
|
||||||
|
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
|
||||||
|
import { MerkleProofResult } from '.././pkg/sdk_client.js';
|
||||||
|
|
||||||
const routes: { [key: string]: string } = {
|
const routes: { [key: string]: string } = {
|
||||||
home: '/src/pages/home/home.html',
|
home: '/src/pages/home/home.html',
|
||||||
process: '/src/pages/process/process.html',
|
process: '/src/pages/process/process.html',
|
||||||
@ -62,14 +66,14 @@ async function handleLocation(path: string) {
|
|||||||
switch (path) {
|
switch (path) {
|
||||||
case 'process':
|
case 'process':
|
||||||
// const { init } = await import('./pages/process/process');
|
// const { init } = await import('./pages/process/process');
|
||||||
const { ProcessListComponent } = await import('./pages/process/process-list-component');
|
//const { ProcessListComponent } = await import('./pages/process/process-list-component');
|
||||||
|
|
||||||
const container2 = document.querySelector('#containerId');
|
const container2 = document.querySelector('#containerId');
|
||||||
const accountComponent = document.createElement('process-list-4nk-component');
|
const accountComponent = document.createElement('process-list-4nk-component');
|
||||||
|
|
||||||
if (!customElements.get('process-list-4nk-component')) {
|
//if (!customElements.get('process-list-4nk-component')) {
|
||||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
//customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||||
}
|
//}
|
||||||
accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;');
|
accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;');
|
||||||
if (container2) container2.appendChild(accountComponent);
|
if (container2) container2.appendChild(accountComponent);
|
||||||
break;
|
break;
|
||||||
@ -94,7 +98,7 @@ async function handleLocation(path: string) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'chat':
|
/*case 'chat':
|
||||||
const { ChatComponent } = await import('./pages/chat/chat-component');
|
const { ChatComponent } = await import('./pages/chat/chat-component');
|
||||||
const chatContainer = document.querySelector('.group-list');
|
const chatContainer = document.querySelector('.group-list');
|
||||||
if (chatContainer) {
|
if (chatContainer) {
|
||||||
@ -104,7 +108,7 @@ async function handleLocation(path: string) {
|
|||||||
const chatComponent = document.createElement('chat-component');
|
const chatComponent = document.createElement('chat-component');
|
||||||
chatContainer.appendChild(chatComponent);
|
chatContainer.appendChild(chatComponent);
|
||||||
}
|
}
|
||||||
break;
|
break;*/
|
||||||
|
|
||||||
case 'signature':
|
case 'signature':
|
||||||
const { SignatureComponent } = await import('./pages/signature/signature-component');
|
const { SignatureComponent } = await import('./pages/signature/signature-component');
|
||||||
@ -134,44 +138,781 @@ export async function init(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const services = await Services.getInstance();
|
const services = await Services.getInstance();
|
||||||
(window as any).myService = services;
|
(window as any).myService = services;
|
||||||
await Database.getInstance();
|
const db = await Database.getInstance();
|
||||||
setTimeout(async () => {
|
db.registerServiceWorker('/src/service-workers/database.worker.js');
|
||||||
let device = await services.getDeviceFromDatabase();
|
const device = await services.getDeviceFromDatabase();
|
||||||
console.log('🚀 ~ setTimeout ~ device:', device);
|
console.log('🚀 ~ setTimeout ~ device:', device);
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
device = await services.createNewDevice();
|
await services.createNewDevice();
|
||||||
} else {
|
} else {
|
||||||
services.restoreDevice(device);
|
services.restoreDevice(device);
|
||||||
}
|
}
|
||||||
await services.restoreProcessesFromDB();
|
|
||||||
await services.restoreSecretsFromDB();
|
|
||||||
|
|
||||||
if (services.isPaired()) {
|
// If we create a new device, we most probably don't have anything in db, but just in case
|
||||||
await navigate('chat');
|
await services.restoreProcessesFromDB();
|
||||||
} else {
|
await services.restoreSecretsFromDB();
|
||||||
const queryString = window.location.search;
|
|
||||||
const urlParams = new URLSearchParams(queryString);
|
// We connect to all relays now
|
||||||
const pairingAddress = urlParams.get('sp_address');
|
await services.connectAllRelays();
|
||||||
if (pairingAddress) {
|
|
||||||
setTimeout(async () => {
|
// We register all the event listeners if we run in an iframe
|
||||||
try {
|
if (window.self !== window.top) {
|
||||||
// check if we have a shared secret with that address
|
await registerAllListeners();
|
||||||
await prepareAndSendPairingTx(pairingAddress);
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to pair:', e);
|
if (services.isPaired()) {
|
||||||
}
|
await navigate('process');
|
||||||
}, 2000);
|
} else {
|
||||||
}
|
await navigate('home');
|
||||||
await navigate('home');
|
}
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await navigate('home');
|
await navigate('home');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function registerAllListeners() {
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
const tokenService = await TokenService.getInstance();
|
||||||
|
|
||||||
|
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.ERROR,
|
||||||
|
error: errorMsg,
|
||||||
|
messageId
|
||||||
|
},
|
||||||
|
origin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handler functions ---
|
||||||
|
const handleRequestLink = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.REQUEST_LINK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modalService = await ModalService.getInstance();
|
||||||
|
const result = await modalService.showConfirmationModal({
|
||||||
|
title: 'Confirmation de liaison',
|
||||||
|
content: `
|
||||||
|
<div class="modal-confirmation">
|
||||||
|
<h3>Liaison avec ${event.origin}</h3>
|
||||||
|
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
|
||||||
|
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
|
||||||
|
<p>Voulez-vous continuer ?</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
confirmText: 'Ajouter un service',
|
||||||
|
cancelText: 'Annuler'
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const errorMsg = 'Failed to pair device: User refused to link';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokens = await tokenService.generateSessionToken(event.origin);
|
||||||
|
const acceptedMsg = {
|
||||||
|
type: MessageType.LINK_ACCEPTED,
|
||||||
|
accessToken: tokens.accessToken,
|
||||||
|
refreshToken: tokens.refreshToken,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
};
|
||||||
|
window.parent.postMessage(
|
||||||
|
acceptedMsg,
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `Failed to generate tokens: ${error}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreatePairing = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.CREATE_PAIRING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (services.isPaired()) {
|
||||||
|
const errorMsg = 'Device already paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 Starting pairing process');
|
||||||
|
const myAddress = services.getDeviceAddress();
|
||||||
|
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]);
|
||||||
|
const pairingId = createPairingProcessReturn.updated_process?.process_id;
|
||||||
|
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string;
|
||||||
|
services.pairDevice(pairingId, [myAddress]);
|
||||||
|
await services.handleApiReturn(createPairingProcessReturn);
|
||||||
|
|
||||||
|
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId);
|
||||||
|
await services.handleApiReturn(createPrdUpdateReturn);
|
||||||
|
const approveChangeReturn = await services.approveChange(pairingId, stateId);
|
||||||
|
await services.handleApiReturn(approveChangeReturn);
|
||||||
|
|
||||||
|
await services.confirmPairing();
|
||||||
|
|
||||||
|
// Send success response
|
||||||
|
const successMsg = {
|
||||||
|
type: MessageType.PAIRING_CREATED,
|
||||||
|
pairingId,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
};
|
||||||
|
window.parent.postMessage(successMsg, event.origin);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to create pairing process: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGetMyProcesses = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.GET_MY_PROCESSES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const myProcesses = await services.getMyProcesses();
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.GET_MY_PROCESSES,
|
||||||
|
myProcesses,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to get processes: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGetProcesses = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.GET_PROCESSES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenService = await TokenService.getInstance();
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken } = event.data;
|
||||||
|
|
||||||
|
// Validate the session token
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const processes = await services.getProcesses();
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.PROCESSES_RETRIEVED,
|
||||||
|
processes,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to get processes: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We got a state for some process and return as many clear attributes as we can
|
||||||
|
const handleDecryptState = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.RETRIEVE_DATA) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tokenService = await TokenService.getInstance();
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { processId, stateId, accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the state for the process
|
||||||
|
const process = await services.getProcess(processId);
|
||||||
|
if (!process) {
|
||||||
|
throw new Error('Can\'t find process');
|
||||||
|
}
|
||||||
|
const state = services.getStateFromId(process, stateId);
|
||||||
|
|
||||||
|
await services.checkConnections(process, stateId);
|
||||||
|
|
||||||
|
let res: Record<string, any> = {};
|
||||||
|
if (state) {
|
||||||
|
// Decrypt all the data we have the key for
|
||||||
|
for (const attribute of Object.keys(state.pcd_commitment)) {
|
||||||
|
if (attribute === 'roles' || state.public_data[attribute]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
|
||||||
|
if (decryptedAttribute) {
|
||||||
|
res[attribute] = decryptedAttribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown state for process', processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.DATA_RETRIEVED,
|
||||||
|
data: res,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to retrieve data: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidateToken = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.VALIDATE_TOKEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = event.data.accessToken;
|
||||||
|
const refreshToken = event.data.refreshToken;
|
||||||
|
if (!accessToken || !refreshToken) {
|
||||||
|
errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await tokenService.validateToken(accessToken, event.origin);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.VALIDATE_TOKEN,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
isValid: isValid,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenewToken = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.RENEW_TOKEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshToken = event.data.refreshToken;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('No refresh token provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
|
||||||
|
|
||||||
|
if (!newAccessToken) {
|
||||||
|
throw new Error('Failed to refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.RENEW_TOKEN,
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `Failed to renew token: ${error}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGetPairingId = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.GET_PAIRING_ID) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPairingId = services.getPairingProcessId();
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.GET_PAIRING_ID,
|
||||||
|
userPairingId,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to get pairing id: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateProcess = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.CREATE_PROCESS) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { processData, privateFields, roles, accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { privateData, publicData } = splitPrivateData(processData, privateFields);
|
||||||
|
|
||||||
|
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
|
||||||
|
if (!createProcessReturn.updated_process) {
|
||||||
|
throw new Error('Empty updated_process in createProcessReturn');
|
||||||
|
}
|
||||||
|
const processId = createProcessReturn.updated_process.process_id;
|
||||||
|
const process = createProcessReturn.updated_process.current_process;
|
||||||
|
const stateId = process.states[0].state_id;
|
||||||
|
await services.handleApiReturn(createProcessReturn);
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
processId,
|
||||||
|
process,
|
||||||
|
processData,
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.PROCESS_CREATED,
|
||||||
|
processCreated: res,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to create process: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNotifyUpdate = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.NOTIFY_UPDATE) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { processId, stateId, accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid32ByteHex(stateId)) {
|
||||||
|
throw new Error('Invalid state id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await services.createPrdUpdate(processId, stateId);
|
||||||
|
await services.handleApiReturn(res);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.UPDATE_NOTIFIED,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to notify update for process: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidateState = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.VALIDATE_STATE) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { processId, stateId, accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await services.approveChange(processId, stateId);
|
||||||
|
await services.handleApiReturn(res);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.STATE_VALIDATED,
|
||||||
|
validatedProcess: res.updated_process,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to validate process: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateProcess = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.UPDATE_PROCESS) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// privateFields is only used if newData contains new fields
|
||||||
|
// roles can be empty meaning that roles from the last commited state are kept
|
||||||
|
const { processId, newData, privateFields, roles, accessToken } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the new data is already in the process or if it's a new field
|
||||||
|
const process = await services.getProcess(processId);
|
||||||
|
if (!process) {
|
||||||
|
throw new Error('Process not found');
|
||||||
|
}
|
||||||
|
let lastState = services.getLastCommitedState(process);
|
||||||
|
if (!lastState) {
|
||||||
|
const firstState = process.states[0];
|
||||||
|
const roles = firstState.roles;
|
||||||
|
if (services.rolesContainsUs(roles)) {
|
||||||
|
const approveChangeRes= await services.approveChange(processId, firstState.state_id);
|
||||||
|
await services.handleApiReturn(approveChangeRes);
|
||||||
|
const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id);
|
||||||
|
await services.handleApiReturn(prdUpdateRes);
|
||||||
|
} else {
|
||||||
|
if (firstState.validation_tokens.length > 0) {
|
||||||
|
// Try to send it again anyway
|
||||||
|
const res = await services.createPrdUpdate(processId, firstState.state_id);
|
||||||
|
await services.handleApiReturn(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait a couple seconds
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
lastState = services.getLastCommitedState(process);
|
||||||
|
if (!lastState) {
|
||||||
|
throw new Error('Process doesn\'t have a commited state yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastStateIndex = services.getLastCommitedStateIndex(process);
|
||||||
|
if (lastStateIndex === null) {
|
||||||
|
throw new Error('Process doesn\'t have a commited state yet');
|
||||||
|
} // Shouldn't happen
|
||||||
|
|
||||||
|
const privateData: Record<string, any> = {};
|
||||||
|
const publicData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const field of Object.keys(newData)) {
|
||||||
|
// Public data are carried along each new state
|
||||||
|
// So the first thing we can do is check if the new data is public data
|
||||||
|
if (lastState.public_data[field]) {
|
||||||
|
// Add it to public data
|
||||||
|
publicData[field] = newData[field];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a public data, it may be either a private data update, or a new field (public of private)
|
||||||
|
// Caller gave us a list of new private fields, if we see it here this is a new private field
|
||||||
|
if (privateFields.includes(field)) {
|
||||||
|
// Add it to private data
|
||||||
|
privateData[field] = newData[field];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now it can be an update of private data or a new public data
|
||||||
|
// We check that the field exists in previous states private data
|
||||||
|
for (let i = lastStateIndex; i >= 0; i--) {
|
||||||
|
const state = process.states[i];
|
||||||
|
if (state.pcd_commitment[field]) {
|
||||||
|
// We don't even check if it's a public field, we would have seen it in the last state
|
||||||
|
privateData[field] = newData[field];
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// This attribute was not modified in that state, we go back to the previous state
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateData[field]) continue;
|
||||||
|
|
||||||
|
// We've get back all the way to the first state without seeing it, it's a new public field
|
||||||
|
publicData[field] = newData[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll let the wasm check if roles are consistent
|
||||||
|
|
||||||
|
const res = await services.updateProcess(process, privateData, publicData, roles);
|
||||||
|
await services.handleApiReturn(res);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.PROCESS_UPDATED,
|
||||||
|
updatedProcess: res.updated_process,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to update process: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDecodePublicData = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return;
|
||||||
|
|
||||||
|
if (!services.isPaired()) {
|
||||||
|
const errorMsg = 'Device not paired';
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken, encodedData } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedData = services.decodeValue(encodedData);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.PUBLIC_DATA_DECODED,
|
||||||
|
decodedData,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to decode data: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHashValue = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.HASH_VALUE) return;
|
||||||
|
|
||||||
|
console.log('handleHashValue', event.data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken, commitedIn, label, fileBlob } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = services.getHashForFile(commitedIn, label, fileBlob);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.VALUE_HASHED,
|
||||||
|
hash,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to hash value: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGetMerkleProof = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.GET_MERKLE_PROOF) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken, processState, attributeName } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const proof = services.getMerkleProofForFile(processState, attributeName);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.MERKLE_PROOF_RETRIEVED,
|
||||||
|
proof,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to get merkle proof: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidateMerkleProof = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { accessToken, merkleProof, documentHash } = event.data;
|
||||||
|
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the proof
|
||||||
|
// We will validate it's a MerkleProofResult in the wasm
|
||||||
|
let parsedMerkleProof: MerkleProofResult;
|
||||||
|
try {
|
||||||
|
parsedMerkleProof= JSON.parse(merkleProof);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Provided merkleProof is not a valid json object');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.MERKLE_PROOF_VALIDATED,
|
||||||
|
isValid: res,
|
||||||
|
messageId: event.data.messageId
|
||||||
|
},
|
||||||
|
event.origin
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const errorMsg = `Failed to get merkle proof: ${e}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
async function handleMessage(event: MessageEvent) {
|
||||||
|
try {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case MessageType.REQUEST_LINK:
|
||||||
|
await handleRequestLink(event);
|
||||||
|
break;
|
||||||
|
case MessageType.CREATE_PAIRING:
|
||||||
|
await handleCreatePairing(event);
|
||||||
|
break;
|
||||||
|
case MessageType.GET_MY_PROCESSES:
|
||||||
|
await handleGetMyProcesses(event);
|
||||||
|
break;
|
||||||
|
case MessageType.GET_PROCESSES:
|
||||||
|
await handleGetProcesses(event);
|
||||||
|
break;
|
||||||
|
case MessageType.RETRIEVE_DATA:
|
||||||
|
await handleDecryptState(event);
|
||||||
|
break;
|
||||||
|
case MessageType.VALIDATE_TOKEN:
|
||||||
|
await handleValidateToken(event);
|
||||||
|
break;
|
||||||
|
case MessageType.RENEW_TOKEN:
|
||||||
|
await handleRenewToken(event);
|
||||||
|
break;
|
||||||
|
case MessageType.GET_PAIRING_ID:
|
||||||
|
await handleGetPairingId(event);
|
||||||
|
break;
|
||||||
|
case MessageType.CREATE_PROCESS:
|
||||||
|
await handleCreateProcess(event);
|
||||||
|
break;
|
||||||
|
case MessageType.NOTIFY_UPDATE:
|
||||||
|
await handleNotifyUpdate(event);
|
||||||
|
break;
|
||||||
|
case MessageType.VALIDATE_STATE:
|
||||||
|
await handleValidateState(event);
|
||||||
|
break;
|
||||||
|
case MessageType.UPDATE_PROCESS:
|
||||||
|
await handleUpdateProcess(event);
|
||||||
|
break;
|
||||||
|
case MessageType.DECODE_PUBLIC_DATA:
|
||||||
|
await handleDecodePublicData(event);
|
||||||
|
break;
|
||||||
|
case MessageType.HASH_VALUE:
|
||||||
|
await handleHashValue(event);
|
||||||
|
break;
|
||||||
|
case MessageType.GET_MERKLE_PROOF:
|
||||||
|
await handleGetMerkleProof(event);
|
||||||
|
break;
|
||||||
|
case MessageType.VALIDATE_MERKLE_PROOF:
|
||||||
|
await handleValidateMerkleProof(event);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unhandled message type: ${event.data.type}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `Error handling message: ${error}`;
|
||||||
|
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: MessageType.LISTENING
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function cleanPage() {
|
async function cleanPage() {
|
||||||
const container = document.querySelector('#containerId');
|
const container = document.querySelector('#containerId');
|
||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
@ -199,7 +940,7 @@ document.addEventListener('navigate', ((e: Event) => {
|
|||||||
const container = document.querySelector('.container');
|
const container = document.querySelector('.container');
|
||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
|
|
||||||
initChat();
|
//initChat();
|
||||||
|
|
||||||
const chatElement = document.querySelector('chat-element');
|
const chatElement = document.querySelector('chat-element');
|
||||||
if (chatElement) {
|
if (chatElement) {
|
||||||
|
|||||||
@ -45,6 +45,21 @@ self.addEventListener('message', async (event) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
event.ports[0].postMessage({ status: 'error', message: error.message });
|
event.ports[0].postMessage({ status: 'error', message: error.message });
|
||||||
}
|
}
|
||||||
|
} else if (data.type === 'BATCH_WRITING') {
|
||||||
|
const { storeName, objects } = data.payload;
|
||||||
|
const db = await openDatabase();
|
||||||
|
const tx = db.transaction(storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
|
||||||
|
for (const { key, object } of objects) {
|
||||||
|
if (key) {
|
||||||
|
await store.put(object, key);
|
||||||
|
} else {
|
||||||
|
await store.put(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.done;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
import Services from './service';
|
|
||||||
import { init, navigate } from '../router';
|
|
||||||
import { RoleDefinition } from 'pkg/sdk_client';
|
|
||||||
import { Member } from 'pkg/sdk_client';
|
|
||||||
|
|
||||||
export default class ChatService {
|
|
||||||
private static instance: ChatService;
|
|
||||||
private stateId: string | null = null;
|
|
||||||
private processId: string | null = null;
|
|
||||||
private paired_member: string[] = [];
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public static async getInstance(): Promise<ChatService> {
|
|
||||||
if (!ChatService.instance) {
|
|
||||||
ChatService.instance = new ChatService();
|
|
||||||
}
|
|
||||||
return ChatService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLocalMember () {
|
|
||||||
try {
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
const currentUser = service.getMemberFromDevice();
|
|
||||||
return currentUser
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error initializing services:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMessagingProcess (commitedIn: string) {
|
|
||||||
try{
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
const stored = service.getProcess(commitedIn)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading Messaging Process', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -84,7 +84,6 @@ export class Database {
|
|||||||
|
|
||||||
request.onsuccess = async () => {
|
request.onsuccess = async () => {
|
||||||
this.db = request.result;
|
this.db = request.result;
|
||||||
await this.initServiceWorker();
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,15 +109,16 @@ export class Database {
|
|||||||
return objectList;
|
return objectList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initServiceWorker() {
|
public async registerServiceWorker(path: string) {
|
||||||
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
||||||
|
console.log('registering worker at', path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get existing service worker registrations
|
// Get existing service worker registrations
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
if (registrations.length === 0) {
|
if (registrations.length === 0) {
|
||||||
// No existing workers: register a new one.
|
// No existing workers: register a new one.
|
||||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
|
||||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||||
} else if (registrations.length === 1) {
|
} else if (registrations.length === 1) {
|
||||||
// One existing worker: update it (restart it) without unregistering.
|
// One existing worker: update it (restart it) without unregistering.
|
||||||
@ -130,7 +130,7 @@ export class Database {
|
|||||||
console.log('Multiple Service Worker(s) detected. Unregistering all...');
|
console.log('Multiple Service Worker(s) detected. Unregistering all...');
|
||||||
await Promise.all(registrations.map(reg => reg.unregister()));
|
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||||
console.log('All previous Service Workers unregistered.');
|
console.log('All previous Service Workers unregistered.');
|
||||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
|
||||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,12 +142,12 @@ export class Database {
|
|||||||
await this.handleServiceWorkerMessage(event.data);
|
await this.handleServiceWorkerMessage(event.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up a periodic check to ensure the service worker is active and to send a SYNC message.
|
// Set up a periodic check to ensure the service worker is active and to send a SCAN message.
|
||||||
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
||||||
const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
|
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const payload = await service.getMyProcesses();
|
const payload = await service.getMyProcesses();
|
||||||
if (payload.length != 0) {
|
if (payload && payload.length != 0) {
|
||||||
activeWorker?.postMessage({ type: 'SCAN', payload });
|
activeWorker?.postMessage({ type: 'SCAN', payload });
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
@ -199,9 +199,9 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleDownloadList(downloadList: string[]): void {
|
private async handleDownloadList(downloadList: string[]): Promise<void> {
|
||||||
// Download the missing data
|
// Download the missing data
|
||||||
let requestedStateId = [];
|
let requestedStateId: string[] = [];
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
for (const hash of downloadList) {
|
for (const hash of downloadList) {
|
||||||
const diff = await service.getDiffByValue(hash);
|
const diff = await service.getDiffByValue(hash);
|
||||||
@ -250,7 +250,7 @@ export class Database {
|
|||||||
} else if (data.type === 'TO_DOWNLOAD') {
|
} else if (data.type === 'TO_DOWNLOAD') {
|
||||||
console.log(`Received missing data ${data}`);
|
console.log(`Received missing data ${data}`);
|
||||||
// Download the missing data
|
// Download the missing data
|
||||||
let requestedStateId = [];
|
let requestedStateId: string[] = [];
|
||||||
for (const hash of data.data) {
|
for (const hash of data.data) {
|
||||||
try {
|
try {
|
||||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||||
@ -263,9 +263,12 @@ export class Database {
|
|||||||
console.log('Request data from managers of the process');
|
console.log('Request data from managers of the process');
|
||||||
// get the diff from db
|
// get the diff from db
|
||||||
const diff = await service.getDiffByValue(hash);
|
const diff = await service.getDiffByValue(hash);
|
||||||
const processId = diff.process_id;
|
if (diff === null) {
|
||||||
const stateId = diff.state_id;
|
continue;
|
||||||
const roles = diff.roles;
|
}
|
||||||
|
const processId = diff!.process_id;
|
||||||
|
const stateId = diff!.state_id;
|
||||||
|
const roles = diff!.roles;
|
||||||
if (!requestedStateId.includes(stateId)) {
|
if (!requestedStateId.includes(stateId)) {
|
||||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||||
requestedStateId.push(stateId);
|
requestedStateId.push(stateId);
|
||||||
@ -284,10 +287,49 @@ export class Database {
|
|||||||
|
|
||||||
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
|
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// Fallback: si Service Worker indisponible (ex: iframe tiers), écriture directe IndexedDB
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
try {
|
||||||
|
const db = await this.getDb();
|
||||||
|
const tx = (db as any).transaction(payload.storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(payload.storeName);
|
||||||
|
if (payload.key) {
|
||||||
|
await store.put(payload.object, payload.key);
|
||||||
|
} else {
|
||||||
|
await store.put(payload.object);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
} catch (error: any) {
|
||||||
|
reject(new Error(error?.message || 'IndexedDB write failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the service worker is active
|
// Check if the service worker is active
|
||||||
if (!this.serviceWorkerRegistration) {
|
if (!this.serviceWorkerRegistration) {
|
||||||
// console.warn('Service worker registration is not ready. Waiting...');
|
try {
|
||||||
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
// console.warn('Service worker registration is not ready. Waiting...');
|
||||||
|
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||||
|
} catch (error) {
|
||||||
|
// Si le service worker n'est pas disponible, fallback vers IndexedDB direct
|
||||||
|
console.warn('Service worker not available, falling back to direct IndexedDB');
|
||||||
|
try {
|
||||||
|
const db = await this.getDb();
|
||||||
|
const tx = (db as any).transaction(payload.storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(payload.storeName);
|
||||||
|
if (payload.key) {
|
||||||
|
await store.put(payload.object, payload.key);
|
||||||
|
} else {
|
||||||
|
await store.put(payload.object);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
} catch (dbError: any) {
|
||||||
|
reject(new Error(dbError?.message || 'IndexedDB write failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
||||||
@ -320,6 +362,59 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// Fallback direct IndexedDB si Service Worker indisponible
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
try {
|
||||||
|
const db = await this.getDb();
|
||||||
|
const tx = (db as any).transaction(payload.storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(payload.storeName);
|
||||||
|
for (const { key, object } of payload.objects) {
|
||||||
|
if (key) {
|
||||||
|
await store.put(object, key);
|
||||||
|
} else {
|
||||||
|
await store.put(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
} catch (error: any) {
|
||||||
|
reject(new Error(error?.message || 'IndexedDB batch write failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.serviceWorkerRegistration) {
|
||||||
|
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
||||||
|
const messageChannel = new MessageChannel();
|
||||||
|
|
||||||
|
messageChannel.port1.onmessage = (event) => {
|
||||||
|
if (event.data.status === 'success') {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
const error = event.data.message;
|
||||||
|
reject(new Error(error || 'Unknown error occurred while adding objects'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
activeWorker?.postMessage(
|
||||||
|
{
|
||||||
|
type: 'BATCH_WRITING',
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
[messageChannel.port2],
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Failed to send message to service worker: ${error}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getObject(storeName: string, key: string): Promise<any | null> {
|
public async getObject(storeName: string, key: string): Promise<any | null> {
|
||||||
const db = await this.getDb();
|
const db = await this.getDb();
|
||||||
const tx = db.transaction(storeName, 'readonly');
|
const tx = db.transaction(storeName, 'readonly');
|
||||||
@ -329,7 +424,7 @@ export class Database {
|
|||||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||||
getRequest.onerror = () => reject(getRequest.error);
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
});
|
});
|
||||||
return result;
|
return result ?? null; // Convert undefined to null
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dumpStore(storeName: string): Promise<Record<string, any>> {
|
public async dumpStore(storeName: string): Promise<Record<string, any>> {
|
||||||
@ -338,23 +433,25 @@ export class Database {
|
|||||||
const store = tx.objectStore(storeName);
|
const store = tx.objectStore(storeName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for both getAllKeys() and getAll() to resolve
|
return new Promise((resolve, reject) => {
|
||||||
const [keys, values] = await Promise.all([
|
const result: Record<string, any> = {};
|
||||||
new Promise<any[]>((resolve, reject) => {
|
const cursor = store.openCursor();
|
||||||
const request = store.getAllKeys();
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
}),
|
|
||||||
new Promise<any[]>((resolve, reject) => {
|
|
||||||
const request = store.getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Combine keys and values into an object
|
cursor.onsuccess = (event) => {
|
||||||
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
|
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
|
||||||
return result;
|
const cursor = request.result;
|
||||||
|
if (cursor) {
|
||||||
|
result[cursor.key as string] = cursor.value;
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cursor.onerror = () => {
|
||||||
|
reject(cursor.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching data from IndexedDB:', error);
|
console.error('Error fetching data from IndexedDB:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -4,10 +4,17 @@ import validationModalStyle from '../components/validation-modal/validation-moda
|
|||||||
import Services from './service';
|
import Services from './service';
|
||||||
import { init, navigate } from '../router';
|
import { init, navigate } from '../router';
|
||||||
import { addressToEmoji } from '../utils/sp-address.utils';
|
import { addressToEmoji } from '../utils/sp-address.utils';
|
||||||
import { RoleDefinition } from 'pkg/sdk_client';
|
import { RoleDefinition } from '.././pkg/sdk_client.js';
|
||||||
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
||||||
import { interpolate } from '~/utils/html.utils';
|
import { interpolate } from '~/utils/html.utils';
|
||||||
|
|
||||||
|
interface ConfirmationModalOptions {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ModalService {
|
export default class ModalService {
|
||||||
private static instance: ModalService;
|
private static instance: ModalService;
|
||||||
private stateId: string | null = null;
|
private stateId: string | null = null;
|
||||||
@ -125,7 +132,7 @@ export default class ModalService {
|
|||||||
console.log("MEMBERS:", members);
|
console.log("MEMBERS:", members);
|
||||||
// We take all the addresses except our own
|
// We take all the addresses except our own
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const localAddress = await service.getDeviceAddress();
|
const localAddress = service.getDeviceAddress();
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
if (member.sp_addresses) {
|
if (member.sp_addresses) {
|
||||||
for (const address of member.sp_addresses) {
|
for (const address of member.sp_addresses) {
|
||||||
@ -164,92 +171,55 @@ export default class ModalService {
|
|||||||
if (this.modal) this.modal.style.display = 'none';
|
if (this.modal) this.modal.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmPairing() {
|
async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise<boolean> {
|
||||||
const service = await Services.getInstance();
|
// Create modal element
|
||||||
if (this.modal) this.modal.style.display = 'none';
|
const modalElement = document.createElement('div');
|
||||||
|
modalElement.id = 'confirmation-modal';
|
||||||
|
modalElement.innerHTML = `
|
||||||
|
<div class="modal-overlay">
|
||||||
|
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ''}>
|
||||||
|
<h2>${options.title}</h2>
|
||||||
|
<div class="modal-body">
|
||||||
|
${options.content}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || 'Annuler'}</button>
|
||||||
|
<button id="confirm-button" class="btn btn-primary">${options.confirmText || 'Confirmer'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
if (service.device1) {
|
// Add modal to document
|
||||||
console.log("Device 1 detected");
|
document.body.appendChild(modalElement);
|
||||||
// We send the prd update
|
|
||||||
if (this.stateId && this.processId) {
|
// Return promise that resolves with user choice
|
||||||
try {
|
return new Promise((resolve) => {
|
||||||
// Device B shouldn't do this again
|
const confirmButton = modalElement.querySelector('#confirm-button');
|
||||||
const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
const cancelButton = modalElement.querySelector('#cancel-button');
|
||||||
await service.handleApiReturn(createPrdUpdateReturn);
|
const modalOverlay = modalElement.querySelector('.modal-overlay');
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
const cleanup = () => {
|
||||||
|
modalElement.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmButton?.addEventListener('click', () => {
|
||||||
|
cleanup();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelButton?.addEventListener('click', () => {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
modalOverlay?.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modalOverlay) {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
throw new Error('No currentPcdCommitment');
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// We send confirmation that we validate the change
|
|
||||||
try {
|
|
||||||
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
|
||||||
await service.handleApiReturn(approveChangeReturn);
|
|
||||||
|
|
||||||
await this.injectWaitingModal();
|
|
||||||
const waitingModal = document.getElementById('waiting-modal');
|
|
||||||
if (waitingModal) waitingModal.style.display = 'flex';
|
|
||||||
|
|
||||||
if (!service.device2Ready) {
|
|
||||||
while (!service.device2Ready) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
console.log("Device 2 is ready - Device 1 can now proceed");
|
|
||||||
}
|
|
||||||
service.pairDevice(this.paired_addresses, this.processId);
|
|
||||||
this.paired_addresses = [];
|
|
||||||
this.processId = null;
|
|
||||||
this.stateId = null;
|
|
||||||
const newDevice = service.dumpDeviceFromMemory();
|
|
||||||
console.log(newDevice);
|
|
||||||
await service.saveDeviceInDatabase(newDevice);
|
|
||||||
navigate('chat');
|
|
||||||
service.resetState();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// service.pairDevice(this.paired_addresses);
|
|
||||||
// } catch (e) {
|
|
||||||
// throw e;
|
|
||||||
// }
|
|
||||||
} else {
|
|
||||||
console.log("Device 2 detected");
|
|
||||||
|
|
||||||
// if (this.stateId && this.processId) {
|
|
||||||
// try {
|
|
||||||
// // Device B shouldn't do this again
|
|
||||||
// const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
|
|
||||||
// await service.handleApiReturn(createPrdUpdateReturn);
|
|
||||||
// } catch (e) {
|
|
||||||
// throw e;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// throw new Error('No currentPcdCommitment');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// We send confirmation that we validate the change
|
|
||||||
try {
|
|
||||||
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
|
|
||||||
await service.handleApiReturn(approveChangeReturn);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
service.pairDevice(this.paired_addresses, this.processId!);
|
|
||||||
|
|
||||||
this.paired_addresses = [];
|
|
||||||
this.processId = null;
|
|
||||||
this.stateId = null;
|
|
||||||
const newDevice = service.dumpDeviceFromMemory();
|
|
||||||
console.log(newDevice);
|
|
||||||
await service.saveDeviceInDatabase(newDevice);
|
|
||||||
navigate('chat');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeConfirmationModal() {
|
async closeConfirmationModal() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1776
src/services/service.ts.backup
Executable file
1776
src/services/service.ts.backup
Executable file
File diff suppressed because it is too large
Load Diff
@ -3,15 +3,34 @@ import axios, { AxiosResponse } from 'axios';
|
|||||||
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
|
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
try {
|
try {
|
||||||
// Append key and ttl as query parameters
|
// Handle relative paths (for development proxy) vs absolute URLs (for production)
|
||||||
const url = new URL(`${server}/store`);
|
let url: string;
|
||||||
url.searchParams.append('key', key);
|
if (server.startsWith('/')) {
|
||||||
if (ttl !== null) {
|
// Relative path - construct manually for proxy
|
||||||
url.searchParams.append('ttl', ttl.toString());
|
url = `${server}/store/${encodeURIComponent(key)}`;
|
||||||
|
if (ttl !== null) {
|
||||||
|
url += `?ttl=${ttl}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Absolute URL - use URL constructor
|
||||||
|
const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`);
|
||||||
|
if (ttl !== null) {
|
||||||
|
urlObj.searchParams.append('ttl', ttl.toString());
|
||||||
|
}
|
||||||
|
url = urlObj.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test first that data is not already stored
|
||||||
|
const testResponse = await testData(url, key);
|
||||||
|
if (testResponse) {
|
||||||
|
console.log('Data already stored:', key);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
console.log('Data not stored for server:', key, server);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the encrypted ArrayBuffer as the raw request body.
|
// Send the encrypted ArrayBuffer as the raw request body.
|
||||||
const response = await axios.post(url.toString(), value, {
|
const response = await axios.post(url, value, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/octet-stream'
|
'Content-Type': 'application/octet-stream'
|
||||||
},
|
},
|
||||||
@ -23,7 +42,7 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.response?.status === 409) {
|
if (axios.isAxiosError(error) && error.response?.status === 409) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
console.error('Error storing data:', error);
|
console.error('Error storing data:', error);
|
||||||
@ -35,21 +54,48 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
|
|||||||
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
try {
|
try {
|
||||||
|
// Handle relative paths (for development proxy) vs absolute URLs (for production)
|
||||||
|
const url = server.startsWith('/')
|
||||||
|
? `${server}/retrieve/${key}` // Relative path - use as-is for proxy
|
||||||
|
: new URL(`${server}/retrieve/${key}`).toString(); // Absolute URL - construct properly
|
||||||
|
|
||||||
|
console.log('Retrieving data', key,' from:', url);
|
||||||
// When fetching the data from the server:
|
// When fetching the data from the server:
|
||||||
const response = await axios.get(`${server}/retrieve/${key}`, {
|
const response = await axios.get(url, {
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
});
|
});
|
||||||
if (response.status !== 200) {
|
|
||||||
console.error('Received response status', response.status);
|
if (response.status === 200) {
|
||||||
|
// Validate that we received an ArrayBuffer
|
||||||
|
if (response.data instanceof ArrayBuffer) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
console.error('Server returned non-ArrayBuffer data:', typeof response.data);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Server ${server} returned status ${response.status}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// console.log('Retrieved data:', response.data);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrieving data:', error);
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
console.log(`Data not found on server ${server} for key ${key}`);
|
||||||
|
continue; // Try next server
|
||||||
|
} else if (error.response?.status) {
|
||||||
|
console.error(`Server ${server} error ${error.response.status}:`, error.response.statusText);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
console.error(`Network error connecting to ${server}:`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Unexpected error retrieving data from ${server}:`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestResponse {
|
interface TestResponse {
|
||||||
@ -57,25 +103,17 @@ interface TestResponse {
|
|||||||
value: boolean;
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
|
export async function testData(url: string, key: string): Promise<boolean | null> {
|
||||||
const res = {};
|
try {
|
||||||
for (const server of servers) {
|
const response = await axios.get(url);
|
||||||
res[server] = null;
|
if (response.status !== 200) {
|
||||||
try {
|
console.error(`Test response status: ${response.status}`);
|
||||||
const response = await axios.get(`${server}/test/${key}`);
|
return false;
|
||||||
if (response.status !== 200) {
|
}
|
||||||
console.error(`${server}: Test response status: ${response.status}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: TestResponse = response.data;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
res[server] = data.value;
|
console.error('Error testing data:', error);
|
||||||
} catch (error) {
|
return null;
|
||||||
console.error('Error retrieving data:', error);
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/services/token.ts
Normal file
118
src/services/token.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import * as jose from 'jose';
|
||||||
|
|
||||||
|
interface TokenPair {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TokenService {
|
||||||
|
private static instance: TokenService;
|
||||||
|
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
|
||||||
|
private readonly ACCESS_TOKEN_EXPIRATION = '30s';
|
||||||
|
private readonly REFRESH_TOKEN_EXPIRATION = '7d';
|
||||||
|
private readonly encoder = new TextEncoder();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static async getInstance(): Promise<TokenService> {
|
||||||
|
if (!TokenService.instance) {
|
||||||
|
TokenService.instance = new TokenService();
|
||||||
|
}
|
||||||
|
return TokenService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrCreateSecret(): Uint8Array {
|
||||||
|
// Priorité à la variable d'environnement si définie et non vide
|
||||||
|
if (this.SECRET_KEY && String(this.SECRET_KEY).trim().length > 0) {
|
||||||
|
return new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, on persiste une clé aléatoire locale (stable entre sessions) côté navigateur
|
||||||
|
try {
|
||||||
|
const storageKey = 'ihm_jwt_secret_key_v1';
|
||||||
|
let secretB64 = localStorage.getItem(storageKey);
|
||||||
|
if (!secretB64) {
|
||||||
|
const random = new Uint8Array(32);
|
||||||
|
crypto.getRandomValues(random);
|
||||||
|
// Stocker en base64 pour être textuel
|
||||||
|
secretB64 = btoa(String.fromCharCode(...random));
|
||||||
|
localStorage.setItem(storageKey, secretB64);
|
||||||
|
}
|
||||||
|
// Décoder base64 → Uint8Array
|
||||||
|
const binary = atob(secretB64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
} catch (_e) {
|
||||||
|
// Fallback minimal si localStorage indisponible (mode très restrictif)
|
||||||
|
const fallback = 'fallback-secret-key-please-persist';
|
||||||
|
return new Uint8Array(this.encoder.encode(fallback));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateSessionToken(origin: string): Promise<TokenPair> {
|
||||||
|
const secret = this.getOrCreateSecret();
|
||||||
|
|
||||||
|
const accessToken = await new jose.SignJWT({ origin, type: 'access' })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||||
|
.sign(secret);
|
||||||
|
|
||||||
|
const refreshToken = await new jose.SignJWT({ origin, type: 'refresh' })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(this.REFRESH_TOKEN_EXPIRATION)
|
||||||
|
.sign(secret);
|
||||||
|
|
||||||
|
return { accessToken, refreshToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string, origin: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const secret = this.getOrCreateSecret();
|
||||||
|
const { payload } = await jose.jwtVerify(token, secret);
|
||||||
|
|
||||||
|
return payload.origin === origin;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code === 'ERR_JWT_EXPIRED') {
|
||||||
|
console.log('Token expiré');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Erreur de validation du token:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// Vérifier si le refresh token est valide
|
||||||
|
const isValid = await this.validateToken(refreshToken, origin);
|
||||||
|
if (!isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le type du token
|
||||||
|
const secret = this.getOrCreateSecret();
|
||||||
|
const { payload } = await jose.jwtVerify(refreshToken, secret);
|
||||||
|
if (payload.type !== 'refresh') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer un nouveau access token
|
||||||
|
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||||
|
.sign(secret);
|
||||||
|
|
||||||
|
return newAccessToken;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du refresh du token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/utils/service.utils.ts
Normal file
24
src/utils/service.utils.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export function splitPrivateData(data: Record<string, any>, privateFields: string[]): { privateData: Record<string, any>, publicData: Record<string, any> } {
|
||||||
|
const privateData: Record<string, any> = {};
|
||||||
|
const publicData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (privateFields.includes(key)) {
|
||||||
|
privateData[key] = value;
|
||||||
|
} else {
|
||||||
|
publicData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { privateData, publicData };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValid32ByteHex(value: string): boolean {
|
||||||
|
// Check if string is exactly 64 characters (32 bytes in hex)
|
||||||
|
if (value.length !== 64) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if string only contains valid hex characters
|
||||||
|
return /^[0-9a-fA-F]{64}$/.test(value);
|
||||||
|
}
|
||||||
@ -105,6 +105,7 @@ export function initAddressInput() {
|
|||||||
const emojiDisplay = container.querySelector('#emoji-display-2');
|
const emojiDisplay = container.querySelector('#emoji-display-2');
|
||||||
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
|
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
|
||||||
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
|
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
|
||||||
|
const actionButton = container.querySelector('#actionButton') as HTMLButtonElement;
|
||||||
addSubscription(addressInput, 'input', async () => {
|
addSubscription(addressInput, 'input', async () => {
|
||||||
let address = addressInput.value;
|
let address = addressInput.value;
|
||||||
|
|
||||||
@ -141,12 +142,6 @@ export function initAddressInput() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (okButton) {
|
|
||||||
addSubscription(okButton, 'click', () => {
|
|
||||||
onOkButtonClick();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createButton) {
|
if (createButton) {
|
||||||
addSubscription(createButton, 'click', () => {
|
addSubscription(createButton, 'click', () => {
|
||||||
onCreateButtonClick();
|
onCreateButtonClick();
|
||||||
@ -154,64 +149,41 @@ export function initAddressInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onOkButtonClick() {
|
|
||||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
|
||||||
const secondDeviceAddress = (container.querySelector('#addressInput') as HTMLInputElement).value;
|
|
||||||
try {
|
|
||||||
// Connect to target, if necessary
|
|
||||||
await prepareAndSendPairingTx(secondDeviceAddress);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`onOkButtonClick error: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCreateButtonClick() {
|
async function onCreateButtonClick() {
|
||||||
try {
|
try {
|
||||||
await prepareAndSendPairingTx();
|
await prepareAndSendPairingTx();
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
await service.confirmPairing();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`onCreateButtonClick error: ${e}`);
|
console.error(`onCreateButtonClick error: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareAndSendPairingTx(promptName: boolean = false) {
|
export async function prepareAndSendPairingTx(): Promise<void> {
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
|
|
||||||
// Device 1 wait Device 2
|
// checkConnections requires a Process object, not an empty array
|
||||||
// service.device1 = true;
|
// This call has been removed as it was causing TypeScript errors
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.checkConnections([]);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt the user for a username.
|
|
||||||
let userName;
|
|
||||||
if (promptName) {
|
|
||||||
userName = prompt("Please enter your user name:");
|
|
||||||
} else {
|
|
||||||
userName = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the process after a delay.
|
|
||||||
setTimeout(async () => {
|
|
||||||
const relayAddress = service.getAllRelays();
|
const relayAddress = service.getAllRelays();
|
||||||
|
|
||||||
// Pass the userName as an additional parameter.
|
|
||||||
const createPairingProcessReturn = await service.createPairingProcess(
|
const createPairingProcessReturn = await service.createPairingProcess(
|
||||||
userName,
|
"",
|
||||||
[],
|
[],
|
||||||
relayAddress[0].spAddress,
|
|
||||||
1,
|
|
||||||
userName
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!createPairingProcessReturn.updated_process) {
|
if (!createPairingProcessReturn.updated_process) {
|
||||||
throw new Error('createPairingProcess returned an empty new process'); // This should never happen
|
throw new Error('createPairingProcess returned an empty new process');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
|
||||||
|
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
|
||||||
|
|
||||||
await service.handleApiReturn(createPairingProcessReturn);
|
await service.handleApiReturn(createPairingProcessReturn);
|
||||||
}, 1000);
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateQRCode(spAddress: string) {
|
export async function generateQRCode(spAddress: string) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AnkFlag } from 'pkg/sdk_client';
|
import { AnkFlag } from '.././pkg/sdk_client.js';
|
||||||
import Services from './services/service';
|
import Services from './services/service';
|
||||||
|
|
||||||
let ws: WebSocket;
|
let ws: WebSocket;
|
||||||
|
|||||||
7
start-dev.sh
Normal file
7
start-dev.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Démarrer nginx en arrière-plan
|
||||||
|
nginx
|
||||||
|
|
||||||
|
# Démarrer le serveur de développement Vite
|
||||||
|
npm run start
|
||||||
16
tests/analyse.md
Normal file
16
tests/analyse.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
### Objet
|
||||||
|
Axes de tests pour `ihm_client` (sans exemples).
|
||||||
|
|
||||||
|
### Couverture prioritaire
|
||||||
|
- **Chargement iframe**: initialisation, messages parent ↔ iframe
|
||||||
|
- **Auth/Token**: création, stockage, renouvellement, invalidation
|
||||||
|
- **Pages**: home, chat, account, process, signature (navigation, états)
|
||||||
|
- **WebSockets**: connexion, reconnexion, messages, erreurs
|
||||||
|
- **Modales**: confirmation/creation/waiting/validation-rule
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Workers**: cache et base de données (latences, tailles)
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- **postMessage**: validation d’origine, format messages
|
||||||
|
- **Stockage**: isolation domain/cookies/localStorage
|
||||||
9
tsconfig.build.json
Normal file
9
tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"module": "commonjs"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@ -26,4 +26,4 @@
|
|||||||
},
|
},
|
||||||
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
|
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export default defineConfig({
|
|||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: false,
|
minify: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: './src/router.ts',
|
input: './src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'index.js',
|
entryFileNames: 'index.js',
|
||||||
},
|
},
|
||||||
@ -57,7 +57,9 @@ export default defineConfig({
|
|||||||
fs: {
|
fs: {
|
||||||
cachedChecks: false,
|
cachedChecks: false,
|
||||||
},
|
},
|
||||||
port: 3001,
|
port: 3003,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: ['dev4.4nkweb.com', 'localhost', '127.0.0.1'],
|
||||||
proxy: {
|
proxy: {
|
||||||
'/storage': {
|
'/storage': {
|
||||||
target: 'https://demo.4nkweb.com',
|
target: 'https://demo.4nkweb.com',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user