Compare commits

..

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

111 changed files with 23564 additions and 6196 deletions

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,4 +0,0 @@
VITE_BASEURL="your_base_url"
VITE_BOOTSTRAPURL="your_bootstrap_url"
VITE_STORAGEURL="your_storage_url"
VITE_BLINDBITURL="your_blindbit_url"

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 }}

100
.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
# ----------------------------
.vscode/
.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/
.vscode
public/ssl/

5
.prettierignore Executable file
View File

@ -0,0 +1,5 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache
.angular

14
.prettierrc Executable file
View File

@ -0,0 +1,14 @@
{
"printWidth": 300,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"endOfLine": "crlf"
}

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"]

128
README.md
View File

@ -1,90 +1,52 @@
# 4NK Client SDK (Iframe & Standalone)
# ihm_client
Une application **Web5 décentralisée** construite avec **TypeScript**, **Vite**, **Web Components** et **WebAssembly (Rust)**.
Cette application est conçue pour fonctionner de manière autonome ou intégrée via Iframe en tant que SDK pour des applications tierces.
## 🏗 Architecture
Le projet suit une architecture modulaire stricte (**Domain-Driven Design**) :
## HOW TO START
- **`src/services/core`** : Services bas niveau (Réseau `network.service.ts`, SDK WASM `sdk.service.ts`).
- **`src/services/domain`** : Logique métier (Wallet `wallet.service.ts`, Process `process.service.ts`, Crypto `crypto.service.ts`).
- **`src/services/service.ts`** : Façade principale (Singleton) orchestrant les sous-services.
- **`src/services/iframe-controller.service.ts`** : Contrôleur dédié à la communication API via `postMessage` (mode Iframe).
- **`src/service-workers`** : Gestionnaires d'arrière-plan modulaires (Base de données, Cache).
- **`src/pages` & `src/components`** : Interface utilisateur basée sur des Web Components natifs (Shadow DOM) et un design system "Glassmorphism".
- **`src/config`** : Configuration centralisée (`constants.ts`).
1 - clone sdk_common, commit name "doc pcd" from 28.10.2024
2 - clone sdk_client, commit name "Ignore messages" from 17.10.2024
3 - clone ihm_client_test3
4 - cargo build in sdk_common
5 - cargo run in sdk_client
6 - npm run build_wasm in ihm_client_test3
7 - npm run start in ihm_client_test3
## 🚀 Démarrage Rapide
## USER STORIES
### Prérequis
- **Node.js** (v20+)
- **Rust & Cargo** (pour la compilation WASM)
- **Nginx** (pour la production ou le reverse-proxy local)
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"
### Installation
1. **Compiler le module WASM :**
```bash
npm run build_wasm
```
*Note : Cette commande compile le code Rust situé dans `../sdk_client` vers `./pkg`.*
2. **Installer les dépendances JS :**
```bash
npm install
```
3. **Lancer en développement :**
```bash
npm run start
```
L'application sera accessible sur `http://localhost:3003`.
## 📦 Build & Production
Pour créer une version de production optimisée dans le dossier `dist/` :
```bash
npm run build
```
### Déploiement Nginx
Utilisez les fichiers de configuration fournis à la racine :
- **`nginx.dev.conf`** : Pour le développement local (proxy vers Vite).
- **`nginx.prod.conf`** : Pour la production (SSL, Headers de sécurité, Service-Worker-Allowed).
## 🔧 Configuration
La configuration de l'application est centralisée dans `src/config/constants.ts`.
Vous pouvez surcharger les URLs via un fichier `.env` à la racine :
| Variable | Description |
| :--- | :--- |
| `VITE_BASEURL` | URL de base de l'infrastructure. |
| `VITE_BOOTSTRAPURL` | URL du relais WebSocket. |
| `VITE_STORAGEURL` | URL du stockage distant. |
| `VITE_BLINDBITURL` | URL de l'indexeur Bitcoin. |
## 🧪 Fonctionnalités Clés
### 1\. Auto-Healing WebSocket
Le service réseau maintient la connexion active avec un système de "Heartbeat" et de reconnexion exponentielle automatique. Aucune action utilisateur requise en cas de coupure réseau.
### 2\. Iframe Persistence & Performance
En mode Iframe, l'application charge son état en mémoire et ne nécessite **aucun rafraîchissement**.
- Stratégie **Cache-First** : Les requêtes `GET_PROCESSES` répondent instantanément via le cache mémoire.
- **Verrou d'initialisation** : Empêche les conflits si l'iframe est rechargée par erreur.
### 3\. Service Worker Modulaire
Le fichier `src/service-workers/sw.ts` agit comme un point d'entrée maître, important dynamiquement la logique de base de données (`database.ts`). Il inclut une logique de nettoyage automatique des anciens workers ("Zombie Killer").
### 4\. UX Moderne
Interface "Glassmorphism" responsive utilisant CSS Grid pour un Layout (App Shell) stable et des Web Components natifs pour l'isolation des styles (Shadow DOM).
## 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,21 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Nicolas Cantu, Sosthene, Omar, Titouan">
<head>
<meta charset="UTF-8">
<meta name="author" content="4NK">
<meta name="description" content="4NK Web5 Platform">
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style/4nk.css">
<script src="https://unpkg.com/html5-qrcode"></script>
<title>4NK Application</title>
<link rel="stylesheet" href="/src/assets/styles/style.css" />
</head>
<body>
<app-layout>
<div id="header-slot" slot="header"></div>
<div id="app-container" slot="content" class="container"></div>
</app-layout>
<script type="module" src="/src/main.ts"></script>
</body>
</head>
<body>
<div id="header-container"></div>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution -->
</div>
<!-- <script type="module" src="/src/index.ts"></script> -->
<script type="module">
import { init } from '/src/router.ts';
(async () => {
await init();
})();
</script>
</body>
</html>

View File

@ -1,48 +0,0 @@
server {
listen 80;
server_name localhost;
# Redirection des requêtes HTTP vers Vite
location / {
proxy_pass http://localhost:3003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /ws/ {
proxy_pass http://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:8081;
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;
}
}

View File

@ -1,99 +0,0 @@
# --- 1. REDIRECTION HTTP VERS HTTPS ---
server {
listen 80;
server_name dev2.4nkweb.com;
return 301 https://$host$request_uri;
}
# --- 2. CONFIGURATION HTTPS PRINCIPALE ---
server {
listen 443 ssl;
server_name dev2.4nkweb.com;
# Chemins des certificats SSL
ssl_certificate /etc/letsencrypt/live/dev2.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev2.4nkweb.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers HIGH:!aNULL:!MD5;
# --- LOCATION POUR VITE (Front-end + HMR WebSocket) ---
location / {
proxy_pass http://localhost:3003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
# --- LOCATION POUR L'AUTRE WEBSOCKET (port 8090) ---
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 POUR SDK_STORAGE (port 8081) ---
location /storage/ {
# Gestion du préflight CORS
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
# Headers CORS pour les requêtes réelles
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
rewrite ^/storage(/.*)$ $1 break;
proxy_pass http://localhost:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
# --- LOCATION POUR TON API (port 8091) ---
location /api/ {
proxy_pass http://localhost:8091;
proxy_http_version 1.1;
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;
}
location /blindbit/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
proxy_pass http://localhost:8000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}

7185
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,45 @@
{
"name": "sdk_client",
"version": "1.0.0",
"type": "module",
"description": "Client SDK 4NK - Web5 Platform",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev2/pkg ../sdk_client --target bundler --dev",
"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": "sudo cp -r dist/* /var/www/html/",
"build:dist": "tsc -p tsconfig.build.json"
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
},
"keywords": [],
"author": "Nicolas Cantu",
"contributors": [
"Sosthene",
"Omar",
"Titouan"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^12.1.1",
"copy-webpack-plugin": "^12.0.2",
"html-webpack-plugin": "^5.6.0",
"prettier": "^3.3.3",
"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-static-copy": "^1.0.6",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2"
},
"dependencies": {
"comlink": "^4.4.2",
"jose": "^6.0.11",
"@angular/elements": "^19.0.1",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-vue": "^5.0.5",
"axios": "^1.7.8",
"html5-qrcode": "^2.3.8",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.3",
"sweetalert2": "^11.14.5",
"vite-plugin-copy": "^0.1.6",
"vite-plugin-html": "^3.2.2",
"vite-plugin-wasm": "^3.3.0"
}
}

BIN
public/assets/4nk_image.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
public/assets/4nk_revoke.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
public/assets/bgd.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
public/assets/camera.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

34
public/assets/home.js Executable file
View File

@ -0,0 +1,34 @@
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(tab.getAttribute('data-tab')).classList.add('active');
});
});
function toggleMenu() {
var menu = document.getElementById('menu');
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
}
//// Modal
function openModal() {
document.getElementById('modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
// Close modal when clicking outside of it
window.onclick = function(event) {
const modal = document.getElementById('modal');
if (event.target === modal) {
closeModal();
}
}

BIN
public/assets/qr_code.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -1,202 +0,0 @@
// public/data.worker.js
const DB_NAME = "4nk";
const DB_VERSION = 1;
const EMPTY32BYTES = String("").padStart(64, "0");
// ============================================
// SERVICE WORKER LIFECYCLE
// ============================================
self.addEventListener("install", (event) => {
event.waitUntil(self.skipWaiting());
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
// ============================================
// INDEXEDDB DIRECT ACCESS (READ-ONLY)
// ============================================
/**
* Ouvre une connexion à la BDD directement depuis le Service Worker
*/
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère un objet spécifique (équivalent à GET_OBJECT)
*/
function getObject(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS)
* Optimisé pour utiliser une seule transaction.
*/
function getMultipleObjects(db, storeName, keys) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const results = [];
let completed = 0;
if (keys.length === 0) resolve([]);
keys.forEach((key) => {
const request = store.get(key);
request.onsuccess = () => {
if (request.result) results.push(request.result);
completed++;
if (completed === keys.length) resolve(results);
};
request.onerror = () => {
console.warn(`[SW] Erreur lecture clé ${key}`);
completed++;
if (completed === keys.length) resolve(results);
};
});
});
}
// ============================================
// SCAN LOGIC
// ============================================
async function scanMissingData(processesToScan) {
let db;
try {
db = await openDB();
} catch (e) {
console.error("[SW] Impossible d'ouvrir la BDD:", e);
return { toDownload: [], diffsToCreate: [] };
}
// 1. Récupération directe des processus
const myProcesses = await getMultipleObjects(
db,
"processes",
processesToScan
);
let toDownload = new Set();
let diffsToCreate = [];
if (myProcesses && myProcesses.length !== 0) {
for (const process of myProcesses) {
if (!process || !process.states) continue;
const firstState = process.states[0];
if (!firstState) continue;
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
if (
(state.public_data && state.public_data[field] !== undefined) ||
field === "roles"
)
continue;
// 2. Vérification directe dans 'data'
const existingData = await getObject(db, "data", hash);
if (!existingData) {
toDownload.add(hash);
// 3. Vérification directe dans 'diffs'
const existingDiff = await getObject(db, "diffs", hash);
if (!existingDiff) {
diffsToCreate.push({
process_id: processId,
state_id: state.state_id,
value_commitment: hash,
roles: state.roles,
field: field,
description: null,
previous_value: null,
new_value: null,
notify_user: false,
need_validation: false,
validation_status: "None",
});
}
} else {
if (toDownload.has(hash)) {
toDownload.delete(hash);
}
}
}
}
}
}
// On ferme la connexion BDD
db.close();
// ✅ LOG PERTINENT UNIQUEMENT : On n'affiche que si on a trouvé quelque chose
if (toDownload.size > 0 || diffsToCreate.length > 0) {
console.log("[Service Worker] 🔄 Scan found items:", {
toDownload: toDownload.size,
diffsToCreate: diffsToCreate.length,
});
}
return {
toDownload: Array.from(toDownload),
diffsToCreate: diffsToCreate,
};
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener("message", async (event) => {
const data = event.data;
if (data.type === "SCAN") {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length !== 0) {
// Appel direct de la nouvelle fonction optimisée
const scanResult = await scanMissingData(myProcessesId);
if (scanResult.toDownload.length !== 0) {
event.source.postMessage({
type: "TO_DOWNLOAD",
data: scanResult.toDownload,
});
}
if (scanResult.diffsToCreate.length > 0) {
event.source.postMessage({
type: "DIFFS_TO_CREATE",
data: scanResult.diffsToCreate,
});
}
}
} catch (error) {
console.error("[Service Worker] Scan error:", error);
// On évite de spammer l'UI avec des erreurs internes du worker
}
}
});

784
public/style/4nk.css Executable file
View File

@ -0,0 +1,784 @@
:root {
--primary-color
: #3A506B;
/* Bleu métallique */
--secondary-color
: #B0BEC5;
/* Gris acier */
--accent-color
: #D68C45;
/* Cuivre */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
background-image: url(../assets/bgd.webp);
background-repeat:no-repeat;
background-size: cover;
background-blend-mode :soft-light;
height: 100vh;
}
.message {
margin: 30px 0;
font-size: 14px;
overflow-wrap: anywhere;
}
.message strong{
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
/** Modal Css */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 3;
}
.modal-content {
width: 55%;
height: 30%;
background-color: white;
border-radius: 4px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-title {
margin: 0;
padding-bottom: 8px;
width: 100%;
font-size: 0.9em;
border-bottom: 1px solid #ccc;
}
.confirmation-box {
/* margin-top: 20px; */
align-content: center;
width: 70%;
height: 20%;
/* padding: 20px; */
font-size: 1.5em;
color: #333333;
top: 5%;
position: relative;
}
.nav-wrapper {
position: fixed;
z-index: 2;
background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */
display: flex;
justify-content: flex-end;
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);
.nav-right-icons {
display: flex;
.notification-container {
position: relative;
display: inline-block;
}
.notification-bell, .burger-menu {
z-index: 3;
height: 20px;
width: 20px;
margin-right: 1rem;
}
.notification-badge {
position: absolute;
top: -.7rem;
left: -.8rem;
background-color: red;
color: white;
border-radius: 50%;
padding: 2.5px 6px;
font-size: 0.8em;
font-weight: bold;
}
}
.notification-board {
position: absolute;
width: 20rem;
min-height: 8rem;
background-color: white;
right: 0.5rem;
display: none;
border-radius: 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
.notification-element {
padding: .8rem 0;
width: 100%;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.notification-element:not(:last-child) {
border-bottom: 1px solid;
}
}
}
.brand-logo {
height: 100%;
width: 100vw;
align-content: center;
position: relative;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.container {
text-align: center;
display: grid;
height: 100vh;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
grid-auto-rows: 10vh 15vh 1fr;
}
.title-container {
grid-column: 2 / 7;
grid-row: 2;
}
.page-container {
grid-column: 2 / 7;
grid-row: 3 ;
justify-content: center;
display: flex;
padding: 1rem;
box-sizing: border-box;
max-height: 60vh;
}
h1 {
font-size: 2em;
margin: 20px 0;
}
@media only screen and (min-width: 600px) {
.tab-container {
display: none;
}
.page-container {
display: flex;
align-items: center;
}
.process-container {
grid-column: 3 / 6;
grid-row: 3 ;
.card {
min-width: 40vw;
}
}
.separator {
width: 2px;
background-color: #78909C;
height: 80%;
margin: 0 0.5em;
}
.tab-content {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
height: 80%;
}
}
@media only screen and (max-width: 600px) {
.process-container {
grid-column: 2 / 7;
grid-row: 3 ;
}
.container {
grid-auto-rows: 10vh 15vh 15vh 1fr;
}
.tab-container {
grid-column: 1 / 8;
grid-row: 3;
}
.page-container {
grid-column: 2 / 7;
grid-row: 4 ;
}
.separator {
display: none;
}
.tabs {
display: flex;
flex-grow: 1;
overflow: hidden;
z-index: 1;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #E0E4D6;
}
.tab {
flex: 1;
text-align: center;
padding: 10px 0;
cursor: pointer;
font-size: 1em;
color: #6200ea;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.tab.active {
border-bottom: 2px solid #6200ea;
font-weight: bold;
}
.card.tab-content {
display: none;
}
.tab-content.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80%;
}
.modal-content {
width: 80%;
height: 20%;
}
}
.qr-code {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.emoji-display {
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
#emoji-display-2{
margin-top: 30px;
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 20px;
}
#okButton{
margin-bottom: 2em;
cursor: pointer;
background-color: #D0D0D7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
margin-top: 10px;
}
.pairing-request {
font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif;
font-size: 14px;
margin-top: 0px;
}
.sp-address-btn {
margin-bottom: 2em;
cursor: pointer;
background-color: #D0D0D7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
}
.camera-card {
display: flex;
justify-content: center;
align-items: center;
/* height: 200px; */
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
text-align: center;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
background-color: #3700b3;
}
.card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 60vh;
justify-content: flex-start;
padding: 1rem;
overflow-y: auto;
}
.card-content {
flex-grow: 1;
flex-direction: column;
display: flex;
justify-content: flex-start;
align-items: center;
text-align: left;
font-size: .8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: .4rem 0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
&.selected {
background-color: rgba(26, 28, 24, .08);
}
}
}
.card-description {
padding: 20px;
font-size: 1em;
color: #333;
width: 90%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0px;
}
.card-action {
width: 100%;
}
.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;
}
.qr-code-scanner {
display: none;
}
/* QR READER */
#qr-reader div {
position: inherit;
}
#qr-reader div img{
top: 15px ;
right: 25px;
margin-top: 5px;
}
/* INPUT CSS **/
.input-container {
position: relative;
width: 100%;
background-color: #ECEFF1;
}
.input-field {
width: 36vw;
padding: 10px 0;
font-size: 1em;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
transition: border-color 0.3s;
}
.input-field:focus {
border-bottom: 2px solid #6200ea;
}
.input-label {
position: absolute;
margin-top: -0.5em;
top: 0;
left: 0;
padding: 10px 0;
font-size: 1em;
color: #999;
pointer-events: none;
transition: transform 0.3s, color 0.3s, font-size 0.3s;
}
.input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label {
transform: translateY(-20px);
font-size: 0.8em;
color: #6200ea;
}
.input-underline {
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: #6200ea;
transition: width 0.3s, left 0.3s;
}
.input-field:focus ~ .input-underline {
width: 100%;
left: 0;
}
.dropdown-content {
position: absolute;
flex-direction: column;
top: 100%;
left: 0;
width: 100%;
max-height: 150px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
z-index: 1;
}
.dropdown-content span {
padding: 10px;
cursor: pointer;
list-style: none;
}
.dropdown-content span:hover {
background-color: #f0f0f0;
}
/** AUTOCOMPLETE **/
select[data-multi-select-plugin] {
display: none !important;
}
.multi-select-component {
width: 36vw;
padding: 5px 0;
font-size: 1em;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
display: flex;
flex-direction: row;
height: auto;
width: 100%;
-o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.autocomplete-list {
border-radius: 4px 0px 0px 4px;
}
.multi-select-component:focus-within {
box-shadow: inset 0px 0px 0px 2px #78ABFE;
}
.multi-select-component .btn-group {
display: none !important;
}
.multiselect-native-select .multiselect-container {
width: 100%;
}
.selected-processes {
background-color: white;
padding: 0.4em;
}
.selected-wrapper {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: 1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.selected-wrapper .selected-label {
max-width: 514px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.selected-wrapper .selected-close {
display: inline-block;
text-decoration: none;
font-size: 14px;
line-height: 1.49em;
margin-left: 5px;
padding-bottom: 10px;
height: 100%;
vertical-align: top;
padding-right: 4px;
opacity: 0.2;
color: #000;
text-shadow: 0 1px 0 #fff;
font-weight: 700;
}
.search-container {
display: flex;
flex-direction: row;
}
.search-container .selected-input {
background: none;
border: 0;
height: 20px;
width: 60px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.search-container .selected-input:focus {
outline: none;
}
.dropdown-icon.active {
transform: rotateX(180deg)
}
.search-container .dropdown-icon {
display: inline-block;
padding: 10px 5px;
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border: 0 !important;
/* needed */
-webkit-appearance: none;
-moz-appearance: none;
/* SVG background image */
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E");
background-position: center;
background-size: 10px;
background-repeat: no-repeat;
}
.search-container ul {
position: absolute;
list-style: none;
padding: 0;
z-index: 3;
margin-top: 29px;
width: 100%;
right: 0px;
background: #fff;
border: 1px solid #ccc;
border-top: none;
border-bottom: none;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.search-container ul :focus {
outline: none;
}
.search-container ul li {
display: block;
text-align: left;
padding: 8px 29px 2px 12px;
border-bottom: 1px solid #ccc;
font-size: 14px;
min-height: 31px;
}
.search-container ul li:first-child {
border-top: 1px solid #ccc;
border-radius: 4px 0px 0 0;
}
.search-container ul li:last-child {
border-radius: 4px 0px 0 0;
}
.search-container ul li:hover.not-cursor {
cursor: default;
}
.search-container ul li:hover {
color: #333;
background-color: #f0f0f0;
;
border-color: #adadad;
cursor: pointer;
}
/* Adding scrool to select options */
.autocomplete-list {
max-height: 130px;
overflow-y: auto;
}
/**************************************** Process page card ******************************************************/
.process-card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-height: 40vh;
max-height: 60vh;
justify-content: space-between;
padding: 1rem;
overflow-y: auto;
}
.process-card-content {
text-align: left;
font-size: .8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: .4rem 0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
&.selected {
background-color: rgba(26, 28, 24, .08);
}
}
.selected-process-zone {
background-color: rgba(26, 28, 24, .08);
}
}
.process-card-description {
padding: 20px;
font-size: 1em;
color: #333;
width: 90%;
}
.process-card-action {
width: 100%;
}

1425
public/style/account.css Executable file

File diff suppressed because it is too large Load Diff

597
public/style/chat.css Executable file
View File

@ -0,0 +1,597 @@
/* Styles de base */
:root {
--primary-color: #3A506B;
/* Bleu métallique */
--secondary-color: #B0BEC5;
/* Gris acier */
--accent-color: #D68C45;
/* Cuivre */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
/* 4NK NAVBAR */
.brand-logo {
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: flex;
}
.notification-bell,
.burger-menu {
height: 20px;
width: 20px;
margin-right: 1rem;
cursor: pointer;
}
.notification-container {
position: relative;
/* Conserve la position pour le notification-board */
display: inline-flex;
align-items: center;
}
.notification-board {
position: absolute;
/* Position absolue pour le placer par rapport au container */
top: 40px;
right: 0;
background-color: white;
border: 1px solid #ccc;
padding: 10px;
width: 200px;
max-height: 300px;
overflow-y: auto;
/* Scroll si les notifications dépassent la taille */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
/* Définit la priorité d'affichage au-dessus des autres éléments */
display: none;
/* Par défaut, la notification est masquée */
}
.notification-item{
cursor: pointer;
}
.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;
}
/* Par défaut, le menu est masqué */
#menu {
display: none;
/* Menu caché par défaut */
transition: display 0.3s ease-in-out;
}
.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 {
display: flex;
flex: 1;
height: 90vh;
margin-top: 9vh;
margin-left: -1%;
text-align: left;
width: 100vw;
}
/* Liste des groupes */
.group-list {
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 {
cursor: pointer;
list-style: none;
padding: 0;
padding-right: 10px;
margin-left: 20px;
}
.group-list li {
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 {
background-color: #34495e;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.group-list .member-container {
position: relative;
}
.group-list .member-container button {
margin-left: 40px;
padding: 5px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
border-radius: 50px;
position: absolute;
top: -25px;
right: -25px;
}
.group-list .member-container button:hover {
background: var(--accent-color)
}
/* 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%;
}
/* 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;
padding: 20px;
overflow-y: auto;
background-color: #f1f1f1;
border-top: 1px solid #ddd;
}
.message-container {
display: flex;
margin: 8px;
}
.message-container .message {
align-self: flex-start;
}
.message-container .message.user {
align-self: flex-end;
margin-left: auto;
color: white;
}
.message {
max-width: 70%;
padding: 10px;
border-radius: 12px;
background:var(--secondary-color);
margin: 2px 0;
}
/* Messages de l'utilisateur */
.message.user {
background: #2196f3;
color: white;
}
.message-time {
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 {
padding: 10px;
background-color: #bdc3c7;
display: flex;
align-items: center;
border-radius: 10px;
margin: 1%;
/* Alignement vertical */
}
.input-area input[type="text"] {
flex: 1;
/* Prend l'espace restant */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.input-area .attachment-icon {
margin: 0 10px;
cursor: pointer;
display: flex;
align-items: center;
}
.input-area button {
padding: 10px;
margin-left: 10px;
background-color: #2980b9;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.input-area button:hover {
background-color: #1f608d;
}
.tabs {
display: flex;
margin: 20px 0px;
gap: 10px;
}
.tabs button {
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 {
background: var(--secondary-color);
color: var(--primary-color);
}
/* Signature */
.signature-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
transition: all 1s ease 0.1s;
visibility: visible;
}
.signature-area.hidden {
opacity: 0;
visibility: hidden;
display: none;
pointer-events: none;
}
.signature-header {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
border-radius: 10px 10px 0 0;
padding-left: 4%;
}
.signature-content {
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 {
height: 20%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-description li {
margin: 1% 0% 1% 0%;
list-style: none;
padding: 2%;
border-radius: 10px;
background-color: var(--primary-color);
color: var(--secondary-color);
width: 20%;
text-align: center;
cursor: pointer;
font-weight: bold;
margin-right: 2%;
overflow: auto;
}
.signature-description li .member-list {
margin-left: -30%;
}
.signature-description li .member-list li {
width: 100%;
}
.signature-description li .member-list li:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
.signature-documents {
height: 80%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-documents-header {
display: flex;
width: 100%;
height: 15%;
align-items: center;
}
#request-document-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
margin-left: 5%;
font-weight: bold;
}
#request-document-button:hover {
background-color: var(--accent-color);
font-weight: bold;
}
#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-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 screen and (max-width: 768px) {
.group-list {
display: none;
/* Masquer la liste des groupes sur les petits écrans */
}
.chat-area {
margin: 0;
}
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: var(--primary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

1664
public/style/signature.css Executable file

File diff suppressed because it is too large Load Diff

818
src/4nk.css Normal file
View File

@ -0,0 +1,818 @@
:host {
--primary-color: #3a506b;
/* Bleu métallique */
--secondary-color: #b0bec5;
/* Gris acier */
--accent-color: #d68c45;
/* Cuivre */
font-family: Arial, sans-serif;
height: 100vh;
font-size: 16px;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
background-image: url(../assets/bgd.webp);
background-repeat: no-repeat;
background-size: cover;
background-blend-mode: soft-light;
height: 100vh;
}
.message {
margin: 30px 0;
font-size: 14px;
overflow-wrap: anywhere;
}
.message strong {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
/** Modal Css */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 3;
}
.modal-content {
width: 55%;
height: 30%;
background-color: white;
border-radius: 4px;
padding: 20px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-title {
margin: 0;
padding-bottom: 8px;
width: 100%;
font-size: 0.9rem;
border-bottom: 1px solid #ccc;
}
.confirmation-box {
/* margin-top: 20px; */
align-content: center;
width: 70%;
height: 20%;
/* padding: 20px; */
font-size: 1.5em;
color: #333333;
top: 5%;
position: relative;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
/* background-color: #CFD8DC; */
display: flex;
justify-content: flex-end;
align-items: center;
color: #37474f;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow:
0px 8px 10px -5px rgba(0, 0, 0, 0.2),
0px 16px 24px 2px rgba(0, 0, 0, 0.14),
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
.nav-right-icons {
display: flex;
.notification-container {
position: relative;
display: inline-block;
}
.notification-bell,
.burger-menu {
z-index: 3;
height: 20px;
width: 20px;
margin-right: 1rem;
}
.notification-badge {
position: absolute;
top: -0.7rem;
left: -0.8rem;
background-color: red;
color: white;
border-radius: 50%;
padding: 2.5px 6px;
font-size: 0.8rem;
font-weight: bold;
}
}
.notification-board {
position: absolute;
width: 20rem;
min-height: 8rem;
background-color: white;
right: 0.5rem;
display: none;
border-radius: 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
.notification-element {
padding: 0.8rem 0;
width: 100%;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
}
.notification-element:not(:last-child) {
border-bottom: 1px solid;
}
}
}
.brand-logo {
height: 100%;
width: 100vw;
align-content: center;
position: relative;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.container {
text-align: center;
display: grid;
height: 100vh;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
grid-auto-rows: 10vh 15vh 1fr;
}
.title-container {
grid-column: 2 / 7;
grid-row: 2;
}
.page-container {
grid-column: 2 / 7;
grid-row: 3;
justify-content: center;
display: flex;
padding: 1rem;
box-sizing: border-box;
max-height: 60vh;
}
h1 {
font-size: 2em;
margin: 20px 0;
}
@media only screen and (min-width: 600px) {
.tab-container {
display: none;
}
.page-container {
display: flex;
align-items: center;
}
.process-container {
grid-column: 3 / 6;
grid-row: 3;
.card {
min-width: 40vw;
}
}
.separator {
width: 2px;
background-color: #78909c;
height: 80%;
margin: 0 0.5em;
}
.tab-content {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
height: 80%;
}
}
@media only screen and (max-width: 600px) {
.process-container {
grid-column: 2 / 7;
grid-row: 3;
}
.container {
grid-auto-rows: 10vh 15vh 15vh 1fr;
}
.tab-container {
grid-column: 1 / 8;
grid-row: 3;
}
.page-container {
grid-column: 2 / 7;
grid-row: 4;
}
.separator {
display: none;
}
.tabs {
display: flex;
flex-grow: 1;
overflow: hidden;
z-index: 1;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #e0e4d6;
}
.tab {
flex: 1;
text-align: center;
padding: 10px 0;
cursor: pointer;
font-size: 1rem;
color: #6200ea;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
}
.tab.active {
border-bottom: 2px solid #6200ea;
font-weight: bold;
}
.card.tab-content {
display: none;
}
.tab-content.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 80%;
}
.modal-content {
width: 80%;
height: 20%;
}
}
.qr-code {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.emoji-display {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
#emoji-display-2 {
margin-top: 30px;
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 20px;
}
#okButton {
margin-bottom: 2em;
cursor: pointer;
background-color: #d0d0d7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
margin-top: 10px;
}
.pairing-request {
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
font-size: 14px;
margin-top: 0px;
}
.create-btn {
margin-bottom: 2em;
cursor: pointer;
background-color: #d0d0d7;
color: white;
border-style: none;
border-radius: 5px;
color: #000;
padding: 2px;
}
.camera-card {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
/* height: 200px; */
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
text-align: center;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
background-color: #3700b3;
}
.card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 60vh;
justify-content: flex-start;
padding: 1rem;
overflow-y: auto;
}
.card-content {
flex-grow: 1;
flex-direction: column;
display: flex;
justify-content: flex-start;
align-items: center;
text-align: left;
font-size: 0.8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: 0.4rem 0;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
&.selected {
background-color: rgba(26, 28, 24, 0.08);
}
}
}
.card-description {
padding: 20px;
font-size: 1rem;
color: #333;
width: 90%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0px;
}
.card-action {
width: 100%;
}
.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, 0.08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
.qr-code-scanner {
display: none;
}
/* QR READER */
#qr-reader div {
position: inherit;
}
#qr-reader div img {
top: 15px;
right: 25px;
margin-top: 5px;
}
/* INPUT CSS **/
.input-container {
position: relative;
width: 100%;
background-color: #eceff1;
}
.input-field {
width: 36vw;
padding: 10px 0;
font-size: 1rem;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
transition: border-color 0.3s;
}
.input-field:focus {
border-bottom: 2px solid #6200ea;
}
.input-label {
position: absolute;
margin-top: -0.5em;
top: 0;
left: 0;
padding: 10px 0;
font-size: 1rem;
color: #999;
pointer-events: none;
transition:
transform 0.3s,
color 0.3s,
font-size 0.3s;
}
.input-field:focus + .input-label,
.input-field:not(:placeholder-shown) + .input-label {
transform: translateY(-20px);
font-size: 0.8em;
color: #6200ea;
}
.input-underline {
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: #6200ea;
transition:
width 0.3s,
left 0.3s;
}
.input-field:focus ~ .input-underline {
width: 100%;
left: 0;
}
.dropdown-content {
position: absolute;
flex-direction: column;
top: 100%;
left: 0;
width: 100%;
max-height: 150px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: none;
z-index: 1;
}
.dropdown-content span {
padding: 10px;
cursor: pointer;
list-style: none;
}
.dropdown-content span:hover {
background-color: #f0f0f0;
}
/** AUTOCOMPLETE **/
select[data-multi-select-plugin] {
display: none !important;
}
.multi-select-component {
width: 36vw;
padding: 5px 0;
font-size: 1rem;
border: none;
border-bottom: 1px solid #ccc;
outline: none;
background: transparent;
display: flex;
flex-direction: row;
height: auto;
width: 100%;
-o-transition:
border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
transition:
border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
}
.autocomplete-list {
border-radius: 4px 0px 0px 4px;
}
.multi-select-component:focus-within {
box-shadow: inset 0px 0px 0px 2px #78abfe;
}
.multi-select-component .btn-group {
display: none !important;
}
.multiselect-native-select .multiselect-container {
width: 100%;
}
.selected-processes {
background-color: white;
padding: 0.4em;
}
.selected-wrapper {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: 1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.selected-wrapper .selected-label {
max-width: 514px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.selected-wrapper .selected-close {
display: inline-block;
text-decoration: none;
font-size: 14px;
line-height: 1.49rem;
margin-left: 5px;
padding-bottom: 10px;
height: 100%;
vertical-align: top;
padding-right: 4px;
opacity: 0.2;
color: #000;
text-shadow: 0 1px 0 #fff;
font-weight: 700;
}
.search-container {
display: flex;
flex-direction: row;
}
.search-container .selected-input {
background: none;
border: 0;
height: 20px;
width: 60px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.search-container .selected-input:focus {
outline: none;
}
.dropdown-icon.active {
transform: rotateX(180deg);
}
.search-container .dropdown-icon {
display: inline-block;
padding: 10px 5px;
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border: 0 !important;
/* needed */
-webkit-appearance: none;
-moz-appearance: none;
/* SVG background image */
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
background-position: center;
background-size: 10px;
background-repeat: no-repeat;
}
.search-container ul {
position: absolute;
list-style: none;
padding: 0;
z-index: 3;
margin-top: 29px;
width: 100%;
right: 0px;
background: #fff;
border: 1px solid #ccc;
border-top: none;
border-bottom: none;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.search-container ul :focus {
outline: none;
}
.search-container ul li {
display: block;
text-align: left;
padding: 8px 29px 2px 12px;
border-bottom: 1px solid #ccc;
font-size: 14px;
min-height: 31px;
}
.search-container ul li:first-child {
border-top: 1px solid #ccc;
border-radius: 4px 0px 0 0;
}
.search-container ul li:last-child {
border-radius: 4px 0px 0 0;
}
.search-container ul li:hover.not-cursor {
cursor: default;
}
.search-container ul li:hover {
color: #333;
background-color: #f0f0f0;
border-color: #adadad;
cursor: pointer;
}
/* Adding scrool to select options */
.autocomplete-list {
max-height: 130px;
overflow-y: auto;
}
/**************************************** Process page card ******************************************************/
.process-card {
min-width: 300px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-height: 40vh;
max-height: 60vh;
justify-content: space-between;
padding: 1rem;
overflow-y: auto;
}
.process-card-content {
text-align: left;
font-size: 0.8em;
position: relative;
left: 2vw;
width: 90%;
.process-title {
font-weight: bold;
padding: 1rem 0;
}
.process-element {
padding: 0.4rem 0;
&:hover {
background-color: rgba(26, 28, 24, 0.08);
}
&.selected {
background-color: rgba(26, 28, 24, 0.08);
}
}
.selected-process-zone {
background-color: rgba(26, 28, 24, 0.08);
}
}
.process-card-description {
padding: 20px;
font-size: 1rem;
color: #333;
width: 90%;
}
.process-card-action {
width: 100%;
}
/**************************************** Select Member Home Page ******************************************************/
.custom-select {
width: 100%;
max-height: 150px;
overflow-y: auto;
direction: ltr;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
}
.custom-select option {
padding: 8px 12px;
cursor: pointer;
}
.custom-select option:hover {
background-color: #f0f0f0;
}
.custom-select::-webkit-scrollbar {
width: 8px;
}
.custom-select::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-select::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.custom-select::-webkit-scrollbar-thumb:hover {
background: #555;
}

View File

@ -1,69 +0,0 @@
import globalCss from './assets/styles/style.css?inline';
export class AppLayout extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
height: 100vh;
width: 100vw;
overflow: hidden; /* Empêche le scroll global sur body */
}
.app-grid {
display: grid;
grid-template-rows: auto 1fr; /* Ligne 1: auto (header), Ligne 2: le reste */
height: 100%;
width: 100%;
}
.header-area {
width: 100%;
z-index: 100;
/* Le header est posé ici, plus besoin de position: fixed */
}
.content-area {
position: relative;
overflow-y: auto; /* C'est ICI que ça scrolle */
overflow-x: hidden;
width: 100%;
height: 100%;
/* Scrollbar jolie */
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
/* Webkit Scrollbar */
.content-area::-webkit-scrollbar { width: 6px; }
.content-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
</style>
<div class="app-grid">
<div class="header-area">
<slot name="header"></slot>
</div>
<div class="content-area">
<slot name="content"></slot>
</div>
</div>
`;
}
}
}
customElements.define('app-layout', AppLayout);

View File

@ -1,133 +0,0 @@
:root {
/* --- 🎨 Palette de Couleurs Moderne --- */
--primary-hue: 220; /* Bleu profond */
--accent-hue: 260; /* Violet vibrant */
--bg-color: #0f172a; /* Fond très sombre (Dark mode par défaut) */
--bg-gradient: radial-gradient(circle at top left, #1e293b, #0f172a);
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--primary: hsl(var(--primary-hue), 90%, 60%);
--accent: hsl(var(--accent-hue), 90%, 65%);
--success: #4ade80;
--error: #f87171;
/* --- 📐 Espacement & Rayons --- */
--radius-sm: 8px;
--radius-md: 16px;
--radius-lg: 24px;
/* --- ⚡ Transitions --- */
--ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
}
/* Reset basique */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
background-image: var(--bg-gradient);
color: var(--text-main);
height: 100vh;
width: 100vw;
overflow-x: hidden;
line-height: 1.6;
}
/* --- ✨ Composants UI Globaux --- */
/* Boutons Modernes */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
background: linear-gradient(135deg, var(--primary), var(--accent));
color: white;
border: none;
border-radius: var(--radius-sm);
font-weight: 600;
cursor: pointer;
transition: transform 0.2s var(--ease-out), box-shadow 0.2s;
text-decoration: none;
font-size: 1rem;
box-shadow: 0 4px 15px rgba(var(--primary-hue), 50, 50, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(var(--primary-hue), 50, 50, 0.5);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: transparent;
border: 1px solid var(--glass-border);
background-color: rgba(255,255,255,0.05);
}
/* Inputs Stylisés */
input, select, textarea {
width: 100%;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
/* Cartes Glassmorphism */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
box-shadow: var(--glass-shadow);
padding: 2rem;
}
/* Titres */
h1, h2, h3 {
color: white;
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(to right, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
/* Utilitaires */
.text-center { text-align: center; }
.mt-4 { margin-top: 1.5rem; }
.mb-4 { margin-bottom: 1.5rem; }
.flex-center { display: flex; justify-content: center; align-items: center; }
.w-full { width: 100%; }
/* Container principal */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
height: 100%;
display: flex;
flex-direction: column;
}

View File

@ -1,242 +0,0 @@
import headerHtml from './header.html?raw';
import globalCss from '../../assets/styles/style.css?inline';
import Services from '../../services/service';
import { BackUp } from '../../types/index';
export class HeaderComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.initLogic();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
width: 100%;
padding: 1rem 2rem;
background: transparent;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1.5rem;
pointer-events: auto; /* Réactive les clics sur la barre */
border-radius: 100px; /* Forme "Pillule" */
background: rgba(15, 23, 42, 0.6); /* Plus sombre */
}
.brand {
font-size: 1.5rem;
font-weight: 900;
letter-spacing: 1px;
color: white;
}
.brand .dot { color: var(--accent); }
.nav-right {
display: flex;
align-items: center;
gap: 1rem;
position: relative;
}
.icon-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background 0.2s;
}
.icon-btn:hover { background: rgba(255,255,255,0.1); }
.menu-dropdown {
position: absolute;
top: 120%;
right: 0;
width: 200px;
display: none;
flex-direction: column;
padding: 0.5rem;
border-radius: 12px;
}
.menu-dropdown a {
color: var(--text-main);
text-decoration: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9rem;
transition: background 0.2s;
text-align: left;
}
.menu-dropdown a:hover { background: rgba(255,255,255,0.1); }
.menu-dropdown a.danger { color: var(--error); }
.menu-dropdown a.danger:hover { background: rgba(248, 113, 113, 0.1); }
.divider { height: 1px; background: var(--glass-border); margin: 5px 0; }
</style>
${headerHtml}
`;
}
}
initLogic() {
const root = this.shadowRoot;
if (!root) return;
// 1. Gestion du Menu Burger
const burgerBtn = root.querySelector('.burger-menu');
const menu = root.getElementById('menu');
if (burgerBtn && menu) {
burgerBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex';
});
document.addEventListener('click', () => {
menu.style.display = 'none';
});
menu.addEventListener('click', (e) => e.stopPropagation());
}
// 2. Attachement des actions (via les IDs, c'est plus sûr)
const btnImport = root.getElementById('btn-import');
const btnExport = root.getElementById('btn-export');
const btnDisconnect = root.getElementById('btn-disconnect');
if (btnImport) {
btnImport.addEventListener('click', () => {
menu!.style.display = 'none';
this.importJSON();
});
}
if (btnExport) {
btnExport.addEventListener('click', () => {
menu!.style.display = 'none';
this.createBackUp();
});
}
if (btnDisconnect) {
btnDisconnect.addEventListener('click', () => {
menu!.style.display = 'none';
this.disconnect();
});
}
}
async disconnect() {
if (!confirm('Êtes-vous sûr de vouloir vous déconnecter ? Toutes les données locales seront effacées.')) return;
console.log('Disconnecting...');
try {
// 1. Nettoyage LocalStorage
localStorage.clear();
// 2. Suppression IndexedDB
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase('4nk');
request.onsuccess = () => {
console.log('IndexedDB deleted successfully');
resolve();
};
request.onerror = () => {
console.warn('Error deleting DB (maybe blocked), continuing...');
resolve();
};
request.onblocked = () => {
console.warn('Database deletion was blocked');
resolve();
};
});
// 3. Suppression Service Workers
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map((registration) => registration.unregister()));
console.log('Service worker unregistered');
// 4. Rechargement violent pour remettre à zéro l'application
window.location.href = window.location.origin;
} catch (error) {
console.error('Error during disconnect:', error);
window.location.href = window.location.origin;
}
}
async 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 {
// On parse le JSON
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(); // Recharger pour appliquer les données
} catch (error) {
console.error(error);
alert("Erreur lors de l'import: fichier invalide.");
}
};
reader.readAsText(file);
}
};
input.click();
}
async createBackUp() {
try {
const service = await Services.getInstance();
const backUp = await service.createBackUp();
if (!backUp) {
alert("Impossible de créer le backup (Pas d'appareil trouvé).");
return;
}
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-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
console.log('Backup téléchargé.');
} catch (e) {
console.error(e);
alert('Erreur lors de la création du backup.');
}
}
}
customElements.define('app-header', HeaderComponent);

View File

@ -1,27 +1,36 @@
<nav class="navbar glass-panel">
<div class="nav-left">
<div class="brand">4NK<span class="dot">.</span></div>
</div>
<div class="nav-right">
<div class="user-profile" id="profile-header-container">
<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>
<button class="icon-btn burger-menu" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="notification-badge"></div>
<div id="notification-board" class="notification-board">
<div class="no-notification">No notifications available</div>
</div>
</div>
<div class="menu-dropdown glass-panel" id="menu">
<a id="btn-import">Import Data</a>
<a id="btn-export">Export Backup</a>
<div class="divider"></div>
<a id="btn-disconnect" class="danger">Disconnect</a>
<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>
</nav>
</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

@ -0,0 +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>

View File

@ -0,0 +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();
}
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

@ -0,0 +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;
}

View File

@ -0,0 +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>

View File

@ -0,0 +1,56 @@
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;

View File

@ -1,31 +0,0 @@
export const APP_CONFIG = {
// --- Cryptographie & Limites ---
U32_MAX: 4294967295,
EMPTY_32_BYTES: String('').padStart(64, '0'),
// --- Économie ---
DEFAULT_AMOUNT: 1000n,
FEE_RATE: 1, // Sat/vByte ou unité arbitraire selon le SDK
// --- Délais & Timeouts (ms) ---
TIMEOUTS: {
POLLING_INTERVAL: 100, // Vérification rapide (ex: handshake)
API_DELAY: 500, // Petit délai pour laisser respirer le réseau (hack)
RETRY_DELAY: 1000, // Délai avant de réessayer une action
FAUCET_WAIT: 2000, // Attente après appel faucet
WORKER_CHECK: 5000, // Vérification périodique du worker
HANDSHAKE: 10000, // Timeout max pour le handshake
KEY_REQUEST: 15000, // Timeout pour recevoir une clé d'un pair
WS_RECONNECT_MAX: 30000, // Délai max entre deux tentatives de reco WS
WS_HEARTBEAT: 30000, // Ping WebSocket
},
// --- URLs (Environnement) ---
URLS: {
BASE: import.meta.env.VITE_BASEURL || 'http://localhost',
BOOTSTRAP: [import.meta.env.VITE_BOOTSTRAPURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8090`],
STORAGE: import.meta.env.VITE_STORAGEURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8081`,
BLINDBIT: import.meta.env.VITE_BLINDBITURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8000`,
},
};

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 };

39
src/index.ts Executable file
View File

@ -0,0 +1,39 @@
// 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

@ -0,0 +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;
}>;
}

View File

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

View File

@ -1,67 +1,30 @@
// Polyfill to prevent "chrome is not defined" errors
// Some dependencies may check for Chrome extension APIs without proper existence checks
if (typeof (globalThis as any).chrome === 'undefined') {
(globalThis as any).chrome = {};
}
import Services from './services/service';
import { Router } from './router/index';
import './components/header/Header';
import './App';
import { IframeController } from './services/iframe-controller.service';
async function bootstrap() {
console.log("🚀 Démarrage de l'application 4NK (Multi-Worker Architecture)...");
try {
// 1. Initialisation des Services (Proxy vers Core & Network Workers)
// Cela va lancer les workers en arrière-plan
const services = await Services.getInstance();
// Injection du Header
const headerSlot = document.getElementById('header-slot');
if (headerSlot) {
headerSlot.innerHTML = '<app-header></app-header>';
}
// 2. Vérification / Création de l'appareil (via le Worker)
const device = await services.getDeviceFromDatabase();
if (!device) {
console.log('✨ Nouvel appareil détecté, création en cours via Worker...');
await services.createNewDevice();
} else {
console.log("Restauration de l'appareil...");
await services.restoreDevice(device);
}
// 3. Initialisation du contrôleur d'Iframe (Reste sur le Main Thread pour écouter window)
await IframeController.init();
// 4. Restauration des données
await services.restoreProcessesFromDB();
if (services.restoreSecretsFromDB) {
await services.restoreSecretsFromDB();
} else {
console.warn("restoreSecretsFromDB non implémenté dans le proxy Services");
}
// 5. Gestion du Routing
const isIframe = window.self !== window.top;
const isPaired = await services.isPaired();
if (isPaired && !isIframe) {
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
window.history.replaceState({}, '', 'process');
Router.handleLocation();
} else {
console.log(isIframe ? '📡 Mode Iframe détecté : Attente API.' : '🆕 Non appairé : Démarrage sur Home.');
Router.init();
}
} catch (error) {
console.error('💥 Erreur critique au démarrage :', error);
}
}
bootstrap();
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

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

View File

@ -0,0 +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
}

23
src/models/process.model.ts Executable file
View File

@ -0,0 +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;
}

59
src/models/signature.models.ts Executable file
View File

@ -0,0 +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;
}

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);

10
src/pages/account/account.html Executable file
View File

@ -0,0 +1,10 @@
<!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

@ -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,275 +0,0 @@
// src/pages/process/Home.ts
import Services from "../../services/service";
import globalCss from "../../assets/styles/style.css?inline";
import homeHtml from "./home.html?raw";
import {
displayEmojis,
generateCreateBtn,
prepareAndSendPairingTx,
addressToEmoji,
} from "../../utils/sp-address.utils";
import { Router } from "../../router/index";
export class HomePage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.initLogic();
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host {
display: block;
width: 100%;
}
.home-layout {
min-height: 80vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* Auth Card */
.auth-container {
width: 100%;
max-width: 450px;
perspective: 1000px;
}
.auth-header { text-align: center; margin-bottom: 2rem; }
.subtitle { color: var(--text-muted); font-size: 0.9rem; }
/* Tabs */
.tabs-nav {
display: flex;
background: rgba(0,0,0,0.2);
padding: 4px;
border-radius: var(--radius-sm);
margin-bottom: 1.5rem;
}
.tab-btn {
flex: 1;
padding: 8px;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
font-weight: 600;
}
.tab-btn.active {
background: var(--primary);
color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
/* Content */
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.input-group label { display: block; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-muted); }
.my-address-display {
margin-top: 1rem;
padding: 10px;
background: rgba(255,255,255,0.03);
border-radius: var(--radius-sm);
display: flex;
justify-content: space-between;
font-family: monospace;
}
/* Loader */
.loader-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-color);
z-index: 2000;
display: flex; justify-content: center; align-items: center;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-step { color: var(--text-muted); font-size: 0.9rem; transition: color 0.3s; }
.loader-step.active { color: var(--primary); font-weight: bold; }
</style>
${homeHtml}
`;
}
}
async initLogic() {
const container = this.shadowRoot;
if (!container) return;
const loaderDiv = container.querySelector(
"#iframe-loader"
) as HTMLDivElement;
const mainContentDiv = container.querySelector(
"#main-content"
) as HTMLDivElement;
const tabs = container.querySelectorAll(".tab");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
// Remplacement de addSubscription pour simplifier ici
container
.querySelectorAll(".tab")
.forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
container
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
container
.querySelector(`#${tab.getAttribute("data-tab") as string}`)
?.classList.add("active");
});
});
const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
try {
await delay(500);
this.addLoaderStep("Initialisation des services...");
const service = await Services.getInstance();
await delay(700);
this.addLoaderStep("Vérification de l'appareil...");
const currentDevice = await service.getDeviceFromDatabase();
const pairingId = currentDevice?.pairing_process_commitment || null;
if (pairingId) {
await delay(300);
this.addLoaderStep("Appairage existant trouvé.");
service.setProcessId(pairingId);
} else {
await delay(300);
this.addLoaderStep("Création d'un appairage sécurisé...");
await prepareAndSendPairingTx();
this.addLoaderStep("Appairage créé avec succès.");
}
// --- SUCCÈS ---
console.log("[Home] Auto-pairing terminé avec succès.");
document.dispatchEvent(
new CustomEvent("app:pairing-ready", {
detail: { success: true },
})
);
if (window.self !== window.top) {
// CAS IFRAME : On ne bouge pas !
// On affiche juste un état "Prêt" dans le loader pour le debug visuel
this.addLoaderStep("Prêt. En attente de l'application parente...");
console.log(
"[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API."
);
} else {
// CAS STANDALONE : On redirige
console.log("[Home] 🚀 Mode Standalone : Redirection vers /process...");
await delay(500);
// On nettoie l'UI avant de partir
if (loaderDiv) loaderDiv.style.display = "none";
if (mainContentDiv) mainContentDiv.style.display = "block";
// Hop, on navigue
Router.navigate("process");
}
container
.querySelectorAll(".tab")
.forEach((t) => t.classList.remove("active"));
container.querySelector('[data-tab="tab2"]')?.classList.add("active");
container
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
container.querySelector("#tab2")?.classList.add("active");
const spAddress = await service.getDeviceAddress();
generateCreateBtn();
displayEmojis(spAddress);
await this.populateMemberSelect();
await delay(1000);
if (loaderDiv) loaderDiv.style.display = "none";
if (mainContentDiv) mainContentDiv.style.display = "block";
console.log("[Home] Init terminée.");
} catch (e: any) {
console.error("[Home] Erreur:", e);
this.addLoaderStep(`Erreur: ${e.message}`);
document.dispatchEvent(
new CustomEvent("app:pairing-ready", {
detail: { success: false, error: e.message },
})
);
}
}
addLoaderStep(text: string) {
const container = this.shadowRoot;
if (!container) return;
const currentStep = container.querySelector(
".loader-step.active"
) as HTMLParagraphElement;
if (currentStep) currentStep.classList.remove("active");
const stepsContainer = container.querySelector(
"#loader-steps-container"
) as HTMLDivElement;
if (stepsContainer) {
const newStep = document.createElement("p");
newStep.className = "loader-step active";
newStep.textContent = text;
stepsContainer.appendChild(newStep);
}
}
async populateMemberSelect() {
const container = this.shadowRoot;
if (!container) return;
const memberSelect = container.querySelector(
"#memberSelect"
) as HTMLSelectElement;
if (!memberSelect) return;
const service = await Services.getInstance();
const members = await service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const emojis = await addressToEmoji(processId);
const option = document.createElement("option");
option.value = processId;
option.textContent = `Member (${emojis})`;
memberSelect.appendChild(option);
}
}
}
customElements.define("home-page", HomePage);

View File

@ -0,0 +1,49 @@
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,51 +1,42 @@
<div class="home-layout">
<div id="iframe-loader" class="loader-overlay">
<div class="loader-content glass-panel">
<div class="spinner"></div>
<div id="loader-steps-container">
<p class="loader-step active">Démarrage du système...</p>
<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>
</div>
<div id="main-content" class="auth-container" style="display: none;">
<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>
<div class="auth-card glass-panel">
<div class="auth-header">
<h1>Bienvenue</h1>
<p class="subtitle">Connectez votre appareil ou créez un compte</p>
</div>
<div class="tabs-nav">
<button class="tab-btn active" data-tab="tab2">Connexion</button>
<button class="tab-btn" data-tab="tab1">Nouveau Compte</button>
</div>
<div id="tab2" class="tab-content active">
<div class="input-group">
<label>Sélectionner un membre</label>
<div class="select-wrapper">
<select id="memberSelect"></select>
</div>
</div>
<div class="my-address-display">
<span>Mon ID :</span>
<span class="emoji-display">...</span>
</div>
<button id="okButton" class="btn w-full mt-4">Se Connecter</button>
</div>
<div id="tab1" class="tab-content">
<div class="qr-section">
<div class="qr-code">
<img src="" alt="Scan QR" />
</div>
<p class="pairing-request"></p>
</div>
<button id="createButton" class="btn w-full mt-4">Créer un compte</button>
</div>
</div>
<button id="okButton" style="display: none">OK</button>
</div>
</div>
</div>

94
src/pages/home/home.ts Executable file
View File

@ -0,0 +1,94 @@
import Routing from '../../services/modal.service';
import Services from '../../services/service';
import { addSubscription } from '../../utils/subscription.utils';
import { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/html.utils';
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
export { QrScannerComponent };
export async function initHomePage(): Promise<void> {
console.log('INIT-HOME');
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
container.querySelectorAll('.tab').forEach((tab) => {
addSubscription(tab, 'click', () => {
container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active'));
container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active');
});
});
const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress();
// generateQRCode(spAddress);
generateCreateBtn ();
displayEmojis(spAddress);
// Add this line to populate the select when the page loads
await populateMemberSelect();
}
//// Modal
export async function openModal(myAddress: string, receiverAddress: string) {
const router = await Routing.getInstance();
router.openLoginModal(myAddress, receiverAddress);
}
// const service = await Services.getInstance()
// service.setNotification()
function scanDevice() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const scannerImg = container.querySelector('#scanner') as HTMLElement;
if (scannerImg) scannerImg.style.display = 'none';
const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;
if (scannerQrCode) scannerQrCode.style.display = 'block';
const scanButton = container?.querySelector('#scan-btn') as HTMLElement;
if (scanButton) scanButton.style.display = 'none';
const reader = container?.querySelector('#qr-reader');
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
}
async function populateMemberSelect() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
if (!memberSelect) {
console.error('Could not find memberSelect element');
return;
}
const service = await Services.getInstance();
const members = service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const process = await service.getProcess(processId);
let memberPublicName;
if (process) {
const publicMemberData = service.getPublicData(process);
if (publicMemberData) {
const extractedName = publicMemberData['memberPublicName'];
if (extractedName !== undefined && extractedName !== null) {
memberPublicName = extractedName;
}
}
}
if (!memberPublicName) {
memberPublicName = 'Unnamed Member';
}
// Récupérer les emojis pour ce processId
const emojis = await addressToEmoji(processId);
const option = document.createElement('option');
option.value = processId;
option.textContent = `${memberPublicName} (${emojis})`;
memberSelect.appendChild(option);
}
}
(window as any).populateMemberSelect = populateMemberSelect;
(window as any).scanDevice = scanDevice;

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>

View File

@ -0,0 +1,50 @@
import { interpolate } from '../../utils/html.utils';
import Services from '../../services/service';
import { Process } from 'pkg/sdk_client';
import { getCorrectDOM } from '~/utils/document.utils';
let currentPageStyle: HTMLStyleElement | null = null;
export async function initProcessElement(id: string, zone: string) {
const processes = await getProcesses();
const container = getCorrectDOM('process-4nk-component');
// const currentProcess = processes.find((process) => process[0] === id)[1];
// const currentProcess = {title: 'Hello', html: '', css: ''};
// await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' });
// const wrapper = document.querySelector('.process-container');
// if (wrapper) {
// wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' });
// injectCss(currentProcess.css);
// }
}
async function loadPage(data?: any) {
const content = document.getElementById('containerId');
if (content && data) {
if (data) {
content.innerHTML = interpolate(content.innerHTML, data);
}
}
}
function injectCss(cssContent: string) {
removeCss(); // Ensure that the previous CSS is removed
currentPageStyle = document.createElement('style');
currentPageStyle.type = 'text/css';
currentPageStyle.appendChild(document.createTextNode(cssContent));
document.head.appendChild(currentPageStyle);
}
function removeCss() {
if (currentPageStyle) {
document.head.removeChild(currentPageStyle);
currentPageStyle = null;
}
}
async function getProcesses(): Promise<Record<string, Process>> {
const service = await Services.getInstance();
const processes = await service.getProcesses();
return processes;
}

View File

@ -1,310 +0,0 @@
import processHtml from "./process.html?raw";
import globalCss from "../../assets/styles/style.css?inline";
import Services from "../../services/service";
export class ProcessListPage extends HTMLElement {
private services!: Services;
// Éléments du DOM
private inputInput!: HTMLInputElement;
private autocompleteList!: HTMLUListElement;
private tagsContainer!: HTMLElement;
private detailsContainer!: HTMLElement;
private okButton!: HTMLButtonElement;
private wrapper!: HTMLElement;
constructor() {
super();
this.attachShadow({ mode: "open" });
}
async connectedCallback() {
this.services = await Services.getInstance();
this.render();
setTimeout(() => this.initLogic(), 0);
}
render() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<style>
${globalCss}
:host { display: block; width: 100%; }
.process-layout { padding: 2rem; display: flex; justify-content: center; }
.dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
.dashboard-header { text-align: center; }
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
.search-input-container { position: relative; display: flex; align-items: center; }
.search-input-container input { padding-right: 40px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); transition: all 0.3s; }
.search-input-container input:focus { background: rgba(255,255,255,0.1); border-color: var(--primary); }
.search-icon { position: absolute; right: 12px; opacity: 0.5; }
.autocomplete-dropdown { list-style: none; margin-top: 5px; padding: 0; background: #1e293b; border: 1px solid var(--glass-border); border-radius: var(--radius-sm); max-height: 200px; overflow-y: auto; display: none; position: absolute; width: 100%; z-index: 10; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
.custom-select-wrapper { position: relative; }
.autocomplete-dropdown li { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.2s; color: var(--text-main); }
.autocomplete-dropdown li:hover { background: var(--primary); color: white; }
.autocomplete-dropdown li.my-process { border-left: 3px solid var(--accent); }
.tags-container { display: flex; flex-wrap: wrap; gap: 8px; min-height: 30px; }
.tag { background: rgba(var(--primary-hue), 50, 50, 0.3); border: 1px solid var(--primary); color: white; padding: 4px 10px; border-radius: 20px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px; animation: popIn 0.2s ease-out; }
.tag-close { cursor: pointer; opacity: 0.7; font-weight: bold; }
.tag-close:hover { opacity: 1; }
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
.details-content { background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); padding: 1rem; min-height: 100px; }
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
.process-item { margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem; }
.process-title-display { font-size: 1.1rem; font-weight: bold; color: var(--accent); margin-bottom: 0.5rem; }
.state-element { background: rgba(255,255,255,0.05); padding: 8px 12px; margin-top: 5px; border-radius: 4px; cursor: pointer; transition: background 0.2s; border: 1px solid transparent; font-family: monospace; font-size: 0.9rem; }
.state-element:hover { background: rgba(255,255,255,0.1); }
.state-element.selected { background: rgba(var(--success), 0.2); border-color: var(--success); }
.dashboard-footer { display: flex; justify-content: flex-end; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
</style>
${processHtml}
`;
}
}
async initLogic() {
const root = this.shadowRoot;
if (!root) return;
this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
this.autocompleteList = root.querySelector("#autocomplete-list") as HTMLUListElement;
this.tagsContainer = root.querySelector("#selected-tags-container") as HTMLElement;
this.detailsContainer = root.querySelector("#process-details") as HTMLElement;
this.okButton = root.querySelector("#go-to-process-btn") as HTMLButtonElement;
this.inputInput.addEventListener("keyup", () => this.handleInput());
this.inputInput.addEventListener("click", () => this.openDropdown());
document.addEventListener("click", (e) => {
const path = e.composedPath();
if (!path.includes(this.wrapper)) {
this.closeDropdown();
}
});
this.okButton.addEventListener("click", () => this.goToProcess());
document.addEventListener("processes-updated", async () => {
await this.populateList(this.inputInput.value);
});
await this.populateList("");
}
// --- Logique Autocomplete Sécurisée ---
async populateList(query: string) {
this.autocompleteList.innerHTML = "";
const mineArray = (await this.services.getMyProcesses()) ?? [];
const allProcesses = await this.services.getProcesses();
const otherProcesses = Object.keys(allProcesses).filter(
(id) => !mineArray.includes(id)
);
const listToShow = [...mineArray, ...otherProcesses];
let count = 0;
for (const pid of listToShow) {
const process = allProcesses[pid];
if (!process) continue;
const name = (await this.services.getProcessName(process)) || pid;
if (
query &&
!name.toLowerCase().includes(query.toLowerCase()) &&
!pid.includes(query)
) {
continue;
}
count++;
const li = document.createElement("li");
const nameSpan = document.createElement("span");
nameSpan.textContent = name;
li.appendChild(nameSpan);
if (mineArray.includes(pid)) {
li.classList.add("my-process");
const small = document.createElement("small");
small.style.opacity = "0.6";
small.style.marginLeft = "8px";
small.textContent = "(Mien)";
li.appendChild(small);
}
li.addEventListener("click", () => {
this.addTag(pid, name);
this.inputInput.value = "";
this.showProcessDetails(pid);
this.closeDropdown();
});
this.autocompleteList.appendChild(li);
}
if (count === 0) {
const empty = document.createElement("li");
empty.textContent = "Aucun résultat";
empty.style.cursor = "default";
empty.style.opacity = "0.5";
this.autocompleteList.appendChild(empty);
}
}
handleInput() {
this.openDropdown();
this.populateList(this.inputInput.value);
}
openDropdown() {
this.autocompleteList.style.display = "block";
}
closeDropdown() {
this.autocompleteList.style.display = "none";
}
// --- Gestion des Tags Sécurisée ---
addTag(pid: string, name: string) {
this.tagsContainer.innerHTML = "";
const tag = document.createElement("div");
tag.className = "tag";
const spanName = document.createElement("span");
spanName.textContent = name;
tag.appendChild(spanName);
const closeBtn = document.createElement("span");
closeBtn.className = "tag-close";
closeBtn.innerHTML = "&times;";
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.removeTag();
});
tag.appendChild(closeBtn);
this.tagsContainer.appendChild(tag);
}
removeTag() {
this.tagsContainer.innerHTML = "";
this.detailsContainer.innerHTML = "";
const emptyState = document.createElement("div");
emptyState.className = "empty-state";
const p = document.createElement("p");
p.textContent = "Aucun processus sélectionné.";
emptyState.appendChild(p);
this.detailsContainer.appendChild(emptyState);
this.okButton.disabled = true;
this.okButton.classList.add("disabled");
}
// --- Détails du processus Sécurisés ---
async showProcessDetails(pid: string) {
this.detailsContainer.textContent = "Chargement...";
const process = await this.services.getProcess(pid);
if (!process) return;
this.detailsContainer.innerHTML = "";
const name = (await this.services.getProcessName(process)) || "Sans nom";
// Description
let description = "Pas de description";
const lastState = await this.services.getLastCommitedState(process);
if (lastState?.pcd_commitment["description"]) {
const diff = await this.services.getDiffByValue(
lastState.pcd_commitment["description"]
);
if (diff) description = diff.value_commitment;
}
const containerDiv = document.createElement("div");
containerDiv.className = "process-item";
// Titre
const titleDiv = document.createElement("div");
titleDiv.className = "process-title-display";
titleDiv.textContent = name;
containerDiv.appendChild(titleDiv);
// Description
const descDiv = document.createElement("div");
descDiv.style.fontSize = "0.9rem";
descDiv.style.marginBottom = "10px";
descDiv.textContent = description;
containerDiv.appendChild(descDiv);
// ID
const idDiv = document.createElement("div");
idDiv.style.fontSize = "0.8rem";
idDiv.style.opacity = "0.7";
idDiv.style.marginBottom = "10px";
idDiv.textContent = `ID: ${pid}`;
containerDiv.appendChild(idDiv);
// Label "États en attente"
const labelDiv = document.createElement("div");
labelDiv.style.fontWeight = "bold";
labelDiv.style.marginTop = "15px";
labelDiv.textContent = "États en attente :";
containerDiv.appendChild(labelDiv);
const uncommitted = await this.services.getUncommitedStates(process);
if (uncommitted.length > 0) {
uncommitted.forEach((state) => {
const el = document.createElement("div");
el.className = "state-element";
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
el.addEventListener("click", () => {
this.shadowRoot
?.querySelectorAll(".state-element")
.forEach((x) => x.classList.remove("selected"));
el.classList.add("selected");
this.okButton.disabled = false;
this.okButton.dataset.target = `${pid}/${state.state_id}`;
});
containerDiv.appendChild(el);
});
} else {
const empty = document.createElement("div");
empty.style.padding = "10px";
empty.style.opacity = "0.6";
empty.textContent = "Aucun état en attente de validation.";
containerDiv.appendChild(empty);
}
this.detailsContainer.appendChild(containerDiv);
}
goToProcess() {
const target = this.okButton.dataset.target;
if (target) {
console.log("Navigation vers", target);
alert("Navigation vers : " + target);
}
}
}
customElements.define("process-list-page", ProcessListPage);

View File

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

View File

@ -1,45 +1,19 @@
<div class="process-layout">
<div class="dashboard-container glass-panel">
<div class="dashboard-header">
<h1>Mes Processus</h1>
<p class="subtitle">Sélectionnez et gérez vos flux de travail</p>
</div>
<div class="title-container">
<h1>Process Selection</h1>
</div>
<div class="search-section">
<div class="input-group">
<label>Rechercher un processus</label>
<div id="autocomplete-wrapper" class="custom-select-wrapper">
<select multiple id="process-select" style="display:none"></select>
<div class="search-input-container">
<input type="text" id="process-input" placeholder="Filtrer par nom ou ID..." autocomplete="off">
<span class="search-icon">🔍</span>
</div>
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
</div>
<div class="process-container">
<div class="process-card">
<div class="process-card-description">
<div class="input-container">
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
<label for="autocomplete" class="input-label">Filter processes :</label>
<div class="selected-processes"></div>
</div>
<div id="selected-tags-container" class="tags-container">
</div>
<div class="process-card-content"></div>
</div>
<div class="divider"></div>
<div class="details-section">
<h3>Détails du processus</h3>
<div id="process-details" class="details-content">
<div class="empty-state">
<p>Aucun processus sélectionné.</p>
</div>
</div>
<div class="process-card-action">
<a class="btn" onclick="goToProcessPage()">OK</a>
</div>
<div class="dashboard-footer">
<button id="go-to-process-btn" class="btn btn-primary" disabled>
Accéder au Processus
<svg style="margin-left:8px" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
</div>

520
src/pages/process/process.ts Executable file
View File

@ -0,0 +1,520 @@
import { addSubscription } from '../../utils/subscription.utils';
import Services from '../../services/service';
import { getCorrectDOM } from '~/utils/html.utils';
import { Process } from 'pkg/sdk_client';
import chatStyle from '../../../public/style/chat.css?inline';
import { Database } from '../../services/database.service';
// Initialize function, create initial tokens with itens that are already selected by the user
export async function init() {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const element = container.querySelector('select') as HTMLSelectElement;
// Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
const wrapper = document.createElement('div');
if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper);
wrapper.classList.add('multi-select-component');
wrapper.classList.add('input-field');
// Create elements of search
const search_div = document.createElement('div');
search_div.classList.add('search-container');
const input = document.createElement('input');
input.classList.add('selected-input');
input.setAttribute('autocomplete', 'off');
input.setAttribute('tabindex', '0');
if (input) {
addSubscription(input, 'keyup', inputChange);
addSubscription(input, 'keydown', deletePressed);
addSubscription(input, 'click', openOptions);
}
const dropdown_icon = document.createElement('a');
dropdown_icon.classList.add('dropdown-icon');
if (dropdown_icon) addSubscription(dropdown_icon, 'click', clickDropdown);
const autocomplete_list = document.createElement('ul');
autocomplete_list.classList.add('autocomplete-list');
search_div.appendChild(input);
search_div.appendChild(autocomplete_list);
search_div.appendChild(dropdown_icon);
// set the wrapper as child (instead of the element)
element.parentNode?.replaceChild(wrapper, element);
// set element as child of wrapper
wrapper.appendChild(element);
wrapper.appendChild(search_div);
addPlaceholder(wrapper);
}
function removePlaceholder(wrapper: HTMLElement) {
const input_search = wrapper.querySelector('.selected-input');
input_search?.removeAttribute('placeholder');
}
function addPlaceholder(wrapper: HTMLElement) {
const input_search = wrapper.querySelector('.selected-input');
const tokens = wrapper.querySelectorAll('.selected-wrapper');
if (!tokens.length && !(document.activeElement === input_search)) input_search?.setAttribute('placeholder', '---------');
}
// Listener of user search
function inputChange(e: Event) {
const target = e.target as HTMLInputElement;
const wrapper = target?.parentNode?.parentNode;
const select = wrapper?.querySelector('select') as HTMLSelectElement;
const dropdown = wrapper?.querySelector('.dropdown-icon');
const input_val = target?.value;
if (input_val) {
dropdown?.classList.add('active');
populateAutocompleteList(select, input_val.trim());
} else {
dropdown?.classList.remove('active');
const event = new Event('click');
dropdown?.dispatchEvent(event);
}
}
// Listen for clicks on the wrapper, if click happens focus on the input
function clickOnWrapper(e: Event) {
const wrapper = e.target as HTMLElement;
if (wrapper.tagName == 'DIV') {
const input_search = wrapper.querySelector('.selected-input');
const dropdown = wrapper.querySelector('.dropdown-icon');
if (!dropdown?.classList.contains('active')) {
const event = new Event('click');
dropdown?.dispatchEvent(event);
}
(input_search as HTMLInputElement)?.focus();
removePlaceholder(wrapper);
}
}
function openOptions(e: Event) {
const input_search = e.target as HTMLElement;
const wrapper = input_search?.parentElement?.parentElement;
const dropdown = wrapper?.querySelector('.dropdown-icon');
if (!dropdown?.classList.contains('active')) {
const event = new Event('click');
dropdown?.dispatchEvent(event);
}
e.stopPropagation();
}
// Function that create a token inside of a wrapper with the given value
function createToken(wrapper: HTMLElement, value: any) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const search = wrapper.querySelector('.search-container');
const inputInderline = container.querySelector('.selected-processes');
// Create token wrapper
const token = document.createElement('div');
token.classList.add('selected-wrapper');
const token_span = document.createElement('span');
token_span.classList.add('selected-label');
token_span.innerText = value;
const close = document.createElement('a');
close.classList.add('selected-close');
close.setAttribute('tabindex', '-1');
close.setAttribute('data-option', value);
close.setAttribute('data-hits', '0');
close.innerText = 'x';
if (close) addSubscription(close, 'click', removeToken);
token.appendChild(token_span);
token.appendChild(close);
inputInderline?.appendChild(token);
}
// Listen for clicks in the dropdown option
function clickDropdown(e: Event) {
const dropdown = e.target as HTMLElement;
const wrapper = dropdown?.parentNode?.parentNode;
const input_search = wrapper?.querySelector('.selected-input') as HTMLInputElement;
const select = wrapper?.querySelector('select') as HTMLSelectElement;
dropdown.classList.toggle('active');
if (dropdown.classList.contains('active')) {
removePlaceholder(wrapper as HTMLElement);
input_search?.focus();
if (!input_search?.value) {
populateAutocompleteList(select, '', true);
} else {
populateAutocompleteList(select, input_search.value);
}
} else {
clearAutocompleteList(select);
addPlaceholder(wrapper as HTMLElement);
}
}
// Clears the results of the autocomplete list
function clearAutocompleteList(select: HTMLSelectElement) {
const wrapper = select.parentNode;
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
if (autocomplete_list) autocomplete_list.innerHTML = '';
}
async function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
const { autocomplete_options } = getOptions(select);
let options_to_show = [];
const service = await Services.getInstance();
const mineArray: string[] = await service.getMyProcesses();
const allProcesses = await service.getProcesses();
const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
const wrapper = select.parentNode;
const input_search = wrapper?.querySelector('.search-container');
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
if (autocomplete_list) autocomplete_list.innerHTML = '';
const addProcessToList = (processId:string, isMine: boolean) => {
const li = document.createElement('li');
li.innerText = processId;
li.setAttribute("data-value", processId);
if (isMine) {
li.classList.add("my-process");
li.style.cssText = `color: var(--accent-color)`;
}
if (li) addSubscription(li, 'click', selectOption);
autocomplete_list?.appendChild(li);
};
mineArray.forEach(processId => addProcessToList(processId, true));
allArray.forEach(processId => addProcessToList(processId, false));
if (mineArray.length === 0 && allArray.length === 0) {
const li = document.createElement('li');
li.classList.add('not-cursor');
li.innerText = 'No options found';
autocomplete_list?.appendChild(li);
}
}
// Listener to autocomplete results when clicked set the selected property in the select option
function selectOption(e: any) {
console.log('🎯 Click event:', e);
console.log('🎯 Target value:', e.target.dataset.value);
const wrapper = e.target.parentNode.parentNode.parentNode;
const select = wrapper.querySelector('select');
const input_search = wrapper.querySelector('.selected-input');
const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`);
console.log('🎯 Selected option:', option);
console.log('🎯 Process ID:', option?.getAttribute('data-process-id'));
if (e.target.dataset.value.includes('messaging')) {
const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]);
const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`);
console.log('🚀 Dispatching newMessagingProcess event:', {
processId,
processName: `Messaging Process ${processId}`
});
// Dispatch l'événement avant la navigation
document.dispatchEvent(new CustomEvent('newMessagingProcess', {
detail: {
processId: processId,
processName: `Messaging Process ${processId}`
}
}));
// Navigation vers le chat
const navigateEvent = new CustomEvent('navigate', {
detail: {
page: 'chat',
processId: processId || ''
}
});
document.dispatchEvent(navigateEvent);
return;
}
option.setAttribute('selected', '');
createToken(wrapper, e.target.dataset.value);
if (input_search.value) {
input_search.value = '';
}
showSelectedProcess(e.target.dataset.value);
input_search.focus();
e.target.remove();
const autocomplete_list = wrapper.querySelector('.autocomplete-list');
if (!autocomplete_list.children.length) {
const li = document.createElement('li');
li.classList.add('not-cursor');
li.innerText = 'No options found';
autocomplete_list.appendChild(li);
}
const event = new Event('keyup');
input_search.dispatchEvent(event);
e.stopPropagation();
}
// function that returns a list with the autcomplete list of matches
function autocomplete(query: string, options: any) {
// No query passed, just return entire list
if (!query) {
return options;
}
let options_return = [];
for (let i = 0; i < options.length; i++) {
if (query.toLowerCase() === options[i].slice(0, query.length).toLowerCase()) {
options_return.push(options[i]);
}
}
return options_return;
}
// Returns the options that are selected by the user and the ones that are not
function getOptions(select: HTMLSelectElement) {
// Select all the options available
const all_options = Array.from(select.querySelectorAll('option')).map((el) => el.value);
// Get the options that are selected from the user
const options_selected = Array.from(select.querySelectorAll('option:checked')).map((el: any) => el.value);
// Create an autocomplete options array with the options that are not selected by the user
const autocomplete_options: any[] = [];
all_options.forEach((option) => {
if (!options_selected.includes(option)) {
autocomplete_options.push(option);
}
});
autocomplete_options.sort();
return {
options_selected,
autocomplete_options,
};
}
// Listener for when the user wants to remove a given token.
function removeToken(e: Event) {
// Get the value to remove
const target = e.target as HTMLSelectElement;
const value_to_remove = target.dataset.option;
const wrapper = target.parentNode?.parentNode?.parentNode;
const input_search = wrapper?.querySelector('.selected-input');
const dropdown = wrapper?.querySelector('.dropdown-icon');
// Get the options in the select to be unselected
const option_to_unselect = wrapper?.querySelector(`select option[value="${value_to_remove}"]`);
option_to_unselect?.removeAttribute('selected');
// Remove token attribute
(target.parentNode as any)?.remove();
dropdown?.classList.remove('active');
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const process = container.querySelector('#' + target.dataset.option);
process?.remove();
}
// Listen for 2 sequence of hits on the delete key, if this happens delete the last token if exist
function deletePressed(e: Event) {
const input_search = e.target as HTMLInputElement;
const wrapper = input_search?.parentNode?.parentNode;
const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
const tokens = wrapper?.querySelectorAll('.selected-wrapper');
if (tokens?.length) {
const last_token_x = tokens[tokens.length - 1].querySelector('a');
let hits = +(last_token_x?.dataset?.hits || 0);
if (key == 8 || key == 46) {
if (!input_search.value) {
if (hits > 1) {
// Trigger delete event
const event = new Event('click');
last_token_x?.dispatchEvent(event);
} else {
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
}
}
} else {
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
}
}
return true;
}
// Dismiss on outside click
addSubscription(document, 'click', () => {
// get select that has the options available
const select = document.querySelectorAll('[data-multi-select-plugin]');
for (let i = 0; i < select.length; i++) {
if (event) {
var isClickInside = select[i].parentElement?.parentElement?.contains(event.target as Node);
if (!isClickInside) {
const wrapper = select[i].parentElement?.parentElement;
const dropdown = wrapper?.querySelector('.dropdown-icon');
const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
//the click was outside the specifiedElement, do something
dropdown?.classList.remove('active');
if (autocomplete_list) autocomplete_list.innerHTML = '';
addPlaceholder(wrapper as HTMLElement);
}
}
}
});
async function showSelectedProcess(elem: MouseEvent) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
if (elem) {
const cardContent = container.querySelector('.process-card-content');
const processes = await getProcesses();
const process = processes.find((process: any) => process[1].title === elem);
if (process) {
const processDiv = document.createElement('div');
processDiv.className = 'process';
processDiv.id = process[0];
const titleDiv = document.createElement('div');
titleDiv.className = 'process-title';
titleDiv.innerHTML = `${process[1].title} : ${process[1].description}`;
processDiv.appendChild(titleDiv);
for (const zone of process.zones) {
const zoneElement = document.createElement('div');
zoneElement.className = 'process-element';
const zoneId = process[1].title + '-' + zone.id;
zoneElement.setAttribute('zone-id', zoneId);
zoneElement.setAttribute('process-title', process[1].title);
zoneElement.setAttribute('process-id', `${process[0]}_${zone.id}`);
zoneElement.innerHTML = `${zone.title}: ${zone.description}`;
addSubscription(zoneElement, 'click', select);
processDiv.appendChild(zoneElement);
}
if (cardContent) cardContent.appendChild(processDiv);
}
}
}
function select(event: Event) {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const target = event.target as HTMLElement;
const oldSelectedProcess = container.querySelector('.selected-process-zone');
oldSelectedProcess?.classList.remove('selected-process-zone');
if (target) {
target.classList.add('selected-process-zone');
}
const name = target.getAttribute('zone-id');
console.log('🚀 ~ select ~ name:', name);
}
function goToProcessPage() {
const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
const target = container.querySelector('.selected-process-zone');
console.log('🚀 ~ goToProcessPage ~ event:', target);
if (target) {
const process = target?.getAttribute('process-id');
console.log('=======================> going to process page', process);
// navigate('process-element/' + process);
document.querySelector('process-list-4nk-component')?.dispatchEvent(
new CustomEvent('processSelected', {
detail: {
process: process,
},
}),
);
}
}
(window as any).goToProcessPage = goToProcessPage;
async function createMessagingProcess(): Promise<void> {
console.log('Creating messaging process');
const service = await Services.getInstance();
const otherMembers = [
{
sp_addresses: [
"tsp1qqd7snxfh44am8f7a3x36znkh4v0dcagcgakfux488ghsg0tny7degq4gd9q4n4us0cyp82643f2p4jgcmtwknadqwl3waf9zrynl6n7lug5tg73a",
"tsp1qqvd8pak9fyz55rxqj90wxazqzwupf2egderc96cn84h3l84z8an9vql85scudrmwvsnltfuy9ungg7pxnhys2ft5wnf2gyr3n4ukvezygswesjuc"
]
},
{
sp_addresses: [
"tsp1qqgl5vawdey6wnnn2sfydcejsr06uzwsjlfa6p6yr8u4mkqwezsnvyqlazuqmxhxd8crk5eq3wfvdwv4k3tn68mkj2nj72jj39d2ngauu4unfx0q7",
"tsp1qqthmj56gj8vvkjzwhcmswftlrf6ye7ukpks2wra92jkehqzrvx7m2q570q5vv6zj6dnxvussx2h8arvrcfwz9sp5hpdzrfugmmzz90pmnganxk28"
]
},
{
sp_addresses: [
"tsp1qqwjtxr9jye7d40qxrsmd6h02egdwel6mfnujxzskgxvxphfya4e6qqjq4tsdmfdmtnmccz08ut24q8y58qqh4lwl3w8pvh86shlmavrt0u3smhv2",
"tsp1qqwn7tf8q2jhmfh8757xze53vg2zc6x5u6f26h3wyty9mklvcy0wnvqhhr4zppm5uyyte4y86kljvh8r0tsmkmszqqwa3ecf2lxcs7q07d56p8sz5"
]
}
];
await service.checkConnections(otherMembers);
const relayAddress = service.getAllRelays().pop();
if (!relayAddress) {
throw new Error('Empty relay address list');
}
const feeRate = 1;
setTimeout(async () => {
const createProcessReturn = await service.createMessagingProcess(otherMembers, relayAddress.spAddress, feeRate);
const updatedProcess = createProcessReturn.updated_process.current_process;
if (!updatedProcess) {
console.error('Failed to retrieved new messaging process');
return;
}
const processId = updatedProcess.states[0].commited_in;
const stateId = updatedProcess.states[0].state_id;
await service.handleApiReturn(createProcessReturn);
const createPrdReturn = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(createPrdReturn);
const approveChangeReturn = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeReturn);
}, 500)
}
async function getDescription(processId: string, process: Process): Promise<string | null> {
const service = await Services.getInstance();
// Get the `commited_in` value of the last state and remove it from the array
const currentCommitedIn = process.states.pop()?.commited_in;
if (currentCommitedIn === undefined) {
return null; // No states available
}
// Find the last state where `commited_in` is different
let lastDifferentState = process.states.findLast(
state => state.commited_in !== currentCommitedIn
);
if (!lastDifferentState) {
// It means that we only have one state that is not commited yet, that can happen with process we just created
// let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
lastDifferentState = process.states.pop();
}
// Take the description out of the state, if any
const description = lastDifferentState!.pcd_commitment['description'];
if (description) {
const userDiff = await service.getDiffByValue(description);
if (userDiff) {
console.log("Successfully retrieved userDiff:", userDiff);
return userDiff.new_value;
} else {
console.log("Failed to retrieve a non-null userDiff.");
}
}
return null;
}

View File

@ -0,0 +1,58 @@
import { SignatureElement } from './signature';
import signatureCss from '../../../public/style/signature.css?raw'
import Services from '../../services/service.js'
class SignatureComponent extends HTMLElement {
_callback: any
signatureElement: SignatureElement | null = null;
constructor() {
super();
console.log('INIT')
this.attachShadow({ mode: 'open' });
this.signatureElement = this.shadowRoot?.querySelector('signature-element') || null;
}
connectedCallback() {
console.log('CALLBACKs')
this.render();
this.fetchData();
if (!customElements.get('signature-element')) {
customElements.define('signature-element', SignatureElement);
}
}
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) {
const signatureElement = document.createElement('signature-element');
this.shadowRoot.innerHTML = `<style>${signatureCss}</style>`;
this.shadowRoot.appendChild(signatureElement);
}
}
}
export { SignatureComponent }
customElements.define('signature-component', SignatureComponent);

View File

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

1758
src/pages/signature/signature.ts Executable file

File diff suppressed because it is too large Load Diff

209
src/router.ts Executable file
View File

@ -0,0 +1,209 @@
import '../public/style/4nk.css';
import { initHeader } from '../src/components/header/header';
import { initChat } from '../src/pages/chat/chat';
import Database from './services/database.service';
import Services from './services/service';
import { cleanSubscriptions } from './utils/subscription.utils';
import { LoginComponent } from './pages/home/home-component';
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
import ModalService from './services/modal.service';
export { Services };
const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html',
process: '/src/pages/process/process.html',
'process-element': '/src/pages/process-element/process-element.html',
account: '/src/pages/account/account.html',
chat: '/src/pages/chat/chat.html',
signature: '/src/pages/signature/signature.html',
};
export let currentRoute = '';
export async function navigate(path: string) {
cleanSubscriptions();
cleanPage();
path = path.replace(/^\//, '');
if (path.includes('/')) {
const parsedPath = path.split('/')[0];
if (!routes[parsedPath]) {
path = 'home';
}
}
await handleLocation(path);
}
async function handleLocation(path: string) {
const parsedPath = path.split('/');
if (path.includes('/')) {
path = parsedPath[0];
}
currentRoute = path;
const routeHtml = routes[path] || routes['home'];
const content = document.getElementById('containerId');
if (content) {
if (path === 'home') {
const login = LoginComponent;
const container = document.querySelector('#containerId');
const accountComponent = document.createElement('login-4nk-component');
accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;');
if (container) container.appendChild(accountComponent);
} else if (path !== 'process') {
const html = await fetch(routeHtml).then((data) => data.text());
content.innerHTML = html;
}
await new Promise(requestAnimationFrame);
injectHeader();
// const modalService = await ModalService.getInstance()
// modalService.injectValidationModal()
switch (path) {
case 'process':
// const { init } = await import('./pages/process/process');
const { ProcessListComponent } = await import('./pages/process/process-list-component');
const container2 = document.querySelector('#containerId');
const accountComponent = document.createElement('process-list-4nk-component');
if (!customElements.get('process-list-4nk-component')) {
customElements.define('process-list-4nk-component', ProcessListComponent);
}
accountComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;');
if (container2) container2.appendChild(accountComponent);
break;
case 'process-element':
if (parsedPath && parsedPath.length) {
const { initProcessElement } = await import('./pages/process-element/process-element');
const parseProcess = parsedPath[1].split('_');
initProcessElement(parseProcess[0], parseProcess[1]);
}
break;
case 'account':
const { AccountComponent } = await import('./pages/account/account-component');
const accountContainer = document.querySelector('.parameter-list');
if (accountContainer) {
if (!customElements.get('account-component')) {
customElements.define('account-component', AccountComponent);
}
const accountComponent = document.createElement('account-component');
accountContainer.appendChild(accountComponent);
}
break;
case 'chat':
const { ChatComponent } = await import('./pages/chat/chat-component');
const chatContainer = document.querySelector('.group-list');
if (chatContainer) {
if (!customElements.get('chat-component')) {
customElements.define('chat-component', ChatComponent);
}
const chatComponent = document.createElement('chat-component');
chatContainer.appendChild(chatComponent);
}
break;
case 'signature':
const { SignatureComponent } = await import('./pages/signature/signature-component');
const container = document.querySelector('.group-list');
if (container) {
if (!customElements.get('signature-component')) {
customElements.define('signature-component', SignatureComponent);
}
const signatureComponent = document.createElement('signature-component');
container.appendChild(signatureComponent);
}
break;
}
}
}
window.onpopstate = async () => {
const services = await Services.getInstance();
if (!services.isPaired()) {
handleLocation('home');
} else {
handleLocation('process');
}
};
export async function init(): Promise<void> {
try {
const services = await Services.getInstance();
(window as any).myService = services;
await Database.getInstance();
setTimeout(async () => {
let device = await services.getDeviceFromDatabase();
console.log('🚀 ~ setTimeout ~ device:', device);
if (!device) {
device = await services.createNewDevice();
} else {
services.restoreDevice(device);
}
await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB();
if (services.isPaired()) {
await navigate('chat');
} else {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const pairingAddress = urlParams.get('sp_address');
if (pairingAddress) {
setTimeout(async () => {
try {
// check if we have a shared secret with that address
await prepareAndSendPairingTx(pairingAddress);
} catch (e) {
console.error('Failed to pair:', e);
}
}, 2000);
}
await navigate('home');
}
}, 200);
} catch (error) {
console.error(error);
await navigate('home');
}
}
async function cleanPage() {
const container = document.querySelector('#containerId');
if (container) container.innerHTML = '';
}
async function injectHeader() {
const headerContainer = document.getElementById('header-container');
if (headerContainer) {
const headerHtml = await fetch('/src/components/header/header.html').then((res) => res.text());
headerContainer.innerHTML = headerHtml;
const script = document.createElement('script');
script.src = '/src/components/header/header.ts';
script.type = 'module';
document.head.appendChild(script);
initHeader();
}
}
(window as any).navigate = navigate;
document.addEventListener('navigate', ((e: Event) => {
const event = e as CustomEvent<{page: string, processId?: string}>;
if (event.detail.page === 'chat') {
const container = document.querySelector('.container');
if (container) container.innerHTML = '';
initChat();
const chatElement = document.querySelector('chat-element');
if (chatElement) {
chatElement.setAttribute('process-id', event.detail.processId || '');
}
}
}));

View File

@ -1,64 +0,0 @@
// src/router/index.ts
// On définit les routes ici
const routes: Record<string, () => Promise<any>> = {
home: () => import('../pages/home/Home'), // Charge Home.ts
process: () => import('../pages/process/ProcessList'), // Charge ProcessList.ts
};
export class Router {
static async init() {
// Gestion du bouton retour navigateur
window.addEventListener('popstate', () => Router.handleLocation());
// Gestion de la navigation initiale
Router.handleLocation();
}
static async navigate(path: string) {
window.history.pushState({}, '', path);
await Router.handleLocation();
}
static async handleLocation() {
const path = window.location.pathname.replace(/^\//, '') || 'home'; // 'home' par défaut
// Nettoyage simple (gestion des sous-routes éventuelles)
const routeKey = path.split('/')[0] || 'home';
const appContainer = document.getElementById('app-container');
if (!appContainer) return;
// 1. Nettoyer le conteneur
appContainer.innerHTML = '';
// 2. Charger la page demandée
try {
if (routes[routeKey]) {
// Import dynamique du fichier TS
await routes[routeKey]();
// Création de l'élément correspondant
let pageElement;
if (routeKey === 'home') {
pageElement = document.createElement('home-page');
} else if (routeKey === 'process') {
pageElement = document.createElement('process-list-page');
}
if (pageElement) {
appContainer.appendChild(pageElement);
}
} else {
console.warn(`Route inconnue: ${routeKey}, redirection vers Home`);
Router.navigate('home');
}
} catch (error) {
console.error('Erreur de chargement de la page:', error);
appContainer.innerHTML = '<h1>Erreur de chargement</h1>';
}
}
}
// On expose navigate globalement pour ton header et autres scripts legacy
(window as any).navigate = (path: string) => Router.navigate(path);

13
src/scanner.js Executable file
View File

@ -0,0 +1,13 @@
function onScanSuccess(decodedText, decodedResult) {
// handle the scanned code as you like, for example:
console.log(`Code matched = ${decodedText}`, decodedResult);
}
function onScanFailure(error) {
// handle scan failure, usually better to ignore and keep scanning.
// for example:
console.warn(`Code scan error = ${error}`);
}
let html5QrcodeScanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: { width: 250, height: 250 } }, /* verbose= */ false);
html5QrcodeScanner.render(onScanSuccess, onScanFailure);

View File

@ -0,0 +1,8 @@
const addResourcesToCache = async (resources) => {
const cache = await caches.open('v1');
await cache.addAll(resources);
};
self.addEventListener('install', (event) => {
event.waitUntil(addResourcesToCache(['/', '/index.html', '/style.css', '/app.js', '/image-list.js', '/star-wars-logo.jpg', '/gallery/bountyHunters.jpg', '/gallery/myLittleVader.jpg', '/gallery/snowTroopers.jpg']));
});

View File

@ -0,0 +1,266 @@
const EMPTY32BYTES = String('').padStart(64, '0');
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting()); // Activate worker immediately
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim()); // Become available to all pages
});
// Event listener for messages from clients
self.addEventListener('message', async (event) => {
const data = event.data;
console.log(data);
if (data.type === 'SCAN') {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length != 0) {
const toDownload = await scanMissingData(myProcessesId);
if (toDownload.length != 0) {
console.log('Sending TO_DOWNLOAD message');
event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload});
}
} else {
event.source.postMessage({ status: 'error', message: 'Empty lists' });
}
} catch (error) {
event.source.postMessage({ status: 'error', message: error.message });
}
} else if (data.type === 'ADD_OBJECT') {
try {
const { storeName, object, key } = data.payload;
const db = await openDatabase();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
if (key) {
await store.put(object, key);
} else {
await store.put(object);
}
event.ports[0].postMessage({ status: 'success', message: '' });
} catch (error) {
event.ports[0].postMessage({ status: 'error', message: error.message });
}
}
});
async function scanMissingData(processesToScan) {
console.log('Scanning for missing data...');
const myProcesses = await getProcesses(processesToScan);
let toDownload = new Set();
// Iterate on each process
if (myProcesses && myProcesses.length != 0) {
for (const process of myProcesses) {
// Iterate on states
const firstState = process.states[0];
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
// iterate on pcd_commitment
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
// Skip public fields
if (state.public_data[field] !== undefined || field === 'roles') continue;
// Check if we have the data in db
const existingData = await getBlob(hash);
if (!existingData) {
toDownload.add(hash);
// We also add an entry in diff, in case it doesn't already exist
await addDiff(processId, state.state_id, hash, state.roles, field);
} else {
// We remove it if we have it in the set
if (toDownload.delete(hash)) {
console.log(`Removing ${hash} from the set`);
}
}
}
}
}
}
console.log(toDownload);
return Array.from(toDownload);
}
async function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('4nk', 1);
request.onerror = (event) => {
reject(request.error);
};
request.onsuccess = (event) => {
resolve(request.result);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('wallet')) {
db.createObjectStore('wallet', { keyPath: 'pre_id' });
}
};
});
}
// Function to get all processes because it is asynchronous
async function getAllProcesses() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
if (!db) {
reject(new Error('Database is not available'));
return;
}
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
};
async function getProcesses(processIds) {
if (!processIds || processIds.length === 0) {
return [];
}
const db = await openDatabase();
if (!db) {
throw new Error('Database is not available');
}
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
const requests = Array.from(processIds).map((processId) => {
return new Promise((resolve) => {
const request = store.get(processId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => {
console.error(`Error fetching process ${processId}:`, request.error);
resolve(undefined);
};
});
});
const results = await Promise.all(requests);
return results.filter(result => result !== undefined);
}
async function getAllDiffsNeedValidation() {
const db = await openDatabase();
const allProcesses = await getAllProcesses();
const tx = db.transaction('diffs', 'readonly');
const store = tx.objectStore('diffs');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = (event) => {
const allItems = event.target.result;
const itemsWithFlag = allItems.filter((item) => item.need_validation);
const processMap = {};
for (const diff of itemsWithFlag) {
const currentProcess = allProcesses.find((item) => {
return item.states.some((state) => state.merkle_root === diff.new_state_merkle_root);
});
if (currentProcess) {
const processKey = currentProcess.merkle_root;
if (!processMap[processKey]) {
processMap[processKey] = {
process: currentProcess.states,
processId: currentProcess.key,
diffs: [],
};
}
processMap[processKey].diffs.push(diff);
}
}
const results = Object.values(processMap).map((entry) => {
const diffs = []
for(const state of entry.process) {
const filteredDiff = entry.diffs.filter(diff => diff.new_state_merkle_root === state.merkle_root);
if(filteredDiff && filteredDiff.length) {
diffs.push(filteredDiff)
}
}
return {
process: entry.process,
processId: entry.processId,
diffs: diffs,
};
});
resolve(results);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
async function getBlob(hash) {
const db = await openDatabase();
const storeName = 'data';
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
return result;
}
async function addDiff(processId, stateId, hash, roles, field) {
const db = await openDatabase();
const storeName = 'diffs';
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
// Check if the diff already exists
const existingDiff = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
if (!existingDiff) {
const newDiff = {
process_id: processId,
state_id: stateId,
value_commitment: hash,
roles: roles,
field: field,
description: null,
previous_value: null,
new_value: null,
notify_user: false,
need_validation: false,
validation_status: 'None'
};
const insertResult = await new Promise((resolve, reject) => {
const putRequest = store.put(newDiff);
putRequest.onsuccess = () => resolve(putRequest.result);
putRequest.onerror = () => reject(putRequest.error);
});
return insertResult;
}
return existingDiff;
}

View File

@ -0,0 +1,38 @@
import Services from './service';
import { init, navigate } from '../router';
import { RoleDefinition } from 'pkg/sdk_client';
import { Member } from 'pkg/sdk_client';
export default class ChatService {
private static instance: ChatService;
private stateId: string | null = null;
private processId: string | null = null;
private paired_member: string[] = [];
constructor() {}
public static async getInstance(): Promise<ChatService> {
if (!ChatService.instance) {
ChatService.instance = new ChatService();
}
return ChatService.instance;
}
async getLocalMember () {
try {
const service = await Services.getInstance();
const currentUser = service.getMemberFromDevice();
return currentUser
} catch (e) {
console.error('Error initializing services:', e);
}
}
async loadMessagingProcess (commitedIn: string) {
try{
const service = await Services.getInstance();
const stored = service.getProcess(commitedIn)
} catch (e) {
console.error('Error loading Messaging Process', e);
}
}
}

View File

@ -1,111 +0,0 @@
import * as Comlink from "comlink";
import type { NetworkBackend } from "../../workers/network.worker";
import Services from "../service";
export class NetworkService {
private worker: Comlink.Remote<NetworkBackend>;
private workerInstance: Worker;
// Cache local
private localRelays: Record<string, string> = {};
// Mécanisme d'attente (Events)
private relayReadyResolver: ((addr: string) => void) | null = null;
private relayReadyPromise: Promise<string> | null = null;
constructor(private bootstrapUrls: string[]) {
this.workerInstance = new Worker(
new URL("../../workers/network.worker.ts", import.meta.url),
{ type: "module" }
);
this.worker = Comlink.wrap<NetworkBackend>(this.workerInstance);
}
public async initRelays() {
await this.worker.setCallbacks(
Comlink.proxy(this.onMessageReceived.bind(this)),
Comlink.proxy(this.onStatusChange.bind(this))
);
for (const url of this.bootstrapUrls) {
this.addWebsocketConnection(url);
}
}
public async addWebsocketConnection(url: string) {
await this.worker.connect(url);
}
public async connectAllRelays() {
for (const url of this.bootstrapUrls) {
this.addWebsocketConnection(url);
}
}
public async sendMessage(flag: string, content: string) {
await this.worker.sendMessage(flag as any, content);
}
// Cette méthode est appelée par le Worker (via Services.ts) ou par onStatusChange
public updateRelay(url: string, spAddress: string) {
this.localRelays[url] = spAddress;
// ✨ EVENT TRIGGER : Si quelqu'un attendait un relais, on le débloque !
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
this.relayReadyResolver(spAddress);
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}
public getAllRelays() {
return this.localRelays;
}
public async getAvailableRelayAddress(): Promise<string> {
// 1. Vérification immédiate (Fast path)
const existing = Object.values(this.localRelays).find(
(addr) => addr && addr !== ""
);
if (existing) return existing;
// 2. Si pas encore là, on crée une "barrière" (Promise)
if (!this.relayReadyPromise) {
console.log("[NetworkService] ⏳ Attente d'un événement Handshake...");
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
this.relayReadyResolver = resolve;
// Timeout de sécurité (10s) pour ne pas bloquer indéfiniment
setTimeout(() => {
if (this.relayReadyResolver) {
reject(new Error("Timeout: Aucun relais reçu après 10s"));
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}, 10000);
});
}
return this.relayReadyPromise;
}
// --- INTERNES ---
private async onMessageReceived(flag: string, content: string, url: string) {
const services = await Services.getInstance();
await services.dispatchToWorker(flag, content, url);
}
private onStatusChange(
url: string,
status: "OPEN" | "CLOSED",
spAddress?: string
) {
if (status === "OPEN" && spAddress) {
// Met à jour et déclenche potentiellement le resolve()
this.updateRelay(url, spAddress);
} else if (status === "CLOSED") {
this.localRelays[url] = "";
}
}
}

View File

@ -1,26 +0,0 @@
import { ApiReturn, Device } from '../../../pkg/sdk_client';
export class SdkService {
private client: any;
async init() {
this.client = await import('../../../pkg/sdk_client');
this.client.setup();
}
public getClient(): any {
if (!this.client) throw new Error('SDK not initialized');
return this.client;
}
// Méthodes utilitaires directes du SDK
public encodeJson(data: any): any {
return this.client.encode_json(data);
}
public encodeBinary(data: any): any {
return this.client.encode_binary(data);
}
public decodeValue(value: number[]): any {
return this.client.decode_value(value);
}
}

View File

@ -1,245 +1,212 @@
import Services from "./service";
import Services from './service';
/**
* Database service managing IndexedDB operations via Web Worker and Service Worker
*/
export class Database {
// ============================================
// PRIVATE PROPERTIES
// ============================================
private static instance: Database;
private db: IDBDatabase | null = null;
private dbName: string = '4nk';
private dbVersion: number = 1;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null;
private serviceWorkerCheckIntervalId: number | null = null;
private indexedDBWorker: Worker | null = null;
private messageIdCounter: number = 0;
private pendingMessages: Map<
number,
{ resolve: (value: any) => void; reject: (error: any) => void }
> = new Map();
private storeDefinitions = {
AnkLabels: {
name: 'labels',
options: { keyPath: 'emoji' },
indices: [],
},
AnkWallet: {
name: 'wallet',
options: { keyPath: 'pre_id' },
indices: [],
},
AnkProcess: {
name: 'processes',
options: {},
indices: [],
},
AnkSharedSecrets: {
name: 'shared_secrets',
options: {},
indices: [],
},
AnkUnconfirmedSecrets: {
name: 'unconfirmed_secrets',
options: { autoIncrement: true },
indices: [],
},
AnkPendingDiffs: {
name: 'diffs',
options: { keyPath: 'value_commitment' },
indices: [
{ name: 'byStateId', keyPath: 'state_id', options: { unique: false } },
{ name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } },
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } },
],
},
AnkData: {
name: 'data',
options: {},
indices: [],
},
};
// ============================================
// INITIALIZATION & SINGLETON
// ============================================
private constructor() {
this.initIndexedDBWorker();
this.initServiceWorker();
}
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Database
public static async getInstance(): Promise<Database> {
if (!Database.instance) {
Database.instance = new Database();
await Database.instance.waitForWorkerReady();
await Database.instance.init();
}
return Database.instance;
}
// ============================================
// INDEXEDDB WEB WORKER
// ============================================
private initIndexedDBWorker(): void {
this.indexedDBWorker = new Worker(
new URL("../workers/database.worker.ts", import.meta.url),
{ type: "module" }
);
this.indexedDBWorker.onmessage = (event) => {
const { id, type, result, error } = event.data;
const pending = this.pendingMessages.get(id);
if (pending) {
this.pendingMessages.delete(id);
if (type === "SUCCESS") {
pending.resolve(result);
} else if (type === "ERROR") {
pending.reject(new Error(error));
}
}
};
this.indexedDBWorker.onerror = (error) => {
console.error("[Database] IndexedDB Worker error:", error);
};
}
private async waitForWorkerReady(): Promise<void> {
return this.sendMessageToWorker("INIT", {});
}
private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> {
// Initialize the database
private async init(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.indexedDBWorker) {
reject(new Error("IndexedDB Worker not initialized"));
return;
}
const request = indexedDB.open(this.dbName, this.dbVersion);
const id = this.messageIdCounter++;
this.pendingMessages.set(id, { resolve, reject });
request.onupgradeneeded = () => {
const db = request.result;
this.indexedDBWorker.postMessage({ type, payload, id });
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
if (!db.objectStoreNames.contains(name)) {
let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
// Timeout de sécurité (30 secondes)
setTimeout(() => {
if (this.pendingMessages.has(id)) {
this.pendingMessages.delete(id);
reject(new Error(`Worker message timeout for type: ${type}`));
}
}, 30000);
indices.forEach(({ name, keyPath, options }) => {
store.createIndex(name, keyPath, options);
});
}
});
};
request.onsuccess = async () => {
this.db = request.result;
await this.initServiceWorker();
resolve();
};
request.onerror = () => {
console.error('Database error:', request.error);
reject(request.error);
};
});
}
// ============================================
// SERVICE WORKER
// ============================================
private initServiceWorker(): void {
this.registerServiceWorker("/data.worker.js");
public async getDb(): Promise<IDBDatabase> {
if (!this.db) {
await this.init();
}
return this.db!;
}
private async registerServiceWorker(path: string): Promise<void> {
if (!("serviceWorker" in navigator)) return;
console.log("[Database] Initializing Service Worker:", path);
public getStoreList(): { [key: string]: string } {
const objectList: { [key: string]: string } = {};
Object.keys(this.storeDefinitions).forEach((key) => {
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
});
return objectList;
}
private async initServiceWorker() {
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
try {
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
const scriptURL =
registration.active?.scriptURL ||
registration.installing?.scriptURL ||
registration.waiting?.scriptURL;
const scope = registration.scope;
if (
scope.includes("/src/service-workers/") ||
(scriptURL && scriptURL.includes("/src/service-workers/"))
) {
console.warn(`[Database] Removing old Service Worker (${scope})`);
await registration.unregister();
}
}
const existingValidWorker = registrations.find((r) => {
const url =
r.active?.scriptURL ||
r.installing?.scriptURL ||
r.waiting?.scriptURL;
return url && url.endsWith(path.replace(/^\//, ""));
});
if (!existingValidWorker) {
console.log("[Database] Registering new Service Worker");
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
path,
{ type: "module", scope: "/" }
);
} else {
console.log("[Database] Service Worker already active");
this.serviceWorkerRegistration = existingValidWorker;
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service Worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
}
navigator.serviceWorker.addEventListener("message", async (event) => {
await this.checkForUpdates();
// Set up a global message listener for responses from the service worker.
navigator.serviceWorker.addEventListener('message', async (event) => {
console.log('Received message from service worker:', event.data);
await this.handleServiceWorkerMessage(event.data);
});
if (this.serviceWorkerCheckIntervalId)
clearInterval(this.serviceWorkerCheckIntervalId);
this.serviceWorkerCheckIntervalId = setInterval(async () => {
const activeWorker =
this.serviceWorkerRegistration?.active ||
(await this.waitForServiceWorkerActivation(
this.serviceWorkerRegistration!
));
// Set up a periodic check to ensure the service worker is active and to send a SYNC message.
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: "SCAN", payload });
if (payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000) as unknown as number;
}, 5000);
} catch (error) {
console.error("[Database] Service Worker error:", error);
console.error('Service Worker registration failed:', error);
}
}
private async waitForServiceWorkerActivation(
registration: ServiceWorkerRegistration
): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
resolve(registration.active);
} else {
const listener = () => {
// Helper function to wait for service worker activation
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
navigator.serviceWorker.removeEventListener(
"controllerchange",
listener
);
resolve(registration.active);
resolve(registration.active);
} else {
const listener = () => {
if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener('controllerchange', listener);
}
};
navigator.serviceWorker.addEventListener("controllerchange", listener);
}
});
});
}
private async checkForUpdates(): Promise<void> {
private async checkForUpdates() {
if (this.serviceWorkerRegistration) {
// Check for updates to the service worker
try {
await this.serviceWorkerRegistration.update();
// If there's a new worker waiting, activate it immediately
if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
} catch (error) {
console.error("Error checking for service worker updates:", error);
console.error('Error checking for service worker updates:', error);
}
}
}
// ============================================
// SERVICE WORKER MESSAGE HANDLERS
// ============================================
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case "TO_DOWNLOAD":
case 'TO_DOWNLOAD':
await this.handleDownloadList(message.data);
break;
case "DIFFS_TO_CREATE":
await this.handleDiffsToCreate(message.data);
break;
default:
console.warn(
"Unknown message type received from service worker:",
message
);
console.warn('Unknown message type received from service worker:', message);
}
}
private async handleDiffsToCreate(diffs: any[]): Promise<void> {
console.log(
`[Database] Creating ${diffs.length} diffs from Service Worker scan`
);
try {
await this.saveDiffs(diffs);
console.log("[Database] Diffs created successfully");
} catch (error) {
console.error("[Database] Error creating diffs:", error);
}
}
private async handleDownloadList(downloadList: string[]): Promise<void> {
let requestedStateId: string[] = [];
private async handleDownloadList(downloadList: string[]): void {
// Download the missing data
let requestedStateId = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
// This should never happen
console.warn(`Missing a diff for hash ${hash}`);
continue;
}
@ -249,17 +216,20 @@ export class Database {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
const blob = new Blob([valueBytes], {
type: "application/octet-stream",
});
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(
new CustomEvent("newDataReceived", {
detail: { processId, stateId, hash },
})
);
document.dispatchEvent(new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
}
}));
} else {
console.log("Request data from managers of the process");
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
@ -271,205 +241,176 @@ export class Database {
}
}
// ============================================
// GENERIC INDEXEDDB OPERATIONS
// ============================================
private handleAddObjectResponse = async (event: MessageEvent) => {
const data = event.data;
console.log('Received response from service worker (ADD_OBJECT):', data);
const service = await Services.getInstance();
if (data.type === 'NOTIFICATIONS') {
service.setNotifications(data.data);
} else if (data.type === 'TO_DOWNLOAD') {
console.log(`Received missing data ${data}`);
// Download the missing data
let requestedStateId = [];
for (const hash of data.data) {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
const diff = await service.getDiffByValue(hash);
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
};
public async getStoreList(): Promise<{ [key: string]: string }> {
return this.sendMessageToWorker("GET_STORE_LIST", {});
}
private handleGetObjectResponse = (event: MessageEvent) => {
console.log('Received response from service worker (GET_OBJECT):', event.data);
};
public async addObject(payload: {
storeName: string;
object: any;
key: any;
}): Promise<void> {
await this.sendMessageToWorker("ADD_OBJECT", payload);
}
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
return new Promise(async (resolve, reject) => {
// Check if the service worker is active
if (!this.serviceWorkerRegistration) {
// console.warn('Service worker registration is not ready. Waiting...');
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
}
public async batchWriting(payload: {
storeName: string;
objects: { key: any; object: any }[];
}): Promise<void> {
await this.sendMessageToWorker("BATCH_WRITING", payload);
}
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
// Create a message channel for communication
const messageChannel = new MessageChannel();
// Handle the response from the service worker
messageChannel.port1.onmessage = (event) => {
if (event.data.status === 'success') {
resolve();
} else {
const error = event.data.message;
reject(new Error(error || 'Unknown error occurred while adding object'));
}
};
// Send the add object request to the service worker
try {
activeWorker?.postMessage(
{
type: 'ADD_OBJECT',
payload,
},
[messageChannel.port2],
);
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
public async getObject(storeName: string, key: string): Promise<any | null> {
return this.sendMessageToWorker("GET_OBJECT", { storeName, key });
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(key);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
return result;
}
public async dumpStore(storeName: string): Promise<Record<string, any>> {
return this.sendMessageToWorker("DUMP_STORE", { storeName });
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
try {
// Wait for both getAllKeys() and getAll() to resolve
const [keys, values] = await Promise.all([
new Promise<any[]>((resolve, reject) => {
const request = store.getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
}),
new Promise<any[]>((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
}),
]);
// Combine keys and values into an object
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
return result;
} catch (error) {
console.error('Error fetching data from IndexedDB:', error);
throw error;
}
}
public async deleteObject(storeName: string, key: string): Promise<void> {
await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key });
const db = await this.getDb();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
try {
await new Promise((resolve, reject) => {
const getRequest = store.delete(key);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
} catch (e) {
throw e;
}
}
public async clearStore(storeName: string): Promise<void> {
await this.sendMessageToWorker("CLEAR_STORE", { storeName });
}
public async requestStoreByIndex(
storeName: string,
indexName: string,
request: string
): Promise<any[]> {
return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", {
storeName,
indexName,
request,
});
}
public async clearMultipleStores(storeNames: string[]): Promise<void> {
for (const storeName of storeNames) {
await this.clearStore(storeName);
}
}
// ============================================
// BUSINESS METHODS - DEVICE
// ============================================
public async saveDevice(device: any): Promise<void> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
try {
const existing = await this.getObject("wallet", "1");
if (existing) {
await this.deleteObject("wallet", "1");
}
} catch (e) {}
await this.addObject({
storeName: "wallet",
object: { pre_id: "1", device },
key: null,
});
}
public async getDevice(): Promise<any | null> {
const result = await this.getObject("wallet", "1");
return result ? result["device"] : null;
}
// ============================================
// BUSINESS METHODS - PROCESS
// ============================================
public async saveProcess(processId: string, process: any): Promise<void> {
await this.addObject({
storeName: "processes",
object: process,
key: processId,
});
}
public async saveProcessesBatch(
processes: Record<string, any>
): Promise<void> {
if (Object.keys(processes).length === 0) return;
await this.batchWriting({
storeName: "processes",
objects: Object.entries(processes).map(([key, value]) => ({
key,
object: value,
})),
});
}
public async getProcess(processId: string): Promise<any | null> {
return this.getObject("processes", processId);
}
public async getAllProcesses(): Promise<Record<string, any>> {
return this.dumpStore("processes");
}
// ============================================
// BUSINESS METHODS - BLOBS
// ============================================
public async saveBlob(hash: string, data: Blob): Promise<void> {
await this.addObject({
storeName: "data",
object: data,
key: hash,
});
}
public async getBlob(hash: string): Promise<Blob | null> {
return this.getObject("data", hash);
}
// ============================================
// BUSINESS METHODS - DIFFS
// ============================================
public async saveDiffs(diffs: any[]): Promise<void> {
if (diffs.length === 0) return;
for (const diff of diffs) {
await this.addObject({
storeName: "diffs",
object: diff,
key: null,
await new Promise((resolve, reject) => {
const clearRequest = store.clear();
clearRequest.onsuccess = () => resolve(clearRequest.result);
clearRequest.onerror = () => reject(clearRequest.error);
});
} catch (e) {
throw e;
}
}
public async getDiff(hash: string): Promise<any | null> {
return this.getObject("diffs", hash);
}
// Request a store by index
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
public async getAllDiffs(): Promise<Record<string, any>> {
return this.dumpStore("diffs");
}
// ============================================
// BUSINESS METHODS - SECRETS
// ============================================
public async getSharedSecret(address: string): Promise<string | null> {
return this.getObject("shared_secrets", address);
}
public async saveSecretsBatch(
unconfirmedSecrets: any[],
sharedSecrets: { key: string; value: any }[]
): Promise<void> {
if (unconfirmedSecrets && unconfirmedSecrets.length > 0) {
for (const secret of unconfirmedSecrets) {
await this.addObject({
storeName: "unconfirmed_secrets",
object: secret,
key: null,
});
}
try {
return new Promise((resolve, reject) => {
const getAllRequest = index.getAll(request);
getAllRequest.onsuccess = () => {
const allItems = getAllRequest.result;
const filtered = allItems.filter(item => item.state_id === request);
resolve(filtered);
};
getAllRequest.onerror = () => reject(getAllRequest.error);
});
} catch (e) {
throw e;
}
if (sharedSecrets && sharedSecrets.length > 0) {
for (const { key, value } of sharedSecrets) {
await this.addObject({
storeName: "shared_secrets",
object: value,
key: key,
});
}
}
}
public async getAllSecrets(): Promise<{
shared_secrets: Record<string, any>;
unconfirmed_secrets: any[];
}> {
const sharedSecrets = await this.dumpStore("shared_secrets");
const unconfirmedSecrets = await this.dumpStore("unconfirmed_secrets");
return {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
}
}

View File

@ -1,58 +0,0 @@
import { MerkleProofResult, ProcessState } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
export class CryptoService {
constructor(private sdk: SdkService) {}
public hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString);
return new Blob([uint8Array as any], { type: 'application/octet-stream' });
}
public hexToUInt8Array(hexString: string): Uint8Array {
if (hexString.length % 2 !== 0) throw new Error('Invalid hex string');
const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return uint8Array;
}
public async blobToHex(blob: Blob): Promise<string> {
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
return Array.from(bytes)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string {
return this.sdk.getClient().hash_value(fileBlob, commitedIn, label);
}
public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
return this.sdk.getClient().get_merkle_proof(processState, attributeName);
}
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
return this.sdk.getClient().validate_merkle_proof(proof, hash);
}
public splitData(obj: Record<string, any>) {
const jsonCompatibleData: Record<string, any> = {};
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
for (const [key, value] of Object.entries(obj)) {
if (this.isFileBlob(value)) {
binaryData[key] = value;
} else {
jsonCompatibleData[key] = value;
}
}
return { jsonCompatibleData, binaryData };
}
private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array;
}
}

View File

@ -1,95 +0,0 @@
import { Process, ProcessState, RoleDefinition } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
import Database from '../database.service';
const EMPTY32BYTES = String('').padStart(64, '0');
export class ProcessService {
private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set();
constructor(private sdk: SdkService, private db: Database) {}
public async getProcess(processId: string): Promise<Process | null> {
if (this.processesCache[processId]) return this.processesCache[processId];
const process = await this.db.getProcess(processId);
if (process) this.processesCache[processId] = process;
return process;
}
public async getProcesses(): Promise<Record<string, Process>> {
if (Object.keys(this.processesCache).length > 0) return this.processesCache;
this.processesCache = await this.db.getAllProcesses();
return this.processesCache;
}
public async saveProcessToDb(processId: string, process: Process) {
await this.db.saveProcess(processId, process);
this.processesCache[processId] = process;
}
public async batchSaveProcesses(processes: Record<string, Process>) {
if (Object.keys(processes).length === 0) return;
await this.db.saveProcessesBatch(processes);
this.processesCache = { ...this.processesCache, ...processes };
}
public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
return process.states.findLast((state) => state.commited_in !== processTip) || null;
}
public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return [];
const processTip = process.states[process.states.length - 1].commited_in;
return process.states.filter((state) => state.commited_in === processTip).filter((state) => state.state_id !== EMPTY32BYTES);
}
public getStateFromId(process: Process, stateId: string): ProcessState | null {
return process.states.find((state) => state.state_id === stateId) || null;
}
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const last = this.getLastCommitedState(process);
if (last?.roles && Object.keys(last.roles).length > 0) return last.roles;
const first = process.states[0];
if (first?.roles && Object.keys(first.roles).length > 0) return first.roles;
return null;
}
public rolesContainsMember(roles: Record<string, RoleDefinition>, memberId: string): boolean {
return Object.values(roles).some((role) => role.members.includes(memberId));
}
public async getMyProcesses(pairingProcessId: string): Promise<string[]> {
const processes = await this.getProcesses();
const newMyProcesses = new Set<string>(this.myProcesses);
if (pairingProcessId) newMyProcesses.add(pairingProcessId);
for (const [processId, process] of Object.entries(processes)) {
if (newMyProcesses.has(processId)) continue;
const roles = this.getRoles(process);
if (roles && this.rolesContainsMember(roles, pairingProcessId)) {
newMyProcesses.add(processId);
}
}
this.myProcesses = newMyProcesses;
return Array.from(this.myProcesses);
}
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) {
return i;
}
}
return null;
}
}

View File

@ -1,93 +0,0 @@
import { Device } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
import Database from '../database.service';
export class WalletService {
constructor(private sdk: SdkService, private db: Database) {}
public isPaired(): boolean {
try {
return this.sdk.getClient().is_paired();
} catch (e) {
return false;
}
}
public getAmount(): BigInt {
return this.sdk.getClient().get_available_amount();
}
public getDeviceAddress(): string {
return this.sdk.getClient().get_address();
}
public getPairingProcessId(): string {
return this.sdk.getClient().get_pairing_process_id();
}
public async createNewDevice(chainTip: number): Promise<string> {
const spAddress = await this.sdk.getClient().create_new_device(0, 'signet');
const device = this.dumpDeviceFromMemory();
if (device.sp_wallet.birthday === 0) {
device.sp_wallet.birthday = chainTip;
device.sp_wallet.last_scan = chainTip;
this.sdk.getClient().restore_device(device);
}
await this.saveDeviceInDatabase(device);
return spAddress;
}
public dumpDeviceFromMemory(): Device {
return this.sdk.getClient().dump_device();
}
public dumpNeuteredDevice(): Device | null {
try {
return this.sdk.getClient().dump_neutered_device();
} catch (e) {
return null;
}
}
public async dumpWallet(): Promise<any> {
return await this.sdk.getClient().dump_wallet();
}
public async getMemberFromDevice(): Promise<string[] | null> {
try {
const device = await this.getDeviceFromDatabase();
if (device) {
const pairedMember = device['paired_member'];
return pairedMember.sp_addresses;
} else {
return null;
}
} catch (e) {
throw new Error(`[WalletService] Échec: ${e}`);
}
}
public async saveDeviceInDatabase(device: Device): Promise<void> {
await this.db.saveDevice(device);
}
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const res = await db.getObject('wallet', '1');
return res ? res['device'] : null;
}
public restoreDevice(device: Device) {
this.sdk.getClient().restore_device(device);
}
public pairDevice(processId: string, spAddressList: string[]): void {
this.sdk.getClient().pair_device(processId, spAddressList);
}
public async unpairDevice(): Promise<void> {
this.sdk.getClient().unpair_device();
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
}
}

View File

@ -1,651 +0,0 @@
import { MessageType } from "../types/index";
import Services from "./service";
import TokenService from "./token.service";
import { cleanSubscriptions } from "../utils/subscription.utils";
import { splitPrivateData, isValid32ByteHex } from "../utils/service.utils";
import { MerkleProofResult } from "../../pkg/sdk_client";
export class IframeController {
private static isInitialized = false;
static async init() {
if (this.isInitialized) return;
if (window.self !== window.top) {
console.log(
"[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API..."
);
await IframeController.registerAllListeners();
} else {
console.log(
"[IframeController] Mode Standalone (pas d'iframe). Listeners API inactifs."
);
}
}
private static async registerAllListeners() {
console.log(
"[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)..."
);
const services = await Services.getInstance();
const tokenService = await TokenService.getInstance();
const errorResponse = (
errorMsg: string,
origin: string,
messageId?: string
) => {
console.error(
`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`
);
window.parent.postMessage(
{
type: MessageType.ERROR,
error: errorMsg,
messageId,
},
origin
);
};
const withToken = async (
event: MessageEvent,
action: () => Promise<void>
) => {
const { accessToken } = event.data;
if (
!accessToken ||
!(await tokenService.validateToken(accessToken, event.origin))
) {
throw new Error("Invalid or expired session token");
}
await action();
};
// --- HANDLERS ---
const handleRequestLink = async (event: MessageEvent) => {
console.log(
`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`
);
const device = await services.getDeviceFromDatabase();
if (device && device.pairing_process_commitment) {
console.log(
"[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts."
);
} else {
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
document.removeEventListener(
"app:pairing-ready",
handler as EventListener
);
clearTimeout(timeoutId);
};
const handler = (e: CustomEvent) => {
cleanup();
if (e.detail && e.detail.success) {
resolve();
} else {
reject(new Error(e.detail?.error || "Auto-pairing failed"));
}
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error("Auto-pairing timed out (Event not received)"));
}, 5000);
document.addEventListener(
"app:pairing-ready",
handler as EventListener
);
});
console.log(`[Router:API] Feu vert de home.ts reçu !`);
}
console.log(`[Router:API] Traitement de la liaison...`);
const tokens = await tokenService.generateSessionToken(event.origin);
window.parent.postMessage(
{
type: MessageType.LINK_ACCEPTED,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
messageId: event.data.messageId,
},
event.origin
);
console.log(
`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`
);
};
const handleCreatePairing = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`);
// 🔥 CORRECTION TS2801 : Ajout de await
if (await services.isPaired()) {
throw new Error(
"Device already paired — ignoring CREATE_PAIRING request"
);
}
await withToken(event, async () => {
console.log("[Router:API] 🚀 Démarrage du processus d'appairage...");
// 🔥 CORRECTION TS2322 : Ajout de await pour récupérer la string
const myAddress = await services.getDeviceAddress();
console.log("[Router:API] 1/7: Création du processus de pairing...");
const createPairingProcessReturn = await services.createPairingProcess(
"",
[myAddress]
);
const pairingId =
createPairingProcessReturn.updated_process?.process_id;
const stateId = createPairingProcessReturn.updated_process
?.current_process?.states[0]?.state_id as string;
if (!pairingId || !stateId) {
throw new Error(
"Pairing process creation failed to return valid IDs"
);
}
console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`);
console.log("[Router:API] 3/7: Enregistrement local de l'appareil...");
// 🔥 CORRECTION TS2322 : myAddress est maintenant une string, plus une Promise
await services.pairDevice(pairingId, [myAddress]);
console.log(
"[Router:API] 4/7: Traitement du retour (handleApiReturn)..."
);
await services.handleApiReturn(createPairingProcessReturn);
console.log("[Router:API] 5/7: Création de la mise à jour PRD...");
const createPrdUpdateReturn = await services.createPrdUpdate(
pairingId,
stateId
);
await services.handleApiReturn(createPrdUpdateReturn);
console.log("[Router:API] 6/7: Approbation du changement...");
const approveChangeReturn = await services.approveChange(
pairingId,
stateId
);
await services.handleApiReturn(approveChangeReturn);
console.log("[Router:API] 7/7: Confirmation finale du pairing...");
console.log("[Router:API] 🎉 Appairage terminé avec succès !");
const successMsg = {
type: MessageType.PAIRING_CREATED,
pairingId,
messageId: event.data.messageId,
};
window.parent.postMessage(successMsg, event.origin);
});
};
const handleGetMyProcesses = async (event: MessageEvent) => {
console.log(
`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`
);
if (!(await services.isPaired())) throw new Error("Device not paired");
await withToken(event, async () => {
const myProcesses = await services.getMyProcesses();
window.parent.postMessage(
{
type: MessageType.GET_MY_PROCESSES,
myProcesses,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleGetProcesses = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
await withToken(event, async () => {
const processes = await services.getProcesses();
window.parent.postMessage(
{
type: MessageType.PROCESSES_RETRIEVED,
processes,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleDecryptState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { processId, stateId } = event.data;
await withToken(event, async () => {
const process = await services.getProcess(processId);
if (!process) throw new Error("Can't find process");
// 🔥 CORRECTION TS2339 & TS2345 : Ajout de await car getStateFromId est async
const state = await services.getStateFromId(process, stateId);
if (!state)
throw new Error(`Unknown state ${stateId} for process ${processId}`);
console.log(
`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`
);
await services.ensureConnections(process, stateId);
const res: Record<string, any> = {};
for (const attribute of Object.keys(state.pcd_commitment)) {
if (
attribute === "roles" ||
(state.public_data && state.public_data[attribute])
) {
continue;
}
const decryptedAttribute = await services.decryptAttribute(
processId,
state,
attribute
);
if (decryptedAttribute) {
res[attribute] = decryptedAttribute;
}
}
console.log(
`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length
} attribut(s) déchiffré(s).`
);
window.parent.postMessage(
{
type: MessageType.DATA_RETRIEVED,
data: res,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleValidateToken = async (event: MessageEvent) => {
// ... (Code identique, pas d'erreurs ici normalement)
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`);
const accessToken = event.data.accessToken;
const refreshToken = event.data.refreshToken;
if (!accessToken || !refreshToken) {
throw new Error("Missing access, refresh token or both");
}
const isValid = await tokenService.validateToken(
accessToken,
event.origin
);
console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`);
window.parent.postMessage(
{
type: MessageType.VALIDATE_TOKEN,
accessToken: accessToken,
refreshToken: refreshToken,
isValid: isValid,
messageId: event.data.messageId,
},
event.origin
);
};
const handleRenewToken = async (event: MessageEvent) => {
// ... (Code identique)
console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`);
const refreshToken = event.data.refreshToken;
if (!refreshToken) throw new Error("No refresh token provided");
const newAccessToken = await tokenService.refreshAccessToken(
refreshToken,
event.origin
);
if (!newAccessToken)
throw new Error("Failed to refresh token (invalid refresh token)");
console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`);
window.parent.postMessage(
{
type: MessageType.RENEW_TOKEN,
accessToken: newAccessToken,
refreshToken: refreshToken,
messageId: event.data.messageId,
},
event.origin
);
};
const handleGetPairingId = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`);
const maxRetries = 10;
const retryDelay = 300;
let pairingId: string | null = null;
for (let i = 0; i < maxRetries; i++) {
const device = await services.getDeviceFromDatabase();
if (device && device.pairing_process_commitment) {
pairingId = device.pairing_process_commitment;
console.log(
`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1
}/${maxRetries})`
);
break;
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
if (!pairingId) {
throw new Error("Device not paired");
}
await withToken(event, async () => {
window.parent.postMessage(
{
type: MessageType.GET_PAIRING_ID,
userPairingId: pairingId,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleCreateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { processData, privateFields, roles } = event.data;
await withToken(event, async () => {
const { privateData, publicData } = splitPrivateData(
processData,
privateFields
);
const createProcessReturn = await services.createProcess(
privateData,
publicData,
roles
);
if (!createProcessReturn.updated_process) {
throw new Error("Empty updated_process in createProcessReturn");
}
const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process;
await services.handleApiReturn(createProcessReturn);
console.log(`[Router:API] 🎉 Processus ${processId} créé.`);
const res = {
processId,
process,
processData,
};
window.parent.postMessage(
{
type: MessageType.PROCESS_CREATED,
processCreated: res,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleNotifyUpdate = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { processId, stateId } = event.data;
await withToken(event, async () => {
if (!isValid32ByteHex(stateId)) throw new Error("Invalid state id");
const res = await services.createPrdUpdate(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleValidateState = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { processId, stateId } = event.data;
await withToken(event, async () => {
const res = await services.approveChange(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.STATE_VALIDATED,
validatedProcess: res.updated_process,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleUpdateProcess = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { processId, newData, privateFields, roles } = event.data;
await withToken(event, async () => {
const res = await services.updateProcess(
processId,
newData,
privateFields,
roles
);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.PROCESS_UPDATED,
updatedProcess: res.updated_process,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleDecodePublicData = async (event: MessageEvent) => {
console.log(
`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`
);
if (!(await services.isPaired())) throw new Error("Device not paired");
const { encodedData } = event.data;
await withToken(event, async () => {
const decodedData = await services.decodeValue(encodedData);
window.parent.postMessage(
{
type: MessageType.PUBLIC_DATA_DECODED,
decodedData,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleHashValue = async (event: MessageEvent) => {
console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`);
const { commitedIn, label, fileBlob } = event.data;
await withToken(event, async () => {
const hash = await services.getHashForFile(commitedIn, label, fileBlob);
window.parent.postMessage(
{
type: MessageType.VALUE_HASHED,
hash,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleGetMerkleProof = async (event: MessageEvent) => {
console.log(
`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`
);
const { processState, attributeName } = event.data;
await withToken(event, async () => {
const proof = await services.getMerkleProofForFile(
processState,
attributeName
);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_RETRIEVED,
proof,
messageId: event.data.messageId,
},
event.origin
);
});
};
const handleValidateMerkleProof = async (event: MessageEvent) => {
console.log(
`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`
);
const { merkleProof, documentHash } = event.data;
await withToken(event, async () => {
let parsedMerkleProof: MerkleProofResult;
try {
parsedMerkleProof = JSON.parse(merkleProof);
} catch (e) {
throw new Error("Provided merkleProof is not a valid json object");
}
const res = await services.validateMerkleProof(
parsedMerkleProof,
documentHash
);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_VALIDATED,
isValid: res,
messageId: event.data.messageId,
},
event.origin
);
});
};
window.removeEventListener("message", handleMessage);
window.addEventListener("message", handleMessage);
async function handleMessage(event: MessageEvent) {
try {
// Switch/case inchangé ...
switch (event.data.type) {
case MessageType.REQUEST_LINK:
await handleRequestLink(event);
break;
case MessageType.CREATE_PAIRING:
await handleCreatePairing(event);
break;
case MessageType.GET_MY_PROCESSES:
await handleGetMyProcesses(event);
break;
case MessageType.GET_PROCESSES:
await handleGetProcesses(event);
break;
case MessageType.RETRIEVE_DATA:
await handleDecryptState(event);
break;
case MessageType.VALIDATE_TOKEN:
await handleValidateToken(event);
break;
case MessageType.RENEW_TOKEN:
await handleRenewToken(event);
break;
case MessageType.GET_PAIRING_ID:
await handleGetPairingId(event);
break;
case MessageType.CREATE_PROCESS:
await handleCreateProcess(event);
break;
case MessageType.NOTIFY_UPDATE:
await handleNotifyUpdate(event);
break;
case MessageType.VALIDATE_STATE:
await handleValidateState(event);
break;
case MessageType.UPDATE_PROCESS:
await handleUpdateProcess(event);
break;
case MessageType.DECODE_PUBLIC_DATA:
await handleDecodePublicData(event);
break;
case MessageType.HASH_VALUE:
await handleHashValue(event);
break;
case MessageType.GET_MERKLE_PROOF:
await handleGetMerkleProof(event);
break;
case MessageType.VALIDATE_MERKLE_PROOF:
await handleValidateMerkleProof(event);
break;
default:
// console.warn("[Router:API] ⚠️ Message non géré reçu:", event.data);
}
} catch (error: any) {
const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
window.parent.postMessage(
{
type: MessageType.LISTENING,
},
"*"
);
console.log(
"[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent."
);
}
}

260
src/services/modal.service.ts Executable file
View File

@ -0,0 +1,260 @@
import modalHtml from '../components/login-modal/login-modal.html?raw';
import modalScript from '../components/login-modal/login-modal.js?raw';
import validationModalStyle from '../components/validation-modal/validation-modal.css?raw';
import Services from './service';
import { init, navigate } from '../router';
import { addressToEmoji } from '../utils/sp-address.utils';
import { RoleDefinition } from 'pkg/sdk_client';
import { initValidationModal } from '~/components/validation-modal/validation-modal';
import { interpolate } from '~/utils/html.utils';
export default class ModalService {
private static instance: ModalService;
private stateId: string | null = null;
private processId: string | null = null;
private constructor() {}
private paired_addresses: string[] = [];
private modal: HTMLElement | null = null;
// Method to access the singleton instance of Services
public static async getInstance(): Promise<ModalService> {
if (!ModalService.instance) {
ModalService.instance = new ModalService();
}
return ModalService.instance;
}
public openLoginModal(myAddress: string, receiverAddress: string) {
const container = document.querySelector('.page-container');
let html = modalHtml;
html = html.replace('{{device1}}', myAddress);
html = html.replace('{{device2}}', receiverAddress);
if (container) container.innerHTML += html;
const modal = document.getElementById('login-modal');
if (modal) modal.style.display = 'flex';
const newScript = document.createElement('script');
newScript.setAttribute('type', 'module');
newScript.textContent = modalScript;
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
}
async injectModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
async injectCreationModal(members: any[]) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text());
html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0]));
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.src = '/src/components/modal/confirmation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
}
}
// Device 1 wait Device 2
async injectWaitingModal() {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text());
container.innerHTML += html;
}
}
async injectValidationModal(processDiff: any) {
const container = document.querySelector('#containerId');
if (container) {
let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text());
html = interpolate(html, {processId: processDiff.processId})
container.innerHTML += html;
// Dynamically load the header JS
const script = document.createElement('script');
script.id = 'validation-modal-script';
script.src = '/src/components/validation-modal/validation-modal.ts';
script.type = 'module';
document.head.appendChild(script);
const css = document.createElement('style');
css.id = 'validation-modal-css';
css.innerText = validationModalStyle;
document.head.appendChild(css);
initValidationModal(processDiff)
}
}
async closeValidationModal() {
const script = document.querySelector('#validation-modal-script');
const css = document.querySelector('#validation-modal-css');
const component = document.querySelector('#validation-modal');
script?.remove();
css?.remove();
component?.remove();
}
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
let members;
if (roleDefinition['pairing']) {
const owner = roleDefinition['pairing'];
members = owner.members;
} else {
throw new Error('No "pairing" role');
}
if (members.length != 1) {
throw new Error('Must have exactly 1 member');
}
console.log("MEMBERS:", members);
// We take all the addresses except our own
const service = await Services.getInstance();
const localAddress = await service.getDeviceAddress();
for (const member of members) {
if (member.sp_addresses) {
for (const address of member.sp_addresses) {
if (address !== localAddress) {
this.paired_addresses.push(address);
}
}
}
}
this.processId = processId;
this.stateId = stateId;
if (members[0].sp_addresses.length === 1) {
await this.injectCreationModal(members);
this.modal = document.getElementById('creation-modal');
console.log("LENGTH:", members[0].sp_addresses.length);
} else {
await this.injectModal(members);
this.modal = document.getElementById('modal');
console.log("LENGTH:", members[0].sp_addresses.length);
}
if (this.modal) this.modal.style.display = 'flex';
// Close modal when clicking outside of it
window.onclick = (event) => {
if (event.target === this.modal) {
this.closeConfirmationModal();
}
};
}
confirmLogin() {
console.log('=============> Confirm Login');
}
async closeLoginModal() {
if (this.modal) this.modal.style.display = 'none';
}
async confirmPairing() {
const service = await Services.getInstance();
if (this.modal) this.modal.style.display = 'none';
if (service.device1) {
console.log("Device 1 detected");
// We send the prd update
if (this.stateId && this.processId) {
try {
// Device B shouldn't do this again
const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
await service.handleApiReturn(createPrdUpdateReturn);
} catch (e) {
throw e;
}
} else {
throw new Error('No currentPcdCommitment');
}
// We send confirmation that we validate the change
try {
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
await this.injectWaitingModal();
const waitingModal = document.getElementById('waiting-modal');
if (waitingModal) waitingModal.style.display = 'flex';
if (!service.device2Ready) {
while (!service.device2Ready) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log("Device 2 is ready - Device 1 can now proceed");
}
service.pairDevice(this.paired_addresses, this.processId);
this.paired_addresses = [];
this.processId = null;
this.stateId = null;
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('chat');
service.resetState();
} catch (e) {
throw e;
}
// try {
// service.pairDevice(this.paired_addresses);
// } catch (e) {
// throw e;
// }
} else {
console.log("Device 2 detected");
// if (this.stateId && this.processId) {
// try {
// // Device B shouldn't do this again
// const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
// await service.handleApiReturn(createPrdUpdateReturn);
// } catch (e) {
// throw e;
// }
// } else {
// throw new Error('No currentPcdCommitment');
// }
// We send confirmation that we validate the change
try {
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
} catch (e) {
throw e;
}
service.pairDevice(this.paired_addresses, this.processId!);
this.paired_addresses = [];
this.processId = null;
this.stateId = null;
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('chat');
}
}
async closeConfirmationModal() {
const service = await Services.getInstance();
await service.unpairDevice();
if (this.modal) this.modal.style.display = 'none';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +1,81 @@
export async function storeData(
servers: string[],
key: string,
value: Blob,
ttl: number | null
): Promise<Response | null> {
import axios, { AxiosResponse } from 'axios';
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
for (const server of servers) {
try {
// 1. Vérification d'existence (GET)
const dataExists = await testData(server, key);
if (dataExists) {
console.log("Data already stored:", key);
continue;
} else {
console.log(
"Data not stored for server, proceeding to POST:",
key,
server
);
// Append key and ttl as query parameters
const url = new URL(`${server}/store`);
url.searchParams.append('key', key);
if (ttl !== null) {
url.searchParams.append('ttl', ttl.toString());
}
// 2. Construction URL
let url: string;
if (server.startsWith("/")) {
url = `${server}/store/${encodeURIComponent(key)}`;
if (ttl !== null) url += `?ttl=${ttl}`;
} else {
const urlObj = new URL(`${server}/store/${encodeURIComponent(key)}`);
if (ttl !== null) urlObj.searchParams.append("ttl", ttl.toString());
url = urlObj.toString();
}
// 3. Envoi (POST) avec Fetch
const response = await fetch(url, {
method: "POST",
// Send the encrypted ArrayBuffer as the raw request body.
const response = await axios.post(url.toString(), value, {
headers: {
"Content-Type": "application/octet-stream",
'Content-Type': 'application/octet-stream'
},
body: value,
});
if (response.ok) {
// Status 200-299
console.log("Data stored successfully:", key);
return response;
} else if (response.status === 409) {
// Conflit (déjà existant), on retourne null comme avant
return null;
} else {
console.error("Received response status", response.status);
console.log('Data stored successfully:', key);
if (response.status !== 200) {
console.error('Received response status', response.status);
continue;
}
return response;
} catch (error) {
console.error("Error storing data:", error);
if (error?.response?.status === 409) {
return null;
}
console.error('Error storing data:', error);
}
}
return null;
}
export async function retrieveData(
servers: string[],
key: string
): Promise<ArrayBuffer | null> {
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
for (const server of servers) {
try {
const url = server.startsWith("/")
? `${server}/retrieve/${key}`
: new URL(`${server}/retrieve/${key}`).toString();
console.log("Retrieving data", key, " from:", url);
const response = await fetch(url, { method: "GET" });
if (response.ok) {
// Transformation en ArrayBuffer
return await response.arrayBuffer();
} else {
if (response.status === 404) {
console.log(`Data not found on server ${server} for key ${key}`);
} else {
console.error(
`Server ${server} returned status ${response.status}: ${response.statusText}`
);
}
continue;
// When fetching the data from the server:
const response = await axios.get(`${server}/retrieve/${key}`, {
responseType: 'arraybuffer'
});
if (response.status !== 200) {
console.error('Received response status', response.status);
continue;
}
// console.log('Retrieved data:', response.data);
return response.data;
} catch (error) {
console.error(`Unexpected error retrieving data from ${server}:`, error);
continue;
console.error('Error retrieving data:', error);
}
}
return null;
return null
}
export async function testData(server: string, key: string): Promise<boolean> {
try {
const testUrl = server.startsWith("/")
? `${server}/test/${encodeURIComponent(key)}`
: new URL(`${server}/test/${encodeURIComponent(key)}`).toString();
// On utilise fetch ici aussi
const response = await fetch(testUrl, { method: "GET" });
// 200 OK = existe
return response.status === 200;
} catch (error) {
// Erreur réseau
console.error("Error testing data:", error);
return false;
}
interface TestResponse {
key: string;
value: boolean;
}
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
const res = {};
for (const server of servers) {
res[server] = null;
try {
const response = await axios.get(`${server}/test/${key}`);
if (response.status !== 200) {
console.error(`${server}: Test response status: ${response.status}`);
continue;
}
const data: TestResponse = response.data;
res[server] = data.value;
} catch (error) {
console.error('Error retrieving data:', error);
return null;
}
}
return res;
}

View File

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

View File

@ -1,50 +0,0 @@
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
export interface BackUp {
device: Device;
secrets: SecretsStore;
processes: Record<string, Process>;
}
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
CREATE_PAIRING = 'CREATE_PAIRING',
PAIRING_CREATED = 'PAIRING_CREATED',
ERROR = 'ERROR',
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
RENEW_TOKEN = 'RENEW_TOKEN',
// Get various information
GET_PAIRING_ID = 'GET_PAIRING_ID',
GET_PROCESSES = 'GET_PROCESSES',
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
RETRIEVE_DATA = 'RETRIEVE_DATA',
DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes
CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
VALIDATE_STATE = 'VALIDATE_STATE',
STATE_VALIDATED = 'STATE_VALIDATED',
// Hash and merkle proof
HASH_VALUE = 'HASH_VALUE',
VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}

View File

@ -0,0 +1,4 @@
export function getCorrectDOM(componentTag: string): Node {
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
return dom;
}

53
src/utils/messageMock.ts Executable file
View File

@ -0,0 +1,53 @@
import { messagesMock as initialMessagesMock } from '../mocks/mock-signature/messagesMock.js';
// Store singleton for messages
class MessageStore {
private readonly STORAGE_KEY = 'chat_messages';
private messages: any[] = [];
constructor() {
this.messages = this.loadFromLocalStorage() || [];
}
private loadFromLocalStorage() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Error loading messages:', error);
return null;
}
}
getMessages() {
return this.messages;
}
setMessages(messages: any[]) {
this.messages = messages;
this.saveToLocalStorage();
}
private saveToLocalStorage() {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.messages));
} catch (error) {
console.error('Error saving messages:', error);
}
}
addMessage(memberId: string | number, message: any) {
const memberMessages = this.messages.find((m) => String(m.memberId) === String(memberId));
if (memberMessages) {
memberMessages.messages.push(message);
} else {
this.messages.push({
memberId: String(memberId),
messages: [message],
});
}
this.saveToLocalStorage();
}
}
export const messageStore = new MessageStore();

96
src/utils/notification.store.ts Executable file
View File

@ -0,0 +1,96 @@
interface INotification {
id: number;
title: string;
description: string;
time?: string;
memberId?: string;
}
class NotificationStore {
private static instance: NotificationStore;
private notifications: INotification[] = [];
private constructor() {
this.loadFromLocalStorage();
}
static getInstance(): NotificationStore {
if (!NotificationStore.instance) {
NotificationStore.instance = new NotificationStore();
}
return NotificationStore.instance;
}
addNotification(notification: INotification) {
this.notifications.push(notification);
this.saveToLocalStorage();
this.updateUI();
}
removeNotification(index: number) {
this.notifications.splice(index, 1);
this.saveToLocalStorage();
this.updateUI();
}
getNotifications(): INotification[] {
return this.notifications;
}
private saveToLocalStorage() {
localStorage.setItem('notifications', JSON.stringify(this.notifications));
}
private loadFromLocalStorage() {
const stored = localStorage.getItem('notifications');
if (stored) {
this.notifications = JSON.parse(stored);
}
}
private updateUI() {
const badge = document.querySelector('.notification-badge') as HTMLElement;
const board = document.querySelector('.notification-board') as HTMLElement;
if (badge) {
badge.textContent = this.notifications.length.toString();
badge.style.display = this.notifications.length > 0 ? 'block' : 'none';
}
if (board) {
this.renderNotificationBoard(board);
}
}
private renderNotificationBoard(board: HTMLElement) {
board.innerHTML = '';
if (this.notifications.length === 0) {
board.innerHTML = '<div class="no-notification">No notifications available</div>';
return;
}
this.notifications.forEach((notif, index) => {
const notifElement = document.createElement('div');
notifElement.className = 'notification-item';
notifElement.innerHTML = `
<div>${notif.title}</div>
<div>${notif.description}</div>
${notif.time ? `<div>${notif.time}</div>` : ''}
`;
notifElement.onclick = () => {
if (notif.memberId) {
window.loadMemberChat(notif.memberId);
}
this.removeNotification(index);
};
board.appendChild(notifElement);
});
}
public refreshNotifications() {
this.updateUI();
}
}
export const notificationStore = NotificationStore.getInstance();

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