Compare commits

..

No commits in common. "wip-pairing" and "create-account" have entirely different histories.

164 changed files with 23158 additions and 32890 deletions

View File

@ -1,144 +0,0 @@
---
description: Règles pour tous aussi pour l'IA et pour Cursor
alwaysApply: true
---
# IHM_CLIENT
voir les fichiers README.md
## Instructions for Claude
### General
* Répond en français
* Code, documente le code, et fait les commits en anglais
### Règles Obligatoires
### Préparation
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
#### ⚙️ Getion de projet
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
* **Planning :** Ne fait pas de roadmap.
#### 🤝 Collaboration et Workflow
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Rapports :** Ne fait pas de rapports apres tes actions.
#### ⚙️ Gestion de l'Environnement et des Configurations
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
* **Nginx :** Ne modifie jamaisles configurations Nginx
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
#### 💻 Qualité du Code et Bonnes Pratiques
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
#### Code
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
* **Fallbacks :** Ne fait pas et supprime les fallbacks
* **Fichiers de définition :** Génère automatiquement les fichiers de définition de type pour chaque fichier TypeScript compilé. Chaque module doit exposer explicitement ses types publics pour permettre linteropérabilité et lanalyse statique par dautres projets.
* **Répertoire de sortie des fichiers compilés :** la structure du code source doit être reproduite à lidentique des dossiers compilés afin dassurer la traçabilité et la reproductibilité des builds.
* **Version ECMAScript :** le code doit rester compatible avec les navigateurs ou environnements qui supportent les fonctionnalités ESNext, ou être transpilé si nécessaire.
* **Bibliothèques et environnements :** Définit les bibliothèques intégrées utilisées par le compilateur pour fournir des types globaux (ex. objets DOM, APIs Web Worker). Tout code doit respecter les interfaces standardisées des environnements navigateur et worker.
* **types propres à Vite et à Node.js :** garantir que les modules supportent à la fois le contexte serveur (Node) et client (navigateur).
* **JavaScript (.js) :** Permet linclusion de fichiers JavaScript (.js) dans la compilation. Le code JavaScript inclus doit respecter les conventions TypeScript (noms, exports, compatibilité de types).
* **skipLibCheck :** Désactive la vérification de type interne des fichiers .d.ts des bibliothèques externes. Les dépendances doivent être validées manuellement lors des mises à jour pour éviter des erreurs de typage masquées.
* **Compatibilité automatique entre modules CommonJS et ESModules desactivée** tous les imports doivent être conformes à la sémantique native ECMAScript.
* **allowSyntheticDefaultImports** Autorise les imports par défaut même lorsque le module nen expose pas formellement. Cette option simplifie la migration depuis CommonJS, mais doit être utilisée avec modération.
* **Mode strict :** Active le mode strict global, qui regroupe plusieurs sous-vérifications (null, any, this, etc.). Tout code doit passer sans avertissement en mode strict pour garantir la robustesse du typage.
* **noImplicitAny :**: Interdit lutilisation implicite du type any. Tout type doit être explicitement déclaré ou inféré, garantissant la traçabilité sémantique.
* **noImplicitReturns :** Impose que toutes les branches de fonction retournent une valeur. Elimine les comportements indéterminés liés à des retours manquants.
* **noUnusedParameters :** Autorise les paramètres non utilisés. Ces paramètres doivent être nommés avec un préfixe conventionnel (_) pour indiquer lintention dignorance.
* **exactOptionalPropertyTypes :** Ne pas permettre une correspondance souple des propriétés optionnelles ({ a?: string } peut accepter {} ou { a: undefined }).
* **forceConsistentCasingInFileNames :**: Impose une casse cohérente entre les noms de fichiers importés et ceux présents sur le disque. Empêche les erreurs de casse entre systèmes de fichiers sensibles et insensibles (Windows, Linux).
* **ESNext :** Utilise la syntaxe modulaire la plus récente. La structure des imports doit suivre le format standard ECMAScript, y compris pour les chemins relatifs.
* **Module Resolution :** la hiérarchie des node_modules doit être stable et conforme aux conventions de résolution.
* **resolveJsonModule :** Autorise limport direct de fichiers JSON en tant que modules. Les JSON importés doivent être statiquement typés (via interfaces ou as const).
* **isolatedModules :** Oblige chaque fichier à pouvoir être transpilé indépendamment. Empêche les dépendances implicites entre fichiers et améliore la compatibilité.
* **experimentalDecorators :** Active le support expérimental des décorateurs (@decorator). Les décorateurs doivent être documentés et limités aux contextes maîtrisés (injection de dépendances, métaprogrammation contrôlée).
* **Chemins :** Utiliser des chemin relatifs et indiquer la racine du projet en configuration. Toutes les références internes doivent être relatives à la racine du projet. Vérifier de limiter l'acces en dehors du projet.
#### 🧪 Tests
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
#### 📚 Documentation
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
* **Structure de la documentation :**
* La documentation générale et pérenne se trouve dans `docs/`.
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
#### 📊 Logging et Gestion des Erreurs
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
#### 🌐 Interactions Externes (BDD, API, Emails)
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
### Base de données
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
#### 🚀 Déploiement
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
* **Lancement :** ne lance aucun déploiement sans demander avant
#### 🚨 Gestion des Problèmes
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
#### 🗄️ Gestion des Fichiers
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
### Mise à jour de ces règles
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
### Application
* Indique l'IA que tu utilise
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.

View File

@ -1,7 +0,0 @@
---
alwaysApply: true
---
lire avec attention: docs/INITIALIZATION_FLOW.md
lire avec attention: docs/IA_agents/*
lire avec attention: docs/docs/*

View File

@ -1,360 +0,0 @@
---
alwaysApply: true
---
# IHM_CLIENT - Analyse du Projet
## Vue d'ensemble
Application client Web5 pour l'écosystème 4NK permettant la gestion sécurisée des appareils, le pairing et les signatures de documents.
## Architecture Technique
### Stack Technologique
- **Frontend**: TypeScript, Vite, HTML5, CSS3
- **SDK**: Rust compilé en WebAssembly
- **Storage**: IndexedDB, Service Workers
- **Communication**: WebSockets, PostMessage API
- **Construction**: Vite avec plugins WASM et top-level await
### Configuration
- **Port**: 3004
- **URL**: https://dev3.4nkweb.com
- **Proxy**: `/storage` → https://dev3.4nkweb.com/storage
- **Base URL**: Variables d'environnement VITE_BASEURL, VITE_BOOTSTRAPURL, VITE_STORAGEURL, VITE_BLINDBITURL
### Structure du Projet
```
ihm_client_dev3/
├── src/
│ ├── components/ # Composants UI réutilisables
│ │ ├── account-nav/
│ │ ├── device-management/
│ │ ├── iframe-pairing/
│ │ ├── login-modal/
│ │ ├── secure-credentials/
│ │ ├── security-mode-selector/
│ │ └── validation-modal/
│ ├── pages/ # Pages de l'application
│ │ ├── account/
│ │ ├── birthday-setup/
│ │ ├── block-sync/
│ │ ├── home/
│ │ ├── pairing/
│ │ ├── security-setup/
│ │ └── wallet-setup/
│ ├── services/ # Services métier
│ │ ├── service.ts # Service principal (singleton)
│ │ ├── database.service.ts
│ │ ├── secure-credentials.service.ts
│ │ ├── security-mode.service.ts
│ │ ├── pairing.service.ts
│ │ ├── iframe-pairing.service.ts
│ │ └── websocket-manager.ts
│ ├── repositories/ # Accès aux données
│ │ ├── device.repository.ts
│ │ └── process.repository.ts
│ ├── models/ # Types et interfaces
│ ├── utils/ # Utilitaires
│ ├── service-workers/ # Workers pour opérations async
│ ├── router.ts # Router principal
│ └── main.ts # Point d'entrée
├── pkg/ # SDK WebAssembly compilé
├── docs/ # Documentation
├── IA_agents/ # Documentation pour IA
├── test-browser/ # Tests Playwright
└── logs/ # Logs centralisés
```
## Architecture Fonctionnelle
### Flux d'Initialisation
1. **Security Setup** → Configuration du mode de sécurité
2. **Wallet Setup** → Création du wallet Bitcoin
3. **Birthday Setup** → Configuration de la date anniversaire
4. **Block Sync** → Synchronisation initiale des blocs
5. **Pairing** → Appairage de l'appareil
6. **Account** → Interface principale
### Système de Modes de Sécurité
| Mode | Nom | Niveau | Stockage Clé PBKDF2 |
|------|-----|--------|---------------------|
| `proton-pass` | Proton Pass | High | WebAuthn du navigateur |
| `os` | OS Authenticator | High | WebAuthn du système |
| `otp` | OTP | High | En clair |
| `password` | Mot de passe | Low | Chiffré avec mot de passe |
| `none` | Aucune | Critical | Clé en dur (déconseillé) |
### Base de Données IndexedDB
**Stores:**
- `pbkdf2keys` : Clés PBKDF2 chiffrées par mode de sécurité
- `wallet` : Wallet chiffré (device + wallet data)
- `credentials` : Credentials de pairing (après pairing)
- `env` : Variables d'environnement internes
- `processes` : Processus de communication
- `labels` : Labels de transactions
- `shared_secrets` : Secrets partagés
- `unconfirmed_secrets` : Secrets non confirmés
- `diffs` : Différences de synchronisation
- `data` : Données générales
### Système de Processus
Le système de processus est générique et réutilisable pour créer des "contrats" entre des membres avec niveaux d'accès différents.
**Concepts:**
- **Process**: Contrat décentralisé entre plusieurs membres
- **State**: État du processus avec données publiques/privées
- **Roles**: Définition des permissions par rôle
- **Members**: Participants identifiés par pairing process ID
**Données:**
- **Public**: Accessibles à tous, portées automatiquement
- **Private**: Chiffrées via PCD commitments, distribuées aux membres autorisés
- **Roles**: Quorum et champs accessibles par rôle
**Cycle de vie:**
1. Création → `createProcess()`
2. Mise à jour → `updateProcess()`
3. Synchronisation PRD → `createPrdUpdate()`
4. Validation → `approveChange()`
5. Commit blockchain → État immuable
6. Accès → `getPublicData()` / `decryptAttribute()`
### Système de Pairing
**But**: Créer une identité numérique vérifiable pour MFA entre appareils
**Caractéristiques:**
- Un wallet peut être appairé à plusieurs appareils
- Processus blockchain avec états commités
- Synchronisation via relais
- Contrôle via 4 mots
**Flux Créateur:**
1. `createPairingProcess()` avec son adresse
2. `generateQRCode()` pour le joiner
3. `waitForJoinerAndUpdateProcess()`
4. `waitForPairingCommitment()`
5. `confirmPairing()`
**Flux Joiner:**
1. `discoverAndJoinPairingProcess()` via QR code
2. `waitForPairingCommitment()`
3. `confirmPairing()`
## Services Principaux
### Services (Singleton)
**Initialisation:**
- WebAssembly SDK
- WebSocket connections
- Database restoration
- Process listening
**Principales méthodes:**
- `getDeviceFromDatabase()` : Récupération du device
- `createPairingProcess()` : Création de processus de pairing
- `createProcess()` : Création de processus générique
- `updateProcess()` : Mise à jour de processus
- `getProcess()` : Récupération de processus
- `checkConnections()` : Vérification des connexions entre membres
- `decryptAttribute()` : Déchiffrement d'attribut privé
- `getPublicData()` : Récupération des données publiques
### Database Service
**Gestion IndexedDB:**
- Singleton avec service worker
- Stores configurables via `storeDefinitions`
- Transactions asynchrones
- Cache management
### Secure Credentials Service
**Gestion des credentials:**
- Génération et stockage de credentials de pairing
- Récupération avec déchiffrement
- Support WebAuthn pour modes haute sécurité
### Security Mode Service
**Gestion des modes:**
- Détection du mode actuel
- Stockage dans IndexedDB
- Authentification selon mode
## Logging et Erreurs
### SecureLogger
**Système centralisé:**
- Niveaux: DEBUG, INFO, WARN, ERROR
- Sanitisation automatique des données sensibles
- Contexte enrichi avec composant et métadonnées
- Formatage cohérent
**Bonnes pratiques:**
- Utiliser `secureLogger` au lieu de `console.*`
- Toujours spécifier le composant
- Ajouter métadonnées utiles
- Messages clairs et concis
- Vérifications réelles avant logs de succès
**Patterns:**
- 🔍 DEBUG : Informations de débogage
- ✅ INFO : Succès et initialisations
- ⚠️ WARN : Avertissements non critiques
- ❌ ERROR : Erreurs critiques
## Router
### Navigation
**Logique de progression:**
1. Si pairing → account
2. Si date anniversaire → pairing
3. Si wallet → birthday-setup
4. Si pbkdf2 → wallet-setup
5. Sinon → security-setup
**Routes principales:**
- `/security-setup` : Configuration sécurité
- `/wallet-setup` : Création wallet
- `/birthday-setup` : Configuration birthday
- `/block-sync` : Synchronisation blocs
- `/pairing` : Appairage
- `/account` : Interface principale
- `/home` : Page d'accueil avec login
### Message Handlers
Gestion des messages PostMessage pour communication iframe:
- `REQUEST_LINK` : Liaison avec site externe
- `CREATE_PAIRING` : Création de pairing
- `GET_PROCESSES` : Récupération des processus
- `CREATE_PROCESS` : Création de processus
- `UPDATE_PROCESS` : Mise à jour de processus
- `VALIDATE_STATE` : Validation d'état
- Etc.
## WebSocket
**Gestion:**
- Connexions multiples (BOOTSTRAP, STORAGE, BLINDBIT)
- Handshake automatique
- Retry automatique en cas de déconnexion
- Event bus pour messages
**Messages:**
- HandshakeMessage : Synchronisation initiale
- NewTxMessage : Nouvelles transactions
- Process updates : Mises à jour de processus
## Dépendances
**Principales:**
- `axios` : Requêtes HTTP
- `jose` : JWT et chiffrement
- `jsonwebtoken` : Tokens JWT
- `pdf-lib` : Manipulation PDF
- `sweetalert2` : Modals UI
**SDK WebAssembly:**
- `pkg/sdk_client.js` : SDK principal
- `pkg/sdk_client_bg.wasm` : Binaire WebAssembly
- Types TypeScript générés
## Tests
**Structure:**
- `test-browser/` : Tests Playwright
- Tests pour chaque parcours utilisateur
- Intégration avec mocked data
## User Stories
34 stories définies couvrant:
- Login (adresse device, QR code)
- Process management
- Account management
- Pairing
- Wallet
- Chat
- Signatures
## Configuration TypeScript
**Strict Mode:**
- `strict: true`
- `noImplicitAny: true`
- `noImplicitReturns: true`
- `forceConsistentCasingInFileNames: true`
**Modules:**
- `module: ESNext`
- `target: ESNext`
- `lib: ["DOM", "DOM.Iterable", "ESNext", "webworker"]`
- `isolatedModules: true`
## Points Critiques
### Sécurité
- Clés PBKDF2 toujours chiffrées (sauf mode OTP)
- Wallet toujours chiffré en base
- WebAuthn pour modes haute sécurité
- Sanitisation automatique des logs
### Performance
- Cache désactivé (`maxCacheSize = 0`)
- Memory management avec retry
- Lazy loading des composants
- Service workers pour opérations async
### Robustesse
- Retry automatique pour opérations critiques
- Vérifications réelles avant logs de succès
- Fallback et navigation automatique
- Gestion des erreurs IndexedDB
## Documentation
**Fichiers clés:**
- `docs/INITIALIZATION_FLOW.md` : Flux d'initialisation détaillé
- `docs/PROCESS_SYSTEM_ARCHITECTURE.md` : Architecture des processus
- `docs/PAIRING_SYSTEM_ANALYSIS.md` : Analyse du pairing
- `docs/LOGGING_GUIDELINES.md` : Guide de logging
- `IA_agents/all.md` : Règles pour IA
- `README.md` : Vue d'ensemble
## Développement
**Scripts:**
- `npm run start` : Serveur de développement
- `npm run build` : Build de production
- `npm run quality` : Vérification qualité
- `npm run lint` : Linting
- `npm run type-check` : Vérification TypeScript
**Prérequis:**
- Node.js 18+
- Rust (pour SDK)
- npm ou yarn
## Points d'Attention
1. **Ordre des modes testés**: `['none', 'otp', 'password', 'os', 'proton-pass']`
2. **Store credentials**: Utilisé uniquement après pairing
3. **Redirection automatique**: 3s après création wallet vers birthday-setup
4. **Synchronisation IndexedDB**: Utilisation directe pour éviter problèmes service worker
5. **Retry automatique**: Jusqu'à 5 tentatives pour vérifications wallet
6. **Vérifications réelles**: Logs de succès uniquement après vérification
7. **WebAssembly memory**: Monitoring et cleanup automatique
8. **Singleton patterns**: Services, Database, ModalService
9. **Message handlers**: Tous nécessitent validation token
10. **Process lifecycle**: Toujours vérifier état committé avant manipulation

View File

@ -1,8 +0,0 @@
---
alwaysApply: true
---
le site tourne sur le port 3004
l'url du site est https://dev3.4nkweb.com
ne déclanche jamais la CI
le relai doit tourner sur 8091

3
.env Normal file
View File

@ -0,0 +1,3 @@
# .env
VITE_API_URL=https://api.example.com
VITE_API_KEY=your_api_key

View File

@ -1,5 +0,0 @@
VITE_BASEURL="your_base_url"
VITE_BOOTSTRAPURL="your_bootstrap_url"
VITE_STORAGEURL="your_storage_url"
VITE_BLINDBITURL="your_blindbit_url"
VITE_JWT_SECRET_KEY="your_secret_key"

View File

@ -1,52 +0,0 @@
{
"extends": [
"@typescript-eslint/recommended",
"eslint:recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
// Qualité du code
"complexity": ["warn", 10],
"max-lines": ["warn", 300],
"max-lines-per-function": ["warn", 50],
"max-params": ["warn", 4],
"max-depth": ["warn", 4],
// TypeScript spécifique
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
// Bonnes pratiques
"no-console": "warn",
"no-debugger": "error",
"no-alert": "warn",
"prefer-const": "error",
"no-var": "error",
"eqeqeq": "error",
"curly": "error",
// Sécurité
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
// Performance
"no-loop-func": "error",
"no-await-in-loop": "warn"
},
"env": {
"browser": true,
"es2021": true,
"node": true
},
"ignorePatterns": [
"dist/",
"node_modules/",
"*.js"
]
}

View File

@ -1,44 +0,0 @@
name: Build and Push to Registry
on:
push:
branches: [ dev ]
env:
REGISTRY: git.4nkweb.com
IMAGE_NAME: 4nk/ihm_client
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up SSH agent
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.USER }}
password: ${{ secrets.TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
ssh: default
build-args: |
ENV_VARS=${{ secrets.ENV_VARS }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}

102
.gitignore vendored
View File

@ -1,103 +1,7 @@
# ----------------------------
# 🦀 Rust
# ----------------------------
target/
pkg/
Cargo.lock
*.rs.bk
**/*.rlib
# ----------------------------
# 🧰 Node / Frontend
# ----------------------------
node_modules/
dist/
build/
.cache/
.next/
out/
.tmp/
temp/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
package-lock.json
yarn.lock
pnpm-lock.yaml
# ----------------------------
# 🧱 IDE / Éditeurs
# ----------------------------
.idea/
*.iml
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.DS_Store
Thumbs.db
# ----------------------------
# ⚙️ Environnements / Secrets
# ----------------------------
.env
.env.local
.env.development.local
.env.production.local
.env.test.local
*.pem
*.crt
*.key
# ----------------------------
# 🌐 SSL / Certificats
# ----------------------------
public/ssl/
certs/
keys/
# ----------------------------
# 📦 Compilations WebAssembly
# ----------------------------
wasm-pack.log
*.wasm
# ----------------------------
# 🧪 Tests / Coverage
# ----------------------------
coverage/
lcov-report/
.nyc_output/
jest-cache/
jest-results.json
# ----------------------------
# 🧍 Runtime / OS / Divers
# ----------------------------
*.pid
*.seed
*.pid.lock
*.bak
*.orig
*.rej
# ----------------------------
# 🧠 Logs / Debug / Dump
# ----------------------------
*.log
*.stackdump
*.dmp
debug.log
error.log
# ----------------------------
# 🚀 Deploy / Production builds
# ----------------------------
.vercel/
.netlify/
firebase/
functions/lib/
sdk_relay
sdk_client
.vscode
public/ssl/

View File

@ -1,15 +1,14 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"printWidth": 300,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"bracketSameLine": false,
"proseWrap": "preserve"
}
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"endOfLine": "crlf"
}

View File

@ -1,8 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next"
]
}

59
.vscode/settings.json vendored
View File

@ -1,59 +0,0 @@
{
// ESLint Configuration
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.workingDirectories": ["."],
"eslint.options": {
"overrideConfigFile": "eslint.config.js"
},
// TypeScript Configuration
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsserver.maxTsServerMemory": 4096,
// Problems Panel Configuration
"problems.showCurrentInStatus": true,
"problems.autoReveal": true,
// File Associations
"files.associations": {
"*.ts": "typescript",
"*.tsx": "typescriptreact"
},
// Editor Configuration
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// TypeScript Specific
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
// Exclude patterns for file watcher
"files.watcherExclude": {
"**/node_modules/**": true,
"**/dist/**": true,
"**/pkg/**": true,
"**/logs/**": true,
"**/.git/**": true
},
// Search exclusion
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/pkg": true,
"**/logs": true,
"**/.git": true
}
}

120
CLAUDE.md
View File

@ -1,120 +0,0 @@
# IHM_CLIENT
voir les fichiers README.md
## Instructions for Claude
### General
* Répond en français
* Code, documente le code, et fait les commits en anglais
### Règles Obligatoires
### Préparation
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
#### ⚙️ Getion de projet
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
* **Planning :** Ne fait pas de roadmap.
#### 🤝 Collaboration et Workflow
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Rapports :** Ne fait pas de rapports apres tes actions.
#### ⚙️ Gestion de l'Environnement et des Configurations
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
* **Nginx :** Ne modifie jamaisles configurations Nginx
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
#### 💻 Qualité du Code et Bonnes Pratiques
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
#### Code
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
* **Fallbacks :** Ne fait pas et supprime les fallbacks
#### 🧪 Tests
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
#### 📚 Documentation
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
* **Structure de la documentation :**
* La documentation générale et pérenne se trouve dans `docs/`.
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
#### 📊 Logging et Gestion des Erreurs
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
* **Données vérifiées :** Vérifiant que les logs reflètent des vérifications réelles et non des déclarations.
* **Log abondamment :** Log les informations et étapes ou états clés ainsi que les identifiants clés.
#### 🌐 Interactions Externes (BDD, API, Emails)
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
### Base de données
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
#### 🚀 Déploiement
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
* **Lancement :** ne lance aucun déploiement sans demander avant
#### 🚨 Gestion des Problèmes
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
#### 🗄️ Gestion des Fichiers
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
### Mise à jour de ces règles
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
### Application
* Indique l'IA que tu utilise
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.

View File

@ -1,193 +0,0 @@
# 🤝 Guide de contribution - 4NK Client
## 📋 Standards de code
### **TypeScript**
- Utiliser des types explicites
- Éviter `any` autant que possible
- Préférer les interfaces aux types
- Documenter les fonctions publiques avec JSDoc
### **Architecture**
- Séparation claire des responsabilités
- Services injectables (éviter les singletons)
- Composants réutilisables
- Gestion d'erreurs centralisée
### **Performance**
- Lazy loading des modules lourds
- Mémoisation des calculs coûteux
- Debouncing des événements fréquents
- Optimisation des re-renders
## 🛠️ Workflow de développement
### **1. Avant de commencer**
```bash
# Installer les dépendances
npm install
# Vérifier la qualité du code
npm run quality
# Lancer les tests
npm test
```
### **2. Pendant le développement**
```bash
# Vérifier les types
npm run type-check
# Linter le code
npm run lint
# Formater le code
npm run prettify
```
### **3. Avant de commiter**
```bash
# Vérification complète
npm run quality:fix
# Tests
npm test
# Build
npm run build
```
## 📝 Standards de commit
### **Format**
```
type(scope): description
[body optionnel]
[footer optionnel]
```
### **Types**
- `feat`: Nouvelle fonctionnalité
- `fix`: Correction de bug
- `docs`: Documentation
- `style`: Formatage, point-virgules, etc.
- `refactor`: Refactoring
- `test`: Ajout de tests
- `chore`: Tâches de maintenance
### **Exemples**
```
feat(pairing): add 4-words pairing support
fix(ui): resolve header display issue
docs(api): update pairing service documentation
```
## 🧪 Tests
### **Structure des tests**
```
src/
├── components/
│ └── __tests__/
├── services/
│ └── __tests__/
└── utils/
└── __tests__/
```
### **Conventions**
- Un fichier de test par fichier source
- Nommage: `*.test.ts` ou `*.spec.ts`
- Couverture minimale: 80%
- Tests unitaires et d'intégration
## 📊 Métriques de qualité
### **Objectifs**
- **Complexité cyclomatique**: < 10
- **Taille des fichiers**: < 300 lignes
- **Couverture de tests**: > 80%
- **Temps de build**: < 30 secondes
### **Outils**
- ESLint pour la qualité du code
- Prettier pour le formatage
- TypeScript pour la sécurité des types
- Bundle analyzer pour la taille
## 🔒 Sécurité
### **Bonnes pratiques**
- Validation des données d'entrée
- Sanitisation des messages
- Gestion sécurisée des tokens
- Logs sans données sensibles
### **Vérifications**
- Aucun `eval()` ou `Function()`
- Validation des URLs et chemins
- Gestion des erreurs sans exposition d'informations
## 📚 Documentation
### **Code**
- JSDoc pour toutes les fonctions publiques
- Commentaires pour la logique complexe
- README technique pour l'architecture
### **API**
- Documentation des endpoints
- Exemples d'utilisation
- Gestion des erreurs
## 🚀 Déploiement
### **Environnements**
- **Development**: `npm run start`
- **Production**: `npm run build && npm run deploy`
### **Vérifications pré-déploiement**
```bash
npm run quality
npm test
npm run build
npm run analyze
```
## 🐛 Signalement de bugs
### **Template**
```
**Description**
Description claire du problème
**Reproduction**
1. Étapes pour reproduire
2. Comportement attendu
3. Comportement actuel
**Environnement**
- OS:
- Navigateur:
- Version:
**Logs**
Logs pertinents (sans données sensibles)
```
## 💡 Suggestions d'amélioration
### **Processus**
1. Créer une issue détaillée
2. Discuter de la faisabilité
3. Implémenter avec tests
4. Documentation mise à jour
### **Critères**
- Amélioration de la performance
- Meilleure expérience utilisateur
- Réduction de la complexité
- Sécurité renforcée

View File

@ -1,61 +1,13 @@
# syntax=docker/dockerfile:1.4
FROM rust:1.82-alpine AS wasm-builder
WORKDIR /build
# Installation des dépendances nécessaires pour la compilation
RUN apk update && apk add --no-cache \
git \
openssh-client \
curl \
nodejs \
npm \
build-base \
pkgconfig \
clang \
llvm \
musl-dev \
nginx
# Installation de wasm-pack
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Configuration SSH basique
RUN mkdir -p /root/.ssh && \
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# On se place dans le bon répertoire parent
WORKDIR /build
# Copie du projet ihm_client
COPY . ihm_client/
# Clonage du sdk_client au même niveau que ihm_client en utilisant la clé SSH montée
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
# Build du WebAssembly avec accès SSH pour les dépendances
WORKDIR /build/sdk_client
RUN --mount=type=ssh wasm-pack build --out-dir ../ihm_client/pkg --target bundler --dev
FROM node:20-alpine
WORKDIR /app
# Installation des dépendances nécessaires
RUN apk update && apk add --no-cache git nginx
# Copie des fichiers du projet
COPY --from=wasm-builder /build/ihm_client/pkg ./pkg
COPY . .
# Installation des dépendances Node.js
RUN npm install
# Copie de la configuration nginx
COPY nginx.dev.conf /etc/nginx/http.d/default.conf
# Script de démarrage
COPY start-dev.sh /start-dev.sh
RUN chmod +x /start-dev.sh
EXPOSE 3003 80
CMD ["/start-dev.sh"]
FROM node:20
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
WORKDIR /app
CMD ["npm", "start"]
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"]

View File

@ -1,121 +0,0 @@
# IHM_CLIENT
voir README.md
voir docs/INITIALIZATION_FLOW.md
### General
* Répond en français
* Code, documente le code, et fait les commits en anglais
### Règles Obligatoires
### Préparation
* **Répertoires :** Les application du services sont dans les autres dossiers à part `logs/`, `test-browser/`.
* **Analyse fine :** Analyse du `README.md` et des `README.md` des applications.
* **Analyse fine :** Analyse finement tous le documents de `IA_agents/`, `docs/`, de `todo/` et le code chaque application.
#### ⚙️ Getion de projet
* **Chiffrages :** Ne fait pas d'estimation du temps de réalisation.
* **Planning :** Ne fait pas de roadmap.
#### 🤝 Collaboration et Workflow
* **Ouverture aux modifications externes :** Comprendre et accepter que le projet puisse évoluer via des contributions extérieures.
* **Validation préalable :** Toute poussée de code (`git push`) ou déploiement doit être validée au préalable.
* **Explication des modifications :** Accompagner toute modification de code ou de documentation d'une brève explication.
* **Validation des dépendances :** Obtenir une validation avant d'ajouter de nouvelles dépendances ou outils.
* **Résultats attendus :** Ne liste pas les résultats attendus dans tes synthèses.
* **Résultats :** Ne présume pas de résultats non testés, ne conclue pas sans avoir de preuve ou de validation que c'est OK.
* **Commits :** Les commits doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Résumés et synthèses :** Les résumés d'actions et tes synthèses doivent être exhaustifs et synthétiques avec `**Motivations :**` `**Modifications :**`, `**Page affectées :**` en bullets points, aucun besoin de totaux par exemple de fichiers modifiés ou de nombre de lignes.
* **Rapports :** Ne fait pas de rapports apres tes actions.
#### ⚙️ Gestion de l'Environnement et des Configurations
* **Accès aux `.env` :** Les fichiers `.env` de production sont inaccessibles et ne doivent pas être modifiés.
* **Mise à jour de `env.example` :** Maintenir `env.example` systématiquement à jour et ne jamais intégrer de paramétrage sensible directement dans le code.
* **Ports :** Ne modifie jamais les ports même si il ne sont pas ceux par défaut.
* **Nginx :** Ne modifie jamaisles configurations Nginx
* **Configurations :** Privilégie les configuations en base de données plutôt que dans les `.env`.
#### 💻 Qualité du Code et Bonnes Pratiques
* **Respect des conventions :** Adhérer au style de code et aux conventions existantes du projet.
* **Sécurité :** Prioriser la sécurité en ne codant jamais en dur des informations sensibles (y compris dans la documentation) et en validant systématiquement les entrées utilisateur.
* **Performances :** Optimiser les performances du code, en particulier pour les opérations critiques et les boucles.
* **Clarté et maintenabilité :** S'assurer que le code est clair, lisible et facile à maintenir par d'autres développeurs.
#### Code
* **Eviter le code mort :** Etudie toujours finement l'existant pour éviter de créer du code mort ou supplémentaire, fait évoluer plutôt que d'ajouter
* **Nouveau code :** Tout code ajouté ou modifié doit être testé et documenté.
* **Lint :** Corrige les erreurs de lint, vérifie apres chaque fichier modifié
* **Fallbacks :** Ne fait pas et supprime les fallbacks
#### 🧪 Tests
* **Couverture des tests :** Rédiger des tests unitaires et d'intégration pour toute nouvelle fonctionnalité ou correction de bug.
* **Outils de test disponibles :** Utiliser `test-browser/` pour la simulation de navigateur et les commandes `curl` pour les tests d'API.
* **Playwright :** Pour chaque parcour impacter, créer des tests Playwright associés dans `test-browser/`.
#### 📚 Documentation
* **Objectif des travaux :** Se concentrer sur la réalisation de la liste des tâches décrite dans `todo/` dans des documents de type `todoX-desc.md`.
* **Travaux en cours:** Lorsqu'une todo est en cous `todo/` mettre à jour l'avancement de l'implémentation dans `TODOX-desc_IMPLEMENTATION.md`.
* **Travaux terminés :** Lorsqu'une todo est en cous `todo/` mettre à jour la desription finale de l'implémentation dans `TODOX-desc_IMPLEMENTATION_COMPLTE.md` et supprimer `TODOX-desc_IMPLEMENTATION.md`.
* **Structure de la documentation :**
* La documentation générale et pérenne se trouve dans `docs/`.
* La documentation spécifique à une situation ou un avancement se trouve dans `todo/`.
* **Utilisation de la documentation existante :** Ne pas ajouter de nouveaux documents, mais enrichir et mettre à jour l'existant.
* **Mise à jour continue :** Mettre à jour toute la documentation (`todo/`, `docs/` et commentaires dans le code) après les modifications ou pour clarifier.
* **Changelog :** Le fichier `CHANGELOG.md` de cette version en cours intègre toutes les todo dans todo/. Ce contenu est repris dans la slash notice de l'application front. Le `CHANGELOG.md` présente toutes les modifications de la version principale et les mises à jour mineurs sont ajoutée à l'update du `CHANGELOG.md` sans enlever d'élément.
#### 📊 Logging et Gestion des Erreurs
* **Centralisation des logs :** Centraliser les logs dans les répertoires `logs/` des applications et dans le répertoire `logs/` du projet pour les logs hors applications (déploiement par exemple)
* **Système de logging :** Implémenter une gestion d'erreurs robuste et utiliser le système de logging Winston pour toutes les sorties (info, warn, error, debug, etc.).
* **Traçabilité :** Logger toutes les valeurs, états clés et retours d'API.
* **Données vérifiées :** Vérifiant que les logs reflètent des vérifications réelles et non des déclarations.
* **Log abondamment :** Log les informations et étapes ou états clés ainsi que les identifiants clés.
#### 🌐 Interactions Externes (BDD, API, Emails)
* **APIs externes :** Gérer les interactions avec les API de manière appropriée, en respectant les limites d'utilisation et en gérant les erreurs.
* **Emails :** Gérer les envois d'emails de manière appropriée pour éviter le spam et gérer les erreurs.
### Base de données
* **Vigilence :** Être vigilant lors des interactions avec la base de données, notamment pour les migrations et les requêtes complexes.
* **Lecture seule :** N'écrit jamais en base, c'est la logique de code ou d'intégration/migration qui doit le faire.
#### 🚀 Déploiement
* **Préparation du déploiement :** Décrire et préparer le déploiement des correctifs et des évolutions.
* **Bilan de déloploiement :** ne fait pas de bilan de déploiement.
* **Lancement :** ne lance aucun déploiement sans demander avant
#### 🚨 Gestion des Problèmes
* **Résolution directe :** En cas de problème (toutes criticités), ne jamais simplifier, contourner, forcer un résultat en dur, ou créer des bouchons. Le problème doit être résolu à sa racine.
#### 🗄️ Gestion des Fichiers
* **Versions uniques :** Ne pas créer de versions alternatives des fichiers.
* **Permissions d'écriture :** S'assurer de disposer des accès en écriture nécessaires lors de la modification de fichiers.
### Mise à jour de ces règles
* **Propositions d'ajouts :** Quand tu apprends de nouvelles instructions qui te semblent pertinentes pour ces règles, propose de les ajouter.
* **Lecture seule :** Tu n'a pas le droit de modifier ces règles, tu peux seulement proposer des ajouts, modifications
* **`CLAUDE.MD` :** Il s'agit de ce fichier la documentation est ici <https://claudecode.io/tutorials/claude-md-setup>, c'est ce fichier que tu peux mettre à jour au fil de l'eau.
### Application
* Indique l'IA que tu utilise
* Ce document constitue la check list que tu dois appliquer obligatoirement en amont et en aval de tes réponses.

198
README.md
View File

@ -1,164 +1,52 @@
# 🚀 4NK Client - Application Web5
# ihm_client
Application client pour l'écosystème 4NK, permettant la gestion sécurisée des appareils, le pairing, et les signatures de documents.
## 📋 Table des matières
- [🚀 Démarrage rapide](#-démarrage-rapide)
- [🏗️ Architecture](#-architecture)
- [🔧 Développement](#-développement)
- [📊 Qualité du code](#-qualité-du-code)
- [🤝 Contribution](#-contribution)
## HOW TO START
## 🚀 Démarrage rapide
### **Prérequis**
- Node.js 18+
- Rust (pour le SDK)
- npm ou yarn
### **Installation**
```bash
# 1. Cloner les dépendances
git clone <sdk_common> # commit "doc pcd" from 28.10.2024
git clone <sdk_client> # commit "Ignore messages" from 17.10.2024
git clone <ihm_client_dev3>
# 2. Build du SDK Rust
cd sdk_common && cargo build
cd ../sdk_client && cargo run
# 3. Build et démarrage de l'application
cd ../ihm_client_dev3
npm install
npm run build_wasm
npm run start
```
### **Scripts disponibles**
```bash
# Développement
npm run start # Serveur de développement
npm run build # Build de production
npm run quality # Vérification de la qualité
npm run quality:fix # Correction automatique
# Tests et analyse
npm run test # Tests unitaires
npm run lint # Linting du code
npm run type-check # Vérification TypeScript
npm run analyze # Analyse du bundle
```
## 🏗️ Architecture
### **Structure du projet**
```
src/
├── components/ # Composants UI réutilisables
├── pages/ # Pages de l'application
├── services/ # Services métier
├── utils/ # Utilitaires et helpers
├── models/ # Types et interfaces
└── service-workers/ # Workers pour les opérations async
```
### **Technologies**
- **Frontend**: TypeScript, Vite, HTML5, CSS3
- **SDK**: Rust (WebAssembly)
- **Storage**: IndexedDB, Service Workers
- **Communication**: WebSockets, PostMessage API
### **WebSocket Relay Configuration**
- Default relay runs locally on `127.0.0.1:8091` and is exposed securely via `wss://relay235.4nkweb.com/ws/`
- Nginx TLS termination is defined in `nginx.relay235.conf` (kept alongside the existing `nginx.dev.conf`)
- Clients must configure `VITE_BOOTSTRAPURL=wss://relay235.4nkweb.com/ws/` to avoid mixed-content issues when the app is served over HTTPS
## 🔧 Développement
### **Standards de code**
- TypeScript strict
- ESLint + Prettier
- Tests unitaires
- Documentation JSDoc
### **Workflow**
1. Créer une branche feature
2. Développer avec tests
3. Vérifier la qualité: `npm run quality`
4. Créer une PR avec description détaillée
## 📊 Qualité du code
### **Métriques cibles**
- Couverture de tests: > 80%
- Complexité cyclomatique: < 10
- Taille des fichiers: < 300 lignes
- Bundle size: < 500KB gzippé
### **Outils de qualité**
- TypeScript strict mode
- ESLint avec règles personnalisées
- Prettier pour le formatage
- Bundle analyzer pour l'optimisation
## 🤝 Contribution
Voir [CONTRIBUTING.md](./CONTRIBUTING.md) pour les détails complets.
### **Démarrage rapide**
```bash
# Fork et clone
git clone <votre-fork>
cd ihm_client_dev3
# Installation
npm install
# Vérification de la qualité
npm run quality
# Développement
npm run start
```
1 - clone sdk_common, commit name "doc pcd" from 28.10.2024
2 - clone sdk_client, commit name "Ignore messages" from 17.10.2024
3 - clone ihm_client_test3
4 - cargo build in sdk_common
5 - cargo run in sdk_client
6 - npm run build_wasm in ihm_client_test3
7 - npm run start in ihm_client_test3
## USER STORIES
1 - I can login with my adress device
2 - I can login with QR code
3 - J'accède à la page Process après ma connexion
4 - Dans l'interface Process, je peux sélectionner un processus avec sa zone
5 - Je reçois des notifications dans la page Process
6 - Dans le menu, je peux importer mes données au format JSON
7 - Dans le menu, je peux accèder à la page Account
8 - Dans la page Account, je peux cliquer sur mon profil via la bulle de profil en haut à gauche et une popup de profil s'ouvre
9 - Dans la popup de profil, je peux voir mes informations personnelles et modifier certaines d'entre elles dont le nom, le prénom
10 - Dans la popup de profil, je peux changer ma photo de profil
11 - Dans la popup de profil, je peux fermer la popup en cliquant sur le bouton "X" en haut à droite de la popup
12 - Dans la popup de profil, je peux cliquer sur le bouton "Export User Data", ce qui me génère un fichier JSON
13 - Dans la popup de profil, je peux cliquer sur le bouton "Delete Account", ce qui me demande à valider mon choix
14 - Dans la popup de profil, je peux cliquer sur le bouton "Logout", ce qui me déconnecte
15 - Dans la popup de profil, je peux cliquer sur le bouton "Export Recovery", ce qui me demandera de confirmer mon choix ou d'annuler, si je confirme, je dois retenir et écrire les 4 mots de récupération, le bouton ne sera plus accessible après cela
16 - Dans l'onglet Pairing de la page Account, je peux ajouter un nouveau "device" en cliquant sur le bouton "Add Device"
17 - Dans l'onglet Pairing de la page Account, je peux supprimer un "device" en cliquant sur l'emoji de la poubelle à côté du device que je souhaite supprimer
18 - Dans l'onglet Pairing de la page Account, je peux cliquer sur le bouton "Scan QR Code" pour scanner le QR Code d'un nouveau device
19 - Dans l'onglet Pairing de la page Account, je peux renommer un "Device" en cliquant sur son nom et en modifiant le nom
20 - Dans l'onglet Wallet de la page Account, je peux ajouter un nouveau "wallet" en cliquant sur le bouton "Add a line"
1 - I can login with my adress device
2 - I can login with QR code
3 - J'accède à la page Process après ma connexion
4 - Dans l'interface Process, je peux sélectionner un processus avec sa zone
5 - Je reçois des notifications dans la page Process
6 - Dans le menu, je peux importer mes données au format JSON
7 - Dans le menu, je peux accèder à la page Account
8 - Dans la page Account, je peux cliquer sur mon profil via la bulle de profil en haut à gauche et une popup de profil s'ouvre
9 - Dans la popup de profil, je peux voir mes informations personnelles et modifier certaines d'entre elles dont le nom, le prénom
10 - Dans la popup de profil, je peux changer ma photo de profil
11 - Dans la popup de profil, je peux fermer la popup en cliquant sur le bouton "X" en haut à droite de la popup
12 - Dans la popup de profil, je peux cliquer sur le bouton "Export User Data", ce qui me génère un fichier JSON
13 - Dans la popup de profil, je peux cliquer sur le bouton "Delete Account", ce qui me demande à valider mon choix
14 - Dans la popup de profil, je peux cliquer sur le bouton "Logout", ce qui me déconnecte
15 - Dans la popup de profil, je peux cliquer sur le bouton "Export Recovery", ce qui me demandera de confirmer mon choix ou d'annuler, si je confirme, je dois retenir et écrire les 4 mots de récupération, le bouton ne sera plus accessible après cela
16 - Dans l'onglet Pairing de la page Account, je peux ajouter un nouveau "device" en cliquant sur le bouton "Add Device"
17 - Dans l'onglet Pairing de la page Account, je peux supprimer un "device" en cliquant sur l'emoji de la poubelle à côté du device que je souhaite supprimer
18 - Dans l'onglet Pairing de la page Account, je peux cliquer sur le bouton "Scan QR Code" pour scanner le QR Code d'un nouveau device
19 - Dans l'onglet Pairing de la page Account, je peux renommer un "Device" en cliquant sur son nom et en modifiant le nom
20 - Dans l'onglet Wallet de la page Account, je peux ajouter un nouveau "wallet" en cliquant sur le bouton "Add a line"
21 - Dans l'onglet Process de la page Account, je peux voir les Process disponibles et voir leur notifications en cliquant sur sur la sonnette à côté du processus
22 - Dans l'onglet Data de la page Account, je peux voir les données importées
23 - Je peux voir le contrat associé à une Data en cliquant sur le contrat dans la ligne de la Data
24 - Dans le menu je peux accèder à la page Chat
25 - Dans la page Chat, je peux voir les Processus
26 - Dans les Processus, je peux voir utilisateurs assignés à un rôle
27 - Dans les Processus, je peux envoyer des messages et des documents en cliquant sur le nom d'un utilisateur en en envoyant "send"
28 - Dans le menu je peux accèder à la page "Signatures"
29 - Je peux voir les documents à signer et vierge en cliquand sur l'emoji ⚙️ à côté du processus
30 - En cliquand sur l'onglet d'un processus, je peux voir les rôles assignés à un utilisateur en cliquant dessus
31 - En cliquand sur l'emoji 📁 à côté d'un rôle, je peux voir les documents associés à ce rôle
32 - Dans la vue des documents associés à un rôle, je peux créer un évènement de nouvelle signature pour tous les rôles associés à ce Processus, avec le bouton "New Request"
33 - En cliquant sur le bouton "New Request", une nouvelle fenêtre s'ouvre pour me permettre de rentrer la description, la visibilité, la date d'échéance, importer des documents, voir les signataires et "Request"
34 - Dans le menu, je peux me déconnecter avec le bouton "Disconnect"
22 - Dans l'onglet Data de la page Account, je peux voir les données importées
23 - Je peux voir le contrat associé à une Data en cliquant sur le contrat dans la ligne de la Data
24 - Dans le menu je peux accèder à la page Chat
25 - Dans la page Chat, je peux voir les Processus
26 - Dans les Processus, je peux voir utilisateurs assignés à un rôle
27 - Dans les Processus, je peux envoyer des messages et des documents en cliquant sur le nom d'un utilisateur en en envoyant "send"
28 - Dans le menu je peux accèder à la page "Signatures"
29 - Je peux voir les documents à signer et vierge en cliquand sur l'emoji ⚙️ à côté du processus
30 - En cliquand sur l'onglet d'un processus, je peux voir les rôles assignés à un utilisateur en cliquant dessus
31 - En cliquand sur l'emoji 📁 à côté d'un rôle, je peux voir les documents associés à ce rôle
32 - Dans la vue des documents associés à un rôle, je peux créer un évènement de nouvelle signature pour tous les rôles associés à ce Processus, avec le bouton "New Request"
33 - En cliquant sur le bouton "New Request", une nouvelle fenêtre s'ouvre pour me permettre de rentrer la description, la visibilité, la date d'échéance, importer des documents, voir les signataires et "Request"
34 - Dans le menu, je peux me déconnecter avec le bouton "Disconnect"
## TO DO
## TO DO

4
doc/BDD_ihm.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,500 +0,0 @@
# 🔍 Analyse approfondie du code - 4NK Client
## 📊 **Résumé exécutif**
Après analyse complète du code au-delà du linting, j'ai identifié plusieurs axes d'amélioration majeurs pour optimiser les performances, la sécurité, la maintenabilité et l'architecture de l'application.
## 🏗️ **1. Architecture et Design Patterns**
### **❌ Problèmes identifiés :**
#### **A. Anti-patterns majeurs**
1. **Singleton excessif** : Tous les services utilisent le pattern Singleton
```typescript
// ❌ Problématique actuelle
export default class Services {
private static instance: Services;
public static async getInstance(): Promise<Services> { ... }
}
```
2. **Couplage fort** : Services directement liés entre eux
```typescript
// ❌ Couplage fort
import Services from './service';
export class Database {
// Utilise directement Services
}
```
3. **Responsabilités mélangées** : Services font trop de choses
- `Services` : 3265 lignes, gère pairing, storage, websockets, UI
- `Database` : 619 lignes, gère storage + communication
### **✅ Solutions recommandées :**
#### **A. Injection de dépendances**
```typescript
// ✅ Architecture recommandée
interface ServiceContainer {
deviceRepo: DeviceRepository;
pairingService: PairingService;
storageService: StorageService;
eventBus: EventBus;
}
class PairingService {
constructor(
private deviceRepo: DeviceRepository,
private eventBus: EventBus,
private logger: Logger
) {}
}
```
#### **B. Pattern Repository**
```typescript
// ✅ Séparation des responsabilités
interface DeviceRepository {
getDevice(): Promise<Device | null>;
saveDevice(device: Device): Promise<void>;
deleteDevice(): Promise<void>;
}
interface ProcessRepository {
getProcesses(): Promise<Process[]>;
saveProcess(process: Process): Promise<void>;
}
```
## 🚀 **2. Performances et Optimisations**
### **❌ Goulots d'étranglement identifiés :**
#### **A. Gestion mémoire défaillante**
1. **Cache désactivé** : `processesCache` existe mais est désactivé (`maxCacheSize = 0`)
```typescript
// ⚠️ État actuel
private processesCache: Record<string, Process> = {};
private maxCacheSize = 0; // Disabled caches completely
private cacheExpiry = 0; // No cache expiry
```
**Note** : Le cache a été désactivé pour économiser la mémoire, mais cela peut impacter les performances pour les applications avec beaucoup de processus.
2. **Event listeners non nettoyés** : Fuites mémoire
```typescript
// ❌ Problème actuel
window.addEventListener('message', handleMessage);
// Jamais supprimé, s'accumule
```
3. **WebSocket non fermé** : Connexions persistantes
```typescript
// ❌ Problème actuel
let ws: WebSocket; // Variable globale
// Pas de cleanup, pas de reconnexion
```
#### **B. Opérations bloquantes**
1. **Encodage synchrone** : Bloque l'UI
```typescript
// ❌ Problème actuel
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData),
};
```
2. **Boucles synchrones** : Bloquent le thread principal
```typescript
// ❌ Problème actuel
while (messageQueue.length > 0) {
const message = messageQueue.shift();
if (message) {
ws.send(message);
}
}
```
### **✅ Solutions recommandées :**
#### **A. Gestion mémoire optimisée**
```typescript
// ✅ Cache avec limite et expiration
class ProcessCache {
private cache = new Map<string, { data: Process; timestamp: number }>();
private maxSize = 100;
private ttl = 5 * 60 * 1000; // 5 minutes
set(key: string, process: Process): void {
if (this.cache.size >= this.maxSize) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
this.cache.set(key, { data: process, timestamp: Date.now() });
}
get(key: string): Process | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
}
```
#### **B. WebSocket avec reconnexion**
```typescript
// ✅ WebSocket robuste
class WebSocketManager {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
connect(url: string): void {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.processMessageQueue();
};
this.ws.onclose = () => {
this.scheduleReconnect(url);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
private scheduleReconnect(url: string): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++;
this.connect(url);
}, this.reconnectDelay * this.reconnectAttempts);
}
}
}
```
#### **C. Encodage asynchrone**
```typescript
// ✅ Encodage non-bloquant
async function encodeDataAsync(data: any): Promise<any> {
return new Promise((resolve) => {
// Utiliser Web Workers pour l'encodage lourd
const worker = new Worker('/workers/encoder.worker.js');
worker.postMessage(data);
worker.onmessage = (e) => resolve(e.data);
});
}
```
## 🔒 **3. Sécurité et Vulnérabilités**
### **❌ Vulnérabilités identifiées :**
#### **A. Exposition de données sensibles**
1. **Clés privées en mémoire** : Stockage non sécurisé
```typescript
// ❌ Problème actuel
private_key: safeDevice.sp_wallet.private_key,
// Clé privée exposée dans les logs et la mémoire
```
2. **Logs avec données sensibles** : Information leakage
```typescript
// ❌ Problème actuel
console.log('encodedPrivateData:', encodedPrivateData);
// Données privées dans les logs
```
3. **Validation d'entrée insuffisante** : Injection possible
```typescript
// ❌ Problème actuel
const parsedMessage = JSON.parse(msgData);
// Pas de validation, pas de sanitisation
```
#### **B. Gestion des erreurs dangereuse**
1. **Stack traces exposés** : Information disclosure
```typescript
// ❌ Problème actuel
console.error('Received an invalid message:', error);
// Stack trace complet exposé
```
2. **Messages d'erreur trop détaillés** : Aide à l'attaquant
```typescript
// ❌ Problème actuel
throw new Error('❌ No relay address available after waiting');
// Information sur l'architecture interne
```
### **✅ Solutions recommandées :**
#### **A. Sécurisation des données sensibles**
```typescript
// ✅ Gestion sécurisée des clés
class SecureKeyManager {
private keyStore: CryptoKey | null = null;
async storePrivateKey(key: string): Promise<void> {
// Chiffrer la clé avant stockage
const encryptedKey = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
await this.getDerivedKey(),
new TextEncoder().encode(key)
);
this.keyStore = encryptedKey;
}
async getPrivateKey(): Promise<string | null> {
if (!this.keyStore) return null;
try {
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: this.keyStore.slice(0, 12) },
await this.getDerivedKey(),
this.keyStore.slice(12)
);
return new TextDecoder().decode(decrypted);
} catch {
return null;
}
}
}
```
#### **B. Validation et sanitisation**
```typescript
// ✅ Validation robuste
class MessageValidator {
static validateWebSocketMessage(data: any): boolean {
if (typeof data !== 'string') return false;
try {
const parsed = JSON.parse(data);
return this.isValidMessageStructure(parsed);
} catch {
return false;
}
}
private static isValidMessageStructure(msg: any): boolean {
return (
typeof msg === 'object' &&
typeof msg.flag === 'string' &&
typeof msg.content === 'object' &&
['Handshake', 'NewTx', 'Cipher', 'Commit'].includes(msg.flag)
);
}
}
```
#### **C. Logging sécurisé**
```typescript
// ✅ Logging sans données sensibles
class SecureLogger {
static logError(message: string, error: Error, context?: any): void {
const sanitizedContext = this.sanitizeContext(context);
console.error(`[${new Date().toISOString()}] ${message}`, {
error: error.message,
context: sanitizedContext,
// Pas de stack trace en production
});
}
private static sanitizeContext(context: any): any {
if (!context) return {};
const sanitized = { ...context };
// Supprimer les données sensibles
delete sanitized.privateKey;
delete sanitized.password;
delete sanitized.token;
return sanitized;
}
}
```
## 🧪 **4. Tests et Qualité**
### **❌ Déficiences actuelles :**
1. **Aucun test unitaire** : Pas de couverture de code
2. **Pas de tests d'intégration** : Fonctionnalités non validées
3. **Pas de tests de performance** : Goulots non identifiés
4. **Pas de tests de sécurité** : Vulnérabilités non détectées
### **✅ Solutions recommandées :**
#### **A. Tests unitaires**
```typescript
// ✅ Tests unitaires
describe('PairingService', () => {
let pairingService: PairingService;
let mockDeviceRepo: jest.Mocked<DeviceRepository>;
let mockEventBus: jest.Mocked<EventBus>;
beforeEach(() => {
mockDeviceRepo = createMockDeviceRepository();
mockEventBus = createMockEventBus();
pairingService = new PairingService(mockDeviceRepo, mockEventBus);
});
it('should create pairing process successfully', async () => {
// Arrange
const mockDevice = createMockDevice();
mockDeviceRepo.getDevice.mockResolvedValue(mockDevice);
// Act
const result = await pairingService.createPairing();
// Assert
expect(result.success).toBe(true);
expect(mockEventBus.emit).toHaveBeenCalledWith('pairing:created');
});
});
```
#### **B. Tests de performance**
```typescript
// ✅ Tests de performance
describe('Performance Tests', () => {
it('should handle large data encoding within time limit', async () => {
const largeData = generateLargeData(1024 * 1024); // 1MB
const startTime = performance.now();
const result = await encodeDataAsync(largeData);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(5000); // 5 secondes max
expect(result).toBeDefined();
});
});
```
## 📈 **5. Métriques et Monitoring**
### **✅ Implémentation recommandée :**
#### **A. Métriques de performance**
```typescript
// ✅ Monitoring des performances
class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
recordMetric(name: string, value: number): void {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name)!.push(value);
}
getAverageMetric(name: string): number {
const values = this.metrics.get(name) || [];
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
getMetrics(): Record<string, number> {
const result: Record<string, number> = {};
for (const [name, values] of this.metrics) {
result[name] = this.getAverageMetric(name);
}
return result;
}
}
```
#### **B. Health checks**
```typescript
// ✅ Vérifications de santé
class HealthChecker {
async checkDatabase(): Promise<boolean> {
try {
await this.database.ping();
return true;
} catch {
return false;
}
}
async checkWebSocket(): Promise<boolean> {
return this.wsManager.isConnected();
}
async getHealthStatus(): Promise<HealthStatus> {
return {
database: await this.checkDatabase(),
websocket: await this.checkWebSocket(),
memory: this.getMemoryUsage(),
timestamp: new Date().toISOString()
};
}
}
```
## 🎯 **6. Plan d'implémentation prioritaire**
### **Phase 1 - Critique (1-2 semaines)**
1. **Sécurisation des données sensibles**
- Chiffrement des clés privées
- Sanitisation des logs
- Validation des entrées
2. **Gestion mémoire**
- Limitation des caches
- Nettoyage des event listeners
- Gestion des WebSockets
### **Phase 2 - Performance (2-3 semaines)**
1. **Architecture modulaire**
- Injection de dépendances
- Pattern Repository
- Séparation des responsabilités
2. **Optimisations**
- Encodage asynchrone
- Lazy loading
- Debouncing
### **Phase 3 - Qualité (3-4 semaines)**
1. **Tests**
- Tests unitaires
- Tests d'intégration
- Tests de performance
2. **Monitoring**
- Métriques de performance
- Health checks
- Alertes
## 📊 **7. Métriques de succès**
### **Objectifs quantifiables :**
- **Performance** : Temps de réponse < 200ms
- **Mémoire** : Utilisation < 100MB
- **Sécurité** : 0 vulnérabilité critique
- **Qualité** : Couverture de tests > 80%
- **Maintenabilité** : Complexité cyclomatique < 10
## 🚀 **8. Bénéfices attendus**
1. **Performance** : 3x plus rapide, 50% moins de mémoire
2. **Sécurité** : Protection des données sensibles
3. **Maintenabilité** : Code modulaire et testable
4. **Évolutivité** : Architecture extensible
5. **Fiabilité** : Moins de bugs, plus de stabilité
---
**Conclusion** : L'application a une base solide mais nécessite des améliorations significatives en architecture, performance et sécurité. Le plan proposé permettra de transformer l'application en une solution robuste et évolutive.

View File

@ -1,648 +0,0 @@
# Documentation de l'Initialisation IHM_CLIENT
## Vue d'ensemble
Le système IHM_CLIENT suit un processus d'initialisation en plusieurs étapes pour créer et sécuriser un wallet Bitcoin. Ce document détaille chaque étape du processus, depuis le choix du mode de sécurité jusqu'au pairing réussi et à la récupération des processus.
## Architecture des Stores IndexedDB
### Stores utilisés :
- **`pbkdf2keys`** : Stockage des clés PBKDF2 chiffrées par mode de sécurité
- **`wallet`** : Stockage du wallet chiffré (device + wallet data)
- **`credentials`** : Stockage des credentials de pairing (utilisé uniquement après pairing)
- **`env`** : Stockage des variables d'environnement internes (mots de passe, constantes)
- **`processes`** : Stockage des processus de communication
- **`labels`** : Stockage des labels de transactions
- **`shared_secrets`** : Stockage des secrets partagés
- **`unconfirmed_secrets`** : Stockage des secrets non confirmés
- **`diffs`** : Stockage des différences de synchronisation
- **`data`** : Stockage des données générales
> Note: The IndexedDB stores are provisioned from the shared `DATABASE_CONFIG` for both the main thread database helper and the `database.worker.js`, ensuring that `credentials` and `env` collections are always created before the pairing flow starts.
## Flux d'Initialisation Complet
### 1. Démarrage de l'Application
**Fichier :** `src/router.ts``checkStorageStateAndNavigate()`
L'application vérifie l'état du storage pour déterminer l'étape suivante :
```typescript
// Logique de progression :
// - Si pairing → account
// - Si date anniversaire → pairing
// - Si wallet → birthday-setup
// - Si pbkdf2 → wallet-setup
// - Sinon → security-setup
```
**États possibles :**
1. **Appareil appairé** → Redirection vers `account`
2. **Date anniversaire configurée** → Redirection vers `home` (pairing)
3. **Wallet existe sans date anniversaire** → Redirection vers `birthday-setup`
4. **Clé PBKDF2 existe** → Redirection vers `wallet-setup`
5. **Aucune configuration** → Redirection vers `security-setup`
### 2. Configuration du Mode de Sécurité
**Fichier :** `src/pages/security-setup/security-setup.ts`
#### 2.1 Sélection du Mode
L'utilisateur choisit parmi les modes disponibles :
| Mode | Nom | Description | Niveau de Sécurité | Clé de Chiffrement PBKDF2 | Stockage de la Clé de Chiffrement |
|------|-----|-------------|-------------------|---------------------------|-----------------------------------|
| `proton-pass` | Proton Pass | Authentification biométrique via Proton Pass | High | Clé WebAuthn générée par le navigateur | Stockée dans le navigateur (WebAuthn credential) |
| `os` | Authentificateur OS | Authentification biométrique du système | High | Clé WebAuthn générée par le système | Stockée dans le système d'exploitation |
| `otp` | OTP | Code à usage unique (Google Authenticator, etc.) | High | Aucune (clé PBKDF2 stockée en clair) | Secret OTP stocké dans l'application OTP |
| `password` | Mot de passe | Chiffrement par mot de passe (non sauvegardé) | Low | Mot de passe utilisateur | Stocké dans le gestionnaire de mots de passe du navigateur |
| `none` | Aucune sécurité | Chiffrement avec clé en dur (non recommandé) | Critical | Clé en dur `4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE` | Intégrée dans le code (non sécurisé) |
#### 2.2 Génération de la Clé PBKDF2
**Fichier :** `src/services/secure-credentials.service.ts``generatePBKDF2Key()`
Pour chaque mode, une clé PBKDF2 est générée et stockée différemment :
##### Mode `proton-pass` et `os` (WebAuthn)
```typescript
// Stockage avec WebAuthn (authentification biométrique)
await webAuthnService.storeKeyWithWebAuthn(pbkdf2Key, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` (ex: "proton-pass")
- **Valeur :** Clé PBKDF2 chiffrée avec WebAuthn
- **Authentification :** Biométrique (empreinte, visage, etc.)
##### Mode `otp`
```typescript
// Génération du secret OTP
const otpSecret = await this.generateOTPSecret();
// Stockage de la clé PBKDF2 en clair
await this.storePBKDF2KeyInStore(pbkdf2Key, securityMode);
// Affichage du QR code
this.displayOTPQRCode(otpSecret);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("otp")
- **Valeur :** Clé PBKDF2 en clair
- **Authentification :** Code OTP généré par l'application
##### Mode `password`
```typescript
// Demande du mot de passe utilisateur
const userPassword = await this.promptForPasswordWithBrowser();
// Chiffrement de la clé PBKDF2
const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword);
// Stockage chiffré
await this.storePBKDF2KeyInStore(encryptedKey, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("password")
- **Valeur :** Clé PBKDF2 chiffrée avec le mot de passe utilisateur
- **Authentification :** Mot de passe utilisateur (non sauvegardé)
##### Mode `none`
```typescript
// Clé de chiffrement en dur
const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE';
// Chiffrement avec la clé en dur
const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey);
// Stockage chiffré
await this.storePBKDF2KeyInStore(encryptedKeyNone, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("none")
- **Valeur :** Clé PBKDF2 chiffrée avec clé en dur
- **Authentification :** Aucune (non sécurisé)
### 3. Création du Wallet
**Fichier :** `src/pages/wallet-setup/wallet-setup.ts`
#### 3.1 Récupération de la Clé PBKDF2
Le système teste tous les modes de sécurité pour trouver la clé PBKDF2 valide :
```typescript
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
for (const mode of allSecurityModes) {
const hasKey = await secureCredentialsService.hasPBKDF2Key(mode);
if (hasKey) {
const key = await secureCredentialsService.retrievePBKDF2Key(mode);
if (key) {
currentMode = mode;
break;
}
}
}
```
#### 3.2 Génération des Clés du Wallet
```typescript
// Génération des clés temporaires
const walletData = {
scan_sk: encryptionService.generateRandomKey(),
spend_key: encryptionService.generateRandomKey(),
network: 'signet',
state: 'birthday_waiting',
created_at: new Date().toISOString()
};
```
#### 3.3 Création du Device via SDK
```typescript
// Création du device avec birthday = 0
const spAddress = await services.sdkClient.create_new_device(0, 'signet');
// Génération forcée du wallet
const wallet = await services.sdkClient.dump_wallet();
```
#### 3.4 Stockage Chiffré du Wallet
```typescript
// Chiffrement du device
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
// Chiffrement du wallet
const encryptedWallet = await encryptionService.encrypt(walletString, pbkdf2Key);
// Stockage dans le store wallet
const walletObject = {
pre_id: '1',
encrypted_device: encryptedDevice,
encrypted_wallet: encryptedWallet
};
```
**Store :** `wallet`
**Clé :** `pre_id` ("1")
**Valeur :** Objet contenant uniquement des données chiffrées
### 4. Configuration de la Date Anniversaire
**Fichier :** `src/pages/birthday-setup/birthday-setup.ts`
#### 4.1 Vérification des Prérequis
Avant de procéder, la page vérifie que tous les prérequis sont remplis :
```typescript
// Vérification de la clé PBKDF2 dans le store pbkdf2keys
const securityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
let pbkdf2KeyFound = false;
for (const mode of securityModes) {
const hasKey = await secureCredentials.hasPBKDF2Key(mode);
if (hasKey) {
const key = await secureCredentials.retrievePBKDF2Key(mode);
if (key) {
pbkdf2KeyFound = true;
break;
}
}
}
// Vérification du wallet en base de données (avec retry pour synchronisation)
let wallet = await services.getDeviceFromDatabase();
if (!wallet) {
// Retry jusqu'à 5 tentatives avec délai de 500ms
for (let attempt = 0; attempt < 5; attempt++) {
await new Promise(resolve => setTimeout(resolve, 500));
wallet = await services.getDeviceFromDatabase();
if (wallet) break;
}
}
// Vérification que le wallet contient les données attendues
if (wallet.sp_wallet && wallet.sp_wallet.birthday !== undefined) {
console.log('✅ Wallet found in database with birthday:', wallet.sp_wallet.birthday);
} else {
throw new Error('Wallet found but missing required data');
}
```
**Points importants :**
- Vérification réelle de la présence de la clé PBKDF2 dans le store `pbkdf2keys`
- Vérification réelle du wallet en base avec retry pour gérer les problèmes de synchronisation
- Validation que le wallet contient bien les données attendues (`sp_wallet`, `birthday`)
#### 4.2 Connexion aux Relais
```typescript
// Connexion aux relais Bitcoin
await services.connectAllRelays();
// Vérification que les relais sont connectés en vérifiant chain_tip
const currentBlockHeight = services.getCurrentBlockHeight();
if (currentBlockHeight !== -1) {
console.log('✅ Relays connected successfully, chain_tip:', currentBlockHeight);
} else {
throw new Error('Relays connected but chain_tip not set');
}
// Vérification que le handshake a été reçu
if (currentBlockHeight > 0) {
console.log('✅ Communication handshake completed, chain_tip:', currentBlockHeight);
} else {
throw new Error('Handshake not received or chain_tip not set');
}
```
**Vérifications réelles :**
- Vérification que `chain_tip` est défini (valeur != -1)
- Vérification que `chain_tip` est positif (indique que le handshake a été reçu)
#### 4.3 Mise à Jour de la Date Anniversaire
**Fichier :** `src/services/service.ts``updateDeviceBlockHeight()`
```typescript
// Mise à jour du birthday du device
await services.updateDeviceBlockHeight();
```
**Processus interne avec vérifications réelles :**
1. **Restauration en mémoire** :
```typescript
this.sdkClient.restore_device(device);
// Vérification que le device a été restauré en mémoire
const restoredDevice = this.dumpDeviceFromMemory();
if (restoredDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
console.log('✅ Device restored in memory with updated birthday:', device.sp_wallet.birthday);
} else {
throw new Error('Device restoration failed');
}
```
2. **Sauvegarde en base de données** :
```typescript
await this.saveDeviceInDatabase(device);
// Vérification que le device a été sauvegardé en base de données
const savedDevice = await this.getDeviceFromDatabase();
if (savedDevice?.sp_wallet?.birthday === device.sp_wallet.birthday) {
console.log('✅ Device saved to database with updated birthday:', device.sp_wallet.birthday);
} else {
throw new Error('Device save verification failed');
}
```
3. **Vérification du scan initial** :
```typescript
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
// Vérification que le scan est terminé en vérifiant last_scan
const deviceAfterScan = this.dumpDeviceFromMemory();
if (deviceAfterScan?.sp_wallet?.last_scan === this.currentBlockHeight) {
console.log('✅ Initial scan completed for new wallet');
} else {
console.warn('⚠️ Initial scan may not be complete');
}
```
4. **Vérification finale** :
```typescript
// Sauvegarde finale avec last_scan mis à jour
await this.saveDeviceInDatabase(device);
// Vérification que le device a été sauvegardé avec last_scan mis à jour
const finalDevice = await this.getDeviceFromDatabase();
if (finalDevice?.sp_wallet?.last_scan === this.currentBlockHeight) {
console.log('✅ New wallet initial scan completed and saved');
} else {
throw new Error('Final save verification failed');
}
```
**Vérification dans birthday-setup.ts :**
```typescript
// Vérifier que le birthday a bien été mis à jour en récupérant le wallet depuis la base
const updatedWallet = await services.getDeviceFromDatabase();
if (updatedWallet?.sp_wallet?.birthday && updatedWallet.sp_wallet.birthday > 0) {
console.log('✅ Birthday updated successfully:', updatedWallet.sp_wallet.birthday);
} else {
throw new Error('Birthday update verification failed');
}
```
#### 4.4 Sauvegarde du Device avec Vérification
**Fichier :** `src/services/service.ts``saveDeviceInDatabase()`
La méthode `saveDeviceInDatabase()` effectue maintenant une vérification réelle après la sauvegarde :
```typescript
// Sauvegarde du wallet chiffré
const putRequest = store.put(walletObject);
putRequest.onsuccess = () => {
console.log('✅ Device saved to database with encryption');
// La vérification se fera dans transaction.oncomplete
};
transaction.oncomplete = async () => {
// Vérifier que le wallet a bien été sauvegardé en le récupérant depuis la base
const verificationDb = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const verificationTx = verificationDb.transaction([walletStore], 'readonly');
const verifyRequest = verificationStore.get('1');
verifyRequest.onsuccess = () => {
const savedData = verifyRequest.result;
if (savedData && savedData.encrypted_device === encryptedDevice) {
console.log('✅ Verified: Device correctly saved in database');
resolve();
} else {
throw new Error('Device save verification failed');
}
};
};
```
**Points importants :**
- Vérification réelle après la transaction pour confirmer que les données sont bien sauvegardées
- Comparaison de `encrypted_device` pour s'assurer que les données sont correctes
- Logs de succès uniquement après vérification réelle
#### 4.5 Redirection vers la Synchronisation des Blocs
Après la mise à jour réussie du birthday, l'application redirige vers la page de synchronisation des blocs :
```typescript
// Redirection vers la page de synchronisation des blocs
window.location.href = '/src/pages/block-sync/block-sync.html';
```
**Page :** `src/pages/block-sync/block-sync.html`
- Interface dédiée pour la synchronisation des blocs
- Affichage de la progression de la synchronisation
- Gestion de la synchronisation initiale du wallet avec le réseau Bitcoin
### 5. Processus de Pairing
**Fichier :** `src/pages/home/home.ts``handleMainPairing()` et `src/utils/sp-address.utils.ts``prepareAndSendPairingTx()`
#### But et Objectif du Pairing
Le processus de pairing dans IHM_CLIENT sert à **créer une identité numérique vérifiable** qui permet :
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus permet de valider les actions critiques nécessitant plusieurs appareils appairés
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
4. **Système d'identité et de chiffrement** : Partage des secrets Silent Payment pour le chiffrement entre appareils appairés
#### Caractéristiques
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus peut inclure N appareils
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
#### 5.1 Vérification du Mode de Sécurité
```typescript
const currentMode = await securityModeService.getCurrentMode();
if (!currentMode) {
// Redirection vers security-setup si aucun mode n'est configuré
window.location.href = '/src/pages/security-setup/security-setup.html';
return;
}
```
#### 5.2 Authentification selon le Mode
##### Mode `proton-pass` et `os`
```typescript
// Authentification WebAuthn
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [{
id: credentialId,
type: 'public-key'
}]
}
});
```
##### Mode `otp`
```typescript
// Demande du code OTP
const otpCode = await this.promptForOTPCode();
// Vérification du code OTP
const isValid = await this.verifyOTPCode(otpCode, otpSecret);
```
##### Mode `password`
```typescript
// Demande du mot de passe
const password = await this.promptForPassword();
// Déchiffrement de la clé PBKDF2
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, password);
```
##### Mode `none`
```typescript
// Déchiffrement avec la clé en dur
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, hardcodedKey);
```
#### 5.3 Génération des Credentials de Pairing
```typescript
// Génération des credentials sécurisés (mot de passe récupéré depuis le store env)
const credentialData = await secureCredentialsService.generateSecureCredentials('');
// Stockage des credentials dans le store credentials
await secureCredentialsService.storeCredentials(credentialData, '');
// Récupération et déchiffrement des credentials (mot de passe récupéré depuis le store env)
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
```
**Store `env` :**
- **Clé :** `CREDENTIALS_PASSWORD`
- **Valeur :** `4nk-secure-password` (mot de passe interne pour les credentials)
- **Description :** Mot de passe interne pour la génération des credentials de pairing
#### 5.4 Création du Processus de Pairing
```typescript
// Création du processus via le SDK
const pairingResult = await services.createPairingProcess({
spendKey: retrievedCredentials.spendKey,
scanKey: retrievedCredentials.scanKey
});
```
### 6. Récupération des Processus
**Fichier :** `src/services/service.ts``restoreProcessesFromDB()`
#### 6.1 Synchronisation des Processus
```typescript
// Récupération des processus depuis la base de données
const processes = await processRepo.getAllProcesses();
// Synchronisation avec le réseau
for (const process of processes) {
await services.syncProcess(process);
}
```
#### 6.2 Mise à Jour de l'État de Pairing
```typescript
// Vérification de l'état de pairing
const isPaired = services.isPaired();
if (isPaired) {
// Redirection vers la page account
await navigate('account');
}
```
## Diagramme de Flux
```mermaid
graph TD
A[Démarrage Application] --> B{Vérification État Storage}
B -->|Aucune config| C[Security Setup]
B -->|PBKDF2 existe| D[Wallet Setup]
B -->|Wallet existe| E[Birthday Setup]
B -->|Birthday configuré| F[Block Sync]
B -->|Appareil appairé| G[Account]
C --> C1[Sélection Mode Sécurité]
C1 --> C2[Génération Clé PBKDF2]
C2 --> C3[Stockage selon Mode]
C3 --> D
D --> D1[Récupération Clé PBKDF2]
D1 --> D2[Création Device SDK]
D2 --> D3[Génération Wallet]
D3 --> D4[Stockage Chiffré avec Vérification]
D4 --> E
E --> E1[Vérification Prérequis]
E1 --> E2{Prérequis OK?}
E2 -->|Non| E3[Redirection vers Setup Précédent]
E2 -->|Oui| E4[Connexion Relais]
E4 --> E5[Vérification Handshake]
E5 --> E6[Mise à Jour Birthday]
E6 --> E7[Vérification Sauvegarde]
E7 --> E8[Vérification Birthday]
E8 --> F
F --> F1[Synchronisation Blocs]
F1 --> F2[Initialisation Services]
F2 --> F3[Scan des Blocs]
F3 --> F4[Mise à Jour last_scan]
F4 --> G
G --> G1[Pairing]
G1 --> G2[Authentification Mode]
G2 --> G3[Génération Credentials]
G3 --> G4[Création Processus Pairing]
G4 --> G5[Récupération Processus]
```
## Sécurité par Mode
### Mode `proton-pass` et `os`
- **Stockage :** Clé PBKDF2 chiffrée avec WebAuthn
- **Authentification :** Biométrique (empreinte, visage)
- **Sécurité :** Élevée (clé matérielle)
### Mode `otp`
- **Stockage :** Clé PBKDF2 en clair
- **Authentification :** Code OTP temporaire
- **Sécurité :** Élevée (authentification à deux facteurs)
### Mode `password`
- **Stockage :** Clé PBKDF2 chiffrée avec mot de passe utilisateur
- **Authentification :** Mot de passe utilisateur
- **Sécurité :** Faible (dépend de la force du mot de passe)
### Mode `none`
- **Stockage :** Clé PBKDF2 chiffrée avec clé en dur
- **Authentification :** Aucune
- **Sécurité :** Critique (non recommandé)
## Gestion des Erreurs
### Erreurs de Chiffrement
- **Clé PBKDF2 introuvable** → Redirection vers `security-setup`
- **Échec de déchiffrement** → Demande de réauthentification
- **Wallet corrompu** → Recréation du wallet
### Erreurs de Réseau
- **Connexion relais échouée** → Retry automatique
- **Synchronisation échouée** → Mode hors ligne
- **Pairing échoué** → Nouvelle tentative
### Erreurs d'Authentification
- **WebAuthn échoué** → Fallback vers autre mode
- **OTP invalide** → Nouvelle demande
- **Mot de passe incorrect** → Nouvelle tentative
### Erreurs de Vérification
- **Vérification des prérequis échouée** → Redirection vers l'étape appropriée
- **Vérification de sauvegarde échouée** → Retry de la sauvegarde avec logs détaillés
- **Vérification de restauration échouée** → Retry de la restauration avec logs détaillés
- **Vérification de handshake échouée** → Retry de la connexion avec logs détaillés
## Système de Vérification Réelle des Logs
Tous les logs de succès sont maintenant émis uniquement après vérification réelle des résultats. Cela permet de :
1. **Détecter les échecs silencieux** : Les opérations qui échouent sans erreur sont détectées par les vérifications
2. **Avoir des logs fiables** : Les logs reflètent la réalité et non juste des déclarations
3. **Faciliter le diagnostic** : Les logs indiquent précisément où et pourquoi un processus échoue
### Vérifications Implémentées
#### Dans `birthday-setup.ts`
- ✅ Vérification réelle de la présence de la clé PBKDF2 dans le store `pbkdf2keys`
- ✅ Vérification réelle du wallet en base avec retry pour gérer les problèmes de synchronisation
- ✅ Validation que le wallet contient bien les données attendues (`sp_wallet`, `birthday`)
- ✅ Vérification que les relais sont connectés en vérifiant `chain_tip`
- ✅ Vérification que le handshake a été reçu (`chain_tip > 0`)
- ✅ Vérification que le birthday a bien été mis à jour en récupérant le wallet depuis la base
#### Dans `updateDeviceBlockHeight()`
- ✅ Vérification que le device est restauré en mémoire en comparant le birthday
- ✅ Vérification que le device est sauvegardé en base en le récupérant après l'opération
- ✅ Vérification que le scan est terminé en vérifiant `last_scan`
- ✅ Vérification que la sauvegarde finale est réussie
#### Dans `saveDeviceInDatabase()`
- ✅ Vérification que le wallet est bien sauvegardé en le récupérant depuis la base après la transaction
- ✅ Comparaison de `encrypted_device` pour confirmer que les données sont correctes
- ✅ Logs de succès uniquement après vérification réelle
### Avantages
- **Fiabilité** : Les logs reflètent la réalité et non juste des déclarations
- **Diagnostic** : Facilite le diagnostic en cas de problème
- **Détection** : Détecte les échecs silencieux qui passeraient inaperçus
- **Traçabilité** : Chaque étape est vérifiée et tracée avec des logs détaillés
## Points d'Attention
1. **Ordre des modes testés** : `['none', 'otp', 'password', 'os', 'proton-pass']`
2. **Store `credentials`** : Utilisé uniquement après pairing
3. **Clé PBKDF2** : Toujours stockée dans `pbkdf2keys` avec `security_mode` comme clé
4. **Wallet** : Toujours stocké chiffré dans le store `wallet`
5. **Redirection automatique** : 3 secondes après création du wallet vers `birthday-setup`
6. **Vérifications réelles** : Tous les logs de succès sont émis uniquement après vérification réelle des résultats
7. **Block Sync** : Nouvelle page intermédiaire entre `birthday-setup` et `pairing` pour la synchronisation des blocs
8. **Prérequis** : Chaque page vérifie ses prérequis en base de données avant de procéder
9. **Synchronisation IndexedDB** : Utilisation directe d'IndexedDB pour éviter les problèmes de synchronisation avec le service worker
10. **Retry automatique** : Retry automatique jusqu'à 5 tentatives avec délai de 500ms pour les vérifications de wallet en base
Cette documentation couvre l'ensemble du processus d'initialisation du système IHM_CLIENT, depuis la configuration de sécurité jusqu'au pairing réussi et à la récupération des processus.

View File

@ -1,220 +0,0 @@
# 4NK Integration Guide
## 🎯 Modes d'utilisation
Le site 4NK peut être utilisé de deux façons :
### 1. **Mode Normal** (Site autonome)
- **URL** : http://localhost:3004
- **Interface** : Header complet + navigation normale
- **Utilisation** : Application standalone
- **Fonctionnalités** : Toutes les fonctionnalités disponibles
### 2. **Mode Iframe** (Intégration externe)
- **URL** : http://localhost:3004 (détection automatique)
- **Interface** : Header masqué + menu intégré dans le contenu
- **Utilisation** : Intégration dans un site externe
- **Fonctionnalités** : Communication bidirectionnelle avec le parent
## 🔧 Détection automatique
Le site détecte automatiquement s'il est chargé dans une iframe :
```javascript
// Détection iframe
if (window.parent !== window) {
// Mode iframe activé
document.body.classList.add('iframe-mode');
// Header masqué automatiquement
}
```
## 📱 Interface adaptative
### Mode Normal
```
┌─────────────────────────────────────┐
│ Header (Navigation, Logo, etc.) │
├─────────────────────────────────────┤
│ Contenu principal │
│ ├── Titre et description │
│ ├── Interface de pairing │
│ └── Boutons d'action │
└─────────────────────────────────────┘
```
### Mode Iframe
```
┌─────────────────────────────────────┐
│ Contenu principal (sans header) │
│ ├── Titre et description │
│ ├── Menu intégré (Home, Account...) │
│ ├── Interface de pairing │
│ └── Boutons d'action │
└─────────────────────────────────────┘
```
## 🔄 Communication iframe
### Messages envoyés au parent
- `IFRAME_READY` : Iframe initialisé
- `MENU_NAVIGATION` : Navigation du menu
- `PAIRING_4WORDS_WORDS_GENERATED` : 4 mots générés
- `PAIRING_4WORDS_STATUS_UPDATE` : Mise à jour du statut
- `PAIRING_4WORDS_SUCCESS` : Pairing réussi
- `PAIRING_4WORDS_ERROR` : Erreur de pairing
- `TEST_RESPONSE` : Réponse à un message de test
- `LISTENING` : Notification que l'iframe écoute les messages
### Messages reçus du parent
- `TEST_MESSAGE` : Test de communication
- `PAIRING_4WORDS_CREATE` : Créer un pairing
- `PAIRING_4WORDS_JOIN` : Rejoindre avec 4 mots
- `LISTENING` : Notification que le parent écoute les messages
- `IFRAME_READY` : Notification que l'iframe est prête (envoyée par l'iframe elle-même)
## 🧪 Tests d'intégration
### Test rapide
```bash
# Ouvrir dans le navigateur
open examples/test-integration.html
```
### Test complet
```bash
# Site externe d'exemple
open examples/external-site.html
```
## 🎨 Styles CSS
Les styles s'adaptent automatiquement :
```css
/* Styles normaux */
.title-container { /* ... */ }
/* Styles iframe */
.iframe-mode .content-menu { /* ... */ }
.iframe-mode .menu-btn { /* ... */ }
```
## 🚀 Utilisation en production
### 1. Site autonome
```html
<!-- Utilisation normale -->
<iframe src="https://your-4nk-site.com" width="100%" height="600px"></iframe>
```
### 1.1 Relai WebSocket
- Relai principal exposé en `wss://relay235.4nkweb.com/ws/`
- Terminaison TLS gérée par `nginx.relay235.conf` (reverse proxy vers le service local sur `127.0.0.1:8091`)
- Variables denvironnement cliente à utiliser : `VITE_BOOTSTRAPURL=wss://relay235.4nkweb.com/ws/`
### 2. Intégration personnalisée
```html
<!-- Site externe -->
<div id="4nk-container">
<iframe
src="https://your-4nk-site.com"
sandbox="allow-scripts allow-same-origin allow-forms"
onload="init4NKIntegration(this)">
</iframe>
</div>
<script>
function init4NKIntegration(iframe) {
// Écouter les messages de l'iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://your-4nk-site.com') return;
const { type, data } = event.data;
switch (type) {
case 'IFRAME_READY':
console.log('4NK iframe ready');
break;
case 'PAIRING_4WORDS_SUCCESS':
console.log('Pairing successful:', data.message);
break;
}
});
// Envoyer des commandes à l'iframe
function createPairing() {
iframe.contentWindow.postMessage({
type: 'PAIRING_4WORDS_CREATE',
data: {}
}, 'https://your-4nk-site.com');
}
}
</script>
```
## 🔒 Sécurité
### Vérification d'origine
```javascript
// Toujours vérifier l'origine des messages
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-4nk-site.com') {
return; // Ignorer les messages non autorisés
}
// Traiter le message
});
```
### Sandbox iframe
```html
<iframe
src="https://your-4nk-site.com"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="clipboard-write">
</iframe>
```
## 📊 Monitoring
### Logs de communication
```javascript
// Activer les logs détaillés
window.DEBUG_IFRAME = true;
// Écouter tous les messages
window.addEventListener('message', (event) => {
console.log('📨 Message received:', {
origin: event.origin,
type: event.data.type,
data: event.data.data
});
});
```
## 🐛 Dépannage
### Problèmes courants
1. **Iframe ne se charge pas**
- Vérifier les paramètres CORS
- Vérifier l'URL de l'iframe
- Vérifier les paramètres sandbox
2. **Messages non reçus**
- Vérifier la vérification d'origine
- Vérifier le format des messages
- Vérifier la console pour les erreurs
3. **Styles cassés**
- Vérifier la classe `iframe-mode`
- Vérifier les styles CSS conditionnels
- Vérifier la détection d'iframe
### Debug mode
```javascript
// Activer le mode debug
localStorage.setItem('4nk-debug', 'true');
// Voir les logs détaillés
console.log('4NK Debug Mode:', localStorage.getItem('4nk-debug'));
```

View File

@ -1,256 +0,0 @@
# Guide des bonnes pratiques de logging
## Vue d'ensemble
Ce document décrit les bonnes pratiques pour l'utilisation des logs dans l'application 4NK. Nous utilisons un système de logging centralisé avec `secureLogger` pour assurer la cohérence et la sécurité.
## Système de logging
### SecureLogger
Le `secureLogger` est le système de logging principal de l'application. Il fournit :
- **Sanitisation automatique** des données sensibles
- **Niveaux de log structurés** (DEBUG, INFO, WARN, ERROR)
- **Contexte enrichi** avec composant et métadonnées
- **Formatage cohérent** des messages
### Import
```typescript
import { secureLogger } from '../services/secure-logger';
```
## Niveaux de log
### DEBUG
Utilisé pour les informations de débogage détaillées, généralement utiles uniquement lors du développement.
```typescript
secureLogger.debug('Memory usage after cleanup: 45.2%', { component: 'Service' });
secureLogger.debug('Checking credentials availability', { component: 'HomePage' });
```
**Quand utiliser :**
- Informations de débogage
- État interne des variables
- Progression des opérations complexes
- Messages avec emoji 🔍
### INFO
Utilisé pour les informations générales sur le fonctionnement de l'application.
```typescript
secureLogger.info('Home/Pairing page loaded', { component: 'HomePage' });
secureLogger.info('Services initialized successfully', { component: 'Service' });
```
**Quand utiliser :**
- Initialisation de composants
- Succès d'opérations
- Messages avec emoji ✅, 🔄, 🚀
- Événements importants du flux utilisateur
### WARN
Utilisé pour les avertissements qui n'empêchent pas le fonctionnement mais méritent attention.
```typescript
secureLogger.warn('High memory detected, performing cleanup', { component: 'Service' });
secureLogger.warn('Home page already initializing, skipping...', { component: 'HomePage' });
```
**Quand utiliser :**
- Conditions non critiques mais inhabituelles
- Messages avec emoji ⚠️
- Opérations de récupération
- Dégradations de performance
### ERROR
Utilisé pour les erreurs qui empêchent le fonctionnement normal.
```typescript
secureLogger.error('Failed to initialize services', error, { component: 'Service' });
secureLogger.error('Authentication failed', error, { component: 'HomePage' });
```
**Quand utiliser :**
- Erreurs critiques
- Messages avec emoji ❌
- Échecs d'opérations importantes
- Exceptions non gérées
## Contexte et métadonnées
### Composant
Toujours spécifier le composant dans le contexte :
```typescript
secureLogger.info('Operation completed', { component: 'HomePage' });
secureLogger.error('Database connection failed', error, { component: 'Service' });
```
### Métadonnées supplémentaires
Ajouter des métadonnées utiles pour le débogage :
```typescript
secureLogger.debug('Wallet details', {
component: 'HomePage',
hasSpendKey: !!wallet.sp_wallet?.spend_key,
hasScanKey: !!wallet.sp_wallet?.scan_key,
birthday: wallet.sp_wallet?.birthday
});
```
## Bonnes pratiques
### 1. Utiliser secureLogger au lieu de console.*
❌ **Mauvais :**
```typescript
console.log('User authenticated');
console.error('Database error:', error);
```
✅ **Bon :**
```typescript
secureLogger.info('User authenticated', { component: 'AuthService' });
secureLogger.error('Database error', error, { component: 'DatabaseService' });
```
### 2. Messages clairs et concis
❌ **Mauvais :**
```typescript
secureLogger.info('x', { component: 'Service' });
secureLogger.info('Processing user request with id 12345 and data {name: "John", email: "john@example.com"}', { component: 'UserService' });
```
✅ **Bon :**
```typescript
secureLogger.info('Processing user request', {
component: 'UserService',
userId: 12345,
userName: 'John'
});
```
### 3. Niveaux appropriés
❌ **Mauvais :**
```typescript
secureLogger.error('User clicked button'); // Pas une erreur
secureLogger.info('Critical system failure'); // Pas une info
```
✅ **Bon :**
```typescript
secureLogger.debug('User clicked button', { component: 'UI' });
secureLogger.error('Critical system failure', error, { component: 'System' });
```
### 4. Contexte enrichi
❌ **Mauvais :**
```typescript
secureLogger.info('Operation failed');
```
✅ **Bon :**
```typescript
secureLogger.error('Operation failed', error, {
component: 'PaymentService',
operation: 'processPayment',
userId: user.id,
amount: payment.amount
});
```
### 5. Gestion des erreurs
```typescript
try {
await riskyOperation();
secureLogger.info('Operation completed successfully', { component: 'Service' });
} catch (error) {
secureLogger.error('Operation failed', error, {
component: 'Service',
operation: 'riskyOperation'
});
throw error;
}
```
## Patterns d'emojis
### DEBUG (🔍)
- `🔍 Checking...`
- `🔍 Debug info:`
- `🔍 Memory usage:`
### INFO (✅, 🔄, 🚀)
- `✅ Operation completed`
- `🔄 Processing...`
- `🚀 Starting...`
- `🔧 Initializing...`
### WARN (⚠️)
- `⚠️ Warning:`
- `⚠️ Skipping...`
- `⚠️ High memory detected`
### ERROR (❌)
- `❌ Error:`
- `❌ Failed to:`
- `❌ Critical error:`
## Exemples par composant
### Service
```typescript
secureLogger.info('Service initialized', { component: 'Service' });
secureLogger.debug('Memory usage: 45.2%', { component: 'Service' });
secureLogger.warn('High memory detected', { component: 'Service' });
secureLogger.error('Service initialization failed', error, { component: 'Service' });
```
### HomePage
```typescript
secureLogger.info('Home page loaded', { component: 'HomePage' });
secureLogger.info('Prerequisites verified', { component: 'HomePage' });
secureLogger.warn('Already initializing, skipping', { component: 'HomePage' });
secureLogger.error('Page initialization failed', error, { component: 'HomePage' });
```
### PairingPage
```typescript
secureLogger.info('Pairing page loaded', { component: 'PairingPage' });
secureLogger.info('Pairing process started', { component: 'PairingPage' });
secureLogger.warn('Pairing already in progress', { component: 'PairingPage' });
secureLogger.error('Pairing failed', error, { component: 'PairingPage' });
```
## Outils de correction
Un script automatique est disponible pour corriger les logs existants :
```bash
node fix-logs.cjs
```
Ce script :
- Remplace `console.*` par `secureLogger.*`
- Ajoute les imports nécessaires
- Détermine automatiquement les niveaux appropriés
- Ajoute le contexte de composant
## Vérification
Pour vérifier que tous les logs utilisent le système centralisé :
```bash
grep -r "console\.\(log\|info\|warn\|error\|debug\)" src/
```
Cette commande ne devrait retourner aucun résultat si tous les logs sont correctement migrés.

View File

@ -1,302 +0,0 @@
# Analyse du Système de Pairing - Version Actuelle
## Vue d'ensemble
Ce document résume l'analyse complète du système de pairing et les corrections apportées pour résoudre les problèmes identifiés.
## But et Objectif du Pairing
Le système de pairing dans 4NK sert avant tout à **créer une identité numérique vérifiable** qui permet :
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus de pairing permet de valider les actions critiques nécessitant plusieurs appareils appairés
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
4. **Système d'identité et de chiffrement** : Le wallet est avant tout un système d'identité et de chiffrement grâce aux secrets partagés du Silent Payment
### Caractéristiques principales
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus de pairing peut inclure N appareils
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
### Différence avec les autres fonctionnalités
- **Backup du wallet** : Géré via les 4 mots dans les autres pages (permet de contrôler le processus pour auto-update)
- **Support** : Géré via les 4 mots permettant un contrôle du processus
- **Partage de secrets** : Les secrets Silent Payment sont partagés entre appareils appairés pour le chiffrement
## Problèmes Identifiés et Solutions
### 1. Problème de `checkConnections` pour le Pairing
**Problème** : La méthode `checkConnections` a été mise à jour il y a un mois pour prendre un `Process` et un `stateId` au lieu d'une liste de membres, mais la gestion des processus de pairing était défaillante.
**Symptômes** :
- `checkConnections` échouait pour les processus de pairing
- Les adresses des membres n'étaient pas correctement récupérées
- Erreur "Not a pairing process" même pour des processus de pairing valides
**Solution Appliquée** :
```typescript
// Correction dans checkConnections pour gérer les pairedAddresses
if (members.size === 0) {
// This must be a pairing process
let publicData: Record<string, any> | null = null;
if (!stateId) {
publicData = process.states[process.states.length - 2]?.public_data;
} else {
publicData = process.states.find(state => state.state_id === stateId)?.public_data || null;
}
// If pairedAddresses is not in the current state, look in previous states
if (!publicData || !publicData['pairedAddresses']) {
// Look for pairedAddresses in previous states
for (let i = process.states.length - 1; i >= 0; i--) {
const state = process.states[i];
if (state.public_data && state.public_data['pairedAddresses']) {
publicData = state.public_data;
break;
}
}
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
members.add({ sp_addresses: decodedAddresses });
}
```
### 2. Problème de `confirmPairing` avec `getPairingProcessId()`
**Problème** : `confirmPairing` échouait car `getPairingProcessId()` utilisait `sdkClient.get_pairing_process_id()` qui n'était pas encore disponible car le processus de pairing n'était pas encore committé.
**Symptômes** :
- Erreur "Failed to get pairing process" dans `confirmPairing`
- Le SDK n'avait pas encore le processus de pairing disponible
- Échec de confirmation du pairing
**Solution Appliquée** :
```typescript
public async confirmPairing(pairingId?: string) {
try {
console.log('confirmPairing');
let processId: string;
if (pairingId) {
processId = pairingId;
console.log('pairingId (provided):', processId);
} else if (this.processId) {
processId = this.processId;
console.log('pairingId (from stored processId):', processId);
} else {
// Try to get pairing process ID, with retry if it fails
let retries = 3;
while (retries > 0) {
try {
processId = this.getPairingProcessId();
console.log('pairingId (from SDK):', processId);
break;
} catch (e) {
retries--;
if (retries === 0) throw e;
console.log(`Failed to get pairing process ID, retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
// ... rest of the method
} catch (e) {
console.error('Failed to confirm pairing');
return;
}
}
```
### 3. Problème de `pairing_process_commitment` à `null`
**Problème** : Le `pairing_process_commitment` restait à `null` dans le device dump car le device n'était pas synchronisé avec l'état committé du processus.
**Symptômes** :
- `pairing_process_commitment: null` dans le device dump
- Le commitment n'était pas synchronisé avec l'état committé du processus
- Échec de la confirmation du pairing
**Solution Appliquée** :
```typescript
// Intégration de updateDevice() dans waitForPairingCommitment
public async waitForPairingCommitment(processId: string, maxRetries: number = 10, retryDelay: number = 1000): Promise<void> {
console.log(`Waiting for pairing process ${processId} to be committed...`);
// First, try to update the device to sync with the committed state
try {
await this.updateDevice();
console.log('Device updated, checking commitment...');
} catch (e) {
console.log('Failed to update device, continuing with polling...', e);
}
for (let i = 0; i < maxRetries; i++) {
try {
const device = this.dumpDeviceFromMemory();
console.log(`Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =`, device.pairing_process_commitment);
// Check if the commitment is set and not null/empty
if (device.pairing_process_commitment &&
device.pairing_process_commitment !== null &&
device.pairing_process_commitment !== '') {
console.log('Pairing process commitment found:', device.pairing_process_commitment);
return;
}
} catch (e) {
console.log(`Attempt ${i + 1}/${maxRetries}: Device not ready yet - ${e}`);
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
throw new Error(`Pairing process ${processId} was not committed after ${maxRetries} attempts`);
}
```
Et simplification du router :
```typescript
console.log("⏳ Waiting for pairing process to be committed...");
await services.waitForPairingCommitment(pairingId);
console.log("🔁 Confirming pairing...");
await services.confirmPairing(pairingId);
```
## Architecture du Système de Pairing
### Flux de Création du Pairing (Créateur)
1. **Création du processus** : `createPairingProcess("", [myAddress])`
2. **Enregistrement du device** : `pairDevice(pairingId, [myAddress])`
3. **Traitement de l'API** : `handleApiReturn(createPairingProcessReturn)`
4. **Création de la mise à jour PRD** : `createPrdUpdate(pairingId, stateId)`
5. **Approbation du changement** : `approveChange(pairingId, stateId)`
6. **Attente du commit avec synchronisation** : `waitForPairingCommitment(pairingId)` (inclut `updateDevice()`)
7. **Confirmation du pairing** : `confirmPairing(pairingId)`
### Flux de Rejoindre le Pairing (Joiner) - ⚠️ INCOHÉRENT
**Problème identifié** : Le joiner n'a pas de flux de confirmation complet.
**Flux actuel (incomplet)** :
1. **Création avec liste vide** : `createPairingProcess("", [])`
2. **Établissement des connexions** : `checkConnections(process)`
3. **Pas de confirmation** : Aucun `waitForPairingCommitment` ou `confirmPairing`
**Flux attendu (cohérent)** :
1. **Récupération du processus existant** : `getPairingProcessId()`
2. **Rejoindre le processus** : Pas de création, mais participation au processus existant
3. **Flux de confirmation complet** : Même flux que le créateur
4. **Attente du commit** : `waitForPairingCommitment()`
5. **Confirmation du pairing** : `confirmPairing()`
### Gestion des Connexions
La méthode `checkConnections` gère maintenant :
- **Processus normaux** : Utilise les rôles pour trouver les membres
- **Processus de pairing** : Utilise `pairedAddresses` des données publiques
- **Recherche dans les états précédents** : Si `pairedAddresses` n'est pas dans l'état actuel
- **Décodage des adresses** : Les données publiques sont encodées et nécessitent un décodage
## Points Clés Appris
### 1. Encodage des Données Publiques
- Les données publiques sont encodées avec `this.sdkClient.encode_json()`
- `pairedAddresses` nécessite un décodage avec `this.decodeValue()`
- Les données ne sont pas directement utilisables sans décodage
### 2. Gestion Multi-Hosts
- Le créateur et le joiner peuvent être sur des hosts différents
- Le joiner doit récupérer les adresses depuis le processus existant
- `this.processId` n'est disponible que sur le même host
### 3. Synchronisation du SDK
- Le SDK n'a pas immédiatement le processus de pairing disponible
- Il faut attendre que le processus soit committé
- `updateDevice()` est nécessaire pour synchroniser l'état
### 4. Gestion des États
- Les processus de pairing peuvent avoir des mises à jour partielles
- Il faut chercher `pairedAddresses` dans les états précédents si nécessaire
- La logique de fallback est cruciale pour la robustesse
## Version Actuelle
### État des Corrections
- ✅ `checkConnections` corrigé pour les processus de pairing
- ✅ `confirmPairing` avec gestion des paramètres et retry
- ✅ `waitForPairingCommitment` avec synchronisation automatique du device
- ✅ Intégration de `updateDevice()` dans `waitForPairingCommitment`
- ✅ Gestion des cas multi-hosts
- ✅ Simplification du flux de création
- ✅ **Flux du joiner implémenté** : Découverte et rejoindre un processus existant
- ✅ **Détection automatique** : Créateur vs Joiner via paramètre URL
### Fonctionnalités Opérationnelles
- **Création de pairing** : ✅ Fonctionne avec les adresses correctes
- **Rejoindre un pairing** : ✅ Flux complet avec découverte et synchronisation
- **Établissement des connexions** : ✅ `checkConnections` trouve les membres
- **Confirmation du pairing** : ✅ Côté créateur et joiner
- **Synchronisation du commitment** : ✅ Côté créateur et joiner
- **Flux simplifié** : ✅ Côté créateur et joiner
### Flux Unifié Créateur vs Joiner
#### Flux du Créateur
1. **Création** : `createPairingProcess()` avec son adresse
2. **QR Code** : `generateQRCode()` pour le joiner
3. **Attente** : `waitForJoinerAndUpdateProcess()` pour détecter le joiner
4. **Synchronisation** : `waitForPairingCommitment()`
5. **Confirmation** : `confirmPairing()`
#### Flux du Joiner
1. **Découverte** : `discoverAndJoinPairingProcess()` via QR code
2. **Synchronisation** : `waitForPairingCommitment()`
3. **Confirmation** : `confirmPairing()`
#### Détection Automatique
- **Créateur** : Pas de paramètre `sp_address` dans l'URL
- **Joiner** : Paramètre `sp_address` présent dans l'URL
- **Logique** : `onCreateButtonClick()` détecte automatiquement le flux
### Améliorations Récentes
#### Synchronisation Automatique du Device
- **Intégration de `updateDevice()`** : Appelé automatiquement dans `waitForPairingCommitment`
- **Gestion des erreurs** : Continue le polling même si `updateDevice()` échoue
- **Logs détaillés** : Suivi complet du processus de synchronisation
- **Temps d'attente augmenté** : 30 tentatives × 2 secondes = 60 secondes max
#### Simplification du Flux
- **Moins d'étapes manuelles** : `updateDevice()` intégré dans `waitForPairingCommitment`
- **Flux plus robuste** : Gestion automatique de la synchronisation
- **Code plus maintenable** : Logique centralisée dans une seule méthode
### Points d'Attention
- Le système nécessite que les deux côtés soient synchronisés
- Les retry automatiques sont implémentés pour la robustesse
- La gestion des erreurs est améliorée avec des logs détaillés
- Le flux est maintenant plus prévisible et fiable
- La synchronisation du device est automatique et robuste
## Recommandations
### Améliorations Futures
1. **Tests automatisés** : Implémenter des tests unitaires et d'intégration pour valider le pairing
2. **Monitoring** : Ajouter des métriques pour surveiller les performances du pairing
3. **Documentation** : Maintenir cette documentation à jour avec les évolutions
4. **Optimisation** : Analyser et optimiser les délais de retry si nécessaire
### Tests et Monitoring
1. **Tests** : Tester le pairing entre différents hosts avec les deux flux
2. **Monitoring** : Surveiller les logs pour identifier les problèmes potentiels
3. **Performance** : Optimiser les délais de retry si nécessaire
4. **Documentation** : Maintenir cette documentation à jour avec les évolutions
Cette analyse fournit une base solide pour comprendre et maintenir le système de pairing. Les corrections majeures ont été implémentées et le système est maintenant opérationnel avec un flux unifié pour le créateur et le joiner.

View File

@ -1,530 +0,0 @@
# Architecture du Système de Processus et Updates
## Vue d'ensemble
Le système de processus est un **système générique et réutilisable** pour créer des "contrats" entre des membres avec des niveaux d'accès différents aux champs de données. C'est la fondation qui permet d'implémenter des fonctionnalités comme le pairing, mais aussi n'importe quel autre type de contrat décentralisé.
## Concepts Fondamentaux
### 1. Processus (Process)
Un **processus** est un contrat décentralisé entre plusieurs membres, commité sur la blockchain. Il représente un accord ou une entité partagée avec :
- **Identifiant unique** : `process_id`
- **États successifs** : Historique des modifications
- **Membres** : Participants au processus
- **Rôles et permissions** : Définition des accès
### 2. État (State)
Chaque processus contient une liste d'**états** représentant l'évolution du processus dans le temps. Chaque état contient :
#### Données Publiques (`public_data`)
- **Accessibles à tous** les membres du processus
- **Inchangées** dans tous les nouveaux états (portées automatiquement)
- **Encodées** mais non chiffrées (encodage JSON/Binary)
- **Exemple** : Nom du processus, adresses appairées, métadonnées
#### Données Privées (via `pcd_commitment`)
- **Chiffrées** et accessibles uniquement aux membres autorisés
- **Commitment** : Hash des données privées (`pcd_commitment[field]`)
- **Clés de déchiffrement** : Stockées dans `state.keys[field]` pour chaque membre autorisé
- **Exemple** : Secrets, clés privées, données sensibles
#### Rôles (`roles`)
- **Définition des permissions** par rôle
- **Validation rules** : Quorum et champs accessibles par rôle
- **Membres** : Liste des IDs de pairing process pour chaque rôle
#### Métadonnées d'État
- `state_id` : Identifiant unique de l'état
- `validation_tokens` : Tokens nécessaires pour la validation
- `validation_result` : Résultat de la validation
### 3. Rôles et Permissions (`RoleDefinition`)
Un rôle définit qui peut accéder à quels champs et comment :
```typescript
interface RoleDefinition {
members: string[]; // IDs de pairing process (identifiants des membres)
validation_rules: ValidationRule[]; // Règles de validation
}
interface ValidationRule {
quorum: number; // Quorum requis (ex: 1.0 = tous, 0.5 = 50%)
fields: string[]; // Champs accessibles pour ce rôle
}
```
**Exemples de rôles** :
- **Administrateur** : Quorum 1.0, accès à tous les champs
- **Membre** : Quorum 0.5, accès aux champs non critiques
- **Lecteur** : Quorum 0, accès en lecture seule aux champs publics
### 4. Membres
Les membres sont identifiés par leur **pairing process ID** (l'identité numérique vérifiable créée lors du pairing).
- Un membre peut participer à plusieurs processus
- Un processus peut avoir plusieurs membres
- Les adresses SP (Silent Payment) sont associées aux membres pour la communication
## Cycle de Vie d'un Processus
### Phase 1 : Création
```typescript
createProcess(
privateData: Record<string, any>, // Données privées initiales
publicData: Record<string, any>, // Données publiques initiales
roles: Record<string, RoleDefinition> // Définition des rôles
): Promise<ApiReturn>
```
**Étapes** :
1. Encodage des données (JSON/Binary)
2. Création du processus via SDK (WebAssembly)
3. Génération du premier état (state 0)
4. Établissement des connexions avec les membres
### Phase 2 : Mise à Jour
```typescript
updateProcess(
process: Process,
privateData: Record<string, any>, // Nouvelles données privées
publicData: Record<string, any>, // Nouvelles données publiques
roles: Record<string, RoleDefinition> | null // Nouveaux rôles (optionnel)
): Promise<ApiReturn>
```
**Logique de classification des champs** :
Le système détermine automatiquement si un champ est public ou privé :
1. **Champ existant dans `public_data`** → Reste public
2. **Champ nouveau dans `privateFields`** → Privé
3. **Champ existant dans `pcd_commitment`** → Reste privé
4. **Sinon** → Nouveau champ public
```typescript
// Logique dans handleUpdateProcess (router.ts:811-846)
for (const field of Object.keys(newData)) {
// 1. Vérifier si c'est déjà public
if (lastState.public_data[field]) {
publicData[field] = newData[field];
continue;
}
// 2. Vérifier si c'est un nouveau champ privé
if (privateFields.includes(field)) {
privateData[field] = newData[field];
continue;
}
// 3. Vérifier si c'était privé dans un état précédent
for (let i = lastStateIndex; i >= 0; i--) {
if (process.states[i].pcd_commitment[field]) {
privateData[field] = newData[field];
break;
}
}
// 4. Sinon, c'est un nouveau champ public
if (!privateData[field]) {
publicData[field] = newData[field];
}
}
```
### Phase 3 : Synchronisation (PRD Update)
**PRD** = Private Data Relay
```typescript
createPrdUpdate(
processId: string,
stateId: string
): Promise<ApiReturn>
```
**Objectif** :
- Synchroniser les **clés de déchiffrement** des données privées avec tous les membres autorisés
- Distribuer les données privées aux membres qui ont les permissions
- Mettre à jour les `state.keys[field]` pour chaque membre autorisé
**Processus** :
1. Création d'un message de mise à jour PRD
2. Transmission via les relais aux membres autorisés
3. Chaque membre reçoit les clés pour les champs auxquels il a accès
### Phase 4 : Validation (Approbation)
```typescript
approveChange(
processId: string,
stateId: string
): Promise<ApiReturn>
```
**Objectif** :
- Valider un état du processus selon le **quorum requis**
- S'assurer que suffisamment de membres ont approuvé le changement
- Marquer l'état comme validé
**Quorum** :
- Si quorum = 1.0 → Tous les membres du rôle doivent approuver
- Si quorum = 0.5 → 50% des membres doivent approuver
- Si quorum = 0 → Auto-approbation (pas de validation requise)
### Phase 5 : Commit sur Blockchain
Une fois validé, l'état est **committé sur la blockchain** :
- Création d'une transaction Bitcoin commitant l'état
- Le `pcd_commitment` est inclut dans la transaction
- L'état devient **immuable** et vérifiable
### Phase 6 : Accès aux Données
#### Données Publiques
Accessibles directement depuis `state.public_data` après décodage :
```typescript
const publicData = service.getPublicData(process);
const decodedValue = service.decodeValue(publicData['fieldName']);
```
#### Données Privées
Nécessitent :
1. **Permission** : Vérifier que le membre a accès au champ
2. **Clé de déchiffrement** : Récupérer `state.keys[field]`
3. **Commitment** : Vérifier `state.pcd_commitment[field]`
4. **Déchiffrement** : Décrypter avec la clé
```typescript
async decryptAttribute(
processId: string,
state: ProcessState,
attribute: string
): Promise<any | null>
```
**Vérification des permissions** :
```typescript
// Vérifier si le membre a accès au champ
for (const role of Object.values(state.roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) {
// Le membre a accès
}
}
}
}
```
Si la clé est manquante, le système demande automatiquement aux autres membres via `requestDataFromPeers()`.
## Flux Complet d'un Update
```
1. Mise à jour demandée
2. Classification automatique des champs (public/privé)
3. updateProcess() → Création d'un nouvel état
4. createPrdUpdate() → Synchronisation des clés privées
5. approveChange() → Validation selon quorum
6. Commit sur blockchain → État immuable
7. Accès aux données via getPublicData() / decryptAttribute()
```
## Exemples d'Utilisation
### Exemple 1 : Pairing (Identité Multi-Appareils)
```typescript
// Création d'un processus de pairing
const pairingProcess = await service.createPairingProcess(
'', // memberPublicName (vide pour pairing)
[creatorAddress] // pairedAddresses (liste des appareils)
);
// Données publiques : Liste des adresses appairées
// Données privées : Secrets Silent Payment partagés
// Rôles : Tous les appareils ont le même niveau d'accès (quorum 1.0)
```
### Exemple 2 : Contrat de Partage avec Niveaux d'Accès
```typescript
// Création d'un contrat avec différents niveaux
const contract = await service.createProcess(
{
secretKey: '...', // Privé : Clé secrète
internalNotes: '...' // Privé : Notes internes
},
{
contractName: 'Mon Contrat', // Public : Nom
description: '...' // Public : Description
},
{
admin: {
members: [adminPairingId],
validation_rules: [
{ quorum: 1.0, fields: ['secretKey', 'internalNotes', 'contractName'] }
]
},
member: {
members: [memberPairingId1, memberPairingId2],
validation_rules: [
{ quorum: 0.5, fields: ['contractName', 'description'] } // Lecture seule des publics
]
}
}
);
```
### Exemple 3 : Vote Décisionnel
```typescript
const voteProcess = await service.createProcess(
{
votes: {} // Privé : Votes individuels
},
{
proposal: 'Proposition...', // Public : Proposition
result: null // Public : Résultat
},
{
voter: {
members: [...voterIds],
validation_rules: [
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
]
}
}
);
```
## Points Importants
### 1. Immutabilité
Une fois qu'un état est **committé**, il devient immuable. Les nouveaux états ajoutent des modifications mais ne modifient jamais les états précédents.
### 2. Portage des Données Publiques
Les données publiques sont **automatiquement portées** dans chaque nouvel état. Pas besoin de les réenvoyer à chaque update.
### 3. Synchronisation Automatique
Le système gère automatiquement la **distribution des clés privées** aux membres autorisés via les relais.
### 4. Quorum Flexible
Le système supporte différents niveaux de quorum selon les besoins :
- **Sécurisé** : Quorum 1.0 (tous doivent approuver)
- **Démocratique** : Quorum 0.5 (majorité)
- **Auto** : Quorum 0 (auto-approbation)
### 5. Extensibilité
Ce système peut être utilisé pour **n'importe quel type de contrat** :
- Gestion documentaire
- Votes décisionnels
- Partage de fichiers
- Contrats intelligents décentralisés
- etc.
## Méthodes Utilitaires
### Récupération des Processus
```typescript
// Récupérer un processus spécifique
getProcess(processId: string): Promise<Process | null>
// Récupérer tous les processus
getProcesses(): Promise<Record<string, Process>>
// Récupérer mes processus (où je suis membre)
getMyProcesses(): Promise<string[] | null>
```
### État Commité
```typescript
// Récupérer le dernier état commité
getLastCommitedState(process: Process): ProcessState | null
// Récupérer l'index du dernier état commité
getLastCommitedStateIndex(process: Process): number | null
```
**Important** : Les états non commités sont des "pending states" qui attendent validation.
### Rôles et Membres
```typescript
// Récupérer les rôles d'un processus (depuis le dernier état commité)
getRoles(process: Process): Record<string, RoleDefinition> | null
// Vérifier si je suis membre d'un processus
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean
// Vérifier si un membre spécifique fait partie des rôles
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean
// Récupérer tous les membres connus
getAllMembers(): Record<string, Member>
```
### Données Publiques
```typescript
// Récupérer les données publiques (depuis le dernier état commité)
getPublicData(process: Process): Record<string, any> | null
// Décoder une valeur encodée
decodeValue(value: number[]): any | null
```
### Données Privées
```typescript
// Déchiffrer un attribut privé
async decryptAttribute(
processId: string,
state: ProcessState,
attribute: string
): Promise<any | null>
```
Cette méthode :
1. Vérifie les permissions (rôles)
2. Récupère la clé de déchiffrement (`state.keys[attribute]`)
3. Demande aux autres membres si la clé est manquante
4. Déchiffre la donnée
## Gestion du Cache et Synchronisation
### Cache des Processus
Les processus sont mis en cache localement pour améliorer les performances :
```typescript
processesCache: Record<string, Process>
```
### Synchronisation avec les Relais
Les relais synchronisent :
- Les nouveaux processus
- Les mises à jour d'états
- Les clés privées (PRD updates)
- Les validations
### Connexion entre Membres
Avant de créer ou mettre à jour un processus, le système établit des **connexions** (secrets partagés) entre tous les membres :
```typescript
checkConnections(process: Process, stateId?: string): Promise<void>
```
Cela crée des secrets Silent Payment entre les membres pour permettre la communication chiffrée.
## Avantages du Système
1. **Décentralisé** : Pas de tiers de confiance, tout sur la blockchain
2. **Vérifiable** : Chaque état est commité et vérifiable
3. **Flexible** : Permissions granulaires par champ et par rôle
4. **Sécurisé** : Chiffrement des données privées, distribution via relais
5. **Générique** : Réutilisable pour n'importe quel type de contrat
6. **Sans tiers** : Les utilisateurs contrôlent leurs propres processus
## Cas d'Usage Avancés
### Gestion Documentaire Collaborative
```typescript
const documentProcess = await service.createProcess(
{
documentContent: encryptedContent, // Privé : Contenu chiffré
versionHistory: [] // Privé : Historique des versions
},
{
documentTitle: 'Document Important', // Public : Titre
lastModified: timestamp, // Public : Dernière modification
author: authorAddress // Public : Auteur
},
{
owner: {
members: [ownerPairingId],
validation_rules: [
{ quorum: 1.0, fields: ['documentContent', 'documentTitle'] }
]
},
editor: {
members: [...editorPairingIds],
validation_rules: [
{ quorum: 0.5, fields: ['documentContent'] } // 50% des éditeurs doivent valider
]
},
viewer: {
members: [...viewerPairingIds],
validation_rules: [
{ quorum: 0, fields: ['documentTitle'] } // Lecture seule, pas de validation
]
}
}
);
```
### Système de Votation
```typescript
const votingProcess = await service.createProcess(
{
votes: {}, // Privé : Votes individuels
voterIds: [] // Privé : Liste des votants
},
{
question: 'Question...', // Public : Question
options: ['A', 'B', 'C'], // Public : Options
deadline: timestamp, // Public : Deadline
result: null // Public : Résultat (mis à jour après)
},
{
voter: {
members: [...allVoterIds],
validation_rules: [
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
]
},
organizer: {
members: [organizerPairingId],
validation_rules: [
{ quorum: 1.0, fields: ['question', 'options', 'deadline', 'result'] }
]
}
}
);
```
### Contrat Intelligent Décentralisé
Le système peut implémenter n'importe quel type de contrat intelligent décentralisé avec :
- Conditions de validation personnalisées (quorum)
- Permissions granulaires par champ
- Audit trail complet (historique des états)
- Vérifiabilité sur la blockchain

View File

@ -1,123 +0,0 @@
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
export default [
js.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json'
},
globals: {
'console': 'readonly',
'window': 'readonly',
'document': 'readonly',
'navigator': 'readonly',
'crypto': 'readonly',
'setTimeout': 'readonly',
'clearTimeout': 'readonly',
'setInterval': 'readonly',
'clearInterval': 'readonly',
'alert': 'readonly',
'confirm': 'readonly',
'prompt': 'readonly',
'fetch': 'readonly',
'localStorage': 'readonly',
'sessionStorage': 'readonly',
'indexedDB': 'readonly',
'IDBDatabase': 'readonly',
'IDBTransaction': 'readonly',
'IDBObjectStore': 'readonly',
'IDBRequest': 'readonly',
'customElements': 'readonly',
'requestAnimationFrame': 'readonly',
'cancelAnimationFrame': 'readonly',
'performance': 'readonly',
'WebAssembly': 'readonly',
'btoa': 'readonly',
'atob': 'readonly',
'self': 'readonly',
'SharedWorker': 'readonly',
'Worker': 'readonly',
'caches': 'readonly'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
// Qualité du code - Règles plus permissives pour commencer
'complexity': ['warn', 15],
'max-lines': ['warn', 500],
'max-lines-per-function': ['warn', 100],
'max-params': ['warn', 6],
'max-depth': ['warn', 6],
// TypeScript spécifique - Plus permissif
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', {
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_',
'ignoreRestSiblings': true
}],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
'@typescript-eslint/prefer-optional-chain': 'warn',
// Bonnes pratiques - Plus permissif
'no-console': 'off', // Permettre console pour le debug
'no-debugger': 'error',
'no-alert': 'warn',
'prefer-const': 'warn',
'no-var': 'error',
'eqeqeq': 'warn',
'curly': 'warn',
// Sécurité
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
// Performance - Plus permissif
'no-loop-func': 'warn',
'no-await-in-loop': 'off' // Permettre await dans les boucles pour l'instant
}
},
{
files: ['**/*.worker.ts', '**/*.worker.js'],
languageOptions: {
globals: {
'self': 'readonly',
'postMessage': 'readonly',
'onmessage': 'readonly',
'importScripts': 'readonly',
'btoa': 'readonly',
'atob': 'readonly',
'crypto': 'readonly',
'console': 'readonly'
}
}
},
{
ignores: [
'dist/',
'node_modules/',
'*.js',
'!eslint.config.js',
'pkg/',
'vite.config.ts',
'test-browser/',
'logs/',
'coverage/',
'.nyc_output/',
'**/*.test.ts',
'**/*.spec.ts'
]
}
];

View File

@ -1,150 +0,0 @@
# 4NK Pairing Integration Example
This example demonstrates how to integrate the 4NK pairing system into an external website using an iframe with channel_message communication.
## Architecture
```
┌─────────────────────────────────────┐
│ External Website (Parent) │
│ ├── Header with site branding │
│ ├── Main content area │
│ └── Iframe (4NK App) │
│ ├── No header (removed) │
│ ├── Menu buttons in content │
│ ├── Pairing interface │
│ └── Communication with parent │
└─────────────────────────────────────┘
```
## Features
### External Site (Parent)
- **Header**: Site branding and navigation
- **Iframe Container**: Hosts the 4NK application
- **Status Panel**: Shows communication status
- **Log System**: Displays real-time communication
- **Controls**: Test communication and refresh
### 4NK Application (Iframe)
- **No Header**: Clean interface without site header
- **Integrated Menu**: Menu buttons within content area
- **Pairing System**: 4-word authentication system
- **Communication**: Bidirectional message passing
## Communication Protocol
### Messages from Parent to Iframe
- `TEST_MESSAGE`: Test communication
- `PAIRING_4WORDS_CREATE`: Request pairing creation
- `PAIRING_4WORDS_JOIN`: Request pairing join with words
### Messages from Iframe to Parent
- `IFRAME_READY`: Iframe initialization complete
- `MENU_NAVIGATION`: Menu button clicked
- `PAIRING_4WORDS_WORDS_GENERATED`: 4 words generated
- `PAIRING_4WORDS_STATUS_UPDATE`: Status update
- `PAIRING_4WORDS_SUCCESS`: Pairing successful
- `PAIRING_4WORDS_ERROR`: Pairing error
- `TEST_RESPONSE`: Response to test message
## Usage
1. **Start the 4NK application**:
```bash
cd /home/ank/dev/ihm_client_dev3
npm run start
```
2. **Open the external site**:
```bash
# Open examples/external-site.html in a browser
# Or serve it via a web server
```
3. **Test the integration**:
- The iframe loads the 4NK application
- Use the "Send Test Message" button to test communication
- Click menu buttons to see navigation messages
- Use the pairing interface to test 4-word authentication
## Security Considerations
- **Origin Verification**: In production, verify `event.origin` in message handlers
- **Sandbox Attributes**: Iframe uses `sandbox` for security
- **CSP Headers**: Consider Content Security Policy headers
- **HTTPS**: Use HTTPS in production for secure communication
## Customization
### Styling the Iframe
```css
.iframe-container {
width: 100%;
height: 600px;
border: 2px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
```
### Adding Custom Messages
```javascript
// Send custom message to iframe
iframe.contentWindow.postMessage({
type: 'CUSTOM_ACTION',
data: { parameter: 'value' }
}, 'http://localhost:3004');
```
### Handling Custom Events
```javascript
window.addEventListener('message', function(event) {
if (event.origin !== 'http://localhost:3004') return;
const { type, data } = event.data;
switch (type) {
case 'CUSTOM_EVENT':
// Handle custom event
break;
}
});
```
## Troubleshooting
### Common Issues
1. **Iframe not loading**: Check CORS settings and iframe src URL
2. **Messages not received**: Verify origin checking and message format
3. **Styling issues**: Check iframe container dimensions and CSS
4. **Communication errors**: Check browser console for error messages
### Debug Mode
Enable debug logging by adding to the iframe:
```javascript
window.DEBUG_IFRAME = true;
```
## Production Deployment
1. **Update Origins**: Change localhost URLs to production domains
2. **Security Headers**: Add appropriate CSP and security headers
3. **Error Handling**: Implement proper error handling and fallbacks
4. **Monitoring**: Add logging and monitoring for communication events
5. **Testing**: Test across different browsers and devices
## API Reference
### Parent Window API
- `sendTestMessage()`: Send test message to iframe
- `clearLog()`: Clear communication log
- `refreshIframe()`: Refresh iframe content
### Iframe API
- `initIframeCommunication()`: Initialize communication
- `initContentMenu()`: Initialize menu buttons
- `createPairingViaIframe()`: Create pairing process
- `joinPairingViaIframe(words)`: Join pairing with words

View File

@ -1,327 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>External Site - 4NK Integration Example</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.header {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.header h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
}
.header p {
color: #666;
text-align: center;
font-size: 14px;
}
.main-content {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.integration-section {
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.integration-section h2 {
color: #333;
margin-bottom: 15px;
font-size: 24px;
}
.integration-section p {
color: #666;
margin-bottom: 20px;
line-height: 1.6;
}
.iframe-container {
position: relative;
width: 100%;
height: 600px;
border: 2px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
}
.status-panel {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
}
.status-panel h3 {
color: #333;
margin-bottom: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #e9ecef;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-weight: 500;
color: #555;
}
.status-value {
color: #007bff;
font-weight: 500;
}
.controls {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
background: #0056b3;
transform: translateY(-2px);
}
.btn.secondary {
background: #6c757d;
}
.btn.secondary:hover {
background: #545b62;
}
.log-container {
background: #1e1e1e;
color: #f8f9fa;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
margin-top: 20px;
}
.log-entry {
margin-bottom: 5px;
padding: 2px 0;
}
.log-entry.info {
color: #17a2b8;
}
.log-entry.success {
color: #28a745;
}
.log-entry.error {
color: #dc3545;
}
.log-entry.warning {
color: #ffc107;
}
</style>
</head>
<body>
<div class="header">
<h1>🏢 External Business Site</h1>
<p>Integrated 4NK Pairing System - Secure Device Authentication</p>
</div>
<div class="main-content">
<div class="integration-section">
<h2>🔐 4NK Pairing Integration</h2>
<p>
This external site demonstrates how to integrate the 4NK pairing system
using an iframe with channel_message communication. The iframe contains
the 4NK application without header, and all menu options are integrated
as buttons within the content.
</p>
<div class="iframe-container">
<iframe
id="4nk-iframe"
src="http://localhost:3004"
title="4NK Pairing System"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
<div class="status-panel">
<h3>📊 Integration Status</h3>
<div class="status-item">
<span class="status-label">Iframe Status:</span>
<span class="status-value" id="iframe-status">Loading...</span>
</div>
<div class="status-item">
<span class="status-label">Communication:</span>
<span class="status-value" id="communication-status">Waiting...</span>
</div>
<div class="status-item">
<span class="status-label">Last Message:</span>
<span class="status-value" id="last-message">None</span>
</div>
</div>
<div class="controls">
<button class="btn" onclick="sendTestMessage()">📤 Send Test Message</button>
<button class="btn secondary" onclick="clearLog()">🗑️ Clear Log</button>
<button class="btn secondary" onclick="refreshIframe()">🔄 Refresh Iframe</button>
</div>
<div class="log-container" id="log-container">
<div class="log-entry info">🚀 External site loaded</div>
<div class="log-entry info">📡 Waiting for iframe communication...</div>
</div>
</div>
</div>
<script>
let messageCount = 0;
// Listen for messages from the iframe
window.addEventListener('message', function(event) {
// Security check - in production, verify event.origin
if (event.origin !== 'http://localhost:3004') {
return;
}
const { type, data } = event.data;
messageCount++;
logMessage(`📨 Received: ${type}`, 'info');
updateStatus('communication-status', 'Active');
updateStatus('last-message', `${type} (${messageCount})`);
// Handle different message types
switch (type) {
case 'IFRAME_READY':
logMessage('✅ 4NK iframe is ready', 'success');
updateStatus('iframe-status', 'Ready');
break;
case 'MENU_NAVIGATION':
logMessage(`🧭 Menu navigation: ${data.page}`, 'info');
break;
case 'PAIRING_4WORDS_WORDS_GENERATED':
logMessage(`🔐 4 words generated: ${data.words}`, 'success');
break;
case 'PAIRING_4WORDS_STATUS_UPDATE':
logMessage(`📊 Status update: ${data.status}`, 'info');
break;
case 'PAIRING_4WORDS_SUCCESS':
logMessage(`✅ Pairing successful: ${data.message}`, 'success');
break;
case 'PAIRING_4WORDS_ERROR':
logMessage(`❌ Pairing error: ${data.error}`, 'error');
break;
default:
logMessage(`❓ Unknown message type: ${type}`, 'warning');
}
});
function logMessage(message, type = 'info') {
const logContainer = document.getElementById('log-container');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(elementId, value) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = value;
}
}
function sendTestMessage() {
const iframe = document.getElementById('4nk-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'TEST_MESSAGE',
data: { message: 'Hello from external site!' }
}, 'http://localhost:3004');
logMessage('📤 Sent test message to iframe', 'info');
}
}
function clearLog() {
const logContainer = document.getElementById('log-container');
logContainer.innerHTML = '<div class="log-entry info">🗑️ Log cleared</div>';
}
function refreshIframe() {
const iframe = document.getElementById('4nk-iframe');
iframe.src = iframe.src;
logMessage('🔄 Iframe refreshed', 'info');
updateStatus('iframe-status', 'Refreshing...');
}
// Initialize
logMessage('🌐 External site initialized', 'success');
</script>
</body>
</html>

View File

@ -1,326 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>4NK Integration Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.test-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.test-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.test-section h3 {
margin-top: 0;
color: #333;
}
.iframe-container {
width: 100%;
height: 500px;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
}
.test-controls {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #0056b3;
}
.btn.secondary {
background: #6c757d;
}
.btn.secondary:hover {
background: #545b62;
}
.log-container {
background: #1e1e1e;
color: #f8f9fa;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
margin-top: 20px;
}
.log-entry {
margin-bottom: 5px;
padding: 2px 0;
}
.log-entry.info { color: #17a2b8; }
.log-entry.success { color: #28a745; }
.log-entry.error { color: #dc3545; }
.log-entry.warning { color: #ffc107; }
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.status-item {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.status-label {
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.status-value {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="test-container">
<h1>🧪 4NK Integration Test</h1>
<p>Test de l'intégration iframe avec communication channel_message</p>
<div class="test-section">
<h3>📱 Interface 4NK (Iframe)</h3>
<div class="iframe-container">
<iframe
id="4nk-iframe"
src="http://localhost:3004"
title="4NK Pairing System"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
</div>
<div class="test-section">
<h3>🎮 Contrôles de Test</h3>
<div class="test-controls">
<button class="btn" onclick="sendTestMessage()">📤 Test Message</button>
<button class="btn" onclick="testCreatePairing()">🔐 Test Create Pairing</button>
<button class="btn" onclick="testJoinPairing()">🔗 Test Join Pairing</button>
<button class="btn secondary" onclick="clearLog()">🗑️ Clear Log</button>
<button class="btn secondary" onclick="refreshIframe()">🔄 Refresh</button>
</div>
</div>
<div class="test-section">
<h3>📊 Status</h3>
<div class="status-grid">
<div class="status-item">
<div class="status-label">Iframe Status</div>
<div class="status-value" id="iframe-status">Loading...</div>
</div>
<div class="status-item">
<div class="status-label">Communication</div>
<div class="status-value" id="communication-status">Waiting...</div>
</div>
<div class="status-item">
<div class="status-label">Messages Received</div>
<div class="status-value" id="message-count">0</div>
</div>
<div class="status-item">
<div class="status-label">Last Message</div>
<div class="status-value" id="last-message">None</div>
</div>
</div>
</div>
<div class="test-section">
<h3>📝 Communication Log</h3>
<div class="log-container" id="log-container">
<div class="log-entry info">🚀 Test page loaded</div>
<div class="log-entry info">📡 Waiting for iframe communication...</div>
</div>
</div>
</div>
<script>
let messageCount = 0;
let iframeReady = false;
// Listen for messages from the iframe
window.addEventListener('message', function(event) {
// Security check - in production, verify event.origin
if (event.origin !== 'http://localhost:3004') {
return;
}
const { type, data } = event.data;
messageCount++;
logMessage(`📨 Received: ${type}`, 'info');
updateStatus('communication-status', 'Active');
updateStatus('message-count', messageCount.toString());
updateStatus('last-message', `${type} (${messageCount})`);
// Handle different message types
switch (type) {
case 'IFRAME_READY':
logMessage('✅ 4NK iframe is ready', 'success');
updateStatus('iframe-status', 'Ready');
iframeReady = true;
break;
case 'MENU_NAVIGATION':
logMessage(`🧭 Menu navigation: ${data.page}`, 'info');
break;
case 'PAIRING_4WORDS_WORDS_GENERATED':
logMessage(`🔐 4 words generated: ${data.words}`, 'success');
break;
case 'PAIRING_4WORDS_STATUS_UPDATE':
logMessage(`📊 Status update: ${data.status}`, 'info');
break;
case 'PAIRING_4WORDS_SUCCESS':
logMessage(`✅ Pairing successful: ${data.message}`, 'success');
break;
case 'PAIRING_4WORDS_ERROR':
logMessage(`❌ Pairing error: ${data.error}`, 'error');
break;
case 'TEST_RESPONSE':
logMessage(`🧪 Test response: ${data.response}`, 'success');
break;
default:
logMessage(`❓ Unknown message type: ${type}`, 'warning');
}
});
function logMessage(message, type = 'info') {
const logContainer = document.getElementById('log-container');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(elementId, value) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = value;
}
}
function sendTestMessage() {
const iframe = document.getElementById('4nk-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'TEST_MESSAGE',
data: { message: 'Hello from test page!' }
}, 'http://localhost:3004');
logMessage('📤 Sent test message to iframe', 'info');
} else {
logMessage('❌ Iframe not ready', 'error');
}
}
function testCreatePairing() {
const iframe = document.getElementById('4nk-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'PAIRING_4WORDS_CREATE',
data: {}
}, 'http://localhost:3004');
logMessage('🔐 Sent create pairing request', 'info');
} else {
logMessage('❌ Iframe not ready', 'error');
}
}
function testJoinPairing() {
const words = prompt('Enter 4 words to test join pairing:');
if (words) {
const iframe = document.getElementById('4nk-iframe');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({
type: 'PAIRING_4WORDS_JOIN',
data: { words: words }
}, 'http://localhost:3004');
logMessage(`🔗 Sent join pairing request with words: ${words}`, 'info');
} else {
logMessage('❌ Iframe not ready', 'error');
}
}
}
function clearLog() {
const logContainer = document.getElementById('log-container');
logContainer.innerHTML = '<div class="log-entry info">🗑️ Log cleared</div>';
}
function refreshIframe() {
const iframe = document.getElementById('4nk-iframe');
iframe.src = iframe.src;
logMessage('🔄 Iframe refreshed', 'info');
updateStatus('iframe-status', 'Refreshing...');
iframeReady = false;
}
// Initialize
logMessage('🌐 Test page initialized', 'success');
// Auto-test after 3 seconds
setTimeout(() => {
if (iframeReady) {
logMessage('🧪 Auto-testing communication...', 'info');
sendTestMessage();
}
}, 3000);
</script>
</body>
</html>

View File

@ -1,148 +0,0 @@
#!/usr/bin/env node
/**
* Script pour corriger automatiquement tous les logs console.* en secureLogger
*/
const fs = require('fs');
const path = require('path');
// Fichiers à corriger
const filesToFix = [
'src/pages/home/home.ts',
'src/pages/pairing/pairing.ts',
'src/pages/wallet-setup/wallet-setup.ts',
'src/pages/security-setup/security-setup.ts',
'src/pages/birthday-setup/birthday-setup.ts',
'src/pages/block-sync/block-sync.ts',
'src/utils/sp-address.utils.ts',
'src/router.ts',
'src/websockets.ts'
];
// Fonction pour déterminer le niveau de log
function determineLogLevel(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('error') || lowerMessage.includes('failed') || lowerMessage.includes('❌')) {
return 'error';
}
if (lowerMessage.includes('warn') || lowerMessage.includes('⚠️') || lowerMessage.includes('skipping')) {
return 'warn';
}
if (lowerMessage.includes('debug') || lowerMessage.includes('🔍') || lowerMessage.includes('checking')) {
return 'debug';
}
return 'info';
}
// Fonction pour déterminer le contexte
function determineContext(filePath, message) {
const fileName = path.basename(filePath, '.ts');
if (fileName.includes('service')) return 'Service';
if (fileName.includes('home')) return 'HomePage';
if (fileName.includes('pairing')) return 'PairingPage';
if (fileName.includes('wallet')) return 'WalletSetup';
if (fileName.includes('security')) return 'SecuritySetup';
if (fileName.includes('birthday')) return 'BirthdaySetup';
if (fileName.includes('block-sync')) return 'BlockSync';
if (fileName.includes('router')) return 'Router';
if (fileName.includes('websocket')) return 'WebSocket';
if (fileName.includes('sp-address')) return 'SPAddressUtils';
return 'Application';
}
// Fonction pour corriger un fichier
function fixFile(filePath) {
if (!fs.existsSync(filePath)) {
console.log(`⚠️ Fichier non trouvé: ${filePath}`);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
let modified = false;
// Ajouter l'import secureLogger si pas déjà présent
if (!content.includes('import { secureLogger }')) {
const importMatch = content.match(/import.*from.*['"][^'"]+['"];?\s*\n/);
if (importMatch) {
const importIndex = content.lastIndexOf(importMatch[0]) + importMatch[0].length;
content = content.slice(0, importIndex) +
`import { secureLogger } from '../services/secure-logger';\n` +
content.slice(importIndex);
modified = true;
}
}
// Remplacer console.log par secureLogger
content = content.replace(
/console\.log\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
(match, message) => {
const level = determineLogLevel(message);
const context = determineContext(filePath, message);
modified = true;
return `secureLogger.${level}('${message}', { component: '${context}' })`;
}
);
// Remplacer console.warn par secureLogger.warn
content = content.replace(
/console\.warn\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
(match, message) => {
const context = determineContext(filePath, message);
modified = true;
return `secureLogger.warn('${message}', { component: '${context}' })`;
}
);
// Remplacer console.error par secureLogger.error
content = content.replace(
/console\.error\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
(match, message) => {
const context = determineContext(filePath, message);
modified = true;
return `secureLogger.error('${message}', { component: '${context}' })`;
}
);
// Remplacer console.info par secureLogger.info
content = content.replace(
/console\.info\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
(match, message) => {
const context = determineContext(filePath, message);
modified = true;
return `secureLogger.info('${message}', { component: '${context}' })`;
}
);
// Remplacer console.debug par secureLogger.debug
content = content.replace(
/console\.debug\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
(match, message) => {
const context = determineContext(filePath, message);
modified = true;
return `secureLogger.debug('${message}', { component: '${context}' })`;
}
);
if (modified) {
fs.writeFileSync(filePath, content);
console.log(`✅ Corrigé: ${filePath}`);
} else {
console.log(`⏭️ Aucune modification: ${filePath}`);
}
}
// Exécuter les corrections
console.log('🔧 Correction des logs console.* en secureLogger...\n');
filesToFix.forEach(file => {
fixFile(file);
});
console.log('\n✅ Correction terminée !');

View File

@ -7,11 +7,11 @@
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style/4nk.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<script src="https://unpkg.com/html5-qrcode"></script>
<title>4NK Application</title>
</head>
<body>
<div id="header-container"></div>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution -->
</div>

View File

@ -1,13 +0,0 @@
#!/bin/bash
# Script pour linter tout le projet et corriger automatiquement les erreurs
set -e
echo "🔍 Running ESLint with --fix option..."
npm run lint
echo ""
echo "✅ Linting completed with auto-fix!"

View File

@ -1,48 +0,0 @@
server {
listen 80;
server_name localhost;
# Redirection des requêtes HTTP vers Vite
location / {
proxy_pass http://localhost:3004;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /ws/ {
proxy_pass http://localhost:8090;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_read_timeout 86400;
}
location /storage/ {
rewrite ^/storage(/.*)$ $1 break;
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /api/ {
proxy_pass http://localhost:8091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS headers
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
}
}

7185
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

View File

@ -2,53 +2,44 @@
"name": "sdk_client",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev3/pkg ../sdk_client --target bundler --dev",
"test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0",
"build": "tsc && vite build",
"deploy:front": "./scripts/deploy_front.sh",
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
"build:dist": "tsc -p tsconfig.build.json",
"lint": "eslint src/ --fix",
"lint:check": "eslint src/",
"type-check": "tsc --noEmit",
"quality": "npm run prettify",
"quality:strict": "npm run type-check && npm run lint:check && npm run prettify",
"quality:fix": "npm run lint && npm run prettify",
"analyze": "npm run build && npx bundle-analyzer dist/assets/*.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --watchAll=false"
"deploy": "sudo cp -r dist/* /var/www/html/",
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@eslint/js": "^9.38.0",
"@testing-library/jest-dom": "^6.1.4",
"@types/jest": "^29.5.8",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"jest": "^29.7.0",
"@rollup/plugin-typescript": "^12.1.1",
"copy-webpack-plugin": "^12.0.2",
"html-webpack-plugin": "^5.6.0",
"prettier": "^3.3.3",
"ts-jest": "^29.1.1",
"rimraf": "^6.0.1",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"vite": "^5.4.11",
"vite-plugin-static-copy": "^1.0.6",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0"
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2"
},
"dependencies": {
"@angular/elements": "^19.0.1",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-vue": "^5.0.5",
"axios": "^1.7.8",
"jose": "^6.0.11",
"jsonwebtoken": "^9.0.2",
"pdf-lib": "^1.17.1",
"html5-qrcode": "^2.3.8",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.3",
"sweetalert2": "^11.14.5",
"vite-plugin-copy": "^0.1.6",
"vite-plugin-html": "^3.2.2"
"vite-plugin-html": "^3.2.2",
"vite-plugin-wasm": "^3.3.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs>
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#3a506b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2c3e50;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Bouclier principal -->
<path d="M16 2L6 6v10c0 8 10 12 10 12s10-4 10-12V6L16 2z"
fill="url(#shieldGradient)"
stroke="#1a252f"
stroke-width="0.5"/>
<!-- Symbole de sécurité au centre -->
<path d="M12 16l3 3 6-6"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"/>
<!-- Points de sécurité -->
<circle cx="16" cy="8" r="1" fill="#ffffff" opacity="0.8"/>
<circle cx="20" cy="12" r="0.8" fill="#ffffff" opacity="0.6"/>
<circle cx="12" cy="12" r="0.8" fill="#ffffff" opacity="0.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 959 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,522 +1,597 @@
/* Chat page base */
/* Styles de base */
:root {
--primary-color: #3A506B;
/* Bleu métallique */
--secondary-color: #B0BEC5;
/* Gris acier */
--accent-color: #D68C45;
/* Cuivre */
}
body {
margin: 0;
min-height: 100vh;
background: #f3f5f9;
color: var(--color-text-primary);
font-family: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
a {
color: inherit;
text-decoration: none;
}
/* Navigation */
.nav-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(232, 238, 244, 0.9));
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.14);
z-index: 25;
backdrop-filter: blur(10px);
}
/* 4NK NAVBAR */
.brand-logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--color-primary);
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
display: flex;
justify-content: space-between;
align-items: center;
color: #37474F;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
}
/* Icônes de la barre de navigation */
.nav-right-icons {
display: inline-flex;
align-items: center;
gap: 18px;
}
.notification-container {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
display: flex;
}
.notification-bell,
.burger-menu {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
cursor: pointer;
transition: color var(--transition-base);
height: 20px;
width: 20px;
margin-right: 1rem;
cursor: pointer;
}
.notification-bell:hover,
.burger-menu:hover {
color: var(--color-primary);
}
.notification-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 18px;
height: 18px;
padding: 0 6px;
border-radius: 999px;
background: var(--color-danger);
color: #fff;
font-size: 0.7rem;
font-weight: 700;
display: none;
}
.notification-badge.is-visible {
display: inline-flex;
align-items: center;
justify-content: center;
.notification-container {
position: relative;
/* Conserve la position pour le notification-board */
display: inline-flex;
align-items: center;
}
.notification-board {
position: absolute;
top: calc(100% + 12px);
right: 0;
width: min(280px, 90vw);
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
border: 1px solid rgba(148, 163, 184, 0.22);
padding: 10px 0;
display: none;
flex-direction: column;
z-index: 40;
position: absolute;
/* Position absolue pour le placer par rapport au container */
top: 40px;
right: 0;
background-color: white;
border: 1px solid #ccc;
padding: 10px;
width: 200px;
max-height: 300px;
overflow-y: auto;
/* Scroll si les notifications dépassent la taille */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
/* Définit la priorité d'affichage au-dessus des autres éléments */
display: none;
/* Par défaut, la notification est masquée */
}
.notification-board.is-visible {
display: flex;
.notification-item{
cursor: pointer;
}
.notification-board .notification-item,
.notification-board .notification-element {
padding: 10px 16px;
color: var(--color-text-primary);
transition: background var(--transition-base);
.notification-badge {
position: absolute;
top: -18px;
right: 35px;
background-color: red;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
display: none;
/* S'affiche seulement lorsqu'il y a des notifications */
z-index: 10;
}
.notification-board .notification-item:hover,
.notification-board .notification-element:hover {
background: rgba(58, 80, 107, 0.1);
/* Par défaut, le menu est masqué */
#menu {
display: none;
/* Menu caché par défaut */
transition: display 0.3s ease-in-out;
}
/* Layout */
.burger-menu {
cursor: pointer;
}
/* Icône burger */
#burger-icon {
cursor: pointer;
}
.menu-content {
display: none;
position: absolute;
top: 3.4rem;
right: 1rem;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
.menu-content a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #e0e0e0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
/* Ajustement pour la barre de navigation fixe */
.container {
margin-top: 90px;
display: grid;
grid-template-columns: clamp(220px, 22%, 280px) minmax(0, 1fr);
gap: 20px;
padding: 24px 32px 48px;
box-sizing: border-box;
display: flex;
flex: 1;
height: 90vh;
margin-top: 9vh;
margin-left: -1%;
text-align: left;
width: 100vw;
}
/* Liste des groupes */
.group-list {
background: linear-gradient(180deg, #1f2c3d, #1b2735);
color: #fff;
border-radius: var(--radius-lg);
padding: 22px 18px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: var(--shadow-sm);
height: calc(100vh - 136px);
overflow-y: auto;
width: 25%;
background-color: #1f2c3d;
color: white;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
border-right: 2px solid #2c3e50;
flex-shrink: 0;
padding-right: 10px;
height: 91vh;
}
.group-list ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
cursor: pointer;
list-style: none;
padding: 0;
padding-right: 10px;
margin-left: 20px;
}
.group-list li {
background: rgba(255, 255, 255, 0.08);
border-radius: var(--radius-md);
padding: 14px 16px;
transition: transform var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
cursor: pointer;
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
background-color: #273646;
cursor: pointer;
transition: background-color 0.3s, box-shadow 0.3s;
}
.group-list li:hover,
.group-list li.active {
background: rgba(255, 255, 255, 0.18);
transform: translateX(4px);
box-shadow: 0 14px 24px rgba(0, 0, 0, 0.2);
.group-list li:hover {
background-color: #34495e;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.member-container {
position: relative;
.group-list .member-container {
position: relative;
}
.member-container button {
position: absolute;
top: -16px;
right: -16px;
padding: 6px 12px;
border-radius: 999px;
border: none;
font-size: 0.75rem;
font-weight: 600;
background: var(--color-primary);
color: #fff;
cursor: pointer;
transition: background var(--transition-base);
.group-list .member-container button {
margin-left: 40px;
padding: 5px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
border-radius: 50px;
position: absolute;
top: -25px;
right: -25px;
}
.member-container button:hover {
background: var(--color-accent);
.group-list .member-container button:hover {
background: var(--accent-color)
}
/* Chat area */
.chat-area,
.signature-area {
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
min-height: calc(100vh - 136px);
/* Zone de chat */
.chat-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
}
.chat-header,
.signature-header {
background: var(--color-primary);
color: #fff;
border-radius: var(--radius-md);
padding: 14px 18px;
font-weight: 600;
font-size: 1.1rem;
text-align: center;
/* En-tête du chat */
.chat-header {
background-color: #34495e;
color: white;
padding: 15px;
font-size: 20px;
font-weight: bold;
border-radius: 10px 10px 0 0;
text-align: center;
}
/* Messages */
.messages {
flex: 1;
overflow-y: auto;
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
background: rgba(58, 80, 107, 0.04);
border-radius: var(--radius-md);
border: 1px solid rgba(148, 163, 184, 0.25);
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f1f1f1;
border-top: 1px solid #ddd;
}
.message-container {
display: flex;
display: flex;
margin: 8px;
}
.message-container .message {
align-self: flex-start;
}
.message-container .message.user {
align-self: flex-end;
margin-left: auto;
color: white;
}
.message {
max-width: 68%;
padding: 10px 14px;
border-radius: var(--radius-lg);
background: var(--color-secondary);
color: var(--color-text-primary);
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.12);
position: relative;
display: inline-flex;
flex-direction: column;
gap: 6px;
max-width: 70%;
padding: 10px;
border-radius: 12px;
background:var(--secondary-color);
margin: 2px 0;
}
/* Messages de l'utilisateur */
.message.user {
margin-left: auto;
background: linear-gradient(135deg, #2196f3, #1363b5);
color: #fff;
background: #2196f3;
color: white;
}
.message-time {
font-size: 0.75rem;
opacity: 0.7;
align-self: flex-end;
font-size: 0.7em;
opacity: 0.7;
margin-left: 0px;
margin-top: 5px;
}
/* Amélioration de l'esthétique des messages */
/* .message.user:before {
content: '';
position: absolute;
top: 10px;
right: -10px;
border: 10px solid transparent;
border-left-color: #3498db;
} */
/* Zone de saisie */
.input-area {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: var(--radius-md);
background: rgba(148, 163, 184, 0.18);
padding: 10px;
background-color: #bdc3c7;
display: flex;
align-items: center;
border-radius: 10px;
margin: 1%;
/* Alignement vertical */
}
.input-area input[type='text'] {
flex: 1;
border: 1px solid rgba(148, 163, 184, 0.4);
border-radius: var(--radius-md);
padding: 10px 12px;
font-size: 0.95rem;
.input-area input[type="text"] {
flex: 1;
/* Prend l'espace restant */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.input-area .attachment-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(58, 80, 107, 0.12);
color: var(--color-primary);
cursor: pointer;
margin: 0 10px;
cursor: pointer;
display: flex;
align-items: center;
}
.input-area button {
padding: 10px 18px;
border: none;
border-radius: var(--radius-md);
background: linear-gradient(135deg, #2980b9, #1f608d);
color: #fff;
font-weight: 600;
cursor: pointer;
transition: transform var(--transition-base), box-shadow var(--transition-base);
padding: 10px;
margin-left: 10px;
background-color: #2980b9;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.input-area button:hover {
transform: translateY(-1px);
box-shadow: 0 12px 22px rgba(41, 128, 185, 0.35);
background-color: #1f608d;
}
.tabs {
display: inline-flex;
gap: 10px;
display: flex;
margin: 20px 0px;
gap: 10px;
}
.tabs button {
padding: 8px 16px;
border-radius: var(--radius-md);
border: none;
background: var(--color-primary);
color: #fff;
font-weight: 600;
cursor: pointer;
transition: transform var(--transition-base), box-shadow var(--transition-base);
padding: 10px 20px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
margin-right: 5px;
border-radius: 10px;
}
.tabs button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 18px rgba(58, 80, 107, 0.25);
background: var(--secondary-color);
color: var(--primary-color);
}
/* Signature */
.signature-area {
gap: 18px;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
transition: all 1s ease 0.1s;
visibility: visible;
}
.signature-area.hidden {
display: none !important;
opacity: 0;
visibility: hidden;
display: none;
pointer-events: none;
}
.signature-header {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
border-radius: 10px 10px 0 0;
padding-left: 4%;
}
.signature-content {
background: rgba(58, 80, 107, 0.08);
color: var(--color-text-primary);
border-radius: var(--radius-md);
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
padding: 10px;
background-color: var(--secondary-color);
color: var(--primary-color);
height: 100%;
border-radius: 10px;
margin: 1%;
display: flex;
flex-direction: column;
align-items: center;
}
.signature-description,
.signature-documents {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 6px;
.signature-description {
height: 20%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-description li {
list-style: none;
padding: 10px 12px;
border-radius: var(--radius-md);
background: var(--color-primary);
color: #fff;
min-width: 140px;
text-align: center;
cursor: pointer;
transition: transform var(--transition-base), background var(--transition-base);
margin: 1% 0% 1% 0%;
list-style: none;
padding: 2%;
border-radius: 10px;
background-color: var(--primary-color);
color: var(--secondary-color);
width: 20%;
text-align: center;
cursor: pointer;
font-weight: bold;
margin-right: 2%;
overflow: auto;
}
.signature-description li:hover {
background: var(--color-accent);
transform: translateY(-2px);
.signature-description li .member-list {
margin-left: -30%;
}
.new-request-btn,
#request-document-button,
.sign-button {
padding: 10px 18px;
border-radius: var(--radius-md);
border: none;
background: linear-gradient(135deg, var(--color-success), #2e7d32);
color: #fff;
font-weight: 600;
cursor: pointer;
transition: transform var(--transition-base), box-shadow var(--transition-base);
.signature-description li .member-list li {
width: 100%;
}
.new-request-btn:hover,
#request-document-button:hover,
.sign-button:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(76, 175, 80, 0.32);
.signature-description li .member-list li:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
/* Modals */
.modal,
.notifications-modal,
.qr-modal,
.request-modal,
.modal-document,
.pairing-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
z-index: 60;
padding: 24px;
box-sizing: border-box;
.signature-documents {
height: 80%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.modal.is-visible,
.notifications-modal.is-visible,
.qr-modal.is-visible,
.request-modal.is-visible,
.modal-document.is-visible,
.pairing-modal.is-visible {
display: flex;
.signature-documents-header {
display: flex;
width: 100%;
height: 15%;
align-items: center;
}
.modal-content,
.notifications-content,
.qr-modal-content,
.request-modal .modal-content,
.modal-document .modal-content,
.pairing-modal-content {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 24px;
width: min(480px, 100%);
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
#request-document-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
margin-left: 5%;
font-weight: bold;
}
.close-modal,
.close-button,
.close-qr-modal,
.close-signature,
.close-contract-popup {
position: absolute;
top: 14px;
right: 14px;
border: none;
background: transparent;
font-size: 1.4rem;
color: var(--color-text-secondary);
cursor: pointer;
#request-document-button:hover {
background-color: var(--accent-color);
font-weight: bold;
}
.close-modal:hover,
.close-button:hover,
.close-qr-modal:hover,
.close-signature:hover,
.close-contract-popup:hover {
color: var(--color-primary);
#close-signature {
cursor: pointer;
align-items: center;
margin-left: auto;
margin-right: 2%;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
border: none;
padding: -3%;
margin-top: -5%;
font-size: 1em;
font-weight: bold;
}
#close-signature:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
/* REQUEST MODAL */
.request-modal {
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;
z-index: 1000;
}
.modal-footer,
.button-group,
.header-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
.modal-content {
background-color: var(--secondary-color);
padding: 20px;
border-radius: 8px;
position: relative;
min-width: 300px;
}
.close-modal {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 1.5em;
cursor: pointer;
font-weight: bold;
}
.close-modal:hover {
color: var(--accent-color);
}
.modal-members {
display: flex;
justify-content: space-between;
}
.modal-members ul li{
list-style: none;
}
.file-upload-container {
margin: 10px 0;
}
.file-list {
margin-top: 10px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
margin: 5px 0;
background: var(--background-color-secondary);
border-radius: 4px;
}
.remove-file {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0 5px;
}
.remove-file:hover {
color: var(--error-color);
}
#message-input {
width: 100%;
height: 50px;
resize: none;
padding: 10px;
box-sizing: border-box;
overflow: auto;
max-width: 100%;
border-radius: 10px;
}
/* Responsive */
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
padding: 18px 18px 36px;
}
@media screen and (max-width: 768px) {
.group-list {
display: none;
/* Masquer la liste des groupes sur les petits écrans */
}
.group-list {
height: auto;
max-height: none;
}
.chat-area {
margin: 0;
}
}
@media (max-width: 768px) {
.nav-wrapper {
height: auto;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 14px 18px;
}
.nav-right-icons {
align-self: flex-end;
}
.container {
padding: 16px 14px 32px;
}
.chat-area,
.signature-area {
min-height: auto;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
@media (max-width: 480px) {
.message {
max-width: 85%;
}
::-webkit-scrollbar-track {
background: var(--primary-color);
border-radius: 5px;
}
.input-area {
flex-direction: column;
align-items: stretch;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 5px;
}
.input-area button {
width: 100%;
}
.tabs {
flex-wrap: wrap;
}
.tabs button {
width: 100%;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

File diff suppressed because it is too large Load Diff

View File

View File

@ -1,68 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="$PROJECT_ROOT/logs"
PID_FILE="$LOG_DIR/ihm_client_dev3.front.pid"
LOG_FILE="$LOG_DIR/ihm_client_dev3.front.log"
PORT=3004
mkdir -p "$LOG_DIR"
echo "[deploy] Ensuring nothing listens on :$PORT..."
if ss -ltnp | grep -q ":$PORT"; then
# Extract PID(s) for the port
PIDS=$(ss -ltnp | awk -v p=":$PORT" '$0 ~ p {print $NF}' | sed -E 's/.*pid=([0-9]+).*/\1/' | sort -u)
for PID in $PIDS; do
if [[ "$PID" =~ ^[0-9]+$ ]]; then
echo "[deploy] Killing PID $PID on port $PORT"
kill -TERM "$PID" || true
# Wait up to 10s for process to exit
for i in {1..10}; do
if ! ps -p "$PID" >/dev/null 2>&1; then break; fi
sleep 1
done
if ps -p "$PID" >/dev/null 2>&1; then
echo "[deploy] Force killing PID $PID"
kill -KILL "$PID" || true
fi
fi
done
fi
echo "[deploy] Cleaning Vite caches and previous dist..."
rm -rf "$PROJECT_ROOT/node_modules/.vite" "$PROJECT_ROOT/.vite" "$PROJECT_ROOT/dist"
echo "[deploy] Building production bundle..."
cd "$PROJECT_ROOT"
npm run build
echo "[deploy] Starting Vite dev server on :$PORT (non-bloquant) ..."
if [[ -f "$PID_FILE" ]]; then
# Clean stale PID file if any
OLD_PID=$(cat "$PID_FILE" || true)
if [[ -n "${OLD_PID}" ]] && ps -p "$OLD_PID" >/dev/null 2>&1; then
echo "[deploy] Previous PID $OLD_PID still running, terminating"
kill -TERM "$OLD_PID" || true
fi
rm -f "$PID_FILE"
fi
nohup npm run start >"$LOG_FILE" 2>&1 &
NEW_PID=$!
echo "$NEW_PID" > "$PID_FILE"
echo "[deploy] Launched PID $NEW_PID. Tail logs: tail -f $LOG_FILE"
echo "[deploy] Verifying port $PORT availability..."
for i in {1..10}; do
if ss -ltnp | grep -q ":$PORT"; then
echo "[deploy] OK: port $PORT is listening."
exit 0
fi
sleep 1
done
echo "[deploy] ERROR: port $PORT not listening after start. Check $LOG_FILE"
exit 1

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +0,0 @@
.account-nav {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.nav-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.nav-btn {
background: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
min-width: 60px;
}
.nav-btn:hover {
background: var(--accent-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.nav-btn.disconnect-btn {
background: #6c757d;
}
.nav-btn.disconnect-btn:hover {
background: #5a6268;
}
.nav-btn.delete-btn {
background: #dc3545;
}
.nav-btn.delete-btn:hover {
background: #c82333;
}
/* Responsive design */
@media (max-width: 768px) {
.account-nav {
top: 10px;
right: 10px;
left: 10px;
padding: 10px;
}
.nav-actions {
gap: 6px;
}
.nav-btn {
padding: 6px 10px;
font-size: 11px;
min-width: 50px;
}
}

View File

@ -1,17 +0,0 @@
<div class="account-nav">
<div class="nav-actions">
<button class="nav-btn" onclick="importJSON()" title="Import backup">📥 Import</button>
<button class="nav-btn" onclick="createBackUp()" title="Export backup">📤 Export</button>
<button class="nav-btn" onclick="navigate('chat')" title="Chat">💬 Chat</button>
<button class="nav-btn" onclick="navigate('signature')" title="Signatures">
✍️ Signatures
</button>
<button class="nav-btn" onclick="navigate('process')" title="Process">⚙️ Process</button>
<button class="nav-btn disconnect-btn" onclick="disconnect()" title="Disconnect">
🚪 Disconnect
</button>
<button class="nav-btn delete-btn" onclick="deleteAccount()" title="Delete account">
🗑️ Delete Account
</button>
</div>
</div>

View File

@ -1,845 +0,0 @@
import Services from '../../services/service';
import { addressToWords } from '../../utils/sp-address.utils';
import { secureLogger } from '../../services/secure-logger';
// Global function declarations
declare global {
interface Window {
importJSON: () => Promise<void>;
createBackUp: () => Promise<void>;
}
}
export class DeviceManagementComponent extends HTMLElement {
private service: Services | null = null;
private currentDeviceWords: string = '';
private pairedDevices: string[] = [];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.init();
}
async init() {
this.service = await Services.getInstance();
await this.loadDeviceData();
this.render();
this.attachEventListeners();
this.injectAccountNav();
}
async loadDeviceData() {
if (!this.service) {return;}
try {
// Get current device address and generate 4 words
const currentAddress = await this.service.getDeviceAddress();
if (currentAddress) {
this.currentDeviceWords = await addressToWords(currentAddress);
}
// Get paired devices from the pairing process
const pairingProcessId = this.service.getPairingProcessId();
if (pairingProcessId) {
const process = await this.service.getProcess(pairingProcessId);
if (process && process.states && process.states.length > 0) {
const lastState = process.states[process.states.length - 1];
const publicData = lastState.public_data;
if (publicData && publicData['pairedAddresses']) {
this.pairedDevices = this.service.decodeValue(publicData['pairedAddresses']) || [];
}
}
}
} catch (error) {
secureLogger.error('Error loading device data', error as Error, { component: 'DeviceManagement' });
}
}
render() {
this.shadowRoot!.innerHTML = `
<style>
:host {
display: block;
font-family: Arial, sans-serif;
}
.device-management {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #3a506b;
margin: 0 0 10px 0;
font-size: 28px;
}
.header p {
color: #666;
margin: 0 0 20px 0;
font-size: 16px;
}
.contract-description {
background: rgba(58, 80, 107, 0.05);
border-radius: 8px;
padding: 20px;
margin-top: 20px;
border-left: 4px solid #3a506b;
}
.contract-description p {
margin: 0 0 10px 0;
color: #3a506b;
font-size: 14px;
}
.contract-description ul {
margin: 0;
padding-left: 20px;
}
.contract-description li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
line-height: 1.4;
}
.current-device {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
border-left: 4px solid #3a506b;
}
.current-device h3 {
margin: 0 0 15px 0;
color: #3a506b;
font-size: 18px;
}
.words-display {
background: white;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: bold;
color: #3a506b;
text-align: center;
margin: 10px 0;
word-spacing: 8px;
}
.copy-btn {
background: #3a506b;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
.copy-btn:hover {
background: #2c3e50;
}
.paired-devices {
margin-bottom: 30px;
}
.paired-devices h3 {
color: #3a506b;
margin: 0 0 15px 0;
font-size: 18px;
}
.device-list {
list-style: none;
padding: 0;
margin: 0;
}
.device-item {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.device-info {
flex: 1;
}
.device-address {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #666;
word-break: break-all;
}
.remove-btn {
background: #f44336;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.remove-btn:hover {
background: #d32f2f;
}
.add-device {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.add-device h3 {
margin: 0 0 15px 0;
color: #3a506b;
font-size: 18px;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: 500;
}
.words-input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
font-family: 'Courier New', monospace;
box-sizing: border-box;
}
.words-input:focus {
outline: none;
border-color: #3a506b;
}
.input-hint {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #3a506b;
color: white;
}
.btn-primary:hover {
background: #2c3e50;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.delete-account-btn {
background: #f44336 !important;
color: white !important;
font-weight: bold;
border: 2px solid #d32f2f;
}
.delete-account-btn:hover {
background: #d32f2f !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3);
}
.status-message {
padding: 10px;
border-radius: 6px;
margin: 10px 0;
font-size: 14px;
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.import-export {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.import-export .btn {
flex: 1;
min-width: 120px;
}
.import-export .btn-danger {
flex: 1.2;
min-width: 150px;
}
.import-export .btn-critical {
flex: 1.3;
min-width: 180px;
background: #dc3545;
color: white;
border: 2px solid #dc3545;
font-weight: bold;
}
.import-export .btn-critical:hover {
background: #c82333;
border-color: #c82333;
}
</style>
<div class="device-management">
<div class="header">
<h1>🔐 Contrat de Pairing</h1>
<p>Gestion sécurisée de vos devices avec authentification 4 mots</p>
<div class="contract-description">
<p><strong>📋 Description du contrat :</strong></p>
<ul>
<li>🔐 <strong>Sécurité :</strong> Chaque device est authentifié par 4 mots uniques</li>
<li>🔗 <strong>Pairing :</strong> Connexion sécurisée entre devices approuvés</li>
<li>🛡 <strong>Protection :</strong> Au moins 1 device doit toujours rester actif</li>
<li>🔄 <strong>Gestion :</strong> Ajout/suppression de devices en temps réel</li>
</ul>
</div>
</div>
<div class="import-export">
<button class="btn btn-secondary" id="importBtn">📥 Importer</button>
<button class="btn btn-secondary" id="exportBtn">📤 Exporter</button>
<button class="btn btn-critical" id="criticalExportBtn">🚨 Export Critique (Clé Privée)</button>
<button class="btn btn-danger" id="deleteAccountBtn">🗑 Supprimer le Compte</button>
</div>
<div class="current-device">
<h3>📱 Device Actuel</h3>
<p>Vos 4 mots d'authentification :</p>
<div class="words-display" id="currentWords">${this.currentDeviceWords}</div>
<button class="copy-btn" id="copyCurrentWords">📋 Copier</button>
</div>
<div class="paired-devices">
<h3>🔗 Devices Appairés (${this.pairedDevices.length})</h3>
<ul class="device-list" id="deviceList">
${this.pairedDevices
.map(
(address, index) => `
<li class="device-item">
<div class="device-info">
<strong>Device ${index + 1}</strong>
<div class="device-address">${address}</div>
</div>
${
this.pairedDevices.length > 1
? `
<button class="remove-btn" data-address="${address}">🗑 Supprimer</button>
`
: ''
}
</li>
`
)
.join('')}
</ul>
</div>
<div class="add-device">
<h3> Ajouter un Device</h3>
<div class="input-group">
<label for="newDeviceWords">4 mots du nouveau device :</label>
<input
type="text"
id="newDeviceWords"
class="words-input"
placeholder="Entrez les 4 mots (ex: abandon ability able about)"
autocomplete="off"
spellcheck="false"
/>
<div class="input-hint">Séparez les mots par des espaces</div>
</div>
<button class="btn btn-primary" id="addDeviceBtn" disabled> Ajouter Device</button>
</div>
<div class="button-group">
<button class="btn btn-success" id="saveChangesBtn" disabled>💾 Sauvegarder</button>
<button class="btn btn-secondary" id="cancelChangesBtn" disabled> Annuler</button>
</div>
<div id="statusMessage"></div>
</div>
`;
}
attachEventListeners() {
// Copy current words
this.shadowRoot!.getElementById('copyCurrentWords')?.addEventListener('click', () => {
navigator.clipboard.writeText(this.currentDeviceWords);
this.showStatus('4 mots copiés dans le presse-papiers !', 'success');
});
// Import/Export buttons
this.shadowRoot!.getElementById('importBtn')?.addEventListener('click', () => {
this.importAccount();
});
this.shadowRoot!.getElementById('exportBtn')?.addEventListener('click', () => {
this.exportAccount();
});
// Critical export button
this.shadowRoot!.getElementById('criticalExportBtn')?.addEventListener('click', () => {
this.criticalExport();
});
// Delete account button
this.shadowRoot!.getElementById('deleteAccountBtn')?.addEventListener('click', () => {
this.deleteAccount();
});
// Add device input validation
const wordsInput = this.shadowRoot!.getElementById('newDeviceWords') as HTMLInputElement;
const addBtn = this.shadowRoot!.getElementById('addDeviceBtn') as HTMLButtonElement;
wordsInput?.addEventListener('input', () => {
const words = wordsInput.value.trim();
const isValid = this.validateWords(words);
addBtn.disabled = !isValid;
if (words && !isValid) {
wordsInput.style.borderColor = '#f44336';
} else {
wordsInput.style.borderColor = '#e0e0e0';
}
});
// Add device button
addBtn?.addEventListener('click', () => {
this.addDevice();
});
// Save/Cancel buttons
this.shadowRoot!.getElementById('saveChangesBtn')?.addEventListener('click', () => {
this.saveChanges();
});
this.shadowRoot!.getElementById('cancelChangesBtn')?.addEventListener('click', () => {
this.cancelChanges();
});
// Remove device buttons (delegated event listener)
this.shadowRoot!.addEventListener('click', e => {
const target = e.target as HTMLElement;
if (target.classList.contains('remove-btn')) {
const address = target.getAttribute('data-address');
if (address) {
this.removeDevice(address);
}
}
});
}
validateWords(words: string): boolean {
const wordArray = words.trim().split(/\s+/);
return wordArray.length === 4 && wordArray.every(word => word.length > 0);
}
async addDevice() {
const wordsInput = this.shadowRoot!.getElementById('newDeviceWords') as HTMLInputElement;
const words = wordsInput.value.trim();
if (!this.validateWords(words)) {
this.showStatus(
'❌ Format invalide. Entrez exactement 4 mots séparés par des espaces.',
'error'
);
return;
}
try {
// Convert words back to address (this would need to be implemented)
// For now, we'll simulate adding a device
const newAddress = `tsp1${Math.random().toString(36).substr(2, 9)}...`;
this.pairedDevices.push(newAddress);
this.showStatus(`✅ Device ajouté avec succès !`, 'success');
this.updateUI();
this.enableSaveButton();
// Clear input
wordsInput.value = '';
wordsInput.style.borderColor = '#e0e0e0';
} catch (error) {
this.showStatus(`❌ Erreur lors de l'ajout du device: ${error}`, 'error');
}
}
removeDevice(address: string) {
if (this.pairedDevices.length <= 1) {
this.showStatus(
'❌ Impossible de supprimer le dernier device. Il doit en rester au moins un.',
'error'
);
return;
}
this.pairedDevices = this.pairedDevices.filter(addr => addr !== address);
this.updateUI();
this.enableSaveButton();
this.showStatus('✅ Device supprimé de la liste', 'success');
}
updateUI() {
const deviceList = this.shadowRoot!.getElementById('deviceList');
if (deviceList) {
deviceList.innerHTML = this.pairedDevices
.map(
(address, index) => `
<li class="device-item">
<div class="device-info">
<strong>Device ${index + 1}</strong>
<div class="device-address">${address}</div>
</div>
${
this.pairedDevices.length > 1
? `
<button class="remove-btn" data-address="${address}">🗑 Supprimer</button>
`
: ''
}
</li>
`
)
.join('');
}
}
enableSaveButton() {
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
if (saveBtn) {saveBtn.disabled = false;}
if (cancelBtn) {cancelBtn.disabled = false;}
}
async saveChanges() {
if (!this.service) {return;}
try {
// Update the pairing process with new devices
const pairingProcessId = this.service.getPairingProcessId();
if (pairingProcessId) {
// This would need to be implemented to update the process
this.showStatus('✅ Modifications sauvegardées !', 'success');
this.disableSaveButtons();
}
} catch (error) {
this.showStatus(`❌ Erreur lors de la sauvegarde: ${error}`, 'error');
}
}
cancelChanges() {
// Reload original data
this.loadDeviceData();
this.render();
this.attachEventListeners();
this.disableSaveButtons();
this.showStatus('❌ Modifications annulées', 'success');
}
disableSaveButtons() {
const saveBtn = this.shadowRoot!.getElementById('saveChangesBtn') as HTMLButtonElement;
const cancelBtn = this.shadowRoot!.getElementById('cancelChangesBtn') as HTMLButtonElement;
if (saveBtn) {saveBtn.disabled = true;}
if (cancelBtn) {cancelBtn.disabled = true;}
}
async importAccount() {
try {
// Create file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async e => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
try {
const text = await file.text();
const _data = JSON.parse(text);
// Data parsed but not used yet (for future use)
// Import the account data
if (window.importJSON) {
await window.importJSON();
this.showStatus('✅ Compte importé avec succès !', 'success');
// Reload the page to apply changes
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
this.showStatus("❌ Fonction d'import non disponible", 'error');
}
} catch (error) {
this.showStatus(`❌ Erreur lors de l'import: ${error}`, 'error');
}
}
};
input.click();
} catch (error) {
this.showStatus(`❌ Erreur lors de l'import: ${error}`, 'error');
}
}
async exportAccount() {
try {
if (window.createBackUp) {
await window.createBackUp();
this.showStatus('✅ Compte exporté avec succès !', 'success');
} else {
this.showStatus("❌ Fonction d'export non disponible", 'error');
}
} catch (error) {
this.showStatus(`❌ Erreur lors de l'export: ${error}`, 'error');
}
}
async criticalExport() {
// Triple confirmation for critical export
const confirm1 = confirm(
'🚨 EXPORT CRITIQUE: Cette action va exposer votre CLÉ PRIVÉE.\n\nCette clé permet de signer des transactions sans interaction sur le 2ème device.\n\nÊtes-vous sûr de vouloir continuer ?'
);
if (!confirm1) {return;}
const confirm2 = confirm(
'⚠️ SÉCURITÉ: Votre clé privée sera visible en clair.\n\nAssurez-vous que personne ne peut voir votre écran.\n\nContinuer ?'
);
if (!confirm2) {return;}
const confirm3 = prompt(
'🔐 DERNIÈRE CONFIRMATION: Cette clé privée donne un accès TOTAL à votre compte.\n\nTapez "EXPORTER" pour confirmer:'
);
if (confirm3 !== 'EXPORTER') {
alert('❌ Export critique annulé');
return;
}
try {
// Get the device's private key
// @ts-ignore - deviceRaw is guaranteed to be non-null after the check below
const deviceRaw = await this.service.getDeviceFromDatabase();
if (!deviceRaw?.sp_wallet) {
throw new Error('Device ou clé privée non trouvée');
}
// TypeScript assertion: deviceRaw is guaranteed to be non-null after the check
const device = deviceRaw!;
// Create critical export data
const criticalData = {
type: 'CRITICAL_EXPORT',
timestamp: new Date().toISOString(),
device_address: device.sp_wallet.address,
private_key: device.sp_wallet.private_key,
pairing_commitment: device.pairing_process_commitment,
warning:
'ATTENTION: Cette clé privée donne un accès total au compte. Gardez-la SECRÈTE et SÉCURISÉE.',
instructions: [
'1. Sauvegardez cette clé dans un endroit sûr',
'2. Ne la partagez JAMAIS avec qui que ce soit',
'3. Utilisez-la uniquement pour signer des transactions critiques',
'4. En cas de compromission, changez immédiatement votre compte',
],
};
// Create and download the file
const blob = new Blob([JSON.stringify(criticalData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `critical-export-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showStatus('🚨 Export critique généré - Clé privée exposée !', 'error');
// Show additional warning
setTimeout(() => {
alert(
'🚨 EXPORT CRITIQUE TERMINÉ\n\nVotre clé privée a été exportée.\n\n⚠ GARDEZ CE FICHIER SÉCURISÉ !'
);
}, 1000);
} catch (error) {
this.showStatus(`❌ Erreur lors de l'export critique: ${error}`, 'error');
}
}
async deleteAccount() {
// First confirmation
if (
!confirm(
'⚠️ Êtes-vous sûr de vouloir supprimer complètement votre compte ?\n\nCette action est IRRÉVERSIBLE et supprimera :\n• Tous vos processus\n• Toutes vos données\n• Votre wallet\n• Votre historique\n\nTapez "SUPPRIMER" pour confirmer.'
)
) {
return;
}
// Second confirmation with text input
const confirmation = prompt('Tapez "SUPPRIMER" pour confirmer la suppression :');
if (confirmation !== 'SUPPRIMER') {
this.showStatus(
'❌ Suppression annulée. Le texte de confirmation ne correspond pas.',
'error'
);
return;
}
try {
if (!this.service) {
this.showStatus('❌ Service non disponible', 'error');
return;
}
// Show loading status
this.showStatus('🗑️ Suppression du compte en cours...', 'success');
// Delete the account
await this.service.deleteAccount();
// Show success message
this.showStatus('✅ Compte supprimé avec succès ! Redirection en cours...', 'success');
// Reload the page to restart the application
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (error) {
secureLogger.error('Erreur lors de la suppression du compte', error as Error, { component: 'DeviceManagement' });
this.showStatus(`❌ Erreur lors de la suppression du compte: ${error}`, 'error');
}
}
showStatus(message: string, type: 'success' | 'error') {
const statusDiv = this.shadowRoot!.getElementById('statusMessage');
if (statusDiv) {
statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
}
}
async injectAccountNav() {
try {
// Load account navigation HTML
const navHtml = await fetch('/src/components/account-nav/account-nav.html').then(res =>
res.text()
);
// Create a container for the navigation
const navContainer = document.createElement('div');
navContainer.innerHTML = navHtml;
navContainer.className = 'account-nav-container';
// Add CSS styles
const style = document.createElement('style');
const cssResponse = await fetch('/src/components/account-nav/account-nav.css');
const cssText = await cssResponse.text();
style.textContent = cssText;
navContainer.appendChild(style);
// Add to document body
document.body.appendChild(navContainer);
secureLogger.info('Account navigation injected', { component: 'DeviceManagement' });
} catch (error) {
secureLogger.error('Error injecting account navigation', error as Error, { component: 'DeviceManagement' });
}
}
}
customElements.define('device-management', DeviceManagementComponent);

View File

@ -0,0 +1,36 @@
<div class="nav-wrapper">
<div id="profile-header-container"></div>
<div class="brand-logo">4NK</div>
<div class="nav-right-icons">
<div class="notification-container">
<div class="bell-icon">
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
/>
</svg>
</div>
<div class="notification-badge"></div>
<div id="notification-board" class="notification-board">
<div class="no-notification">No notifications available</div>
</div>
</div>
<div class="burger-menu">
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
</svg>
<div class="menu-content" id="menu">
<!-- <a onclick="unpair()">Revoke</a> -->
<a onclick="importJSON()">Import</a>
<a onclick="createBackUp()">Export</a>
<a onclick="navigate('account')">Account</a>
<a onclick="navigate('chat')">Chat</a>
<a onclick="navigate('signature')">Signatures</a>
<a onclick="navigate('process')">Process</a>
<a onclick="disconnect()">Disconnect</a>
</div>
</div>
</div>
</div>

220
src/components/header/header.ts Executable file
View File

@ -0,0 +1,220 @@
import ModalService from '~/services/modal.service';
import { INotification } from '../../models/notification.model';
import { currentRoute, navigate } from '../../router';
import Services from '../../services/service';
import { BackUp } from '~/models/backup.model';
let notifications = [];
export async function unpair() {
const service = await Services.getInstance();
await service.unpairDevice();
navigate('home');
}
(window as any).unpair = unpair;
function toggleMenu() {
const menu = document.getElementById('menu');
if (menu) {
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
}
}
(window as any).toggleMenu = toggleMenu;
async function getNotifications() {
const service = await Services.getInstance();
notifications = service.getNotifications();
return notifications;
}
function openCloseNotifications() {
const notifications = document.querySelector('.notification-board') as HTMLDivElement;
notifications.style.display = notifications?.style.display === 'none' ? 'block' : 'none';
}
(window as any).openCloseNotifications = openCloseNotifications;
export async function initHeader() {
if (currentRoute === 'account') {
// Charger le profile-header
const profileContainer = document.getElementById('profile-header-container');
if (profileContainer) {
const profileHeaderHtml = await fetch('/src/components/profile-header/profile-header.html').then((res) => res.text());
profileContainer.innerHTML = profileHeaderHtml;
// Initialiser les données du profil
loadUserProfile();
}
}
if (currentRoute === 'home') {
hideSomeFunctionnalities();
} else {
fetchNotifications();
setInterval(fetchNotifications, 2 * 60 * 1000);
}
}
function hideSomeFunctionnalities() {
const bell = document.querySelector('.bell-icon') as HTMLDivElement;
if (bell) bell.style.display = 'none';
const notifBadge = document.querySelector('.notification-badge') as HTMLDivElement;
if (notifBadge) notifBadge.style.display = 'none';
const actions = document.querySelectorAll('.menu-content a') as NodeListOf<HTMLAnchorElement>;
const excludedActions = ['Import', 'Export'];
for (const action of actions) {
if (!excludedActions.includes(action.innerHTML)) {
action.style.display = 'none';
}
}
}
async function setNotification(notifications: any[]): Promise<void> {
const badge = document.querySelector('.notification-badge') as HTMLDivElement;
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
if (notifications?.length) {
badge.innerText = notifications.length.toString();
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
notificationBoard.querySelectorAll('.notification-element')?.forEach((elem) => elem.remove());
noNotifications.style.display = 'none';
for (const notif of notifications) {
const notifElement = document.createElement('div');
notifElement.className = 'notification-element';
notifElement.setAttribute('notif-id', notif.processId);
notifElement.innerHTML = `
<div>Validation required : </div>
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
`;
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
notificationBoard.appendChild(notifElement);
notifElement.addEventListener('click', async () => {
const modalService = await ModalService.getInstance();
modalService.injectValidationModal(notif);
});
}
} else {
noNotifications.style.display = 'block';
}
}
async function fetchNotifications() {
const service = await Services.getInstance();
const data = service.getNotifications();
setNotification(data);
}
async function loadUserProfile() {
// Charger les données du profil depuis le localStorage
const userName = localStorage.getItem('userName');
const userLastName = localStorage.getItem('userLastName');
const userAvatar = localStorage.getItem('userAvatar') || 'https://via.placeholder.com/150';
const userBanner = localStorage.getItem('userBanner') || 'https://via.placeholder.com/800x200';
// Mettre à jour les éléments du DOM
const nameElement = document.querySelector('.user-name');
const lastNameElement = document.querySelector('.user-lastname');
const avatarElement = document.querySelector('.avatar');
const bannerElement = document.querySelector('.banner-image');
if (nameElement) nameElement.textContent = userName;
if (lastNameElement) lastNameElement.textContent = userLastName;
if (avatarElement) (avatarElement as HTMLImageElement).src = userAvatar;
if (bannerElement) (bannerElement as HTMLImageElement).src = userBanner;
}
async function importJSON() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const content: BackUp = JSON.parse(e.target?.result as string);
const service = await Services.getInstance();
await service.importJSON(content);
alert('Import réussi');
window.location.reload();
} catch (error) {
alert("Erreur lors de l'import: " + error);
}
};
reader.readAsText(file);
}
};
input.click();
}
(window as any).importJSON = importJSON;
async function createBackUp() {
const service = await Services.getInstance();
const backUp = await service.createBackUp();
if (!backUp) {
console.error("No device to backup");
return;
}
try {
const backUpJson = JSON.stringify(backUp, null, 2)
const blob = new Blob([backUpJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '4nk-backup.json';
a.click();
URL.revokeObjectURL(url);
console.log('Backup successfully prepared for download');
} catch (e) {
console.error(e);
}
}
(window as any).createBackUp = createBackUp;
async function disconnect() {
console.log('Disconnecting...');
try {
localStorage.clear();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase('4nk');
request.onsuccess = () => {
console.log('IndexedDB deleted successfully');
resolve();
};
request.onerror = () => reject(request.error);
request.onblocked = () => {
console.log('Database deletion was blocked');
resolve();
};
});
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(registration => registration.unregister()));
console.log('Service worker unregistered');
navigate('home');
setTimeout(() => {
window.location.href = window.location.origin;
}, 100);
} catch (error) {
console.error('Error during disconnect:', error);
// force reload
window.location.href = window.location.origin;
}
}
(window as any).disconnect = disconnect;

View File

@ -1,143 +0,0 @@
import { MessageType } from '../../models/process.model';
import { secureLogger } from '../../services/secure-logger';
export class IframePairingComponent {
private iframe: HTMLIFrameElement | null = null;
private isReady = false;
private messageId = 0;
constructor() {
this.init();
}
private init() {
// Listen for messages from iframe
window.addEventListener('message', this.handleMessage.bind(this));
}
private handleMessage(event: MessageEvent) {
const { type, data } = event.data;
switch (type) {
case 'IFRAME_READY':
secureLogger.info('Iframe pairing service is ready', { component: 'IframePairingComponent' });
this.isReady = true;
break;
case MessageType.PAIRING_4WORDS_WORDS_GENERATED:
this.onWordsGenerated(data);
break;
case MessageType.PAIRING_4WORDS_STATUS_UPDATE:
this.onStatusUpdate(data);
break;
case MessageType.PAIRING_4WORDS_SUCCESS:
this.onPairingSuccess(data);
break;
case MessageType.PAIRING_4WORDS_ERROR:
this.onPairingError(data);
break;
}
}
public createHiddenIframe(): void {
if (this.iframe) {
return; // Already created
}
// Create hidden iframe
this.iframe = document.createElement('iframe');
this.iframe.src = '/src/pages/iframe-pairing.html';
this.iframe.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
width: 1px;
height: 1px;
border: none;
opacity: 0;
pointer-events: none;
`;
document.body.appendChild(this.iframe);
secureLogger.debug('Hidden iframe created for pairing', { component: 'IframePairingComponent' });
}
public async createPairing(): Promise<void> {
if (!this.isReady) {
throw new Error('Iframe pairing service not ready');
}
const messageId = ++this.messageId;
this.iframe?.contentWindow?.postMessage(
{
type: MessageType.PAIRING_4WORDS_CREATE,
data: {},
messageId,
},
'*'
);
}
public async joinPairing(words: string): Promise<void> {
if (!this.isReady) {
throw new Error('Iframe pairing service not ready');
}
const messageId = ++this.messageId;
this.iframe?.contentWindow?.postMessage(
{
type: MessageType.PAIRING_4WORDS_JOIN,
data: { words },
messageId,
},
'*'
);
}
private onWordsGenerated(data: any) {
secureLogger.info('4 words generated', { component: 'IframePairingComponent', hasWords: !!data.words });
// Emit custom event for the parent application
window.dispatchEvent(
new CustomEvent('pairing-words-generated', {
detail: { words: data.words },
})
);
}
private onStatusUpdate(data: any) {
secureLogger.debug('Pairing status update', { component: 'IframePairingComponent', status: data.status });
// Emit custom event for the parent application
window.dispatchEvent(
new CustomEvent('pairing-status-update', {
detail: { status: data.status, type: data.type },
})
);
}
private onPairingSuccess(data: any) {
secureLogger.info('Pairing successful', { component: 'IframePairingComponent', message: data.message });
// Emit custom event for the parent application
window.dispatchEvent(
new CustomEvent('pairing-success', {
detail: { message: data.message },
})
);
}
private onPairingError(data: any) {
secureLogger.error('Pairing error', { component: 'IframePairingComponent', error: data.error });
// Emit custom event for the parent application
window.dispatchEvent(
new CustomEvent('pairing-error', {
detail: { error: data.error },
})
);
}
public destroy(): void {
if (this.iframe) {
this.iframe.remove();
this.iframe = null;
}
this.isReady = false;
}
}

View File

@ -1,14 +1,14 @@
<div id="login-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="confirmation-box">
<div class="message">
Attempting to pair device with address
<strong>{{device1}}</strong>
with device with address
<strong>{{device2}}</strong>
</div>
<div>Awaiting pairing validation...</div>
</div>
</div>
</div>
<div id="login-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="confirmation-box">
<div class="message">
Attempting to pair device with address
<strong>{{device1}}</strong>
with device with address
<strong>{{device2}}</strong>
</div>
<div>Awaiting pairing validation...</div>
</div>
</div>
</div>

View File

@ -1,15 +1,13 @@
import Routing from '/src/services/routing.service.ts';
const router = await Routing.getInstance();
export async function confirmLogin() {
router.confirmLogin();
}
export async function closeLoginModal() {
router.closeLoginModal();
}
/* eslint-disable no-undef */
window.confirmLogin = confirmLogin;
window.closeLoginModal = closeLoginModal;
/* eslint-enable no-undef */
import Routing from '/src/services/routing.service.ts';
const router = await Routing.getInstance();
export async function confirmLogin() {
router.confirmLogin();
}
export async function closeLoginModal() {
router.closeLoginModal();
}
window.confirmLogin = confirmLogin;
window.closeLoginModal = closeLoginModal;

View File

@ -0,0 +1,16 @@
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to pair device?<br />
Attempting to pair device with address <br />
<strong>{{device1}}</strong> <br />
with device with address <br />
<strong>{{device2}}</strong>
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import ModalService from '../../services/modal.service';
const modalService = await ModalService.getInstance();
export async function confirm() {
modalService.confirmPairing();
}
export async function closeConfirmationModal() {
modalService.closeConfirmationModal();
}
(window as any).confirm = confirm;
(window as any).closeConfirmationModal = closeConfirmationModal;

View File

@ -0,0 +1,14 @@
<div id="creation-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Do you want to create a 4NK member?<br />
Attempting to create a member with address <br />
<strong>{{device1}}</strong> <br />
</div>
<div class="confirmation-box">
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
<div id="waiting-modal" class="modal">
<div class="modal-content">
<div class="modal-title">Login</div>
<div class="message">
Waiting for Device 2...
</div>
</div>
</div>

View File

@ -0,0 +1,73 @@
import QrScanner from 'qr-scanner';
import Services from '../../services/service';
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
export default class QrScannerComponent extends HTMLElement {
videoElement: any;
wrapper: any;
qrScanner: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.wrapper = document.createElement('div');
this.wrapper.style.position = 'relative';
this.wrapper.style.width = '150px';
this.wrapper.style.height = '150px';
this.videoElement = document.createElement('video');
this.videoElement.style.width = '100%';
document.body?.append(this.wrapper);
this.wrapper.prepend(this.videoElement);
}
connectedCallback() {
this.initializeScanner();
}
async initializeScanner() {
if (!this.videoElement) {
console.error('Video element not found!');
return;
}
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
highlightScanRegion: true,
highlightCodeOutline: true,
});
try {
await QrScanner.hasCamera();
this.qrScanner.start();
this.videoElement.style = 'height: 200px; width: 200px';
this.shadowRoot?.appendChild(this.wrapper);
} catch (e) {
console.error('No camera found or error starting the QR scanner', e);
}
}
async onQrCodeScanned(result: any) {
console.log(`QR Code detected:`, result);
const data = result.data;
const scannedUrl = new URL(data);
// Extract the 'sp_address' parameter
const spAddress = scannedUrl.searchParams.get('sp_address');
if (spAddress) {
// Call the sendPairingTx function with the extracted sp_address
try {
await prepareAndSendPairingTx(spAddress);
} catch (e) {
console.error('Failed to pair:', e);
}
}
this.qrScanner.stop(); // if you want to stop scanning after one code is detected
}
disconnectedCallback() {
if (this.qrScanner) {
this.qrScanner.destroy();
}
}
}
customElements.define('qr-scanner', QrScannerComponent);

View File

@ -1,264 +0,0 @@
.secure-credentials-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.credentials-header {
text-align: center;
margin-bottom: 30px;
}
.credentials-header h2 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 2rem;
}
.credentials-description {
color: #6c757d;
font-size: 1.1rem;
line-height: 1.5;
}
.credentials-section {
background: white;
padding: 25px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.credentials-section h3 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 1.3rem;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.password-strength {
margin-top: 8px;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 500;
}
.password-strength.weak {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.password-strength.medium {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.password-strength.strong {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.credentials-info {
background: #f8f9fa;
padding: 20px;
border-radius: 6px;
margin-bottom: 20px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
border-bottom: none;
}
.info-item .label {
font-weight: 600;
color: #495057;
}
.info-item .value {
color: #6c757d;
font-family: 'Courier New', monospace;
}
.credentials-actions {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.credentials-messages {
margin-top: 20px;
}
.message {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 10px;
font-weight: 500;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.message.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.security-indicator {
margin-top: 30px;
text-align: center;
}
.security-badge {
display: inline-flex;
align-items: center;
gap: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 25px;
border-radius: 25px;
font-weight: 600;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.security-icon {
font-size: 1.2rem;
}
.security-text {
font-size: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.secure-credentials-container {
padding: 15px;
}
.credentials-section {
padding: 20px;
}
.credentials-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}

View File

@ -1,108 +0,0 @@
<div class="secure-credentials-container">
<div class="credentials-header">
<h2>🔐 Credentials Sécurisés</h2>
<p class="credentials-description">
Gestion sécurisée des clés de spend et de scan avec PBKDF2 et credentials du navigateur
</p>
</div>
<div class="credentials-actions">
<!-- Création de credentials -->
<div id="create-credentials-section" class="credentials-section">
<h3>Créer de nouveaux credentials</h3>
<form id="create-credentials-form">
<div class="form-group">
<label for="password">Mot de passe sécurisé :</label>
<input
type="password"
id="password"
name="password"
placeholder="Entrez un mot de passe fort"
required
minlength="8"
/>
<div id="password-strength" class="password-strength"></div>
</div>
<div class="form-group">
<label for="confirm-password">Confirmer le mot de passe :</label>
<input
type="password"
id="confirm-password"
name="confirm-password"
placeholder="Confirmez le mot de passe"
required
/>
</div>
<button type="submit" id="create-credentials-btn" class="btn btn-primary">
🔐 Créer les credentials sécurisés
</button>
</form>
</div>
<!-- Accès aux credentials existants -->
<div id="access-credentials-section" class="credentials-section" style="display: none;">
<h3>Accéder aux credentials existants</h3>
<form id="access-credentials-form">
<div class="form-group">
<label for="access-password">Mot de passe :</label>
<input
type="password"
id="access-password"
name="access-password"
placeholder="Entrez votre mot de passe"
required
/>
</div>
<button type="submit" id="access-credentials-btn" class="btn btn-secondary">
🔓 Accéder aux credentials
</button>
</form>
</div>
<!-- Gestion des credentials -->
<div id="manage-credentials-section" class="credentials-section" style="display: none;">
<h3>Gestion des credentials</h3>
<div class="credentials-info">
<div class="info-item">
<span class="label">Status :</span>
<span id="credentials-status" class="value">Chargement...</span>
</div>
<div class="info-item">
<span class="label">Clé de spend :</span>
<span id="spend-key-status" class="value">-</span>
</div>
<div class="info-item">
<span class="label">Clé de scan :</span>
<span id="scan-key-status" class="value">-</span>
</div>
<div class="info-item">
<span class="label">Créé le :</span>
<span id="credentials-timestamp" class="value">-</span>
</div>
</div>
<div class="credentials-actions">
<button id="refresh-credentials-btn" class="btn btn-info">
🔄 Actualiser
</button>
<button id="delete-credentials-btn" class="btn btn-danger">
🗑️ Supprimer les credentials
</button>
</div>
</div>
</div>
<!-- Messages de statut -->
<div id="credentials-messages" class="credentials-messages"></div>
<!-- Indicateur de sécurité -->
<div class="security-indicator">
<div class="security-badge">
<span class="security-icon">🛡️</span>
<span class="security-text">Credentials sécurisés avec PBKDF2</span>
</div>
</div>
</div>

View File

@ -1,392 +0,0 @@
/**
* SecureCredentialsComponent - Composant pour la gestion des credentials sécurisés
* Interface utilisateur pour la gestion des clés de spend et de scan avec PBKDF2
*/
import { SecureCredentialsService } from '../../services/secure-credentials.service';
import { secureLogger } from '../../services/secure-logger';
import { eventBus } from '../../services/event-bus';
export class SecureCredentialsComponent {
private container: HTMLElement | null = null;
private isInitialized = false;
constructor() {
this.init();
}
/**
* Initialise le composant
*/
private async init(): Promise<void> {
try {
this.container = document.getElementById('secure-credentials-container');
if (!this.container) {
throw new Error('Secure credentials container not found');
}
await this.loadHTML();
await this.loadCSS();
this.attachEventListeners();
await this.updateUI();
this.isInitialized = true;
secureLogger.info('SecureCredentialsComponent initialized', {
component: 'SecureCredentialsComponent',
operation: 'init',
});
} catch (error) {
secureLogger.error('Failed to initialize SecureCredentialsComponent', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'init',
});
}
}
/**
* Charge le HTML du composant
*/
private async loadHTML(): Promise<void> {
try {
const response = await fetch('/src/components/secure-credentials/secure-credentials.html');
const html = await response.text();
this.container!.innerHTML = html;
} catch (error) {
secureLogger.error('Failed to load secure credentials HTML', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'loadHTML',
});
}
}
/**
* Charge le CSS du composant
*/
private async loadCSS(): Promise<void> {
try {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/src/components/secure-credentials/secure-credentials.css';
document.head.appendChild(link);
} catch (error) {
secureLogger.error('Failed to load secure credentials CSS', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'loadCSS',
});
}
}
/**
* Attache les écouteurs d'événements
*/
private attachEventListeners(): void {
// Formulaire de création de credentials
const createForm = document.getElementById('create-credentials-form') as HTMLFormElement;
if (createForm) {
createForm.addEventListener('submit', this.handleCreateCredentials.bind(this));
}
// Formulaire d'accès aux credentials
const accessForm = document.getElementById('access-credentials-form') as HTMLFormElement;
if (accessForm) {
accessForm.addEventListener('submit', this.handleAccessCredentials.bind(this));
}
// Validation du mot de passe en temps réel
const passwordInput = document.getElementById('password') as HTMLInputElement;
if (passwordInput) {
passwordInput.addEventListener('input', this.handlePasswordInput.bind(this));
}
// Boutons d'action
const refreshBtn = document.getElementById('refresh-credentials-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', this.handleRefreshCredentials.bind(this));
}
const deleteBtn = document.getElementById('delete-credentials-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', this.handleDeleteCredentials.bind(this));
}
// Écouter les événements du service
eventBus.on('credentials:created', this.handleCredentialsCreated.bind(this));
eventBus.on('credentials:deleted', this.handleCredentialsDeleted.bind(this));
}
/**
* Gère la création de credentials
*/
private async handleCreateCredentials(event: Event): Promise<void> {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const password = formData.get('password') as string;
const confirmPassword = formData.get('confirm-password') as string;
if (password !== confirmPassword) {
this.showMessage('Les mots de passe ne correspondent pas', 'error');
return;
}
try {
this.showMessage('Création des credentials en cours...', 'info');
// Générer les credentials
const secureCredentialsService = SecureCredentialsService.getInstance();
const credentials = await secureCredentialsService.generateSecureCredentials(password);
// Stocker les credentials
await secureCredentialsService.storeCredentials(credentials, password);
this.showMessage('Credentials créés et stockés avec succès !', 'success');
await this.updateUI();
// Émettre l'événement
eventBus.emit('credentials:created', { credentials });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
this.showMessage(`Erreur lors de la création des credentials: ${errorMessage}`, 'error');
secureLogger.error('Failed to create credentials', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'handleCreateCredentials',
});
}
}
/**
* Gère l'accès aux credentials
*/
private async handleAccessCredentials(event: Event): Promise<void> {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const password = formData.get('access-password') as string;
try {
this.showMessage('Récupération des credentials...', 'info');
const secureCredentialsService = SecureCredentialsService.getInstance();
const credentials = await secureCredentialsService.retrieveCredentials(password);
if (credentials) {
this.showMessage('Credentials récupérés avec succès !', 'success');
await this.updateCredentialsInfo(credentials);
await this.updateUI();
} else {
this.showMessage('Aucun credential trouvé ou mot de passe incorrect', 'error');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
this.showMessage(`Erreur lors de la récupération des credentials: ${errorMessage}`, 'error');
secureLogger.error('Failed to access credentials', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'handleAccessCredentials',
});
}
}
/**
* Gère la validation du mot de passe en temps réel
*/
private handlePasswordInput(event: Event): void {
const input = event.target as HTMLInputElement;
const password = input.value;
const strengthDiv = document.getElementById('password-strength');
if (strengthDiv) {
strengthDiv.className = 'password-strength';
if (password.length === 0) {
strengthDiv.textContent = '';
return;
}
// Simple password strength check
const score =
password.length >= 12 ? (password.length >= 16 ? 5 : 4) : password.length >= 8 ? 3 : 2;
if (score < 3) {
strengthDiv.className += ' weak';
strengthDiv.textContent = 'Mot de passe faible';
} else if (score < 5) {
strengthDiv.className += ' medium';
strengthDiv.textContent = 'Mot de passe moyen';
} else {
strengthDiv.className += ' strong';
strengthDiv.textContent = 'Mot de passe fort';
}
}
}
/**
* Gère l'actualisation des credentials
*/
private async handleRefreshCredentials(): Promise<void> {
try {
await this.updateUI();
this.showMessage('Credentials actualisés', 'success');
} catch {
this.showMessage("Erreur lors de l'actualisation", 'error');
}
}
/**
* Gère la suppression des credentials
*/
private async handleDeleteCredentials(): Promise<void> {
if (
!confirm(
'Êtes-vous sûr de vouloir supprimer tous les credentials ? Cette action est irréversible.'
)
) {
return;
}
try {
// TODO: Implement credentials deletion
secureLogger.warn('Credentials deletion requested but not implemented', {
component: 'SecureCredentials',
});
this.showMessage('Suppression des credentials non implémentée', 'warning');
await this.updateUI();
// Émettre l'événement
eventBus.emit('credentials:deleted');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
this.showMessage(`Erreur lors de la suppression: ${errorMessage}`, 'error');
secureLogger.error('Failed to delete credentials', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'handleDeleteCredentials',
});
}
}
/**
* Met à jour l'interface utilisateur
*/
private async updateUI(): Promise<void> {
try {
const secureCredentialsService = SecureCredentialsService.getInstance();
const hasCredentials = await secureCredentialsService.hasCredentials();
const createSection = document.getElementById('create-credentials-section');
const accessSection = document.getElementById('access-credentials-section');
const manageSection = document.getElementById('manage-credentials-section');
if (hasCredentials) {
createSection!.style.display = 'none';
accessSection!.style.display = 'block';
manageSection!.style.display = 'block';
} else {
createSection!.style.display = 'block';
accessSection!.style.display = 'none';
manageSection!.style.display = 'none';
}
// Mettre à jour le statut
const statusElement = document.getElementById('credentials-status');
if (statusElement) {
statusElement.textContent = hasCredentials ? 'Disponibles' : 'Non disponibles';
statusElement.className = hasCredentials ? 'value success' : 'value error';
}
} catch (error) {
secureLogger.error('Failed to update UI', error as Error, {
component: 'SecureCredentialsComponent',
operation: 'updateUI',
});
}
}
/**
* Met à jour les informations des credentials
*/
private async updateCredentialsInfo(credentials: any): Promise<void> {
const spendKeyStatus = document.getElementById('spend-key-status');
const scanKeyStatus = document.getElementById('scan-key-status');
const timestampElement = document.getElementById('credentials-timestamp');
if (spendKeyStatus) {
spendKeyStatus.textContent = credentials.spendKey ? 'Disponible' : 'Non disponible';
spendKeyStatus.className = credentials.spendKey ? 'value success' : 'value error';
}
if (scanKeyStatus) {
scanKeyStatus.textContent = credentials.scanKey ? 'Disponible' : 'Non disponible';
scanKeyStatus.className = credentials.scanKey ? 'value success' : 'value error';
}
if (timestampElement && credentials.timestamp) {
const date = new Date(credentials.timestamp);
timestampElement.textContent = date.toLocaleString();
}
}
/**
* Affiche un message à l'utilisateur
*/
private showMessage(message: string, type: 'success' | 'error' | 'warning' | 'info'): void {
const messagesContainer = document.getElementById('credentials-messages');
if (!messagesContainer) {
return;
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
messageDiv.textContent = message;
messagesContainer.appendChild(messageDiv);
// Supprimer le message après 5 secondes
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 5000);
}
/**
* Gère l'événement de création de credentials
*/
private handleCredentialsCreated(_data: any): void {
secureLogger.info('Credentials created', { component: 'SecureCredentials' });
this.showMessage('Credentials créés avec succès !', 'success');
this.updateUI();
}
/**
* Gère l'événement de suppression de credentials
*/
private handleCredentialsDeleted(): void {
this.showMessage('Credentials supprimés', 'info');
this.updateUI();
}
/**
* Détruit le composant
*/
destroy(): void {
if (this.isInitialized) {
// Nettoyer les écouteurs d'événements
eventBus.off('credentials:created', this.handleCredentialsCreated.bind(this));
eventBus.off('credentials:deleted', this.handleCredentialsDeleted.bind(this));
this.isInitialized = false;
secureLogger.info('SecureCredentialsComponent destroyed', {
component: 'SecureCredentialsComponent',
operation: 'destroy',
});
}
}
}
// Export du composant
export default SecureCredentialsComponent;

View File

@ -1,297 +0,0 @@
.security-mode-selector {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.security-mode-header {
text-align: center;
margin-bottom: 30px;
}
.security-mode-header h2 {
color: #2c3e50;
margin-bottom: 10px;
}
.security-mode-header p {
color: #7f8c8d;
font-size: 16px;
}
.security-options {
display: grid;
gap: 20px;
margin-bottom: 30px;
}
.security-option {
border: 2px solid #e1e8ed;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
background: #fff;
}
.security-option:hover {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15);
transform: translateY(-2px);
}
.security-option.selected {
border-color: #27ae60;
background: #f8fff8;
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15);
}
.option-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.option-icon {
font-size: 24px;
margin-right: 12px;
}
.option-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
flex: 1;
}
.security-level {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.security-level.high {
background: #d4edda;
color: #155724;
}
.security-level.medium {
background: #fff3cd;
color: #856404;
}
.security-level.low {
background: #f8d7da;
color: #721c24;
}
.security-level.critical {
background: #f5c6cb;
color: #721c24;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.option-description {
color: #5a6c7d;
margin-bottom: 12px;
line-height: 1.5;
}
.option-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.feature {
background: #e8f5e8;
color: #2d5a2d;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.option-warnings {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.warning {
background: #ffeaa7;
color: #d63031;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.warning.critical {
background: #fab1a0;
color: #d63031;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
}
.security-actions {
display: flex;
justify-content: center;
gap: 15px;
padding-top: 20px;
border-top: 1px solid #e1e8ed;
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #27ae60;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #229954;
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c0392b;
}
.btn-danger:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}
.modal-content {
background: white;
border-radius: 12px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e1e8ed;
}
.modal-header h3 {
margin: 0;
color: #e74c3c;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e1e8ed;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.warning-actions {
margin-top: 20px;
padding: 15px;
background: #fff3cd;
border-radius: 8px;
border-left: 4px solid #ffc107;
}
.warning-actions label {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
}
.warning-actions input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
}
/* Responsive */
@media (max-width: 768px) {
.security-mode-selector {
padding: 15px;
}
.security-options {
gap: 15px;
}
.security-option {
padding: 15px;
}
.option-header {
flex-direction: column;
align-items: flex-start;
}
.option-title {
margin: 8px 0;
}
.security-actions {
flex-direction: column;
}
}

View File

@ -1,113 +0,0 @@
<div id="security-mode-selector" class="security-mode-selector">
<div class="security-mode-header">
<h2>🔐 Mode de Sécurisation</h2>
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div class="security-options">
<!-- Proton Pass -->
<div class="security-option" data-mode="proton-pass">
<div class="option-header">
<div class="option-icon">🔒</div>
<div class="option-title">Proton Pass</div>
<div class="security-level high">Sécurisé</div>
</div>
<div class="option-description">
Utilise Proton Pass pour l'authentification biométrique et la gestion des clés
</div>
<div class="option-features">
<span class="feature">✅ Authentification biométrique</span>
<span class="feature">✅ Chiffrement end-to-end</span>
<span class="feature">✅ Synchronisation sécurisée</span>
</div>
</div>
<!-- OS Authenticator -->
<div class="security-option" data-mode="os">
<div class="option-header">
<div class="option-icon">🖥️</div>
<div class="option-title">Authentificateur OS</div>
<div class="security-level high">Sécurisé</div>
</div>
<div class="option-description">
Utilise l'authentificateur intégré de votre système d'exploitation
</div>
<div class="option-features">
<span class="feature">✅ Windows Hello / Touch ID / Face ID</span>
<span class="feature">✅ Chiffrement matériel</span>
<span class="feature">✅ Protection par mot de passe</span>
</div>
</div>
<!-- Application 2FA -->
<div class="security-option" data-mode="2fa">
<div class="option-header">
<div class="option-icon">📱</div>
<div class="option-title">Application 2FA</div>
<div class="security-level low">⚠️ Non sécurisé</div>
</div>
<div class="option-description">
Stockage en clair avec authentification par application 2FA
</div>
<div class="option-warnings">
<span class="warning">⚠️ Clés stockées en clair</span>
<span class="warning">⚠️ Risque de compromission</span>
<span class="warning">⚠️ Non recommandé pour des données sensibles</span>
</div>
</div>
<!-- Aucune sécurité -->
<div class="security-option" data-mode="none">
<div class="option-header">
<div class="option-icon">🚨</div>
<div class="option-title">Aucune Sécurité</div>
<div class="security-level critical">DANGEREUX</div>
</div>
<div class="option-description">
Stockage en clair sans aucune protection
</div>
<div class="option-warnings">
<span class="warning critical">🚨 Clés stockées en clair</span>
<span class="warning critical">🚨 Accès non protégé</span>
<span class="warning critical">🚨 RISQUE ÉLEVÉ</span>
</div>
</div>
</div>
<div class="security-actions">
<button id="confirm-security-mode" class="btn-primary" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" class="btn-secondary">
Annuler
</button>
</div>
<!-- Modal de confirmation pour les modes non sécurisés -->
<div id="security-warning-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div class="modal-body">
<div id="warning-content">
<!-- Contenu généré dynamiquement -->
</div>
<div class="warning-actions">
<label>
<input type="checkbox" id="understand-risks">
Je comprends les risques et souhaite continuer
</label>
</div>
</div>
<div class="modal-footer">
<button id="confirm-risky-mode" class="btn-danger" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" class="btn-secondary">
Choisir un Autre Mode
</button>
</div>
</div>
</div>
</div>

View File

@ -1,369 +0,0 @@
/**
* SecurityModeSelector - Composant de sélection du mode de sécurisation
* Permet à l'utilisateur de choisir comment sécuriser ses clés privées
*/
export type SecurityMode = 'proton-pass' | 'os' | '2fa' | 'none';
export interface SecurityModeConfig {
mode: SecurityMode;
name: string;
description: string;
securityLevel: 'high' | 'medium' | 'low' | 'critical';
requiresConfirmation: boolean;
warnings: string[];
}
export class SecurityModeSelector {
private container: HTMLElement;
private selectedMode: SecurityMode | null = null;
private onModeSelected: (mode: SecurityMode) => void;
private onCancel: () => void;
constructor(
container: HTMLElement,
onModeSelected: (mode: SecurityMode) => void,
onCancel: () => void
) {
this.container = container;
this.onModeSelected = onModeSelected;
this.onCancel = onCancel;
this.init();
}
private init(): void {
this.render();
this.attachEventListeners();
}
private render(): void {
this.container.innerHTML = `
<div class="security-mode-selector">
<div class="security-mode-header">
<h2>🔐 Mode de Sécurisation</h2>
<p>Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div class="security-options">
${this.getSecurityOptionsHTML()}
</div>
<div class="security-actions">
<button id="confirm-security-mode" class="btn-primary" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" class="btn-secondary">
Annuler
</button>
</div>
${this.getWarningModalHTML()}
</div>
`;
}
private getSecurityOptionsHTML(): string {
const options = this.getSecurityModes();
return options.map(option => `
<div class="security-option" data-mode="${option.mode}">
<div class="option-header">
<div class="option-icon">${this.getModeIcon(option.mode)}</div>
<div class="option-title">${option.name}</div>
<div class="security-level ${option.securityLevel}">
${this.getSecurityLevelText(option.securityLevel)}
</div>
</div>
<div class="option-description">${option.description}</div>
${this.getModeFeaturesHTML(option)}
</div>
`).join('');
}
private getModeFeaturesHTML(option: SecurityModeConfig): string {
if (option.securityLevel === 'low' || option.securityLevel === 'critical') {
return `
<div class="option-warnings">
${option.warnings.map(warning => `
<span class="warning ${option.securityLevel === 'critical' ? 'critical' : ''}">
${warning}
</span>
`).join('')}
</div>
`;
} else {
return `
<div class="option-features">
${this.getModeFeatures(option.mode).map(feature => `
<span class="feature">${feature}</span>
`).join('')}
</div>
`;
}
}
private getWarningModalHTML(): string {
return `
<div id="security-warning-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3> Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div class="modal-body">
<div id="warning-content">
<!-- Contenu généré dynamiquement -->
</div>
<div class="warning-actions">
<label>
<input type="checkbox" id="understand-risks">
Je comprends les risques et souhaite continuer
</label>
</div>
</div>
<div class="modal-footer">
<button id="confirm-risky-mode" class="btn-danger" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" class="btn-secondary">
Choisir un Autre Mode
</button>
</div>
</div>
</div>
`;
}
private getSecurityModes(): SecurityModeConfig[] {
return [
{
mode: 'proton-pass',
name: 'Proton Pass',
description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
securityLevel: 'high',
requiresConfirmation: false,
warnings: []
},
{
mode: 'os',
name: 'Authentificateur OS',
description: 'Utilise l\'authentificateur intégré de votre système d\'exploitation',
securityLevel: 'high',
requiresConfirmation: false,
warnings: []
},
{
mode: '2fa',
name: 'Application 2FA',
description: 'Stockage en clair avec authentification par application 2FA',
securityLevel: 'low',
requiresConfirmation: true,
warnings: [
'⚠️ Clés stockées en clair',
'⚠️ Risque de compromission',
'⚠️ Non recommandé pour des données sensibles'
]
},
{
mode: 'none',
name: 'Aucune Sécurité',
description: 'Stockage en clair sans aucune protection',
securityLevel: 'critical',
requiresConfirmation: true,
warnings: [
'🚨 Clés stockées en clair',
'🚨 Accès non protégé',
'🚨 RISQUE ÉLEVÉ'
]
}
];
}
private getModeIcon(mode: SecurityMode): string {
const icons = {
'proton-pass': '🔒',
'os': '🖥️',
'browser': '🌐',
'2fa': '📱',
'none': '🚨'
};
return icons[mode];
}
private getSecurityLevelText(level: string): string {
const texts = {
'high': 'Sécurisé',
'medium': 'Moyennement sécurisé',
'low': '⚠️ Non sécurisé',
'critical': 'DANGEREUX'
};
return texts[level as keyof typeof texts];
}
private getModeFeatures(mode: SecurityMode): string[] {
const features = {
'proton-pass': [
'✅ Authentification biométrique',
'✅ Chiffrement end-to-end',
'✅ Synchronisation sécurisée'
],
'os': [
'✅ Windows Hello / Touch ID / Face ID',
'✅ Chiffrement matériel',
'✅ Protection par mot de passe'
],
'2fa': [],
'none': []
};
return features[mode];
}
private attachEventListeners(): void {
// Sélection d'un mode
this.container.addEventListener('click', (e) => {
const option = (e.target as HTMLElement).closest('.security-option');
if (option) {
this.selectMode((option as HTMLElement).dataset.mode as SecurityMode);
}
});
// Confirmation du mode
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'confirm-security-mode') {
this.confirmSelection();
}
});
// Annulation
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'cancel-security-mode') {
this.onCancel();
}
});
// Gestion de la modal d'avertissement
this.attachWarningModalListeners();
}
private attachWarningModalListeners(): void {
// Checkbox de compréhension des risques
this.container.addEventListener('change', (e) => {
if ((e.target as HTMLElement).id === 'understand-risks') {
const checkbox = e.target as HTMLInputElement;
const confirmBtn = this.container.querySelector('#confirm-risky-mode') as HTMLButtonElement;
if (confirmBtn) {
confirmBtn.disabled = !checkbox.checked;
}
}
});
// Confirmation du mode risqué
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'confirm-risky-mode') {
this.hideWarningModal();
this.onModeSelected(this.selectedMode!);
}
});
// Annulation du mode risqué
this.container.addEventListener('click', (e) => {
if ((e.target as HTMLElement).id === 'cancel-risky-mode') {
this.hideWarningModal();
this.clearSelection();
}
});
}
private selectMode(mode: SecurityMode): void {
// Désélectionner tous les modes
this.container.querySelectorAll('.security-option').forEach(option => {
option.classList.remove('selected');
});
// Sélectionner le nouveau mode
const selectedOption = this.container.querySelector(`[data-mode="${mode}"]`);
if (selectedOption) {
selectedOption.classList.add('selected');
this.selectedMode = mode;
this.updateConfirmButton();
}
}
private updateConfirmButton(): void {
const confirmBtn = this.container.querySelector('#confirm-security-mode') as HTMLButtonElement;
if (confirmBtn) {
confirmBtn.disabled = !this.selectedMode;
}
}
private confirmSelection(): void {
if (!this.selectedMode) {return;}
const modeConfig = this.getSecurityModes().find(m => m.mode === this.selectedMode);
if (modeConfig?.requiresConfirmation) {
this.showWarningModal(modeConfig);
} else {
this.onModeSelected(this.selectedMode);
}
}
private showWarningModal(modeConfig: SecurityModeConfig): void {
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
const warningContent = this.container.querySelector('#warning-content') as HTMLElement;
if (modal && warningContent) {
warningContent.innerHTML = `
<div style="margin-bottom: 20px;">
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
<p>${modeConfig.description}</p>
</div>
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
<h5 style="color: #721c24; margin-top: 0;"> Risques identifiés :</h5>
<ul style="color: #721c24; margin-bottom: 0;">
${modeConfig.warnings.map(warning => `<li>${warning}</li>`).join('')}
</ul>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-top: 15px;">
<p style="color: #856404; margin: 0;">
<strong>Recommandation :</strong>
${modeConfig.securityLevel === 'low'
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
}
</p>
</div>
`;
modal.style.display = 'flex';
}
}
private hideWarningModal(): void {
const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
if (modal) {
modal.style.display = 'none';
}
}
private clearSelection(): void {
this.selectedMode = null;
this.container.querySelectorAll('.security-option').forEach(option => {
option.classList.remove('selected');
});
this.updateConfirmButton();
}
public show(): void {
this.container.style.display = 'block';
}
public hide(): void {
this.container.style.display = 'none';
}
public destroy(): void {
this.container.innerHTML = '';
}
}

View File

@ -1,70 +1,70 @@
.validation-modal {
display: block; /* Show the modal for demo purposes */
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
height: fit-content;
}
.modal-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.validation-box {
margin-bottom: 15px;
width: 100%;
}
.expansion-panel-header {
background-color: #e0e0e0;
padding: 10px;
cursor: pointer;
}
.expansion-panel-body {
display: none;
background-color: #fafafa;
padding: 10px;
border-top: 1px solid #ddd;
}
.expansion-panel-body pre {
background-color: #f6f8fa;
padding: 10px;
border-left: 4px solid #d1d5da;
overflow-x: auto;
}
.diff {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.diff-side {
width: 48%;
padding: 10px;
}
.diff-old {
background-color: #fee;
border: 1px solid #f00;
}
.diff-new {
background-color: #e6ffe6;
border: 1px solid #0f0;
}
.radio-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.validation-modal {
display: block; /* Show the modal for demo purposes */
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
height: fit-content;
}
.modal-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.validation-box {
margin-bottom: 15px;
width: 100%;
}
.expansion-panel-header {
background-color: #e0e0e0;
padding: 10px;
cursor: pointer;
}
.expansion-panel-body {
display: none;
background-color: #fafafa;
padding: 10px;
border-top: 1px solid #ddd;
}
.expansion-panel-body pre {
background-color: #f6f8fa;
padding: 10px;
border-left: 4px solid #d1d5da;
overflow-x: auto;
}
.diff {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.diff-side {
width: 48%;
padding: 10px;
}
.diff-old {
background-color: #fee;
border: 1px solid #f00;
}
.diff-new {
background-color: #e6ffe6;
border: 1px solid #0f0;
}
.radio-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}

View File

@ -1,9 +1,11 @@
<div id="validation-modal" class="validation-modal">
<div class="modal-content">
<div class="modal-title">Validate Process {{processId}}</div>
<div class="validation-box"></div>
<div class="modal-action">
<button onclick="validate()">Validate</button>
</div>
</div>
</div>
<div id="validation-modal" class="validation-modal">
<div class="modal-content">
<div class="modal-title">Validate Process {{processId}}</div>
<div class="validation-box">
</div>
<div class="modal-action">
<button onclick="validate()">Validate</button>
</div>
</div>
</div>

View File

@ -1,57 +1,56 @@
import ModalService from '../../services/modal.service';
import { secureLogger } from '../../services/secure-logger';
async function validate() {
secureLogger.debug('Validation modal triggered', { component: 'ValidationModal' });
const modalservice = await ModalService.getInstance();
modalservice.closeValidationModal();
}
export async function initValidationModal(processDiffs: any) {
secureLogger.debug('Initializing validation modal', { component: 'ValidationModal', data: processDiffs });
for (const diff of processDiffs.diffs) {
let diffs = '';
for (const value of diff) {
diffs += `
<div class="radio-buttons">
<label>
<input type="radio" name="validation1" value="old" />
Keep Old
</label>
<label>
<input type="radio" name="validation1" value="new" />
Keep New
</label>
</div>
<div class="diff">
<div class="diff-side diff-old">
<pre>-${value.previous_value}</pre>
</div>
<div class="diff-side diff-new">
<pre>+${value.new_value}</pre>
</div>
</div>
`;
}
const state = `
<div class="expansion-panel">
<div class="expansion-panel-header">State ${diff[0].new_state_merkle_root}</div>
<div class="expansion-panel-body">
${diffs}
</div>
</div>
`;
const box = document.querySelector('.validation-box');
if (box) {box.innerHTML += state;}
}
document.querySelectorAll('.expansion-panel-header').forEach(header => {
header.addEventListener('click', function (event) {
const target = event.target as HTMLElement;
const body = target.nextElementSibling as HTMLElement;
if (body?.style) {body.style.display = body.style.display === 'block' ? 'none' : 'block';}
});
});
}
(window as any).validate = validate;
import ModalService from '~/services/modal.service';
async function validate() {
console.log('==> VALIDATE');
const modalservice = await ModalService.getInstance();
modalservice.closeValidationModal();
}
export async function initValidationModal(processDiffs: any) {
console.log("🚀 ~ initValidationModal ~ processDiffs:", processDiffs)
for(const diff of processDiffs.diffs) {
let diffs = ''
for(const value of diff) {
diffs+= `
<div class="radio-buttons">
<label>
<input type="radio" name="validation1" value="old" />
Keep Old
</label>
<label>
<input type="radio" name="validation1" value="new" />
Keep New
</label>
</div>
<div class="diff">
<div class="diff-side diff-old">
<pre>-${value.previous_value}</pre>
</div>
<div class="diff-side diff-new">
<pre>+${value.new_value}</pre>
</div>
</div>
`
}
const state = `
<div class="expansion-panel">
<div class="expansion-panel-header">State ${diff[0].new_state_merkle_root}</div>
<div class="expansion-panel-body">
${diffs}
</div>
</div>
`
const box = document.querySelector('.validation-box')
if(box) box.innerHTML += state
}
document.querySelectorAll('.expansion-panel-header').forEach((header) => {
header.addEventListener('click', function (event) {
const target = event.target as HTMLElement;
const body = target.nextElementSibling as HTMLElement;
if (body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
});
});
}
(window as any).validate = validate;

10
src/decs.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare class AccountComponent extends HTMLElement {
_callback: any;
constructor();
connectedCallback(): void;
fetchData(): Promise<void>;
set callback(fn: any);
get callback(): any;
render(): void;
}
export { AccountComponent };

View File

@ -1,3 +1,39 @@
export { default as Services } from './services/service';
export { default as Database } from './services/database.service';
export { MessageType } from './models/process.model';
// import Services from './services/service';
// document.addEventListener('DOMContentLoaded', async () => {
// try {
// const services = await Services.getInstance();
// setTimeout( async () => {
// let device = await services.getDevice()
// console.log("🚀 ~ setTimeout ~ device:", device)
// if(!device) {
// device = await services.createNewDevice();
// } else {
// await services.restoreDevice(device)
// }
// await services.restoreProcesses();
// await services.restoreMessages();
// const amount = await services.getAmount();
// if (amount === 0n) {
// const faucetMsg = await services.createFaucetMessage();
// await services.sendFaucetMessage(faucetMsg);
// }
// if (services.isPaired()) { await services.injectProcessListPage() }
// else {
// const queryString = window.location.search;
// const urlParams = new URLSearchParams(queryString)
// const pairingAddress = urlParams.get('sp_address')
// if(pairingAddress) {
// setTimeout(async () => await services.sendPairingTx(pairingAddress), 2000)
// }
// }
// }, 500);
// } catch (error) {
// console.error(error);
// }
// });

View File

@ -1,22 +1,22 @@
import { DocumentSignature } from '../models/signature.models';
export interface Group {
id: number;
name: string;
description: string;
roles: Array<{
name: string;
members: Array<{ id: string | number; name: string }>;
documents?: Array<any>;
}>;
commonDocuments: Array<{
id: number;
name: string;
visibility: string;
description: string;
createdAt?: string | null;
deadline?: string | null;
signatures?: DocumentSignature[];
status?: string;
}>;
}
import { DocumentSignature } from '~/models/signature.models';
export interface Group {
id: number;
name: string;
description: string;
roles: Array<{
name: string;
members: Array<{ id: string | number; name: string }>;
documents?: Array<any>;
}>;
commonDocuments: Array<{
id: number;
name: string;
visibility: string;
description: string;
createdAt?: string | null;
deadline?: string | null;
signatures?: DocumentSignature[];
status?: string;
}>;
}

View File

@ -1,7 +1,7 @@
export interface Member {
id: string | number;
name: string;
email?: string;
avatar?: string;
processRoles?: Array<{ processId: number | string; role: string }>;
}
export interface Member {
id: string | number;
name: string;
email?: string;
avatar?: string;
processRoles?: Array<{ processId: number | string; role: string }>;
}

View File

@ -1 +1,30 @@
// Main entry point - no custom elements needed for current implementation
import { SignatureComponent } from './pages/signature/signature-component';
import { SignatureElement } from './pages/signature/signature';
import { ChatComponent } from './pages/chat/chat-component';
import { ChatElement } from './pages/chat/chat';
import { AccountComponent } from './pages/account/account-component';
import { AccountElement } from './pages/account/account';
export { SignatureComponent, SignatureElement, ChatComponent, ChatElement, AccountComponent, AccountElement };
declare global {
interface HTMLElementTagNameMap {
'signature-component': SignatureComponent;
'signature-element': SignatureElement;
'chat-component': ChatComponent;
'chat-element': ChatElement;
'account-component': AccountComponent;
'account-element': AccountElement;
}
}
// Configuration pour le mode indépendant
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
// Initialiser les composants si nécessaire
customElements.define('signature-component', SignatureComponent);
customElements.define('signature-element', SignatureElement);
customElements.define('chat-component', ChatComponent);
customElements.define('chat-element', ChatElement);
customElements.define('account-component', AccountComponent);
customElements.define('account-element', AccountElement);
}

View File

@ -0,0 +1,272 @@
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
export const STORAGE_KEYS = {
pairing: 'pairingRows',
wallet: 'walletRows',
process: 'processRows',
data: 'dataRows',
};
// Initialiser le stockage des lignes par défaut dans le localStorage
export const defaultRows = [
{
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
column2: '🎊😑🎄😩',
column3: 'Laptop',
},
{
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
column2: '🎏🎕😧🌥',
column3: 'Phone',
},
];
export const mockNotifications: { [key: string]: Notification[] } = {};
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
export const mockDataRows = [
{
column1: 'User Project',
column2: 'private',
column3: 'User',
column4: '6 months',
column5: 'NDA signed',
column6: 'Contract #123',
processName: 'User Process',
zone: 'A',
},
{
column1: 'Process Project',
column2: 'private',
column3: 'Process',
column4: '1 year',
column5: 'Terms accepted',
column6: 'Contract #456',
processName: 'Process Management',
zone: 'B',
},
{
column1: 'Member Project',
column2: 'private',
column3: 'Member',
column4: '3 months',
column5: 'GDPR compliant',
column6: 'Contract #789',
processName: 'Member Process',
zone: 'C',
},
{
column1: 'Peer Project',
column2: 'public',
column3: 'Peer',
column4: '2 years',
column5: 'IP rights',
column6: 'Contract #101',
processName: 'Peer Process',
zone: 'D',
},
{
column1: 'Payment Project',
column2: 'confidential',
column3: 'Payment',
column4: '1 year',
column5: 'NDA signed',
column6: 'Contract #102',
processName: 'Payment Process',
zone: 'E',
},
{
column1: 'Deposit Project',
column2: 'private',
column3: 'Deposit',
column4: '6 months',
column5: 'Terms accepted',
column6: 'Contract #103',
processName: 'Deposit Process',
zone: 'F',
},
{
column1: 'Artefact Project',
column2: 'public',
column3: 'Artefact',
column4: '1 year',
column5: 'GDPR compliant',
column6: 'Contract #104',
processName: 'Artefact Process',
zone: 'G',
},
{
column1: 'Resolve Project',
column2: 'private',
column3: 'Resolve',
column4: '2 years',
column5: 'IP rights',
column6: 'Contract #105',
processName: 'Resolve Process',
zone: 'H',
},
{
column1: 'Backup Project',
column2: 'public',
column3: 'Backup',
column4: '1 year',
column5: 'NDA signed',
column6: 'Contract #106',
processName: 'Backup Process',
zone: 'I',
},
];
export const mockProcessRows = [
{
process: 'User Project',
role: 'User',
notification: {
messages: [
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
],
},
},
{
process: 'Member Project',
role: 'Member',
notification: {
messages: [
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
],
},
},
{
process: 'Peer Project',
role: 'Peer',
notification: {
unread: 2,
total: 4,
messages: [
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
],
},
},
{
process: 'Deposit Project',
role: 'Deposit',
notification: {
unread: 1,
total: 10,
messages: [
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
{
process: 'Artefact Project',
role: 'Artefact',
notification: {
unread: 0,
total: 3,
messages: [
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
{
process: 'Resolve Project',
role: 'Resolve',
notification: {
unread: 5,
total: 12,
messages: [
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
],
},
},
];
export const mockContracts = {
'Contract #123': {
title: 'User Project Agreement',
date: '2024-01-15',
parties: ['Company XYZ', 'User Team'],
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
content: 'This agreement establishes the terms and conditions for user project management.',
},
'Contract #456': {
title: 'Process Management Contract',
date: '2024-02-01',
parties: ['Company XYZ', 'Process Team'],
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
content: 'This contract defines the process management standards and procedures.',
},
'Contract #789': {
title: 'Member Access Agreement',
date: '2024-03-15',
parties: ['Company XYZ', 'Member Team'],
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
content: 'This agreement outlines the terms for member access and privileges.',
},
'Contract #101': {
title: 'Peer Collaboration Agreement',
date: '2024-04-01',
parties: ['Company XYZ', 'Peer Network'],
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
content: 'This contract establishes peer collaboration and networking guidelines.',
},
'Contract #102': {
title: 'Payment Processing Agreement',
date: '2024-05-01',
parties: ['Company XYZ', 'Payment Team'],
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
content: 'This agreement defines payment processing terms and conditions.',
},
'Contract #103': {
title: 'Deposit Management Contract',
date: '2024-06-01',
parties: ['Company XYZ', 'Deposit Team'],
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
content: 'This contract outlines deposit management procedures and security measures.',
},
'Contract #104': {
title: 'Artefact Handling Agreement',
date: '2024-07-01',
parties: ['Company XYZ', 'Artefact Team'],
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
content: 'This agreement establishes artefact handling and preservation guidelines.',
},
'Contract #105': {
title: 'Resolution Protocol Agreement',
date: '2024-08-01',
parties: ['Company XYZ', 'Resolution Team'],
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
content: 'This contract defines the resolution process and protocol standards.',
},
'Contract #106': {
title: 'Backup Service Agreement',
date: '2024-09-01',
parties: ['Company XYZ', 'Backup Team'],
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
content: 'This agreement outlines backup service terms and recovery procedures.',
},
};

View File

@ -0,0 +1,45 @@
export interface Row {
column1: string;
column2: string;
column3: string;
}
// Types supplémentaires nécessaires
export interface Contract {
title: string;
date: string;
parties: string[];
terms: string[];
content: string;
}
export interface WalletRow {
column1: string; // Label
column2: string; // Wallet
column3: string; // Type
}
export interface DataRow {
column1: string; // Name
column2: string; // Visibility
column3: string; // Role
column4: string; // Duration
column5: string; // Legal
column6: string; // Contract
processName: string;
zone: string;
}
export interface Notification {
message: string;
timestamp: string;
isRead: boolean;
}
// Déplacer l'interface en dehors de la classe, au début du fichier
export interface NotificationMessage {
id: number;
read: boolean;
date: string;
message: string;
}

View File

@ -0,0 +1,52 @@
export const groupsMock = [
{
id: 1,
name: 'Group 🚀 ',
roles: [
{
id: 1,
name: 'Role 1',
members: [
{ id: 1, name: 'Member 1' },
{ id: 2, name: 'Member 2' },
],
},
{
id: 2,
name: 'Role 2',
members: [
{ id: 3, name: 'Member 3' },
{ id: 4, name: 'Member 4' },
],
},
],
},
{
id: 2,
name: 'Group ₿',
roles: [
{
id: 3,
name: 'Role 1',
members: [
{ id: 5, name: 'Member 5' },
{ id: 6, name: 'Member 6' },
],
},
],
},
{
id: 3,
name: 'Group 🪙',
roles: [
{
id: 4,
name: 'Role 1',
members: [
{ id: 7, name: 'Member 7' },
{ id: 8, name: 'Member 8' },
],
},
],
},
];

View File

@ -0,0 +1,64 @@
export const messagesMock = [
{
memberId: 1, // Conversations avec Mmber 1
messages: [
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
],
},
{
memberId: 2, // Conversations avec Member 2
messages: [
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
],
},
{
memberId: 3, // Conversations avec Member 3
messages: [
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 4, // Conversations avec Member 4
messages: [
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 5, // Conversations avec Member 5
messages: [
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 6, // Conversations avec Member 6
messages: [
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 7, // Conversations avec Member 7
messages: [
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 8, // Conversations avec Member 8
messages: [
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
];

View File

@ -0,0 +1,471 @@
// Définir les rôles autorisés
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
const VISIBILITY_LEVELS = {
PUBLIC: 'public',
CONFIDENTIAL: 'confidential',
PRIVATE: 'private',
};
const DOCUMENT_STATUS = {
DRAFT: 'draft',
PENDING: 'pending',
IN_REVIEW: 'in_review',
APPROVED: 'approved',
REJECTED: 'rejected',
EXPIRED: 'expired',
};
// Fonction pour créer un rôle
function createRole(name, members) {
if (!VALID_ROLES.includes(name)) {
throw new Error(`Role "${name}" is not valid.`);
}
return { name, members };
}
export const groupsMock = [
{
id: 1,
name: 'Processus 1',
description: 'Description du processus 1',
commonDocuments: [
{
id: 101,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 102,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 103,
name: 'Procédures générales',
description: 'Document vierge pour les procédures générales',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 104,
name: 'Urgency A',
description: "Document vierge pour le plan d'urgence A",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 105,
name: 'Urgency B',
description: "Document vierge pour le plan d'urgence B",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 106,
name: 'Urgency C',
description: "Document vierge pour le plan d'urgence C",
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 107,
name: 'Document à signer',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'User',
members: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
documents: [
{
id: 1,
name: 'Document User A',
description: 'Description du document User A.',
visibility: 'public',
createdAt: '2024-01-01',
deadline: '2024-02-01',
signatures: [
{
member: { id: 1, name: 'Alice' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 2, name: 'Bob' },
signed: false,
},
],
},
{
id: 2,
name: 'Document User B',
description: 'Document vierge pour le rôle User',
visibility: 'confidential',
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 7,
name: 'Document User C',
description: 'Document vierge pour validation utilisateur',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 8,
name: 'Document User D',
description: 'Document vierge pour approbation utilisateur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Process',
members: [
{ id: 3, name: 'Charlie' },
{ id: 4, name: 'David' },
],
documents: [
{
id: 3,
name: 'Document Process A',
description: 'Description du document Process A.',
visibility: 'confidential',
createdAt: '2024-01-10',
deadline: '2024-03-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-12',
},
],
},
{
id: 9,
name: 'Document Process B',
description: 'Document vierge pour processus interne',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 10,
name: 'Document Process C',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 11,
name: 'Document Process D',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.PENDING,
createdAt: '2024-01-15',
deadline: '2024-02-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 4, name: 'David' },
signed: false,
},
],
},
{
id: 12,
name: 'Document Process E',
description: 'Document vierge pour validation processus',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.PENDING,
createdAt: '2024-01-15',
deadline: '2024-02-01',
signatures: [
{
member: { id: 3, name: 'Charlie' },
signed: true,
signedAt: '2024-01-15',
},
{
member: { id: 4, name: 'David' },
signed: false,
},
],
},
],
},
{
name: 'Backup',
members: [
{ id: 15, name: 'Oscar' },
{ id: 16, name: 'Patricia' },
],
documents: [
{
id: 11,
name: 'Document Backup A',
description: 'Document vierge pour sauvegarde',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
{
id: 2,
name: 'Processus 2',
description: 'Description du processus 2',
commonDocuments: [
{
id: 201,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 202,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 203,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 204,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 205,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'Artefact',
members: [
{ id: 17, name: 'Quinn' },
{ id: 18, name: 'Rachel' },
],
documents: [
{
id: 12,
name: 'Document Artefact A',
description: 'Document vierge pour artefact',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 13,
name: 'Document Artefact B',
description: 'Document vierge pour validation artefact',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Resolve',
members: [
{ id: 19, name: 'Sam' },
{ id: 20, name: 'Tom' },
],
documents: [
{
id: 14,
name: 'Document Resolve A',
description: 'Document vierge pour résolution',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
{
id: 3,
name: 'Processus 3',
description: 'Description du processus 3',
commonDocuments: [
{
id: 301,
name: 'Règlement intérieur',
description: 'Document vierge pour le règlement intérieur',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 302,
name: 'Charte de confidentialité',
description: 'Document vierge pour la charte de confidentialité',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 303,
name: 'Procédures générales',
description: 'Document vierge pour les procédures générales',
visibility: VISIBILITY_LEVELS.PUBLIC,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
roles: [
{
name: 'Deposit',
members: [
{ id: 21, name: 'Uma' },
{ id: 22, name: 'Victor' },
],
documents: [
{
id: 15,
name: 'Document Deposit A',
description: 'Document vierge pour dépôt',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 16,
name: 'Document Deposit B',
description: 'Document vierge pour validation dépôt',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
{
name: 'Payment',
members: [
{ id: 23, name: 'Walter' },
{ id: 24, name: 'Xena' },
],
documents: [
{
id: 17,
name: 'Document Payment B',
description: 'Document vierge pour paiement',
visibility: VISIBILITY_LEVELS.PRIVATE,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
{
id: 18,
name: 'Document Payment C',
description: 'Document vierge pour validation paiement',
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
status: DOCUMENT_STATUS.DRAFT,
createdAt: null,
deadline: null,
signatures: [],
},
],
},
],
},
];

View File

@ -0,0 +1,105 @@
export const membersMock = [
// Processus 1
{
id: 1,
name: 'Alice',
avatar: 'A',
email: 'alice@company.com',
processRoles: [{ processId: 1, role: 'User' }],
},
{
id: 2,
name: 'Bob',
avatar: 'B',
email: 'bob@company.com',
processRoles: [{ processId: 1, role: 'User' }],
},
{
id: 3,
name: 'Charlie',
avatar: 'C',
email: 'charlie@company.com',
processRoles: [{ processId: 1, role: 'Process' }],
},
{
id: 4,
name: 'David',
avatar: 'D',
email: 'david@company.com',
processRoles: [{ processId: 1, role: 'Process' }],
},
{
id: 15,
name: 'Oscar',
avatar: 'O',
email: 'oscar@company.com',
processRoles: [{ processId: 1, role: 'Backup' }],
},
{
id: 16,
name: 'Patricia',
avatar: 'P',
email: 'patricia@company.com',
processRoles: [{ processId: 1, role: 'Backup' }],
},
// Processus 2
{
id: 17,
name: 'Quinn',
avatar: 'Q',
email: 'quinn@company.com',
processRoles: [{ processId: 2, role: 'Artefact' }],
},
{
id: 18,
name: 'Rachel',
avatar: 'R',
email: 'rachel@company.com',
processRoles: [{ processId: 2, role: 'Artefact' }],
},
{
id: 19,
name: 'Sam',
avatar: 'S',
email: 'sam@company.com',
processRoles: [{ processId: 2, role: 'Resolve' }],
},
{
id: 20,
name: 'Tom',
avatar: 'T',
email: 'tom@company.com',
processRoles: [{ processId: 2, role: 'Resolve' }],
},
// Processus 3
{
id: 21,
name: 'Uma',
avatar: 'U',
email: 'uma@company.com',
processRoles: [{ processId: 3, role: 'Deposit' }],
},
{
id: 22,
name: 'Victor',
avatar: 'V',
email: 'victor@company.com',
processRoles: [{ processId: 3, role: 'Deposit' }],
},
{
id: 23,
name: 'Walter',
avatar: 'W',
email: 'walter@company.com',
processRoles: [{ processId: 3, role: 'Payment' }],
},
{
id: 24,
name: 'Xena',
avatar: 'X',
email: 'xena@company.com',
processRoles: [{ processId: 3, role: 'Payment' }],
},
];

View File

@ -0,0 +1,64 @@
export const messagesMock = [
{
memberId: 1, // Conversations avec Mmber 1
messages: [
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
],
},
{
memberId: 2, // Conversations avec Member 2
messages: [
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
],
},
{
memberId: 3, // Conversations avec Member 3
messages: [
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 4, // Conversations avec Member 4
messages: [
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 5, // Conversations avec Member 5
messages: [
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 6, // Conversations avec Member 6
messages: [
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 7, // Conversations avec Member 7
messages: [
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
{
memberId: 8, // Conversations avec Member 8
messages: [
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
],
},
];

View File

@ -1,7 +1,7 @@
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
import { Device, Process, SecretsStore } from "pkg/sdk_client";
export interface BackUp {
device: Device;
secrets: SecretsStore;
processes: Record<string, Process>;
device: Device,
secrets: SecretsStore,
processes: Record<string, Process>,
}

View File

@ -1,30 +1,30 @@
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}
// Quelles sont les données utiles pour le user ???
export interface IUser {
id: string;
information?: any;
}
// Quelles sont les données utiles pour les messages ???
export interface IMessage {
id: string;
message: any;
}
export interface UserDiff {
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
field: string;
previous_value: string;
new_value: string;
notify_user: boolean;
need_validation: boolean;
// validated: bool,
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
}
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}
// Quelles sont les données utiles pour le user ???
export interface IUser {
id: string;
information?: any;
}
// Quelles sont les données utiles pour les messages ???
export interface IMessage {
id: string;
message: any;
}
export interface UserDiff {
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
field: string;
previous_value: string;
new_value: string;
notify_user: boolean;
need_validation: boolean;
// validated: bool,
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
}

View File

@ -1,79 +1,23 @@
export interface IProcess {
id: number;
name: string;
description: string;
icon?: string;
zoneList: IZone[];
}
export interface IZone {
id: number;
name: string;
path: string;
// Est-ce que la zone a besoin d'une icone ?
icon?: string;
}
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}
/* eslint-disable no-unused-vars */
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
CREATE_PAIRING = 'CREATE_PAIRING',
PAIRING_CREATED = 'PAIRING_CREATED',
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
RENEW_TOKEN = 'RENEW_TOKEN',
// Get various information
GET_PAIRING_ID = 'GET_PAIRING_ID',
GET_PROCESSES = 'GET_PROCESSES',
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
RETRIEVE_DATA = 'RETRIEVE_DATA',
DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes
CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED',
CREATE_CONVERSATION = 'CREATE_CONVERSATION',
CONVERSATION_CREATED = 'CONVERSATION_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
VALIDATE_STATE = 'VALIDATE_STATE',
STATE_VALIDATED = 'STATE_VALIDATED',
// Hash and merkle proof
HASH_VALUE = 'HASH_VALUE',
VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
// Private key access notifications
PRIVATE_KEY_ACCESSED = 'PRIVATE_KEY_ACCESSED',
// 4 words pairing via iframe
PAIRING_4WORDS_CREATE = 'PAIRING_4WORDS_CREATE',
PAIRING_4WORDS_JOIN = 'PAIRING_4WORDS_JOIN',
PAIRING_4WORDS_WORDS_GENERATED = 'PAIRING_4WORDS_WORDS_GENERATED',
PAIRING_4WORDS_STATUS_UPDATE = 'PAIRING_4WORDS_STATUS_UPDATE',
PAIRING_4WORDS_SUCCESS = 'PAIRING_4WORDS_SUCCESS',
PAIRING_4WORDS_ERROR = 'PAIRING_4WORDS_ERROR',
}
/* eslint-enable no-unused-vars */
export interface IProcess {
id: number;
name: string;
description: string;
icon?: string;
zoneList: IZone[];
}
export interface IZone {
id: number;
name: string;
path: string;
// Est-ce que la zone a besoin d'une icone ?
icon?: string;
}
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}

View File

@ -1,59 +1,59 @@
export interface Group {
id: number;
name: string;
description?: string;
roles: {
id?: number;
name: string;
members: { id: string | number; name: string }[];
documents?: {
id: number;
name: string;
description?: string;
visibility: string;
createdAt: string | null;
deadline: string | null;
signatures: DocumentSignature[];
status?: string;
files?: Array<{ name: string; url: string }>;
}[];
}[];
}
export interface Message {
id: number;
sender: string;
text?: string;
time: string;
type: 'text' | 'file';
fileName?: string;
fileData?: string;
}
export interface MemberMessages {
memberId: string;
messages: Message[];
}
export interface DocumentSignature {
signed: boolean;
member: {
name: string;
};
signedAt?: string;
}
export interface RequestParams {
processId: number;
processName: string;
roleId: number;
roleName: string;
documentId: number;
documentName: string;
}
export interface Notification {
memberId: string;
text: string;
time: string;
}
export interface Group {
id: number;
name: string;
description?: string;
roles: {
id?: number;
name: string;
members: { id: string | number; name: string }[];
documents?: {
id: number;
name: string;
description?: string;
visibility: string;
createdAt: string | null;
deadline: string | null;
signatures: DocumentSignature[];
status?: string;
files?: Array<{ name: string; url: string }>;
}[];
}[];
}
export interface Message {
id: number;
sender: string;
text?: string;
time: string;
type: 'text' | 'file';
fileName?: string;
fileData?: string;
}
export interface MemberMessages {
memberId: string;
messages: Message[];
}
export interface DocumentSignature {
signed: boolean;
member: {
name: string;
};
signedAt?: string;
}
export interface RequestParams {
processId: number;
processName: string;
roleId: number;
roleName: string;
documentId: number;
documentName: string;
}
export interface Notification {
memberId: string;
text: string;
time: string;
}

View File

@ -0,0 +1,62 @@
import { AccountElement } from './account';
import accountCss from '../../../public/style/account.css?raw';
import Services from '../../services/service.js';
class AccountComponent extends HTMLElement {
_callback: any;
accountElement: AccountElement | null = null;
constructor() {
super();
console.log('INIT');
this.attachShadow({ mode: 'open' });
this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
}
connectedCallback() {
console.log('CALLBACKs');
this.render();
this.fetchData();
if (!customElements.get('account-element')) {
customElements.define('account-element', AccountElement);
}
}
async fetchData() {
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
const data = await (window as any).myService?.getProcesses();
} else {
const service = await Services.getInstance();
const data = await service.getProcesses();
}
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
const style = document.createElement('style');
style.textContent = accountCss;
const accountElement = document.createElement('account-element');
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(accountElement);
}
}
}
export { AccountComponent };
customElements.define('account-component', AccountComponent);

View File

@ -1,64 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<title>Account</title>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
font-family: Arial, sans-serif;
overflow-x: hidden;
}
.account-container {
max-width: 900px;
margin: 20px auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
min-height: calc(100vh - 40px);
}
.account-header {
background: linear-gradient(135deg, #3a506b 0%, #2c3e50 100%);
color: white;
padding: 30px;
text-align: center;
}
.account-header h1 {
margin: 0 0 10px 0;
font-size: 32px;
font-weight: 300;
}
.account-header p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.account-content {
padding: 0;
}
</style>
</head>
<body>
<div class="account-container">
<div class="account-header">
<h1>🔐 Mon Compte</h1>
<p>Gestion sécurisée de vos devices et contrats de pairing</p>
</div>
<div class="account-content">
<device-management></device-management>
</div>
</div>
<script type="module" src="/src/components/device-management/device-management.ts"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Account</title>
</head>
<body>
<account-component></account-component>
<script type="module" src="./account.ts"></script>
</body>
</html>

1487
src/pages/account/account.ts Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,95 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration de la Date Anniversaire - 4NK</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.status {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.progress {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: #667eea;
width: 0%;
transition: width 0.3s ease;
}
</style>
</head>
<body>
<div class="container">
<h1>🎂 Configuration de la Date Anniversaire</h1>
<p class="subtitle">Mise à jour de la date anniversaire et scan des blocs</p>
<div class="status loading" id="status">
🔄 Connexion aux relais...
</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<script type="module" src="./birthday-setup.ts"></script>
</body>
</html>

View File

@ -1,241 +0,0 @@
/**
* Page de configuration de la date anniversaire
* Mise à jour de la date anniversaire et scan des blocs
*/
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
import { secureLogger } from '../../services/secure-logger';
let isInitializing = false;
// Type definition for update functions - parameters are template names
/* eslint-disable no-unused-vars */
interface UpdateFunctions {
updateStatus: (message: string, type: 'loading' | 'success' | 'error') => void;
updateProgress: (percent: number) => void;
}
/* eslint-enable no-unused-vars */
async function initializeServices() {
secureLogger.info('🔄 Importing services...', { component: 'BirthdaySetup' });
const serviceModule = await import('../../services/service');
secureLogger.debug(`Service module imported: ${Object.keys(serviceModule)}`, {
component: 'BirthdaySetup',
});
const Services = serviceModule.default;
if (!Services) {
throw new Error('Services class not found in default export');
}
try {
const services = await Services.getInstance();
secureLogger.info('✅ Services instance obtained successfully', {
component: 'BirthdaySetup',
});
return services;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Out of memory') || errorMessage.includes('insufficient memory')) {
secureLogger.error('🚫 Memory error detected', { component: 'BirthdaySetup' });
throw new Error(
'WebAssembly initialization failed due to insufficient memory. Please refresh the page.'
);
}
throw error;
}
}
function redirectToSetup(page: string, updateFunctions: UpdateFunctions): void {
secureLogger.warn(`Redirecting to ${page}...`, { component: 'BirthdaySetup' });
updateFunctions.updateStatus(`⚠️ Redirection vers ${page}...`, 'loading');
setTimeout(() => {
window.location.href = `/src/pages/${page}/${page}.html`;
}, 1000);
}
async function verifyPrerequisites(updateFunctions: UpdateFunctions) {
const pbkdf2KeyResult = await checkPBKDF2Key();
if (!pbkdf2KeyResult) {
redirectToSetup('security-setup', updateFunctions);
return null;
}
const wallet = await checkWalletWithRetries();
if (!wallet) {
redirectToSetup('wallet-setup', updateFunctions);
return null;
}
if (wallet.sp_wallet?.birthday === undefined) {
throw new Error('Wallet found but missing required data (sp_wallet or birthday)');
}
secureLogger.info(`Wallet found in database with birthday: ${wallet.sp_wallet.birthday}`, {
component: 'BirthdaySetup',
});
return { wallet, pbkdf2KeyResult };
}
async function waitForBlockHeight(services: any, updateFunctions: UpdateFunctions) {
updateFunctions.updateStatus('⏳ Attente de la synchronisation avec le réseau...', 'loading');
updateFunctions.updateProgress(40);
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for block height from handshake'));
}, 15000);
const checkBlockHeight = () => {
const blockHeight = services.getCurrentBlockHeight();
if (blockHeight !== -1 && blockHeight > 0) {
secureLogger.info(`✅ Block height set from handshake: ${blockHeight}`, {
component: 'BirthdaySetup',
});
clearTimeout(timeout);
resolve();
} else {
setTimeout(checkBlockHeight, 100);
}
};
checkBlockHeight();
});
const currentBlockHeight = services.getCurrentBlockHeight();
if (currentBlockHeight !== -1 && currentBlockHeight > 0) {
secureLogger.info(`Relays connected successfully, chain_tip: ${currentBlockHeight}`, {
component: 'BirthdaySetup',
});
} else {
throw new Error('Handshake not received or chain_tip not set');
}
}
async function updateBirthday(services: any, updateFunctions: UpdateFunctions) {
updateFunctions.updateStatus('🎂 Mise à jour de la date anniversaire...', 'loading');
updateFunctions.updateProgress(60);
secureLogger.info('🔄 Calling updateDeviceBlockHeight()...', { component: 'BirthdaySetup' });
await services.updateDeviceBlockHeight();
secureLogger.info('✅ updateDeviceBlockHeight() completed successfully', {
component: 'BirthdaySetup',
});
}
async function verifyBirthdayUpdate(services: any, updateFunctions: UpdateFunctions) {
updateFunctions.updateStatus('🔍 Vérification de la mise à jour...', 'loading');
updateFunctions.updateProgress(70);
secureLogger.info('🔄 Verifying birthday update...', { component: 'BirthdaySetup' });
const updatedWallet = await services.getDeviceFromDatabase();
if (updatedWallet?.sp_wallet?.birthday && updatedWallet.sp_wallet.birthday > 0) {
secureLogger.info(`Birthday updated successfully: ${updatedWallet.sp_wallet.birthday}`, {
component: 'BirthdaySetup',
});
} else {
secureLogger.error('Birthday update verification failed', new Error('Verification failed'), {
component: 'BirthdaySetup',
data: {
birthday: updatedWallet?.sp_wallet?.birthday,
hasSpWallet: !!updatedWallet?.sp_wallet,
},
});
throw new Error(
`Birthday update verification failed: expected birthday > 0, got ${updatedWallet?.sp_wallet?.birthday}`
);
}
}
function redirectToBlockSync(updateFunctions: UpdateFunctions) {
updateFunctions.updateStatus('🔄 Redirection vers la synchronisation des blocs...', 'loading');
updateFunctions.updateProgress(100);
secureLogger.info('🎉 Birthday setup completed successfully - redirecting to block sync', {
component: 'BirthdaySetup',
});
setTimeout(() => {
secureLogger.info('🔄 Executing redirect now...', { component: 'BirthdaySetup' });
window.location.href = '/src/pages/block-sync/block-sync.html';
}, 1000);
}
document.addEventListener('DOMContentLoaded', async () => {
if (isInitializing) {
secureLogger.warn('⚠️ Birthday setup page already initializing, skipping...', {
component: 'BirthdaySetup',
});
return;
}
isInitializing = true;
secureLogger.info('🎂 Birthday setup page loaded', { component: 'BirthdaySetup' });
secureLogger.debug(`Current URL: ${window.location.href}`, { component: 'BirthdaySetup' });
secureLogger.debug(`Referrer: ${document.referrer}`, { component: 'BirthdaySetup' });
const status = document.getElementById('status') as HTMLDivElement;
const progressBar = document.getElementById('progressBar') as HTMLDivElement;
let lastStatusMessage = '';
let lastStatusType: 'loading' | 'success' | 'error' | null = null;
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
if (lastStatusMessage === message && lastStatusType === type) {
return;
}
lastStatusMessage = message;
lastStatusType = type;
status.textContent = message;
status.className = `status ${type}`;
status.setAttribute('data-status', type);
}
function updateProgress(percent: number) {
progressBar.style.width = `${percent}%`;
}
const updateFunctions: UpdateFunctions = { updateStatus, updateProgress };
try {
updateStatus('🌐 Connexion aux relais...', 'loading');
updateProgress(20);
let services;
try {
services = await initializeServices();
} catch (error) {
secureLogger.error('Services not available', error as Error, { component: 'BirthdaySetup' });
updateStatus('❌ Erreur: Services non disponibles', 'error');
throw error;
}
updateStatus('🔍 Vérification des prérequis...', 'loading');
updateProgress(20);
const prerequisites = await verifyPrerequisites(updateFunctions);
if (!prerequisites) {
return; // Already redirected
}
secureLogger.info('✅ All prerequisites verified', { component: 'BirthdaySetup' });
await services.connectAllRelays();
await waitForBlockHeight(services, updateFunctions);
await updateBirthday(services, updateFunctions);
await verifyBirthdayUpdate(services, updateFunctions);
redirectToBlockSync(updateFunctions);
} catch (error) {
secureLogger.error('Error during birthday setup', error as Error, {
component: 'BirthdaySetup',
});
updateStatus('❌ Erreur lors de la configuration de la date anniversaire', 'error');
} finally {
isInitializing = false;
}
});

View File

@ -1,172 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synchronisation des Blocs - 4NK</title>
<link rel="stylesheet" href="../../4nk.css">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 600px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 2.5rem;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
font-size: 1.1rem;
}
.status {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 1rem;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.progress {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: #667eea;
width: 0%;
transition: width 0.3s ease;
}
.sync-details {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: left;
}
.sync-details h3 {
margin-top: 0;
color: #495057;
text-align: center;
margin-bottom: 15px;
}
.sync-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #e9ecef;
}
.sync-item:last-child {
border-bottom: none;
}
.sync-status {
font-weight: bold;
}
.sync-status.pending {
color: #ffc107;
}
.sync-status.completed {
color: #28a745;
}
.sync-status.error {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<h1>🔄 Synchronisation des Blocs</h1>
<p class="subtitle">Synchronisation avec le réseau Bitcoin pour récupérer l'historique des transactions</p>
<div class="status loading" id="status">
🔄 Initialisation de la synchronisation...
</div>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="sync-details">
<h3>📊 Détails de la synchronisation</h3>
<div class="sync-item">
<span>Hauteur de bloc actuelle:</span>
<span id="currentBlock" class="sync-status pending">En attente...</span>
</div>
<div class="sync-item">
<span>Date anniversaire:</span>
<span id="birthday" class="sync-status pending">En attente...</span>
</div>
<div class="sync-item">
<span>Blocs à scanner:</span>
<span id="blocksToScan" class="sync-status pending">En attente...</span>
</div>
<div class="sync-item">
<span>Blocs scannés:</span>
<span id="blocksScanned" class="sync-status pending">0</span>
</div>
<div class="sync-item">
<span>Transactions trouvées:</span>
<span id="transactionsFound" class="sync-status pending">0</span>
</div>
</div>
</div>
<script type="module" src="./block-sync.ts"></script>
</body>
</html>

View File

@ -1,276 +0,0 @@
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
import { secureLogger } from '../../services/secure-logger';
import Services from '../../services/service';
document.addEventListener('DOMContentLoaded', async () => {
secureLogger.info('🔄 Block sync page loaded', { component: 'BlockSync' });
const status = document.getElementById('status') as HTMLElement;
const progressBar = document.getElementById('progressBar') as HTMLElement;
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
if (status) {
status.textContent = message;
status.className = `status ${type}`;
}
}
function updateProgress(percentage: number) {
if (progressBar) {
progressBar.style.width = `${percentage}%`;
}
}
function updateSyncItem(
elementId: string,
value: string,
status: 'pending' | 'completed' | 'error' = 'pending'
) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = value;
element.className = `sync-status ${status}`;
}
}
// Gestion du bouton continuer (définie avant le try pour être toujours disponible)
if (continueBtn) {
continueBtn.addEventListener('click', async () => {
secureLogger.info('🔗 Redirecting to pairing page...', { component: 'BlockSync' });
window.location.href = '/src/pages/pairing/pairing.html';
});
}
try {
// Vérifier les prérequis
secureLogger.debug('🔍 Verifying prerequisites...', { component: 'BlockSync' });
updateStatus('🔍 Vérification des prérequis...', 'loading');
const pbkdf2KeyResult = await checkPBKDF2Key();
if (!pbkdf2KeyResult) {
secureLogger.warn('⚠️ PBKDF2 key not found, redirecting to security-setup...', {
component: 'BlockSync',
});
updateStatus('⚠️ Redirection vers la configuration de sécurité...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/security-setup/security-setup.html';
}, 1000);
return;
}
const wallet = await checkWalletWithRetries();
if (!wallet) {
secureLogger.warn('⚠️ Wallet not found, redirecting to wallet-setup...', {
component: 'BlockSync',
});
updateStatus('⚠️ Redirection vers la configuration du wallet...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
}, 1000);
return;
}
if (!wallet.sp_wallet?.birthday || wallet.sp_wallet.birthday === 0) {
secureLogger.warn('⚠️ Birthday not configured, redirecting to birthday-setup...', {
component: 'BlockSync',
});
updateStatus('⚠️ Redirection vers la configuration de la date anniversaire...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}, 1000);
return;
}
secureLogger.info('✅ All prerequisites verified for block sync', { component: 'BlockSync' });
updateStatus('✅ Prerequisites verified', 'success');
// Initialiser les services
secureLogger.info('🔄 Waiting for services to be ready...', { component: 'BlockSync' });
updateStatus('🔄 Initialisation des services...', 'loading');
let services: Services;
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
try {
secureLogger.info(
'🔄 Attempting to get services (attempt ${attempts + 1}/${maxAttempts})...',
{ component: 'BlockSync' }
);
services = await Services.getInstance();
secureLogger.info('✅ Services initialized successfully', { component: 'BlockSync' });
break;
} catch (error) {
attempts++;
secureLogger.warn(
`Services initialization failed (attempt ${attempts}/${maxAttempts})`,
error as Error,
{ component: 'BlockSync' }
);
if (attempts >= maxAttempts) {
throw new Error('Failed to initialize services after maximum attempts');
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (!services!) {
throw new Error('Services not initialized');
}
// Vérifier si le wallet est déjà synchronisé
const currentBlockHeight = services.getCurrentBlockHeight();
if (currentBlockHeight === -1) {
secureLogger.warn('⚠️ Block height not available, connecting to relays...', {
component: 'BlockSync',
});
updateStatus('⚠️ Connexion aux relays...', 'loading');
// Attendre que les services se connectent aux relays
await services.connectAllRelays();
// Attendre que la hauteur de bloc soit définie
await services.waitForBlockHeight();
}
const finalBlockHeight = services.getCurrentBlockHeight();
const birthday = wallet.sp_wallet.birthday;
const lastScan = wallet.sp_wallet.last_scan || 0;
const toScan = Math.max(0, finalBlockHeight - lastScan);
secureLogger.info(
'📊 Sync info: current=${finalBlockHeight}, birthday=${birthday}, lastScan=${lastScan}, toScan=${toScan}',
{ component: 'BlockSync' }
);
// Mettre à jour l'interface
updateSyncItem('currentBlock', finalBlockHeight.toString(), 'completed');
updateSyncItem('birthday', birthday.toString(), 'completed');
updateSyncItem('lastScan', lastScan.toString(), 'completed');
if (toScan === 0) {
secureLogger.info('✅ Wallet already synchronized', { component: 'BlockSync' });
updateStatus('✅ Wallet déjà synchronisé', 'success');
updateSyncItem('blocksToScan', '0', 'completed');
updateSyncItem('blocksScanned', '0', 'completed');
updateSyncItem('transactionsFound', '0', 'completed');
// Activer le bouton et rediriger automatiquement
if (continueBtn) {
continueBtn.disabled = false;
continueBtn.textContent = 'Aller au pairing';
}
// Auto-redirection après 3 secondes
setTimeout(() => {
secureLogger.info('🔗 Auto-redirecting to pairing page...', { component: 'BlockSync' });
window.location.href = '/src/pages/pairing/pairing.html';
}, 3000);
return;
}
// Afficher la barre de progression
updateProgress(0);
updateStatus('🔄 Synchronisation en cours...', 'loading');
updateSyncItem('blocksToScan', toScan.toString(), 'pending');
// Intercepter les messages de progression du scan
// Fonction pour intercepter les messages de progression
const originalConsoleLog = console.log;
console.log = (...args: any[]) => {
const message = args.join(' ');
const containsProgress = message.includes('Scan progress:');
secureLogger.debug('SDK console output intercepted', {
component: 'BlockSync',
containsProgress
});
if (containsProgress) {
// Extraire les informations de progression
const progressMatch = message.match(/Scan progress: (\d+)\/(\d+) \((\d+)%\)/);
if (progressMatch) {
const currentBlock = parseInt(progressMatch[1]);
const totalBlocks = parseInt(progressMatch[2]);
const percentage = parseInt(progressMatch[3]);
// Mettre à jour l'interface avec les détails de progression
updateStatus(
`🔍 Synchronisation des blocs: ${currentBlock}/${totalBlocks} (${percentage}%)`,
'loading'
);
updateProgress(percentage);
// Mettre à jour les éléments de synchronisation
updateSyncItem('blocksScanned', currentBlock.toString(), 'pending');
updateSyncItem('blocksToScan', (totalBlocks - currentBlock).toString(), 'pending');
secureLogger.debug('SDK progress update parsed', {
component: 'BlockSync',
currentBlock,
totalBlocks,
percentage
});
}
}
// Appeler la fonction console.log originale
originalConsoleLog.apply(console, args);
};
try {
// Effectuer la synchronisation
await services.updateDeviceBlockHeight();
secureLogger.info('✅ Block scan completed successfully', { component: 'BlockSync' });
// Restaurer la fonction console.log originale
console.log = originalConsoleLog;
updateStatus('✅ Synchronisation terminée', 'success');
updateProgress(100);
updateSyncItem('blocksScanned', toScan.toString(), 'completed');
updateSyncItem('blocksToScan', '0', 'completed');
updateSyncItem('transactionsFound', '0', 'completed');
// Activer le bouton et rediriger automatiquement
if (continueBtn) {
continueBtn.disabled = false;
continueBtn.textContent = 'Aller au pairing';
}
// Auto-redirection après 3 secondes
setTimeout(() => {
secureLogger.info('🔗 Auto-redirecting to pairing page...', { component: 'BlockSync' });
window.location.href = '/src/pages/pairing/pairing.html';
}, 3000);
} catch (error) {
// Restaurer la fonction console.log originale en cas d'erreur
console.log = originalConsoleLog;
secureLogger.error('Error during block scan', error as Error, { component: 'BlockSync' });
updateStatus(`❌ Erreur lors de la synchronisation: ${(error as Error).message}`, 'error');
updateSyncItem('blocksToScan', 'Erreur', 'error');
throw error;
}
} catch (error) {
secureLogger.error('Error in block sync page', error as Error, { component: 'BlockSync' });
updateStatus(`❌ Erreur: ${(error as Error).message}`, 'error');
// Rediriger vers la page appropriée selon l'erreur
const errorMessage = (error as Error).message;
if (errorMessage.includes('PBKDF2') || errorMessage.includes('security')) {
setTimeout(() => {
window.location.href = '/src/pages/security-setup/security-setup.html';
}, 2000);
} else if (errorMessage.includes('wallet') || errorMessage.includes('device')) {
setTimeout(() => {
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
}, 2000);
} else if (errorMessage.includes('birthday')) {
setTimeout(() => {
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}, 2000);
}
}
});

View File

@ -0,0 +1,49 @@
import { ChatElement } from './chat';
import chatCss from '../../../public/style/chat.css?raw';
import Services from '../../services/service.js';
class ChatComponent extends HTMLElement {
_callback: any;
chatElement: ChatElement | null = null;
constructor() {
super();
console.log('INIT');
this.attachShadow({ mode: 'open' });
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
}
connectedCallback() {
console.log('CALLBACKs');
this.render();
if (!customElements.get('chat-element')) {
customElements.define('chat-element', ChatElement);
}
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot) {
// Créer l'élément chat-element
const chatElement = document.createElement('chat-element');
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
this.shadowRoot.appendChild(chatElement);
}
}
}
export { ChatComponent };
customElements.define('chat-component', ChatComponent);

13
src/pages/chat/chat.html Executable file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat</title>
</head>
<body>
<chat-component></chat-component>
<script type="module" src="./chat.ts"></script>
</body>
</html>

1748
src/pages/chat/chat.ts Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,49 @@
import loginHtml from './home.html?raw';
import loginScript from './home.ts?raw';
import loginCss from '../../4nk.css?raw';
import { initHomePage } from './home';
import { secureLogger } from '../../services/secure-logger';
export class LoginComponent extends HTMLElement {
_callback: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
secureLogger.info('Login component connected', { component: 'LoginComponent' });
this.render();
setTimeout(() => {
initHomePage();
}, 500);
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
secureLogger.error('Callback is not a function', { component: 'LoginComponent' });
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot)
{this.shadowRoot.innerHTML = `
<style>
${loginCss}
</style>${loginHtml}
<script type="module">
${loginScript}
</scipt>
`;}
}
}
if (!customElements.get('login-4nk-component')) {
customElements.define('login-4nk-component', LoginComponent);
}
import loginHtml from './home.html?raw';
import loginScript from './home.ts?raw';
import loginCss from '../../4nk.css?raw';
import { initHomePage } from './home';
export class LoginComponent extends HTMLElement {
_callback: any;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
console.log('CALLBACK LOGIN PAGE');
this.render();
setTimeout(() => {
initHomePage();
}, 500);
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot)
this.shadowRoot.innerHTML = `
<style>
${loginCss}
</style>${loginHtml}
<script type="module">
${loginScript}
</scipt>
`;
}
}
if (!customElements.get('login-4nk-component')) {
customElements.define('login-4nk-component', LoginComponent);
}

View File

@ -1,22 +1,42 @@
<div class="pairing-container">
<!-- Main Pairing Interface -->
<div id="main-pairing" class="card pairing-card">
<div class="card-header">
<h2>🔐 4NK Pairing</h2>
<p class="card-description">Secure device pairing with WebAuthn authentication</p>
</div>
<div class="pairing-request"></div>
<div class="status-container">
<div class="status-indicator" id="main-status">
<!-- Content will be set by JavaScript -->
</div>
</div>
<div class="account-actions">
<button id="deleteAccountButton" class="danger-btn">🗑️ Delete Account</button>
</div>
</div>
</div>
<div class="title-container">
<h1>Create Account / New Session</h1>
</div>
<div class="tab-container">
<div class="tabs">
<div class="tab active" data-tab="tab1">Create an account</div>
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
</div>
</div>
<div class="page-container">
<div id="tab1" class="card tab-content active">
<div class="card-description">Create an account :</div>
<div class="pairing-request"></div>
<!-- <div class="card-image qr-code">
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
</div> -->
<button id="createButton" class="create-btn"></button>
</div>
<div class="separator"></div>
<div id="tab2" class="card tab-content">
<div class="card-description">Add a device for an existing member :</div>
<div class="card-image camera-card">
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
<button id="scan-btn" onclick="scanDevice()">Scan</button>
<div class="qr-code-scanner">
<div id="qr-reader" style="width: 200px; display: contents"></div>
<div id="qr-reader-results"></div>
</div>
</div>
<p>Or</p>
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
<div id="emoji-display-2"></div> -->
<div class="card-description">Chose a member :</div>
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
<!-- Options -->
</select>
<button id="okButton" style="display: none">OK</button>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +0,0 @@
<!-- Menu buttons for iframe integration -->
<div class="content-menu">
<button class="menu-btn active" data-page="home">🏠 Home</button>
<button class="menu-btn" data-page="account">👤 Account</button>
<button class="menu-btn" data-page="settings">⚙️ Settings</button>
<button class="menu-btn" data-page="help">❓ Help</button>
</div>
<div class="pairing-container">
<!-- Creator Flow -->
<div id="creator-flow" class="card pairing-card" style="display: none">
<div class="card-header">
<h2>🔐 Create New Pairing</h2>
</div>
<div class="pairing-request"></div>
<div class="words-display-container">
<div class="words-label">Share these 4 words with the other device:</div>
<div class="words-content" id="creator-words"></div>
<button class="copy-btn" id="copyWordsBtn">📋 Copy Words</button>
</div>
<div class="status-container">
<div class="status-indicator" id="creator-status">
<div class="spinner"></div>
<span>Creating pairing process...</span>
</div>
</div>
<button id="createButton" class="primary-btn">Create Pairing</button>
</div>
<!-- Joiner Flow -->
<div id="joiner-flow" class="card pairing-card" style="display: none">
<div class="card-header">
<h2>🔗 Join Existing Pairing</h2>
<p class="card-description">Enter the 4 words from the creator device</p>
</div>
<div class="input-container">
<input
type="text"
id="wordsInput"
placeholder="Enter 4 words (e.g., abandon ability able about)"
class="words-input"
autocomplete="off"
spellcheck="false"
/>
<div class="input-hint">Separate words with spaces</div>
</div>
<div class="words-display" id="words-display-2"></div>
<div class="status-container">
<div class="status-indicator" id="joiner-status">
<span>Ready to join</span>
</div>
</div>
<button id="joinButton" class="primary-btn" disabled>Join Pairing</button>
</div>
<!-- Loading State -->
<div id="loading-flow" class="card pairing-card">
<div class="loading-container">
<div class="spinner large"></div>
<h2>Initializing...</h2>
<p>Setting up secure pairing</p>
</div>
</div>
</div>

View File

@ -1,77 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>4NK Pairing - Hidden</title>
<style>
body {
margin: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
background: transparent;
}
.hidden-container {
position: absolute;
top: -9999px;
left: -9999px;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<div class="hidden-container">
<!-- This iframe is completely hidden and only used for pairing communication -->
<div id="pairing-status">Ready</div>
</div>
<script type="module">
import { MessageType } from '../models/process.model';
import IframePairingService from '../services/iframe-pairing.service';
import { secureLogger } from '../services/secure-logger';
// Initialize the iframe pairing service
const pairingService = IframePairingService.getInstance();
// Listen for messages from parent window
window.addEventListener('message', event => {
const { type, data } = event.data;
switch (type) {
case MessageType.PAIRING_4WORDS_CREATE:
secureLogger.info('Parent requested pairing creation', {
component: 'IframePairingPage'
});
pairingService.createPairing();
break;
case MessageType.PAIRING_4WORDS_JOIN:
secureLogger.info('Parent requested pairing join', {
component: 'IframePairingPage',
hasWords: Boolean(data?.words)
});
pairingService.joinPairing(data.words);
break;
}
});
// Notify parent that iframe is ready
window.parent.postMessage(
{
type: 'IFRAME_READY',
data: { service: 'pairing' },
},
'*'
);
secureLogger.info('Hidden iframe pairing service ready', {
component: 'IframePairingPage'
});
</script>
</body>
</html>

View File

@ -1,186 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pairing - 4NK</title>
<link rel="stylesheet" href="../../4nk.css">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 800px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 2.5rem;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
font-size: 1.1rem;
}
.status {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 1rem;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.status.loading {
background: #e3f2fd;
color: #1976d2;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
/* Styles pour le contenu de pairing */
.pairing-container {
margin-top: 20px;
}
.card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e9ecef;
}
.card-header h2 {
margin: 0 0 10px 0;
color: #333;
font-size: 1.5rem;
}
.card-description {
color: #666;
margin: 0 0 20px 0;
}
.status-container {
margin: 20px 0;
}
.status-indicator {
padding: 15px;
border-radius: 6px;
text-align: center;
font-weight: 500;
}
.status-indicator.loading {
background: #e3f2fd;
color: #1976d2;
}
.status-indicator.success {
background: #e8f5e8;
color: #2e7d32;
}
.status-indicator.error {
background: #ffebee;
color: #c62828;
}
.account-actions {
text-align: center;
margin-top: 20px;
}
.danger-btn {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
}
.danger-btn:hover {
background: #c82333;
}
.pairing-request {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.debug-info {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
font-family: monospace;
font-size: 0.9rem;
}
.debug-info h3 {
margin: 0 0 10px 0;
color: #333;
font-size: 1rem;
}
.debug-info pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 Pairing</h1>
<p class="subtitle">Appairage sécurisé des appareils</p>
<div class="status loading" id="status">
🔄 Initialisation en cours...
</div>
<div class="pairing-container" id="pairingContainer">
<!-- Le contenu de pairing sera injecté ici -->
</div>
</div>
<script type="module" src="./pairing.ts"></script>
</body>
</html>

View File

@ -1,180 +0,0 @@
import { DeviceReaderService } from '../../services/device-reader.service';
import { secureLogger } from '../../services/secure-logger';
import { checkPBKDF2Key, checkWalletWithRetries } from '../../utils/prerequisites.utils';
// Extend WindowEventMap to include custom events
declare global {
interface WindowEventMap {
'pairing-words-generated': CustomEvent;
'pairing-success': CustomEvent;
'pairing-error': CustomEvent;
}
}
let isInitializing = false;
document.addEventListener('DOMContentLoaded', async () => {
if (isInitializing) {
secureLogger.warn('⚠️ Pairing page already initializing, skipping...', { component: 'PairingPage' });
return;
}
isInitializing = true;
secureLogger.info('🔐 Pairing page loaded', { component: 'PairingPage' });
const status = document.getElementById('status') as HTMLElement;
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
if (status) {
if (type === 'error') {
status.innerHTML = `
<div class="error-container">
<div class="error-icon"></div>
<div class="error-content">
<div class="error-title">Error</div>
<div class="error-message">${message}</div>
</div>
</div>
`;
} else if (type === 'success') {
status.innerHTML = `
<div class="success-container">
<div class="success-icon"></div>
<div class="success-content">
<div class="success-title">Success</div>
<div class="success-message">${message}</div>
</div>
</div>
`;
} else {
status.innerHTML = `
<div class="loading-container">
<div class="spinner"></div>
<div class="loading-message">${message}</div>
</div>
`;
}
status.className = `status ${type}`;
}
}
// Vérifier les prérequis en base de données
secureLogger.debug('🔍 Verifying prerequisites...', { component: 'PairingPage' });
updateStatus('🔍 Vérification des prérequis...', 'loading');
try {
secureLogger.info('🔧 Getting device reader service...', { component: 'PairingPage' });
// const deviceReader = DeviceReaderService.getInstance(); // Not used yet
// Vérifier que le PBKDF2 key existe d'abord
const pbkdf2KeyResult = await checkPBKDF2Key();
if (!pbkdf2KeyResult) {
secureLogger.warn('⚠️ PBKDF2 key not found, redirecting to security-setup...', { component: 'PairingPage' });
updateStatus('⚠️ Redirection vers la configuration de sécurité...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/security-setup/security-setup.html';
}, 1000);
return;
}
// Vérifier que le wallet existe en base
const wallet = await checkWalletWithRetries();
if (!wallet) {
secureLogger.warn('⚠️ Wallet still not found after retries, redirecting to wallet-setup...', { component: 'PairingPage' });
updateStatus('⚠️ Redirection vers la configuration du wallet...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
}, 1000);
return;
}
// Vérifier que le wallet contient bien les données attendues
if (wallet.sp_wallet?.birthday !== undefined) {
secureLogger.info(`Wallet found in database with birthday: ${wallet.sp_wallet.birthday}`, { component: 'Pairing' });
} else {
throw new Error('Wallet found but missing required data (sp_wallet or birthday)');
}
// Vérifier que le birthday est configuré (> 0)
if (!wallet.sp_wallet.birthday || wallet.sp_wallet.birthday === 0) {
secureLogger.warn('⚠️ Birthday not configured, redirecting to birthday-setup...', { component: 'PairingPage' });
updateStatus('⚠️ Redirection vers la configuration de la date anniversaire...', 'loading');
setTimeout(() => {
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}, 1000);
return;
}
secureLogger.info('✅ All prerequisites verified for pairing page', { component: 'PairingPage' });
// Charger le contenu de pairing depuis home.html
updateStatus('🔄 Initialisation du pairing...', 'loading');
// Injecter le contenu de pairing dans la zone de contenu
const pairingContainer = document.getElementById('pairingContainer');
if (pairingContainer) {
// Créer un contenu HTML complet pour la page de pairing
const pairingContent = `
<div class="pairing-container">
<!-- Main Pairing Interface -->
<div id="main-pairing" class="card pairing-card">
<div class="card-header">
<h2>🔐 4NK Pairing</h2>
<p class="card-description">Secure device pairing with WebAuthn authentication</p>
</div>
<div class="pairing-request"></div>
<div class="status-container">
<div class="status-indicator" id="main-status">
<!-- Content will be set by JavaScript -->
</div>
</div>
<div class="account-actions">
<button id="deleteAccountButton" class="danger-btn">🗑 Delete Account</button>
</div>
</div>
</div>
`;
pairingContainer.innerHTML = pairingContent;
}
// Importer et initialiser la logique de pairing depuis home.ts
const { initHomePage } = await import('../home/home');
await initHomePage();
updateStatus('✅ Prêt pour le pairing', 'success');
setTimeout(() => {
if (status) {
status.style.display = 'none';
}
}, 2000);
secureLogger.info('✅ Pairing page initialization completed', { component: 'PairingPage' });
} catch (error) {
secureLogger.error('Error initializing pairing page', error as Error, { component: 'Pairing' });
updateStatus(`❌ Erreur: ${(error as Error).message}`, 'error');
// Si l'erreur est liée aux prérequis, rediriger vers la page appropriée
const errorMessage = (error as Error).message;
if (errorMessage.includes('PBKDF2') || errorMessage.includes('security')) {
secureLogger.error('⚠️ Security error detected, redirecting to security-setup...', { component: 'PairingPage' });
setTimeout(() => {
window.location.href = '/src/pages/security-setup/security-setup.html';
}, 2000);
} else if (errorMessage.includes('wallet') || errorMessage.includes('device')) {
secureLogger.error('⚠️ Wallet error detected, redirecting to wallet-setup...', { component: 'PairingPage' });
setTimeout(() => {
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
}, 2000);
} else if (errorMessage.includes('birthday')) {
secureLogger.error('⚠️ Birthday error detected, redirecting to birthday-setup...', { component: 'PairingPage' });
setTimeout(() => {
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}, 2000);
}
} finally {
isInitializing = false;
}
});

View File

@ -0,0 +1,51 @@
import processHtml from './process-element.html?raw';
import processScript from './process-element.ts?raw';
import processCss from '../../4nk.css?raw';
import { initProcessElement } from './process-element';
export class ProcessListComponent extends HTMLElement {
_callback: any;
id: string = '';
zone: string = '';
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
console.log('CALLBACK PROCESS LIST PAGE');
this.render();
setTimeout(() => {
initProcessElement(this.id, this.zone);
}, 500);
}
set callback(fn) {
if (typeof fn === 'function') {
this._callback = fn;
} else {
console.error('Callback is not a function');
}
}
get callback() {
return this._callback;
}
render() {
if (this.shadowRoot)
this.shadowRoot.innerHTML = `
<style>
${processCss}
</style>${processHtml}
<script type="module">
${processScript}
</scipt>
`;
}
}
if (!customElements.get('process-4nk-component')) {
customElements.define('process-4nk-component', ProcessListComponent);
}

View File

@ -0,0 +1,5 @@
<div class="title-container">
<h1>Process {{processTitle}}</h1>
</div>
<div class="process-container"></div>

Some files were not shown because too many files have changed in this diff Show More