Compare commits

...

167 Commits

Author SHA1 Message Date
ec99d101ab Merge pull request 'dev' (#8) from dev into cicd
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m8s
Reviewed-on: #8
2025-08-13 10:05:04 +00:00
Sosthene
0dd928d28b Merge branch 'bug_pairing_not_my_processes' into dev 2025-08-08 08:28:24 +02:00
Sosthene
5ba45a29be MyProcesses always include pairing 2025-08-08 08:27:46 +02:00
Sosthene
8541427b87 Merge branch 'fix_missing_human_readable' into dev 2025-08-08 08:25:57 +02:00
7b86318dec Fix error on pairing 2025-07-31 13:28:24 +02:00
omaroughriss
205796d22a Merge remote-tracking branch 'origin/dev' into cicd
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m7s
2025-07-23 13:40:06 +02:00
b072495cea revert 9a601056b70c856366fbb227e75f996c29683358
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m9s
revert Update cicd branche to dev
2025-07-23 11:26:52 +00:00
omaroughriss
9a601056b7 Update cicd branche to dev 2025-07-23 13:22:53 +02:00
Sosthene
d3e207c6da [bug] fix types mismatch with Device 2025-07-16 11:31:29 +02:00
Sosthene
cb5297e6fe Refactoring of handleUpdateProcess to try to commit again processes that have not been commited 2025-07-08 17:24:51 +02:00
Sosthene
f0151fa55e Don't try to batch write to db if objects is empty 2025-07-08 17:24:51 +02:00
Sosthene
5192745a48 Refactor handshake message handling using bach writing 2025-07-08 17:24:50 +02:00
Sosthene
a027004bd0 [bug] Set cache in restoreProcessesFromDb, not Backup 2025-07-08 17:24:14 +02:00
Sosthene
aae11200d4 Add batchSaveProcessesToDb() 2025-07-08 17:24:14 +02:00
Sosthene
dbb7f67154 Don't automatically connect to realys in init() 2025-07-08 17:24:14 +02:00
Sosthene
58fed7a53b use a processCache for optimization 2025-07-08 17:24:14 +02:00
Sosthene
19b2ab994e Batch writes processes at initialization 2025-07-08 17:24:14 +02:00
Sosthene
93d610e942 Add batchWriting() to database 2025-07-08 17:24:14 +02:00
Sosthene
1dad1d4e2b Add BATCH_WRITING to database.worker 2025-07-08 17:24:14 +02:00
Sosthene
5a98fac745 Update our pairing addresses on receiving updates 2025-07-08 17:22:22 +02:00
Sosthene
18d46531a0 Correctly handle states in pairDevice() 2025-07-08 17:22:22 +02:00
Sosthene
62ccfec315 Add GET_MEMBER_ADDRESSES and ADD_DEVICE messages 2025-07-07 15:26:40 +02:00
Sosthene
e9fc0b8454 Rm await on getDeviceAddress() 2025-07-07 15:24:55 +02:00
Sosthene
5119d04243 Don't retry commit message for Not enough members to validate errors 2025-07-07 15:24:15 +02:00
Sosthene
5a8c31df32 Add getUncommitedStates() 2025-07-07 15:23:47 +02:00
Sosthene
deebcefc3d Track states on pairing process 2025-07-07 15:23:05 +02:00
Sosthene
d9b8817ecc Create connections with devices in a pairing process 2025-07-07 15:22:23 +02:00
Sosthene
d8c2b22c3d Rm uneccessary async on decodeValue() 2025-07-07 15:21:25 +02:00
Sosthene
39f24114e1 Rm uneccessary async on getDeviceAddress() 2025-07-07 15:20:00 +02:00
Sosthene
189bd3d252 Don't throw error if unpaired while trying to get my processes 2025-07-04 12:26:11 +02:00
Sosthene
989263d44a handleValidateMerkleProof 2025-07-03 17:56:03 +02:00
Sosthene
7391a08a01 Add VALIDATE_MERKLE_PROOF MessageType 2025-07-03 17:54:36 +02:00
Sosthene
4e109e8fba Add validateMerkleProof 2025-07-03 17:54:07 +02:00
omaroughriss
13b605a850 Update port
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m5s
2025-07-03 11:38:40 +02:00
omaroughriss
0a860bd559 Add CICD
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m3s
2025-07-02 15:40:44 +02:00
omaroughriss
a8b0248b5f Minor updates 2025-07-02 15:39:51 +02:00
omaroughriss
0dc3c83c3c Add a start script 2025-07-02 15:39:32 +02:00
omaroughriss
1a87a4db14 Add nginx config 2025-07-02 15:37:34 +02:00
omaroughriss
67cd7a1662 Add Dockerfile 2025-07-02 15:36:42 +02:00
Sosthene
44f0d8c6c9 [bug] fix rolesContainsMember 2025-07-02 13:50:54 +02:00
Sosthene
10589b056f Solve potential race conditions in dumpStore() 2025-07-02 12:40:44 +02:00
Sosthene
926f41d270 Fix race condition on getMyProcesses 2025-07-02 12:40:44 +02:00
Sosthene
7c39795cef Add HASH_VALUE and GET_MERKLE_PROOF 2025-07-02 12:40:44 +02:00
Sosthene
207b308173 Add getMerkleProofForFile 2025-07-02 12:40:44 +02:00
Sosthene
337a6adc60 Add HASH_VALUE and GET_MERKLE_PROOF 2025-06-30 22:45:50 +02:00
Sosthene
d8422de94e Add getMerkleProofForFile 2025-06-30 22:45:25 +02:00
Sosthene
9edcc2e897 Add HASH and MERKLE MessageType 2025-06-30 19:49:41 +02:00
Sosthene
f5fae245e2 [bug] validateToken was bypassed 2025-06-30 19:49:06 +02:00
ed4fa732f7 Remove redundant log and dead code 2025-06-26 10:52:28 +02:00
ac11893e93 Add messageId to each message, refactor listeners 2025-06-25 16:59:20 +02:00
929e7ee36d [bug] fix updateProcess 2025-06-25 14:40:08 +02:00
c2a4b598a7 Validate token for getProcesses request 2025-06-25 14:39:42 +02:00
2bd2fdff98 Add mock VITE_JWT_SECRET_KEY 2025-06-25 14:39:12 +02:00
13731da7e1 Add getLastCommitedStateIndex 2025-06-25 14:38:52 +02:00
Sosthene
965f5da9a9 Add DECODE_PUBLIC_DATA MessageType 2025-06-24 18:22:10 +02:00
Sosthene
18ef18db71 Add DECODE_PUBLIC_DATA api 2025-06-24 15:52:23 +02:00
Sosthene
50a92995d7 Add decodeValue api 2025-06-23 19:45:21 +02:00
Sosthene
17bdcec317 [bug] Fix access verification in decryptAttribute 2025-06-23 19:45:08 +02:00
Sosthene
25caed410e [bug] checkConnections didn't get the member addresses 2025-06-23 19:44:36 +02:00
Sosthene
cf57681c31 Add handleUpdateProcess in router 2025-06-15 22:17:58 +02:00
Sosthene
91ba7205cc Fix return value of handleDecryptState 2025-06-15 22:17:22 +02:00
Sosthene
d31e18d4ae Use new utils splitPrivateData and isValid32ByteHex 2025-06-15 22:16:32 +02:00
Sosthene
6076c342f8 [bug] Do not request keys when we are not in the role in decryptAttribute 2025-06-15 22:12:22 +02:00
Sosthene
bb5d3ff16d [bug] Correctly encode data in updateProcess 2025-06-15 22:11:02 +02:00
Sosthene
a3fe29e4a0 [bug] Correctly iterate on members addresses in checkConnection 2025-06-15 22:10:25 +02:00
Sosthene
0d51f9d056 [bug] wrong variable name in handleDecryptState 2025-06-15 22:08:41 +02:00
Sosthene
c0d402b234 Add service.utils with splitPrivateData and isValid32ByteHex 2025-06-15 22:07:23 +02:00
Sosthene
dfae77de58 Remove dead code 2025-06-13 21:06:10 +02:00
Sosthene
e1494d5bf4 Reorganise MessageType and remove dead code from models 2025-06-13 21:04:15 +02:00
Sosthene
ed23adf8f1 Rename folderCreated to processData 2025-06-13 20:35:35 +02:00
Sosthene
2a7c0d6675 Add notify and validate api 2025-06-12 17:33:22 +02:00
Sosthene
25dba4e67b minor fixes 2025-06-12 17:26:51 +02:00
Sosthene
65d43686cb Add handleCreateProcess 2025-06-12 14:36:12 +02:00
Sosthene
18e82de549 Minor fixes and refactor in router 2025-06-12 14:36:12 +02:00
f4d8f8652f Profile and Folder event handler return object 2025-06-12 14:36:12 +02:00
39f2b086b5 Make createAndSend{Profile,Folder}Tx returns object 2025-06-12 14:36:12 +02:00
00bc3d8ad2 Add getMyProcesses api 2025-06-12 14:36:12 +02:00
b52ff937f0 Add GET_PAIRING_ID event listener 2025-06-11 15:40:15 +02:00
d6e06f3594 Generate the pdf beside the json file 2025-06-10 13:19:57 +02:00
05f13224fa Add generateProcessPdf to service 2025-06-10 13:18:57 +02:00
06295fe591 Add pdf-lib 2025-06-10 13:18:12 +02:00
72d43210de Add transaction check logic for document validation 2025-06-10 09:30:52 +02:00
73cee5d144 Center buttons and allow file picker 2025-06-09 10:23:49 +02:00
85fe8cc251 Refactor getDocumentValidation 2025-06-06 23:05:28 +02:00
ec9fe0f62c refactor decryptAttribute 2025-06-06 23:05:15 +02:00
b6a2a5fc3b Add getHashForFile 2025-06-06 23:04:57 +02:00
7417aec7e0 [bug] createProfileProcess 2025-06-06 23:04:17 +02:00
f42aca7eb9 createProcess split json and binary data and encode them separately 2025-06-06 23:03:47 +02:00
0f0b5d1af3 Download all encrypted data in separate files on process creation 2025-06-06 23:02:51 +02:00
84aa6298e3 [bug] rm useless variable 2025-06-05 17:23:04 +02:00
14b539595f [bug] wrong number of args to createPairingProcess 2025-06-05 15:42:42 +02:00
99400a71f7 Various bug fixes in Services 2025-06-05 15:42:12 +02:00
c5b58d999f Download the new process in json file 2025-06-05 15:39:33 +02:00
23a3b2a9e8 adding document when creating process takes a fileBlob type 2025-06-05 15:39:10 +02:00
6167d59501 document validation takes json certificates 2025-06-05 15:38:06 +02:00
b828e5197a Correct typing in showProcess 2025-06-05 15:36:51 +02:00
26ba3e6e93 [bug] Check for state_id when looking for a state 2025-06-04 15:38:50 +02:00
df726d929a Add fullscreen mode for modal - Fix css issue 2025-06-04 14:38:10 +02:00
0e44a01218 Add fullscreen mode for modal 2025-06-04 14:24:32 +02:00
8260c6c5da Add some decoding logic in service 2025-06-04 09:14:47 +02:00
8eb6f36b64 Refactor process creation for pairing/profile/folder use cases 2025-06-04 09:14:47 +02:00
e15da5c22a createProcess does encoding before creating a new process 2025-06-04 09:14:47 +02:00
a8b3631dc1 [bug] strict equality for address comparison 2025-06-04 09:14:47 +02:00
89e9b3e4e0 Add Process tab 2025-05-22 22:35:26 +02:00
c4db22f626 [bug] Actually pass the file object to wasm as bytes 2025-05-22 22:35:26 +02:00
accd427cab Add RoleDefintion type to role 2025-05-22 15:39:05 +02:00
381dcdf7a8 Update getProcesses iframe handler 2025-05-22 15:38:10 +02:00
0cbc07cf63 Update addFolder iframe handler 2025-05-22 14:37:43 +02:00
3c59105aa6 Add getProcesses iframe handler 2025-05-22 14:37:21 +02:00
325d2cbf13 Upodate processes creation and minor fixes 2025-05-22 14:36:19 +02:00
d4f1f36376 Add upload file button on create process screen 2025-05-22 11:20:22 +02:00
f6edadc535 Comment out fn (maybe use it in lecoffre stack) 2025-05-21 12:08:02 +02:00
0099a8c858 Minor changes 2025-05-21 12:00:10 +02:00
0e0c3946d2 Update tjwt logic to use refresh token 2025-05-21 11:58:16 +02:00
0a2a2674f8 Add jose jwt dependencies 2025-05-21 11:54:37 +02:00
9d461d63d7 Delete dead code 2025-05-21 11:53:06 +02:00
2f68c652dd Remove events listeners before adding, prevent duplication 2025-05-20 17:51:39 +02:00
147f4cfa7d register events listeners only if we're in an iframe 2025-05-20 17:51:00 +02:00
235aecd6a7 don't await service worker registering 2025-05-20 17:50:34 +02:00
e1f2483924 [bug] missing await on home 2025-05-20 17:50:10 +02:00
0c2df347ec Remove redundant event listeners adding in home 2025-05-20 17:49:53 +02:00
abfe581f29 Add document validation interface (mocked for now) 2025-05-20 17:49:14 +02:00
b66ee42ddd Add document validation logic 2025-05-20 17:48:50 +02:00
aecdcd93e1 Fix showQrCodeModal 2025-05-20 17:48:32 +02:00
c63e2a6fe9 Add missing async in addRowPairing 2025-05-20 17:47:16 +02:00
67963bfb02 Add process creation tab 2025-05-20 17:45:28 +02:00
4b12b560e1 Remove useless code 2025-05-20 17:44:22 +02:00
28c151254c [bug] missing await in getProcessId 2025-05-20 17:43:45 +02:00
5d0c617bbb Add createProcess() service method 2025-05-20 17:42:26 +02:00
ae88959496 Add process creation logic 2025-05-20 17:41:51 +02:00
e5a958b0b9 Add validation rule modal 2025-05-20 17:41:23 +02:00
6b77ec2972 [bug] Outdated api call in connectAddresses + don't connect with ourselves 2025-05-20 15:06:14 +02:00
a1ce472cad Update messages types 2025-05-06 16:52:48 +02:00
db48386f05 Minor updates 2025-05-06 16:51:21 +02:00
39b50d6789 Update handleRenewToken 2025-05-06 16:50:48 +02:00
86393e6cfa Handle token validation messages 2025-05-06 16:50:17 +02:00
bf06b6634a Update handleRequestLink 2025-05-06 16:47:56 +02:00
cfc9514656 Add refresh token 2025-05-06 16:44:40 +02:00
0f364c7c6e Update to use jose tokens library 2025-05-06 16:43:51 +02:00
ee7c79a7d5 Update css 2025-04-27 16:42:18 +02:00
37bdb3dad3 Add showConfirmationModal 2025-04-27 16:42:01 +02:00
ecba13594b Add tokenService 2025-04-27 16:39:55 +02:00
4c534973d2 [bug] fix rolesContainsMember 2025-04-27 16:39:18 +02:00
eca4d4de85 Replace handleSource by registeringAllListeners 2025-04-27 16:38:35 +02:00
824a0b88f6 handleSource link-service 2025-04-04 16:35:12 +02:00
e224921f86 handle partial_tx in handleApiReturn 2025-04-04 12:53:12 +02:00
cf18e46e17 getTokensFromFaucet 2025-04-04 12:52:32 +02:00
e6cf1c3658 Remove pairing from url params 2025-04-04 12:51:12 +02:00
b9851c587e Registering service worker must be called beside db initialisation 2025-03-31 15:52:34 +02:00
d601d94bf6 Export Database 2025-03-31 15:04:10 +02:00
d19ba72b4a build input is index.ts 2025-03-31 15:03:58 +02:00
2d0e15533a Build boilerplate 2025-03-28 12:53:24 +01:00
94ee8842e3 export Services from index.ts 2025-03-28 12:52:59 +01:00
7b7d13ce6c Various fix to build again the project 2025-03-28 12:52:47 +01:00
cc9396c4b8 Neutralize chat page 2025-03-28 12:52:00 +01:00
51c906866e Neutralize process page 2025-03-28 12:51:12 +01:00
3f42cb27a7 Slight refactoring and cleanup 2025-03-26 14:44:00 +01:00
2601418aaf pass membersList as argument when needed 2025-03-26 14:43:27 +01:00
455fe53fe2 getRoles and getPublicData both returns value in the first uncommited state if necessary 2025-03-26 14:41:35 +01:00
2f847514f0 Refactor getLastCommitedState 2025-03-26 14:41:00 +01:00
a0888f8c90 handleHandshakeMsg check we're part of a new state before adding it 2025-03-26 14:40:41 +01:00
f2e2aeaa9a One device pairing 2025-03-26 14:39:59 +01:00
2855365851 Don't call openPairingConfirmationModal from handleApiReturn 2025-03-26 13:42:31 +01:00
bb277706fd load myprocesses ok 2025-03-24 17:02:24 +01:00
05dddd9567 new member name service ok 2025-03-21 10:47:18 +01:00
d54ce71f02 delete device mocked ok 2025-03-19 16:22:44 +01:00
a42141246d load pairing device ok 2025-03-19 16:09:12 +01:00
42 changed files with 3882 additions and 1245 deletions

1
.env
View File

@ -1,3 +1,4 @@
# .env
VITE_API_URL=https://api.example.com
VITE_API_KEY=your_api_key
VITE_JWT_SECRET_KEY="your_secret_key"

43
.github/workflows/cicd.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Build and Push to Registry
on:
push:
branches: [ cicd ]
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 }}:${{ gitea.sha }}

View File

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

48
nginx.dev.conf Normal file
View File

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

184
package-lock.json generated
View File

@ -10,11 +10,15 @@
"license": "ISC",
"dependencies": {
"@angular/elements": "^19.0.1",
"@types/jsonwebtoken": "^9.0.9",
"@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",
"jose": "^6.0.11",
"jsonwebtoken": "^9.0.2",
"pdf-lib": "^1.17.1",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.3",
"sweetalert2": "^11.14.5",
@ -933,6 +937,24 @@
"node": ">= 8"
}
},
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1391,12 +1413,28 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.14.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
@ -2120,6 +2158,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -2827,6 +2871,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4264,6 +4317,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jose": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -4314,6 +4376,61 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -4359,6 +4476,48 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@ -4772,6 +4931,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@ -4873,6 +5038,24 @@
"resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz",
"integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw=="
},
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/pdf-lib/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@ -5364,7 +5547,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",

View File

@ -5,11 +5,12 @@
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev1/pkg ../sdk_client --target bundler --dev",
"build_wasm": "wasm-pack build --out-dir ../ihm_client/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0",
"build": "tsc && vite build",
"deploy": "sudo cp -r dist/* /var/www/html/",
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\""
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
"build:dist": "tsc -p tsconfig.build.json"
},
"keywords": [],
"author": "",
@ -30,11 +31,15 @@
},
"dependencies": {
"@angular/elements": "^19.0.1",
"@types/jsonwebtoken": "^9.0.9",
"@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",
"jose": "^6.0.11",
"jsonwebtoken": "^9.0.2",
"pdf-lib": "^1.17.1",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.3",
"sweetalert2": "^11.14.5",

View File

@ -77,6 +77,99 @@ body {
position: relative;
}
/* Confirmation Modal Styles */
#confirmation-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.modal-confirmation {
text-align: left;
padding: 10px;
}
.modal-confirmation h3 {
margin-bottom: 15px;
color: var(--primary-color);
font-size: 1.1em;
}
.modal-confirmation p {
margin: 8px 0;
font-size: 0.9em;
line-height: 1.4;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.modal-footer button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.9em;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-secondary {
background: var(--secondary-color);
color: white;
}
/* Responsive adjustments */
@media only screen and (max-width: 600px) {
.modal-content {
width: 95%;
margin: 10px;
padding: 15px;
}
.modal-confirmation h3 {
font-size: 1em;
}
.modal-confirmation p {
font-size: 0.85em;
}
}
.nav-wrapper {
position: fixed;

View File

@ -427,24 +427,43 @@ body {
/* Style pour la modal de confirmation */
.confirm-delete-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.confirm-delete-content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1100;
max-width: 400px;
width: 90%;
text-align: center;
}
.confirm-delete-content h3 {
margin-top: 0;
color: #333;
}
.confirm-delete-content p {
margin: 15px 0;
color: #666;
}
.confirm-delete-buttons {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.confirm-delete-buttons button {
@ -452,25 +471,27 @@ body {
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.confirm-btn {
.confirm-delete-buttons .confirm-btn {
background-color: #dc3545;
color: white;
}
.cancel-btn {
.confirm-delete-buttons .confirm-btn:hover {
background-color: #c82333;
}
.confirm-delete-buttons .cancel-btn {
background-color: #6c757d;
color: white;
}
.delete-account-section {
border-top: 1px solid #eee;
padding-top: 15px;
margin-top: 15px;
.confirm-delete-buttons .cancel-btn:hover {
background-color: #5a6268;
}
/*-------------------------------------- Export--------------------------------------*/
.export-section {
margin: 20px 0;
@ -1423,3 +1444,64 @@ body {
font-size: 12px;
color: #666;
}
.pairing-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.pairing-modal-content {
background-color: white;
padding: 2rem;
border-radius: 8px;
width: 90%;
max-width: 500px;
}
.pairing-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: bold;
}
.button-group {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1rem;
}
.button-group button {
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.confirm-button {
background-color: var(--accent-color);
color: white;
border: none;
}
.cancel-button {
background-color: #ccc;
border: none;
}

View File

@ -9,7 +9,7 @@ let notifications = [];
export async function unpair() {
const service = await Services.getInstance();
await service.unpairDevice();
navigate('home');
await navigate('home');
}
(window as any).unpair = unpair;
@ -28,7 +28,7 @@ function toggleMenu() {
async function getNotifications() {
const service = await Services.getInstance();
notifications = service.getNotifications();
notifications = service.getNotifications() || [];
return notifications;
}
function openCloseNotifications() {
@ -102,8 +102,8 @@ async function setNotification(notifications: any[]): Promise<void> {
async function fetchNotifications() {
const service = await Services.getInstance();
const data = service.getNotifications();
setNotification(data);
const data = service.getNotifications() || [];
await setNotification(data);
}
async function loadUserProfile() {
@ -204,7 +204,7 @@ async function disconnect() {
await Promise.all(registrations.map(registration => registration.unregister()));
console.log('Service worker unregistered');
navigate('home');
await navigate('home');
setTimeout(() => {
window.location.href = window.location.origin;

View File

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

View File

@ -55,7 +55,7 @@ export default class QrScannerComponent extends HTMLElement {
if (spAddress) {
// Call the sendPairingTx function with the extracted sp_address
try {
await prepareAndSendPairingTx(spAddress);
await prepareAndSendPairingTx();
} catch (e) {
console.error('Failed to pair:', e);
}

View File

@ -0,0 +1,42 @@
<div id="validation-rule-modal" style="
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
">
<div style="
background: white;
padding: 2rem;
border-radius: 0.5rem;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
">
<h2 style="font-size: 1.2rem; font-weight: bold; margin-bottom: 1rem;">
Add Validation Rule
</h2>
<label style="display: block; margin-bottom: 0.5rem;">
Quorum:
<input id="vr-quorum" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<label style="display: block; margin-bottom: 0.5rem;">
Min Sig Member:
<input id="vr-minsig" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<label style="display: block; margin-bottom: 1rem;">
Fields (comma-separated):
<input id="vr-fields" type="text" placeholder="e.g. field1, field2" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
</label>
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
<button id="vr-cancel" style="padding: 0.5rem 1rem;">Cancel</button>
<button id="vr-submit" style="padding: 0.5rem 1rem; background-color: #4f46e5; color: white; border: none; border-radius: 0.375rem;">Add</button>
</div>
</div>
</div>

View File

@ -0,0 +1,61 @@
export interface ValidationRule {
quorum: number;
fields: string[];
min_sig_member: number;
}
/**
* Loads and injects the modal HTML into the document if not already loaded.
*/
export async function loadValidationRuleModal(templatePath: string = '/src/components/validation-rule-modal/validation-rule-modal.html') {
if (document.getElementById('validation-rule-modal')) return;
const res = await fetch(templatePath);
const html = await res.text();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const modal = tempDiv.querySelector('#validation-rule-modal');
if (!modal) {
throw new Error('Modal HTML missing #validation-rule-modal');
}
document.body.appendChild(modal);
}
/**
* Opens the modal and lets the user input a ValidationRule.
* Calls the callback with the constructed rule on submit.
*/
export function showValidationRuleModal(onSubmit: (rule: ValidationRule) => void) {
const modal = document.getElementById('validation-rule-modal')!;
const quorumInput = document.getElementById('vr-quorum') as HTMLInputElement;
const minsigInput = document.getElementById('vr-minsig') as HTMLInputElement;
const fieldsInput = document.getElementById('vr-fields') as HTMLInputElement;
const cancelBtn = document.getElementById('vr-cancel')!;
const submitBtn = document.getElementById('vr-submit')!;
// Reset values
quorumInput.value = '';
minsigInput.value = '';
fieldsInput.value = '';
modal.style.display = 'flex';
cancelBtn.onclick = () => {
modal.style.display = 'none';
};
submitBtn.onclick = () => {
const rule: ValidationRule = {
quorum: parseInt(quorumInput.value),
min_sig_member: parseInt(minsigInput.value),
fields: fieldsInput.value.split(',').map(f => f.trim()).filter(Boolean),
};
modal.style.display = 'none';
onSubmit(rule);
};
}

View File

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

View File

@ -1,18 +1,18 @@
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 { 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 };
export { SignatureComponent, SignatureElement, AccountComponent, AccountElement };
declare global {
interface HTMLElementTagNameMap {
'signature-component': SignatureComponent;
'signature-element': SignatureElement;
'chat-component': ChatComponent;
'chat-element': ChatElement;
/*'chat-component': ChatComponent;
'chat-element': ChatElement;*/
'account-component': AccountComponent;
'account-element': AccountElement;
}
@ -23,8 +23,8 @@ 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('chat-component', ChatComponent);
customElements.define('chat-element', ChatElement);*/
customElements.define('account-component', AccountComponent);
customElements.define('account-element', AccountElement);
}

View File

@ -21,3 +21,43 @@ export interface INotification {
sendToNotificationPage?: boolean;
path?: string;
}
export enum MessageType {
// Establish connection and keep alive
LISTENING = 'LISTENING',
REQUEST_LINK = 'REQUEST_LINK',
LINK_ACCEPTED = 'LINK_ACCEPTED',
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

@ -20,23 +20,25 @@ declare global {
updateNavbarBanner: (bannerUrl: string) => void;
saveBannerToLocalStorage: (bannerUrl: string) => void;
loadSavedBanner: () => void;
cancelAddRow: () => void;
cancelAddRowPairing: () => void;
saveName: (cell: HTMLElement, input: HTMLInputElement) => void;
showProcessNotifications: (processName: string) => void;
handleLogout: () => void;
initializeEventListeners: () => void;
showProcess: () => void;
showProcessCreation: () => void;
showDocumentValidation: () => void;
updateNavbarName: (name: string) => void;
updateNavbarLastName: (lastName: string) => void;
showAlert: (title: string, text?: string, icon?: string) => void;
addRow: () => void;
confirmRow: () => void;
cancelRow: () => void;
deleteRow: (button: HTMLButtonElement) => void;
addRowPairing: () => void;
confirmRowPairing: () => void;
cancelRowPairing: () => void;
deleteRowPairing: (button: HTMLButtonElement) => void;
generateRecoveryWords: () => string[];
exportUserData: () => void;
updateActionButtons: () => void;
showQRCodeModal: (address: string) => void;
showQRCodeModal: (pairingId: string) => void;
}
}
@ -47,11 +49,31 @@ import { addressToEmoji } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/document.utils';
import accountStyle from '../../../public/style/account.css?inline';
import Services from '../../services/service';
import { getProcessCreation } from './process-creation';
import { getDocumentValidation } from './document-validation';
import { createProcessTab } from './process';
let isAddingRow = false;
let currentRow: HTMLTableRowElement | null = null;
let currentMode: keyof typeof STORAGE_KEYS = 'pairing';
interface Process {
states: Array<{
committed_in: string;
keys: {};
pcd_commitment: {
counter: string;
};
public_data: {
memberPublicName?: string;
};
roles: {
pairing?: {};
};
state_id: string;
validation_tokens: Array<any>;
}>;
}
class AccountElement extends HTMLElement {
private dom: Node;
@ -141,7 +163,7 @@ class AccountElement extends HTMLElement {
<!-- User Info Section -->
<div class="popup-info">
<p><strong>Name:</strong> <span class="editable" id="popup-name"></span></p>
<p><strong>Last Name:</strong> <span class="editable" id="popup-lastname"></span></p>
<!--<p><strong>Last Name:</strong> <span class="editable" id="popup-lastname"></span></p>-->
<p><strong>Address:</strong> 🏠 🌍 🗽🎊😩-🎊😑🎄😩</p>
</div>
@ -154,18 +176,22 @@ class AccountElement extends HTMLElement {
</ul>
<ul class="parameter-list-ul" onclick="window.showPairing()">Pairing 🔗</ul>
<ul class="parameter-list-ul" onclick="window.showWallet()">Wallet 👛</ul>
<!-- <ul class="parameter-list-ul" onclick="window.showWallet()">Wallet 👛</ul> -->
<ul class="parameter-list-ul" onclick="window.showProcess()">Process </ul>
<ul class="parameter-list-ul" onclick="window.showData()">Data 💾</ul>
<ul class="parameter-list-ul" onclick="window.showProcessCreation()">Process Creation</ul>
<ul class="parameter-list-ul" onclick="window.showDocumentValidation()">Document Validation</ul>
<!-- <ul class="parameter-list-ul" onclick="window.showData()">Data 💾</ul> -->
</div>
<!-- Parameter Area -->
<div class="parameter-area">
<div class="content-container">
<div id="pairing-content"></div>
<div id="wallet-content"></div>
<!-- <div id="wallet-content"></div> -->
<div id="process-content"></div>
<div id="data-content"></div>
<div id="process-creation-content"></div>
<div id="document-validation-content"></div>
<!-- <div id="data-content"></div> -->
</div>
</div>
</div>
@ -175,6 +201,8 @@ class AccountElement extends HTMLElement {
window.showPairing = () => this.showPairing();
window.showWallet = () => this.showWallet();
window.showProcess = () => this.showProcess();
window.showProcessCreation = () => this.showProcessCreation();
window.showDocumentValidation = () => this.showDocumentValidation();
window.showData = () => this.showData();
window.addWalletRow = () => this.addWalletRow();
window.confirmWalletRow = () => this.confirmWalletRow();
@ -184,10 +212,10 @@ class AccountElement extends HTMLElement {
window.handleLogout = () => this.handleLogout();
window.confirmDeleteAccount = () => this.confirmDeleteAccount();
window.showContractPopup = (contractId: string) => this.showContractPopup(contractId);
window.addRow = () => this.addRow();
window.deleteRow = (button: HTMLButtonElement) => this.deleteRow(button);
window.confirmRow = () => this.confirmRow();
window.cancelRow = () => this.cancelRow();
window.addRowPairing = () => this.addRowPairing();
window.deleteRowPairing = (button: HTMLButtonElement) => this.deleteRowPairing(button);
window.confirmRowPairing = () => this.confirmRowPairing();
window.cancelRowPairing = () => this.cancelRowPairing();
window.updateNavbarBanner = (bannerUrl: string) => this.updateNavbarBanner(bannerUrl);
window.saveBannerToLocalStorage = (bannerUrl: string) => this.saveBannerToLocalStorage(bannerUrl);
window.loadSavedBanner = () => this.loadSavedBanner();
@ -199,7 +227,7 @@ class AccountElement extends HTMLElement {
window.updateActionButtons = () => this.updateActionButtons();
window.openAvatarPopup = () => this.openAvatarPopup();
window.closeAvatarPopup = () => this.closeAvatarPopup();
window.showQRCodeModal = (address: string) => this.showQRCodeModal(address);
window.showQRCodeModal = (pairingId: string) => this.showQRCodeModal(pairingId);
if (!localStorage.getItem('rows')) {
localStorage.setItem('rows', JSON.stringify(defaultRows));
@ -488,7 +516,7 @@ private getConfirmFunction(): string {
case 'process':
return 'window.confirmProcessRow()';
default:
return 'window.confirmRow()';
return 'window.confirmRowPairing()';
}
}
@ -499,50 +527,94 @@ private getCancelFunction(): string {
case 'process':
return 'window.cancelProcessRow()';
default:
return 'window.cancelRow()';
return 'window.cancelRowPairing()';
}
}
// Fonctions de gestion des tableaux
private addRow(): void {
private async addRowPairing(): Promise<void> {
if (isAddingRow) return;
isAddingRow = true;
const table = this.shadowRoot?.querySelector<HTMLTableElement>('#pairing-table tbody');
if (!table) return;
currentRow = table.insertRow();
const cells = ['SP Address', 'Device Name', 'SP Emojis'];
// Créer la popup
const modal = document.createElement('div');
modal.className = 'pairing-modal';
modal.innerHTML = `
<div class="pairing-modal-content">
<h3>Add New Device</h3>
<div class="pairing-form">
<div class="form-group">
<label for="sp-address">SP Address</label>
<input type="text" id="sp-address" class="edit-input" placeholder="Enter SP Address">
</div>
<div class="form-group">
<label for="device-name">Device Name</label>
<input type="text" id="device-name" class="edit-input" placeholder="Enter Device Name">
</div>
<div class="form-group">
<label for="sp-emojis">SP Emojis</label>
<input type="text" id="sp-emojis" class="edit-input" readonly>
</div>
<div class="button-group">
<button class="confirm-button">Confirm</button>
<button class="cancel-button">Cancel</button>
</div>
</div>
</div>
`;
cells.forEach((_, index) => {
const cell = currentRow!.insertCell();
const input = document.createElement('input');
input.type = 'text';
input.className = 'edit-input';
this.shadowRoot?.appendChild(modal);
// Ajouter un événement pour mettre à jour automatiquement les emojis
if (index === 0) {
input.addEventListener('input', async (e) => {
const addressInput = e.target as HTMLInputElement;
const emojiCell = currentRow!.cells[2];
const emojis = await addressToEmoji(addressInput.value);
if (emojiCell.querySelector('input')) {
(emojiCell.querySelector('input') as HTMLInputElement).value = emojis;
}
});
}
// Ajouter les event listeners
const spAddressInput = modal.querySelector('#sp-address') as HTMLInputElement;
const spEmojisInput = modal.querySelector('#sp-emojis') as HTMLInputElement;
const deviceNameInput = modal.querySelector('#device-name') as HTMLInputElement;
const confirmButton = modal.querySelector('.confirm-button');
const cancelButton = modal.querySelector('.cancel-button');
if (index === 2) {
input.readOnly = true;
}
cell.appendChild(input);
// Mettre à jour les emojis automatiquement
spAddressInput?.addEventListener('input', async () => {
const emojis = await addressToEmoji(spAddressInput.value);
if (spEmojisInput) spEmojisInput.value = emojis;
});
const deleteCell = currentRow.insertCell();
deleteCell.style.width = '40px';
// Gérer la confirmation
confirmButton?.addEventListener('click', () => {
const spAddress = spAddressInput?.value.trim();
const deviceName = deviceNameInput?.value.trim();
const spEmojis = spEmojisInput?.value.trim();
this.updateActionButtons();
if (!spAddress || !deviceName) {
this.showAlert('Please fill in all required fields');
return;
}
//if (spAddress.length !== 118) {
// this.showAlert('SP Address must be exactly 118 characters long');
// return;
//}
const newRow: Row = {
column1: spAddress,
column2: deviceName,
column3: spEmojis || ''
};
const storageKey = STORAGE_KEYS[currentMode];
const rows: Row[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
rows.push(newRow);
localStorage.setItem(storageKey, JSON.stringify(rows));
this.updateTableContent(rows);
modal.remove();
isAddingRow = false;
});
// Gérer l'annulation
cancelButton?.addEventListener('click', () => {
modal.remove();
isAddingRow = false;
});
}
// Fonctions de mise à jour de l'interface
@ -552,7 +624,6 @@ private updateTableContent(rows: Row[]): void {
tbody.innerHTML = rows.map(row => `
<tr>
<td>${row.column1}</td>
<td class="device-name" onclick="window.editDeviceName(this)">${row.column2}</td>
<td>${row.column3}</td>
<td>
@ -563,7 +634,7 @@ private updateTableContent(rows: Row[]): void {
onclick="window.showQRCodeModal('${encodeURIComponent(row.column1)}')">
</td>
<td>
<button class="delete-button" onclick="window.deleteRow(this)">
<button class="delete-button" onclick="window.deleteRowPairing(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill="red">
<path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
</svg>
@ -575,7 +646,7 @@ private updateTableContent(rows: Row[]): void {
private confirmRow(): void {
private confirmRowPairing(): void {
if (!currentRow) return;
const inputs = currentRow.getElementsByTagName('input');
@ -611,7 +682,7 @@ private confirmRow(): void {
this.updateTableContent(rows);
}
private cancelRow(): void {
private cancelRowPairing(): void {
if (!currentRow) return;
currentRow.remove();
@ -626,11 +697,11 @@ private resetButtonContainer(): void {
if (!buttonContainer) return;
buttonContainer.innerHTML = `
<button class="add-row-button button-style" onclick="window.addRow()">Add a line</button>
<button class="add-row-button button-style" onclick="window.addRowPairing()">Add a line</button>
`;
}
private deleteRow(button: HTMLButtonElement): void {
private deleteRowPairing(button: HTMLButtonElement): void {
const row = button.closest('tr');
if (!row) return;
@ -643,21 +714,53 @@ private deleteRow(button: HTMLButtonElement): void {
return;
}
const index = Array.from(table.children).indexOf(row);
row.style.transition = 'opacity 0.3s, transform 0.3s';
row.style.opacity = '0';
row.style.transform = 'translateX(-100%)';
// Créer la modal de confirmation
const modal = document.createElement('div');
modal.className = 'confirm-delete-modal';
modal.innerHTML = `
<div class="confirm-delete-content">
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this device?</p>
<div class="confirm-delete-buttons">
<button class="cancel-btn">Cancel</button>
<button class="confirm-btn">Delete</button>
</div>
</div>
`;
setTimeout(() => {
row.remove();
this.shadowRoot?.appendChild(modal);
// Gérer les boutons de la modal
const confirmBtn = modal.querySelector('.confirm-btn');
const cancelBtn = modal.querySelector('.cancel-btn');
confirmBtn?.addEventListener('click', () => {
// Calculer l'index AVANT de supprimer la ligne du DOM
const index = Array.from(table.children).indexOf(row);
const storageKey = STORAGE_KEYS[currentMode];
const rows = JSON.parse(localStorage.getItem(storageKey) || '[]');
// Supprimer du localStorage
if (index > -1) {
rows.splice(index, 1);
localStorage.setItem(storageKey, JSON.stringify(rows));
}
}, 300);
// Animation et suppression du DOM
row.style.transition = 'opacity 0.3s, transform 0.3s';
row.style.opacity = '0';
row.style.transform = 'translateX(-100%)';
setTimeout(() => {
row.remove();
}, 300);
modal.remove();
});
cancelBtn?.addEventListener('click', () => {
modal.remove();
});
}
private editDeviceName(cell: HTMLTableCellElement): void {
@ -682,7 +785,7 @@ private editDeviceName(cell: HTMLTableCellElement): void {
input.focus();
}
private finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): void {
private async finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): Promise<void> {
const newValue = input.value.trim();
if (newValue === '') {
cell.textContent = cell.getAttribute('data-original-value') || '';
@ -690,24 +793,25 @@ private finishEditing(cell: HTMLTableCellElement, input: HTMLInputElement): void
return;
}
const row = cell.closest('tr');
if (!row) return;
try {
const service = await Services.getInstance();
const pairingProcessId = service.getPairingProcessId();
const process = await service.getProcess(pairingProcessId);
const table = row.closest('tbody');
if (!table) return;
// Mettre à jour le nom via le service
await service.updateMemberPublicName(process, newValue);
const index = Array.from(table.children).indexOf(row);
const storageKey = STORAGE_KEYS[currentMode];
const rows: Row[] = JSON.parse(localStorage.getItem(storageKey) || '[]');
if (rows[index]) {
rows[index].column2 = newValue;
localStorage.setItem(storageKey, JSON.stringify(rows));
// Mettre à jour l'interface
cell.textContent = newValue;
cell.classList.remove('editing');
} catch (error) {
console.error('Failed to update name:', error);
// Restaurer l'ancienne valeur en cas d'erreur
cell.textContent = cell.getAttribute('data-original-value') || '';
cell.classList.remove('editing');
}
cell.textContent = newValue;
cell.classList.remove('editing');
}
// Fonction pour gérer le téléchargement de l'avatar
private handleAvatarUpload(event: Event): void {
const input = event.target as HTMLInputElement;
@ -729,63 +833,62 @@ private handleAvatarUpload(event: Event): void {
}
}
private showProcess(): void {
//console.log("showProcess called");
currentMode = 'process';
private async showProcessCreation(): Promise<void> {
this.hideAllContent();
const headerTitle = this.shadowRoot?.getElementById('header-title');
if (headerTitle) headerTitle.textContent = 'Process';
const processContent = this.shadowRoot?.getElementById('process-content');
if (processContent) {
processContent.style.display = 'block';
processContent.innerHTML = `
<div class="parameter-header" id="parameter-header">Process</div>
<div class="table-container">
<table class="parameter-table" id="process-table">
<thead>
<tr>
<th>Process Name</th>
<th>Role</th>
<th>Notifications</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`;
this.updateProcessTableContent(mockProcessRows);
const container = this.shadowRoot?.getElementById('process-creation-content');
if (container) {
getProcessCreation(container);
}
}
// Fonction utilitaire pour mettre à jour le contenu du tableau Process
private updateProcessTableContent(rows: any[]): void {
const tbody = this.shadowRoot?.querySelector('#process-table tbody');
if (!tbody) return;
tbody.innerHTML = rows.map(row => `
<tr>
<td>${row.process}</td>
<td>${row.role}</td>
<td>
<div class="notification-container" onclick="window.showProcessNotifications('${row.process}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16">
<path d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416H416c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3H160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"/>
</svg>
<span class="notification-count" data-process="${row.process}">
${row.notification?.messages?.filter((m: any) => !m.read).length || 0}/${row.notification?.messages?.length || 0}
</span>
</div>
</td>
</tr>
`).join('');
private async showDocumentValidation(): Promise<void> {
this.hideAllContent();
const container = this.shadowRoot?.getElementById('document-validation-content');
if (container) {
getDocumentValidation(container);
}
}
private async showProcess(): Promise<void> {
this.hideAllContent();
const container = this.shadowRoot?.getElementById('process-content');
if (container) {
const service = await Services.getInstance();
const myProcesses = await service.getMyProcesses();
if (myProcesses && myProcesses.length != 0) {
const myProcessesDataUnfiltered: { name: string, publicData: Record<string, any> }[] = await Promise.all(myProcesses.map(async processId => {
const process = await service.getProcess(processId);
const lastState = service.getLastCommitedState(process);
if (!lastState) {
return {
name: '',
publicData: {}
};
}
const description = await service.decryptAttribute(processId, lastState, 'description');
const name = description ? description : 'N/A';
const publicData = await service.getPublicData(process);
if (!publicData) {
return {
name: '',
publicData: {}
};
}
return {
name: name,
publicData: publicData
};
}));
const myProcessesData = myProcessesDataUnfiltered.filter(
(p) => p.name !== '' && Object.keys(p.publicData).length != 0
);
createProcessTab(container, myProcessesData);
} else {
createProcessTab(container, []);
}
}
}
private showProcessNotifications(processName: string): void {
const process = mockProcessRows.find(p => p.process === processName);
@ -852,9 +955,11 @@ private handleLogout(): void {
// Fonctions de gestion des contrats
private showContractPopup(contractId: string) {
// Empêcher la navigation par défaut
event?.preventDefault();
private showContractPopup(contractId: string, event?: Event) {
if (event) {
event.preventDefault();
}
// Check if the contract exists in mockContracts
const contract = mockContracts[contractId as keyof typeof mockContracts];
if (!contract) {
@ -862,7 +967,6 @@ private showContractPopup(contractId: string) {
return;
}
// Créer la popup
const popup = document.createElement('div');
popup.className = 'contract-popup-overlay';
popup.innerHTML = `
@ -881,10 +985,8 @@ private showContractPopup(contractId: string) {
</div>
`;
// Ajouter la popup au body
this.shadowRoot?.appendChild(popup);
// Gérer la fermeture
const closeBtn = popup.querySelector('.close-contract-popup');
const closePopup = () => popup.remove();
@ -896,7 +998,7 @@ private showContractPopup(contractId: string) {
// Fonction utilitaire pour cacher tous les contenus
private hideAllContent(): void {
const contents = ['pairing-content', 'wallet-content', 'process-content', 'data-content'];
const contents = ['pairing-content', 'wallet-content', 'process-content', 'process-creation-content', 'data-content', 'document-validation-content'];
contents.forEach(id => {
const element = this.shadowRoot?.getElementById(id);
if (element) {
@ -930,7 +1032,6 @@ private async showPairing(): Promise<void> {
<table class="parameter-table" id="pairing-table">
<thead>
<tr>
<th>SP Address</th>
<th>Device Name</th>
<th>SP Emojis</th>
<th>QR Code</th>
@ -940,7 +1041,7 @@ private async showPairing(): Promise<void> {
<tbody></tbody>
</table>
<div class="button-container">
<button class="add-row-button button-style" onclick="window.addRow()">Add a line</button>
<button class="add-row-button button-style" onclick="window.addRowPairing()">Add a device</button>
</div>
</div>
`;
@ -960,8 +1061,7 @@ private async showPairing(): Promise<void> {
const pairingProcess = await service.getProcess(pairingProcessId);
console.log('Pairing Process:', pairingProcess);
const userName = pairingProcess?.states?.[0]?.metadata?.userName
|| pairingProcess?.states?.[0]?.metadata?.name
const userName = pairingProcess?.states?.[0]?.public_data?.memberPublicName
|| localStorage.getItem('userName')
console.log('Username found:', userName);
@ -1209,10 +1309,10 @@ private openAvatarPopup(): void {
<strong>Name:</strong>
<input type="text" id="userName" value="${savedName}" class="editable">
</div>
<div class="info-row">
<!--<div class="info-row">
<strong>Last Name:</strong>
<input type="text" id="userLastName" value="${savedLastName}" class="editable">
</div>
</div>-->
<div class="info-row">
<strong>Address:</strong>
<span>${savedAddress}</span>
@ -1460,16 +1560,16 @@ private initializeEventListeners() {
}
}
private showQRCodeModal(address: string): void {
private showQRCodeModal(pairingId: string): void {
const modal = document.createElement('div');
modal.className = 'qr-modal';
modal.innerHTML = `
<div class="qr-modal-content">
<span class="close-qr-modal">&times;</span>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${address}"
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${pairingId}"
alt="QR Code Large"
class="qr-code-large">
<div class="qr-address">${decodeURIComponent(address)}</div>
<div class="qr-address">${decodeURIComponent(pairingId)}</div>
</div>
`;

View File

@ -0,0 +1,321 @@
import { ProcessState } from '../../../pkg/sdk_client';
import Services from '../../services/service';
interface State {
file: File | null;
fileHash: string | null;
certificate: ProcessState | null;
commitmentHashes: string[];
}
export interface Vin {
txid: string; // The txid of the previous transaction (being spent)
vout: number; // The output index in the previous tx
prevout: {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
value: number;
};
scriptsig: string;
scriptsig_asm: string;
witness: string[];
is_coinbase: boolean;
sequence: number;
}
export interface TransactionInfo {
txid: string;
version: number;
locktime: number;
vin: Vin[];
vout: any[];
size: number;
weight: number;
fee: number;
status: {
confirmed: boolean;
block_height: number;
block_hash: string;
block_time: number;
};
}
export function getDocumentValidation(container: HTMLElement) {
const state: State = {
file: null,
fileHash: null,
certificate: null,
commitmentHashes: []
}
container.innerHTML = '';
container.style.cssText = `
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
gap: 2rem;
`;
function createDropButton(
label: string,
onDrop: (file: File, updateVisuals: (file: File) => void) => void,
accept: string = '*/*'
): HTMLElement {
const wrapper = document.createElement('div');
wrapper.style.cssText = `
width: 200px;
height: 100px;
border: 2px dashed #888;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
background: #f8f8f8;
text-align: center;
padding: 0.5rem;
box-sizing: border-box;
`;
const title = document.createElement('div');
title.textContent = label;
const filename = document.createElement('div');
filename.style.cssText = `
font-size: 0.85rem;
margin-top: 0.5rem;
color: #444;
word-break: break-word;
text-align: center;
`;
wrapper.appendChild(title);
wrapper.appendChild(filename);
const updateVisuals = (file: File) => {
wrapper.style.borderColor = 'green';
wrapper.style.background = '#e6ffed';
filename.textContent = file.name;
};
// === Hidden file input ===
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = accept;
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = () => {
const file = fileInput.files?.[0];
if (file) {
onDrop(file, updateVisuals);
fileInput.value = ''; // reset so same file can be re-selected
}
};
// === Handle drag-and-drop ===
wrapper.ondragover = e => {
e.preventDefault();
wrapper.style.background = '#e0e0e0';
};
wrapper.ondragleave = () => {
wrapper.style.background = '#f8f8f8';
};
wrapper.ondrop = e => {
e.preventDefault();
wrapper.style.background = '#f8f8f8';
const file = e.dataTransfer?.files?.[0];
if (file) {
onDrop(file, updateVisuals);
}
};
// === Handle click to open file manager ===
wrapper.onclick = () => {
fileInput.click();
};
return wrapper;
}
const fileDropButton = createDropButton('Drop file', async (file, updateVisuals) => {
try {
state.file = file;
updateVisuals(file);
console.log('Loaded file:', state.file);
checkReady();
} catch (err) {
alert('Failed to drop the file.');
console.error(err);
}
});
const certDropButton = createDropButton('Drop certificate', async (file, updateVisuals) => {
try {
const text = await file.text();
const json = JSON.parse(text);
if (
typeof json === 'object' &&
json !== null &&
typeof json.pcd_commitment === 'object' &&
typeof json.state_id === 'string'
) {
state.certificate = json as ProcessState;
state.commitmentHashes = Object.values(json.pcd_commitment).map((h: string) =>
h.toLowerCase()
);
updateVisuals(file);
console.log('Loaded certificate, extracted hashes:', state.commitmentHashes);
checkReady();
} else {
alert('Invalid certificate structure.');
}
} catch (err) {
alert('Failed to parse certificate JSON.');
console.error(err);
}
});
const buttonRow = document.createElement('div');
buttonRow.style.display = 'flex';
buttonRow.style.gap = '2rem';
buttonRow.appendChild(fileDropButton);
buttonRow.appendChild(certDropButton);
container.appendChild(buttonRow);
async function checkReady() {
if (state.file && state.certificate && state.commitmentHashes.length > 0) {
// We take the commited_in and all pcd_commitment keys to reconstruct all the possible hash
const fileBlob = {
type: state.file.type,
data: new Uint8Array(await state.file.arrayBuffer())
};
const service = await Services.getInstance();
const commitedIn = state.certificate.commited_in;
if (!commitedIn) return;
const [prevTxid, prevTxVout] = commitedIn.split(':');
const processId = state.certificate.process_id;
const stateId = state.certificate.state_id;
const process = await service.getProcess(processId);
if (!process) return;
// Get the transaction that comes right after the commited_in
const nextState = service.getNextStateAfterId(process, stateId);
if (!nextState) {
alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
return;
}
const [outspentTxId, _] = nextState.commited_in.split(':');
console.log(outspentTxId);
// Check that the commitment transaction exists, and that it commits to the state id
const txInfo = await fetchTransaction(outspentTxId);
if (!txInfo) {
console.error(`Validation error: Can't fetch new state commitment transaction`);
alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
return;
}
// We must check that this transaction indeed spend the commited_in we have in the certificate
let found = false;
for (const vin of txInfo.vin) {
if (vin.txid === prevTxid) {
found = true;
break;
}
}
if (!found) {
console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
alert('❌ Validation failed: Unconsistent commitment transactions history.');
return;
}
// set found back to false for next check
found = false;
// is the state_id commited in the transaction?
for (const vout of txInfo.vout) {
console.log(vout);
if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
found = true;
} else {
continue;
}
if (vout.scriptpubkey_asm) {
const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
if (hash) {
if (hash !== stateId) {
console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
alert('❌ Validation failed: Transaction does not commit to that state.');
return;
}
}
}
}
if (!found) {
alert('❌ Validation failed: Transaction does not contain data.');
return;
}
// set found back to false for next check
found = false;
for (const label of Object.keys(state.certificate.pcd_commitment)) {
// Compute the hash for this label
console.log(`Computing hash with label ${label}`)
const fileHex = service.getHashForFile(commitedIn, label, fileBlob);
console.log(`Found hash ${fileHex}`);
found = state.commitmentHashes.includes(fileHex);
if (found) break;
}
if (found) {
alert('✅ Validation successful: file hash found in pcd_commitment.');
} else {
alert('❌ Validation failed: file hash NOT found in pcd_commitment.');
}
}
}
async function fetchTransaction(txid: string): Promise<TransactionInfo> {
const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
}
const outspend: TransactionInfo = await response.json();
return outspend;
}
function extractHexFromScriptAsm(scriptAsm: string): string | null {
const parts = scriptAsm.trim().split(/\s+/);
const last = parts[parts.length - 1];
// Basic validation: must be 64-char hex (32 bytes)
if (/^[0-9a-fA-F]{64}$/.test(last)) {
return last.toLowerCase();
}
return null;
}
}

View File

@ -0,0 +1,196 @@
import { ValidationRule, RoleDefinition } from '../../../pkg/sdk_client';
import { showValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
export function createKeyValueSection(title: string, id: string, isRoleSection = false) {
const section = document.createElement('div');
section.id = id;
section.style.cssText = 'margin-bottom: 2rem; background: #fff; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);';
const titleEl = document.createElement('h2');
titleEl.textContent = title;
titleEl.style.cssText = 'font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;';
section.appendChild(titleEl);
const rowContainer = document.createElement('div');
section.appendChild(rowContainer);
const addBtn = document.createElement('button');
addBtn.textContent = '+ Add Row';
addBtn.style.cssText = `
margin-top: 1rem;
padding: 0.5rem 1rem;
border: 1px solid #888;
border-radius: 0.375rem;
background-color: #f9f9f9;
cursor: pointer;
`;
section.appendChild(addBtn);
const roleRowStates: {
roleNameInput: HTMLInputElement;
membersInput: HTMLInputElement;
storagesInput: HTMLInputElement;
validationRules: ValidationRule[];
}[] = [];
type fileBlob = {
type: string,
data: Uint8Array
};
const nonRoleRowStates: {
keyInput: HTMLInputElement,
valueInput: HTMLInputElement,
fileInput: HTMLInputElement,
fileBlob: fileBlob | null
}[] = [];
const inputStyle = 'flex: 1; height: 2.5rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem;';
const createRow = () => {
const row = document.createElement('div');
row.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 0.5rem; align-items: center;';
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cssText = 'background: none; border: none; font-size: 1.2rem; cursor: pointer;';
deleteBtn.onclick = () => {
row.remove();
updateDeleteButtons();
};
if (isRoleSection) {
const roleName = document.createElement('input');
const members = document.createElement('input');
const storages = document.createElement('input');
roleName.placeholder = 'Role name';
members.placeholder = 'members';
storages.placeholder = 'storages';
[roleName, members, storages].forEach(input => {
input.type = 'text';
input.style.cssText = inputStyle;
});
const ruleButton = document.createElement('button');
ruleButton.textContent = 'Add Validation Rule';
ruleButton.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
const rules: ValidationRule[] = [];
ruleButton.onclick = () => {
showValidationRuleModal(rule => {
rules.push(rule);
ruleButton.textContent = `Rules (${rules.length})`;
});
};
row.appendChild(roleName);
row.appendChild(members);
row.appendChild(storages);
row.appendChild(ruleButton);
row.appendChild(deleteBtn);
roleRowStates.push({ roleNameInput: roleName, membersInput: members, storagesInput: storages, validationRules: rules });
} else {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.onchange = async () => {
const file = fileInput.files?.[0];
if (!file) return;
const buffer = await file.arrayBuffer();
const uint8 = new Uint8Array(buffer);
rowState.fileBlob = {
type: file.type,
data: uint8,
};
valueInput.value = `📄 ${file.name}`;
valueInput.disabled = true;
attachBtn.textContent = `📎 ${file.name}`;
};
const attachBtn = document.createElement('button');
attachBtn.textContent = '📎 Attach';
attachBtn.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
attachBtn.onclick = () => fileInput.click();
const keyInput = document.createElement('input');
const valueInput = document.createElement('input');
const rowState = {
keyInput,
valueInput,
fileInput,
fileBlob: null as fileBlob | null
};
nonRoleRowStates.push(rowState);
keyInput.placeholder = 'Key';
valueInput.placeholder = 'Value';
[keyInput, valueInput].forEach(input => {
input.type = 'text';
input.style.cssText = inputStyle;
});
row.appendChild(keyInput);
row.appendChild(valueInput);
row.appendChild(attachBtn);
row.appendChild(fileInput);
row.appendChild(deleteBtn);
}
rowContainer.appendChild(row);
updateDeleteButtons();
};
const updateDeleteButtons = () => {
const rows = Array.from(rowContainer.children);
rows.forEach(row => {
const btn = row.querySelector('button:last-child') as HTMLButtonElement;
if (rows.length === 1) {
btn.disabled = true;
btn.style.visibility = 'hidden';
} else {
btn.disabled = false;
btn.style.visibility = 'visible';
}
});
};
createRow();
addBtn.addEventListener('click', createRow);
return {
element: section,
getData: () => {
if (isRoleSection) {
const data: Record<string, RoleDefinition> = {};
for (const row of roleRowStates) {
const key = row.roleNameInput.value.trim();
if (!key) continue;
data[key] = {
members: row.membersInput.value.split(',').map(x => x.trim()).filter(Boolean),
storages: row.storagesInput.value.split(',').map(x => x.trim()).filter(Boolean),
validation_rules: row.validationRules
};
}
return data;
} else {
const data: Record<string, string | fileBlob> = {};
for (const row of nonRoleRowStates) {
const key = row.keyInput.value.trim();
if (!key) continue;
if (row.fileBlob) {
data[key] = row.fileBlob;
} else {
data[key] = row.valueInput.value.trim();
}
}
return data;
}
}
};
}

View File

@ -0,0 +1,91 @@
import { createKeyValueSection } from './key-value-section';
import { loadValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
import Services from '../../services/service';
import { RoleDefinition } from '../../../pkg/sdk_client';
export async function getProcessCreation(container: HTMLElement) {
await loadValidationRuleModal();
container.style.display = 'block';
container.innerHTML = `<div class="parameter-header">Process Creation</div>`;
const privateSec = createKeyValueSection('Private Data', 'private-section');
const publicSec = createKeyValueSection('Public Data', 'public-section');
const rolesSec = createKeyValueSection('Roles', 'roles-section', true);
container.appendChild(privateSec.element);
container.appendChild(publicSec.element);
container.appendChild(rolesSec.element);
const btn = document.createElement('button');
btn.textContent = 'Create Process';
btn.style.cssText = `
display: block;
margin: 2rem auto 0;
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: bold;
background-color: #4f46e5;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
`;
btn.onclick = async () => {
const privateData = privateSec.getData();
const publicData = publicSec.getData();
const roles = rolesSec.getData() as Record<string, RoleDefinition>;
console.log('Private:', privateData);
console.log('Public:', publicData);
console.log('Roles:', roles);
const service = await Services.getInstance();
const createProcessResult = await service.createProcess(privateData, publicData, roles);
const processId = createProcessResult.updated_process!.process_id;
const stateId = createProcessResult.updated_process!.current_process.states[0].state_id;
await service.handleApiReturn(createProcessResult);
// Now we want to validate the update and register the first state of our new process
const updateProcessResult = await service.createPrdUpdate(processId, stateId);
await service.handleApiReturn(createProcessResult);
const approveChangeResult = await service.approveChange(processId, stateId);
await service.handleApiReturn(approveChangeResult);
if (approveChangeResult) {
const process = await service.getProcess(processId);
let newState = service.getStateFromId(process, stateId);
if (!newState) return;
for (const label of Object.keys(newState.keys)) {
const hash = newState.pcd_commitment[label];
const encryptedData = await service.getBlobFromDb(hash);
const filename = `${label}-${hash.slice(0,8)}.bin`;
const blob = new Blob([encryptedData], { type: "application/octet-stream" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
}
await service.generateProcessPdf(processId, newState);
// Add processId to the state we export
newState['process_id'] = processId;
const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `process_${processId}_${stateId}.json`;
a.click();
URL.revokeObjectURL(url); // Clean up
}
};
container.appendChild(btn);
}

View File

@ -0,0 +1,66 @@
export function createProcessTab(container: HTMLElement, processes: { name: string, publicData: Record<string, any> }[]): HTMLElement {
container.id = 'process-tab';
container.style.display = 'block';
container.style.cssText = 'padding: 1.5rem;';
const title = document.createElement('h2');
title.textContent = 'Processes';
title.style.cssText = 'font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;';
container.appendChild(title);
processes.forEach(proc => {
const card = document.createElement('div');
card.style.cssText = 'margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; background: #fff;';
const nameEl = document.createElement('h3');
nameEl.textContent = proc.name;
nameEl.style.cssText = 'font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem;';
card.appendChild(nameEl);
const dataList = document.createElement('div');
for (const [key, value] of Object.entries(proc.publicData)) {
const item = document.createElement('div');
item.style.cssText = 'margin-bottom: 0.5rem;';
const label = document.createElement('strong');
label.textContent = key + ': ';
item.appendChild(label);
// Let's trim the quotes
const trimmed = value.replace(/^'|'$/g, '');
let parsed;
try {
parsed = JSON.parse(trimmed);
} catch (_) {
parsed = trimmed;
}
if (parsed && typeof parsed === 'object') {
const saveBtn = document.createElement('button');
saveBtn.textContent = '💾 Save as JSON';
saveBtn.style.cssText = 'margin-left: 0.5rem; padding: 0.25rem 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
saveBtn.onclick = () => {
const blob = new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${proc.name}_${key}.json`;
a.click();
URL.revokeObjectURL(url);
};
item.appendChild(saveBtn);
} else {
const span = document.createElement('span');
span.textContent = String(parsed);
item.appendChild(span);
}
dataList.appendChild(item);
}
card.appendChild(dataList);
container.appendChild(card);
});
return container;
}

View File

@ -1,4 +1,4 @@
import { ChatElement } from './chat';
/*import { ChatElement } from './chat';
import chatCss from '../../../public/style/chat.css?raw';
import Services from '../../services/service.js';
@ -46,4 +46,4 @@ class ChatComponent extends HTMLElement {
}
export { ChatComponent };
customElements.define('chat-component', ChatComponent);
customElements.define('chat-component', ChatComponent);*/

View File

@ -1,3 +1,4 @@
<!--
<!DOCTYPE html>
<html lang="en">

View File

@ -1,4 +1,4 @@
declare global {
/*declare global {
interface Window {
loadMemberChat: (memberId: string | number) => void;
}
@ -385,37 +385,38 @@ class ChatElement extends HTMLElement {
}
}
private async lookForChildren(): Promise<string | null> {
// Filter processes for the children of current process
const service = await Services.getInstance();
if (!this.selectedChatProcessId) {
console.error('No process id');
return null;
}
const children: string[] = await service.getChildrenOfProcess(this.selectedChatProcessId);
// TODO rewrite that
// private async lookForChildren(): Promise<string | null> {
// // Filter processes for the children of current process
// const service = await Services.getInstance();
// if (!this.selectedChatProcessId) {
// console.error('No process id');
// return null;
// }
// const children: string[] = await service.getChildrenOfProcess(this.selectedChatProcessId);
const processRoles = this.processRoles;
const selectedMember = this.selectedMember;
for (const child of children) {
const roles = service.getRoles(JSON.parse(child));
// Check that we and the other members are in the role
if (!service.isChildRole(processRoles, roles)) {
console.error('Child process roles are not a subset of parent')
continue;
}
if (!service.rolesContainsMember(roles, selectedMember)) {
console.error('Member is not part of the process');
continue;
}
if (!service.rolesContainsUs(roles)) {
console.error('We\'re not part of child process');
continue;
}
return child;
}
// const processRoles = this.processRoles;
// const selectedMember = this.selectedMember;
// for (const child of children) {
// const roles = service.getRoles(JSON.parse(child));
// // Check that we and the other members are in the role
// if (!service.isChildRole(processRoles, roles)) {
// console.error('Child process roles are not a subset of parent')
// continue;
// }
// if (!service.rolesContainsMember(roles, selectedMember)) {
// console.error('Member is not part of the process');
// continue;
// }
// if (!service.rolesContainsUs(roles)) {
// console.error('We\'re not part of child process');
// continue;
// }
// return child;
// }
return null;
}
// return null;
// }
private async loadAllMembers() {
const groupList = this.shadowRoot?.querySelector('#group-list');
@ -689,18 +690,6 @@ class ChatElement extends HTMLElement {
this.selectedChatProcessId = dmProcessId;
}
/* TODO
console.log("Je suis messagesProcess", messagesProcess);
// --- GET THE STATE ID ---
const messagesProcessStateId = messagesProcess?.states?.[0]?.state_id;
console.log("Je suis messagesProcessStateId", messagesProcessStateId);
// --- GET THE DIFF FROM THE STATE ID ---
if (messagesProcessStateId) {
const diffFromStateId = await this.getDiffByStateId(messagesProcessStateId);
console.log("Je suis diffFromStateId", diffFromStateId);
}*/
// Récupérer les messages depuis les états du processus
const allMessages: any[] = [];
@ -1072,7 +1061,7 @@ class ChatElement extends HTMLElement {
await this.loadAllProcesses(processSet);
break;
case 'members':
await this.lookForMyDms():
await this.lookForMyDms();
await this.loadAllMembers();
break;
default:
@ -1087,7 +1076,9 @@ class ChatElement extends HTMLElement {
const service = await Services.getInstance();
const allProcesses: Record<string, Process> = await service.getProcesses();
console.log('All processes:', allProcesses);
const myProcesses: string[] = await service.getMyProcesses();
console.log('My processes:', myProcesses);
const groupList = this.shadowRoot?.querySelector('#group-list');
if (!groupList) {
@ -1116,7 +1107,7 @@ class ChatElement extends HTMLElement {
});
});
//trier les processus : ceux de l'utilisateur en premier
// Trier les processus : ceux de l'utilisateur en premier
const sortedEntries = Object.entries(allProcesses).sort(
([keyA], [keyB]) => {
const inSetA = myProcesses.includes(keyA);
@ -1743,6 +1734,5 @@ class ChatElement extends HTMLElement {
}
customElements.define('chat-element', ChatElement);
export { ChatElement };
export { ChatElement };*/

View File

@ -1,9 +1,11 @@
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 { displayEmojis, generateQRCode, generateCreateBtn, addressToEmoji} from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/html.utils';
import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component';
import { navigate, registerAllListeners } from '../../router';
export { QrScannerComponent };
export async function initHomePage(): Promise<void> {
console.log('INIT-HOME');
@ -21,7 +23,7 @@ export async function initHomePage(): Promise<void> {
const service = await Services.getInstance();
const spAddress = await service.getDeviceAddress();
// generateQRCode(spAddress);
generateCreateBtn ();
generateCreateBtn();
displayEmojis(spAddress);
// Add this line to populate the select when the page loads
@ -59,7 +61,7 @@ async function populateMemberSelect() {
}
const service = await Services.getInstance();
const members = service.getAllMembersSorted();
const members = await service.getAllMembersSorted();
for (const [processId, member] of Object.entries(members)) {
const process = await service.getProcess(processId);

View File

@ -1,49 +1,49 @@
import processHtml from './process.html?raw';
import processScript from './process.ts?raw';
import processCss from '../../4nk.css?raw';
import { init } from './process';
// 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' });
}
// 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);
}
// 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');
}
}
// set callback(fn) {
// if (typeof fn === 'function') {
// this._callback = fn;
// } else {
// console.error('Callback is not a function');
// }
// }
get callback() {
return this._callback;
}
// get callback() {
// return this._callback;
// }
render() {
if (this.shadowRoot)
this.shadowRoot.innerHTML = `
<style>
${processCss}
</style>${processHtml}
<script type="module">
${processScript}
</scipt>
// 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);
}
// if (!customElements.get('process-list-4nk-component')) {
// customElements.define('process-list-4nk-component', ProcessListComponent);
// }

View File

@ -1,4 +1,4 @@
<div class="title-container">
<!-- <div class="title-container">
<h1>Process Selection</h1>
</div>
@ -16,4 +16,4 @@
<a class="btn" onclick="goToProcessPage()">OK</a>
</div>
</div>
</div>
</div> -->

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,17 @@
import '../public/style/4nk.css';
import { initHeader } from '../src/components/header/header';
import { initChat } from '../src/pages/chat/chat';
/*import { initChat } from '../src/pages/chat/chat';*/
import Database from './services/database.service';
import Services from './services/service';
import TokenService from './services/token';
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 };
import { MessageType } from './models/process.model';
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
import { MerkleProofResult } from 'pkg/sdk_client';
const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html',
process: '/src/pages/process/process.html',
@ -62,14 +66,14 @@ async function handleLocation(path: string) {
switch (path) {
case 'process':
// const { init } = await import('./pages/process/process');
const { ProcessListComponent } = await import('./pages/process/process-list-component');
//const { ProcessListComponent } = await import('./pages/process/process-list-component');
const container2 = document.querySelector('#containerId');
const accountComponent = document.createElement('process-list-4nk-component');
if (!customElements.get('process-list-4nk-component')) {
customElements.define('process-list-4nk-component', ProcessListComponent);
}
//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;
@ -94,7 +98,7 @@ async function handleLocation(path: string) {
}
break;
case 'chat':
/*case 'chat':
const { ChatComponent } = await import('./pages/chat/chat-component');
const chatContainer = document.querySelector('.group-list');
if (chatContainer) {
@ -104,7 +108,7 @@ async function handleLocation(path: string) {
const chatComponent = document.createElement('chat-component');
chatContainer.appendChild(chatComponent);
}
break;
break;*/
case 'signature':
const { SignatureComponent } = await import('./pages/signature/signature-component');
@ -134,44 +138,741 @@ 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);
const db = await Database.getInstance();
db.registerServiceWorker('/src/service-workers/database.worker.js');
const 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 (!device) {
await services.createNewDevice();
} else {
services.restoreDevice(device);
}
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);
// If we create a new device, we most probably don't have anything in db, but just in case
await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB();
// We connect to all relays now
await services.connectAllRelays();
// We register all the event listeners if we run in an iframe
if (window.self !== window.top) {
await registerAllListeners();
}
if (services.isPaired()) {
await navigate('account');
} else {
await navigate('home');
}
} catch (error) {
console.error(error);
await navigate('home');
}
}
export async function registerAllListeners() {
const services = await Services.getInstance();
const tokenService = await TokenService.getInstance();
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
window.parent.postMessage(
{
type: MessageType.ERROR,
error: errorMsg,
messageId
},
origin
);
}
// --- Handler functions ---
const handleRequestLink = async (event: MessageEvent) => {
if (event.data.type !== MessageType.REQUEST_LINK) {
return;
}
const modalService = await ModalService.getInstance();
const result = await modalService.showConfirmationModal({
title: 'Confirmation de liaison',
content: `
<div class="modal-confirmation">
<h3>Liaison avec ${event.origin}</h3>
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
<p>Voulez-vous continuer ?</p>
</div>
`,
confirmText: 'Ajouter un service',
cancelText: 'Annuler'
}, true);
try {
if (!result) {
throw new Error('User refused to link');
}
if (!services.isPaired()) {
// New device - do pairing process
console.log('🚀 The device is not paired');
await prepareAndSendPairingTx();
await services.confirmPairing();
}
} catch (error) {
const errorMsg = `Failed to pair device: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
try {
const tokens = await tokenService.generateSessionToken(event.origin);
const acceptedMsg = {
type: MessageType.LINK_ACCEPTED,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
messageId: event.data.messageId
};
window.parent.postMessage(
acceptedMsg,
event.origin
);
} catch (error) {
const errorMsg = `Failed to generate tokens: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleGetMyProcesses = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_MY_PROCESSES) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const myProcesses = await services.getMyProcesses();
window.parent.postMessage(
{
type: MessageType.GET_MY_PROCESSES,
myProcesses,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to get processes: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleGetProcesses = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_PROCESSES) {
return;
}
const tokenService = await TokenService.getInstance();
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { accessToken } = event.data;
// Validate the session token
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const processes = await services.getProcesses();
window.parent.postMessage(
{
type: MessageType.PROCESSES_RETRIEVED,
processes,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to get processes: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
/// We got a state for some process and return as many clear attributes as we can
const handleDecryptState = async (event: MessageEvent) => {
if (event.data.type !== MessageType.RETRIEVE_DATA) {
return;
}
const tokenService = await TokenService.getInstance();
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
// Retrieve the state for the process
const process = await services.getProcess(processId);
if (!process) {
throw new Error('Can\'t find process');
}
const state = services.getStateFromId(process, stateId);
let res: Record<string, any> = {};
if (state) {
// Decrypt all the data we have the key for
for (const attribute of Object.keys(state.pcd_commitment)) {
if (attribute === 'roles' || state.public_data[attribute]) {
continue;
}
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
if (decryptedAttribute) {
res[attribute] = decryptedAttribute;
}
}
} else {
throw new Error('Unknown state for process', processId);
}
window.parent.postMessage(
{
type: MessageType.DATA_RETRIEVED,
data: res,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to retrieve data: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleValidateToken = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_TOKEN) {
return;
}
const accessToken = event.data.accessToken;
const refreshToken = event.data.refreshToken;
if (!accessToken || !refreshToken) {
errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId);
}
const isValid = await tokenService.validateToken(accessToken, event.origin);
window.parent.postMessage(
{
type: MessageType.VALIDATE_TOKEN,
accessToken: accessToken,
refreshToken: refreshToken,
isValid: isValid,
messageId: event.data.messageId
},
event.origin
);
};
const handleRenewToken = async (event: MessageEvent) => {
if (event.data.type !== MessageType.RENEW_TOKEN) {
return;
}
try {
const refreshToken = event.data.refreshToken;
if (!refreshToken) {
throw new Error('No refresh token provided');
}
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
if (!newAccessToken) {
throw new Error('Failed to refresh token');
}
window.parent.postMessage(
{
type: MessageType.RENEW_TOKEN,
accessToken: newAccessToken,
refreshToken: refreshToken,
messageId: event.data.messageId
},
event.origin
);
} catch (error) {
const errorMsg = `Failed to renew token: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleGetPairingId = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_PAIRING_ID) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const userPairingId = services.getPairingProcessId();
window.parent.postMessage(
{
type: MessageType.GET_PAIRING_ID,
userPairingId,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to get pairing id: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleCreateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.CREATE_PROCESS) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { processData, privateFields, roles, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const { privateData, publicData } = splitPrivateData(processData, privateFields);
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
if (!createProcessReturn.updated_process) {
throw new Error('Empty updated_process in createProcessReturn');
}
const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process;
const stateId = process.states[0].state_id;
await services.handleApiReturn(createProcessReturn);
const res = {
processId,
process,
processData,
}
window.parent.postMessage(
{
type: MessageType.PROCESS_CREATED,
processCreated: res,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to create process: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleNotifyUpdate = async (event: MessageEvent) => {
if (event.data.type !== MessageType.NOTIFY_UPDATE) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
if (!isValid32ByteHex(stateId)) {
throw new Error('Invalid state id');
}
const res = await services.createPrdUpdate(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.UPDATE_NOTIFIED,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to notify update for process: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleValidateState = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_STATE) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { processId, stateId, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const res = await services.approveChange(processId, stateId);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.STATE_VALIDATED,
validatedProcess: res.updated_process,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to validate process: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleUpdateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.UPDATE_PROCESS) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
}
try {
// privateFields is only used if newData contains new fields
// roles can be empty meaning that roles from the last commited state are kept
const { processId, newData, privateFields, roles, accessToken } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
// Check if the new data is already in the process or if it's a new field
const process = await services.getProcess(processId);
if (!process) {
throw new Error('Process not found');
}
let lastState = services.getLastCommitedState(process);
if (!lastState) {
const firstState = process.states[0];
const roles = firstState.roles;
if (services.rolesContainsUs(roles)) {
const approveChangeRes= await services.approveChange(processId, firstState.state_id);
await services.handleApiReturn(approveChangeRes);
const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id);
await services.handleApiReturn(prdUpdateRes);
} else {
if (firstState.validation_tokens.length > 0) {
// Try to send it again anyway
const res = await services.createPrdUpdate(processId, firstState.state_id);
await services.handleApiReturn(res);
}
}
// Wait a couple seconds
await new Promise(resolve => setTimeout(resolve, 2000));
lastState = services.getLastCommitedState(process);
if (!lastState) {
throw new Error('Process doesn\'t have a commited state yet');
}
}
const lastStateIndex = services.getLastCommitedStateIndex(process);
if (lastStateIndex === null) {
throw new Error('Process doesn\'t have a commited state yet');
} // Shouldn't happen
const privateData: Record<string, any> = {};
const publicData: Record<string, any> = {};
for (const field of Object.keys(newData)) {
// Public data are carried along each new state
// So the first thing we can do is check if the new data is public data
if (lastState.public_data[field]) {
// Add it to public data
publicData[field] = newData[field];
continue;
}
// If it's not a public data, it may be either a private data update, or a new field (public of private)
// Caller gave us a list of new private fields, if we see it here this is a new private field
if (privateFields.includes(field)) {
// Add it to private data
privateData[field] = newData[field];
continue;
}
// Now it can be an update of private data or a new public data
// We check that the field exists in previous states private data
for (let i = lastStateIndex; i >= 0; i--) {
const state = process.states[i];
if (state.pcd_commitment[field]) {
// We don't even check if it's a public field, we would have seen it in the last state
privateData[field] = newData[field];
break;
} else {
// This attribute was not modified in that state, we go back to the previous state
continue;
}
}
if (privateData[field]) continue;
// We've get back all the way to the first state without seeing it, it's a new public field
publicData[field] = newData[field];
}
// We'll let the wasm check if roles are consistent
const res = await services.updateProcess(process, privateData, publicData, roles);
await services.handleApiReturn(res);
window.parent.postMessage(
{
type: MessageType.PROCESS_UPDATED,
updatedProcess: res.updated_process,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to update process: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleDecodePublicData = async (event: MessageEvent) => {
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return;
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId);
return;
}
try {
const { accessToken, encodedData } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const decodedData = services.decodeValue(encodedData);
window.parent.postMessage(
{
type: MessageType.PUBLIC_DATA_DECODED,
decodedData,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to decode data: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleHashValue = async (event: MessageEvent) => {
if (event.data.type !== MessageType.HASH_VALUE) return;
console.log('handleHashValue', event.data);
try {
const { accessToken, commitedIn, label, fileBlob } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const hash = services.getHashForFile(commitedIn, label, fileBlob);
window.parent.postMessage(
{
type: MessageType.VALUE_HASHED,
hash,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to hash value: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleGetMerkleProof = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_MERKLE_PROOF) return;
try {
const { accessToken, processState, attributeName } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
const proof = services.getMerkleProofForFile(processState, attributeName);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_RETRIEVED,
proof,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to get merkle proof: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
const handleValidateMerkleProof = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return;
try {
const { accessToken, merkleProof, documentHash } = event.data;
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token');
}
// Try to parse the proof
// We will validate it's a MerkleProofResult in the wasm
let parsedMerkleProof: MerkleProofResult;
try {
parsedMerkleProof= JSON.parse(merkleProof);
} catch (e) {
throw new Error('Provided merkleProof is not a valid json object');
}
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
window.parent.postMessage(
{
type: MessageType.MERKLE_PROOF_VALIDATED,
isValid: res,
messageId: event.data.messageId
},
event.origin
);
} catch (e) {
const errorMsg = `Failed to get merkle proof: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
window.removeEventListener('message', handleMessage);
window.addEventListener('message', handleMessage);
async function handleMessage(event: MessageEvent) {
try {
switch (event.data.type) {
case MessageType.REQUEST_LINK:
await handleRequestLink(event);
break;
case MessageType.GET_MY_PROCESSES:
await handleGetMyProcesses(event);
break;
case MessageType.GET_PROCESSES:
await handleGetProcesses(event);
break;
case MessageType.RETRIEVE_DATA:
await handleDecryptState(event);
break;
case MessageType.VALIDATE_TOKEN:
await handleValidateToken(event);
break;
case MessageType.RENEW_TOKEN:
await handleRenewToken(event);
break;
case MessageType.GET_PAIRING_ID:
await handleGetPairingId(event);
break;
case MessageType.CREATE_PROCESS:
await handleCreateProcess(event);
break;
case MessageType.NOTIFY_UPDATE:
await handleNotifyUpdate(event);
break;
case MessageType.VALIDATE_STATE:
await handleValidateState(event);
break;
case MessageType.UPDATE_PROCESS:
await handleUpdateProcess(event);
break;
case MessageType.DECODE_PUBLIC_DATA:
await handleDecodePublicData(event);
break;
case MessageType.HASH_VALUE:
await handleHashValue(event);
break;
case MessageType.GET_MERKLE_PROOF:
await handleGetMerkleProof(event);
break;
case MessageType.VALIDATE_MERKLE_PROOF:
await handleValidateMerkleProof(event);
break;
default:
console.warn(`Unhandled message type: ${event.data.type}`);
}
} catch (error) {
const errorMsg = `Error handling message: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId);
}
}
window.parent.postMessage(
{
type: MessageType.LISTENING
},
'*'
);
}
async function cleanPage() {
const container = document.querySelector('#containerId');
if (container) container.innerHTML = '';
@ -199,7 +900,7 @@ document.addEventListener('navigate', ((e: Event) => {
const container = document.querySelector('.container');
if (container) container.innerHTML = '';
initChat();
//initChat();
const chatElement = document.querySelector('chat-element');
if (chatElement) {

View File

@ -45,6 +45,21 @@ self.addEventListener('message', async (event) => {
} catch (error) {
event.ports[0].postMessage({ status: 'error', message: error.message });
}
} else if (data.type === 'BATCH_WRITING') {
const { storeName, objects } = data.payload;
const db = await openDatabase();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
for (const { key, object } of objects) {
if (key) {
await store.put(object, key);
} else {
await store.put(object);
}
}
await tx.done;
}
});

View File

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

View File

@ -84,7 +84,6 @@ export class Database {
request.onsuccess = async () => {
this.db = request.result;
await this.initServiceWorker();
resolve();
};
@ -110,15 +109,16 @@ export class Database {
return objectList;
}
private async initServiceWorker() {
public async registerServiceWorker(path: string) {
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
console.log('registering worker at', path);
try {
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
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' });
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { 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.
@ -130,7 +130,7 @@ export class Database {
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' });
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
}
@ -142,12 +142,12 @@ export class Database {
await this.handleServiceWorkerMessage(event.data);
});
// Set up a periodic check to ensure the service worker is active and to send a SYNC message.
// Set up a periodic check to ensure the service worker is active and to send a SCAN message.
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration));
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload.length != 0) {
if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000);
@ -199,9 +199,9 @@ export class Database {
}
}
private async handleDownloadList(downloadList: string[]): void {
private async handleDownloadList(downloadList: string[]): Promise<void> {
// Download the missing data
let requestedStateId = [];
let requestedStateId: string[] = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
@ -250,7 +250,7 @@ export class Database {
} else if (data.type === 'TO_DOWNLOAD') {
console.log(`Received missing data ${data}`);
// Download the missing data
let requestedStateId = [];
let requestedStateId: string[] = [];
for (const hash of data.data) {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
@ -263,9 +263,12 @@ export class Database {
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 (diff === null) {
continue;
}
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);
@ -320,6 +323,38 @@ export class Database {
});
}
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
return new Promise(async (resolve, reject) => {
if (!this.serviceWorkerRegistration) {
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
}
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.status === 'success') {
resolve();
} else {
const error = event.data.message;
reject(new Error(error || 'Unknown error occurred while adding objects'));
}
};
try {
activeWorker?.postMessage(
{
type: 'BATCH_WRITING',
payload,
},
[messageChannel.port2],
);
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
public async getObject(storeName: string, key: string): Promise<any | null> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
@ -338,23 +373,25 @@ export class Database {
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);
}),
]);
return new Promise((resolve, reject) => {
const result: Record<string, any> = {};
const cursor = store.openCursor();
// Combine keys and values into an object
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
return result;
cursor.onsuccess = (event) => {
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
const cursor = request.result;
if (cursor) {
result[cursor.key as string] = cursor.value;
cursor.continue();
} else {
resolve(result);
}
};
cursor.onerror = () => {
reject(cursor.error);
};
});
} catch (error) {
console.error('Error fetching data from IndexedDB:', error);
throw error;

View File

@ -8,6 +8,13 @@ import { RoleDefinition } from 'pkg/sdk_client';
import { initValidationModal } from '~/components/validation-modal/validation-modal';
import { interpolate } from '~/utils/html.utils';
interface ConfirmationModalOptions {
title: string;
content: string;
confirmText?: string;
cancelText?: string;
}
export default class ModalService {
private static instance: ModalService;
private stateId: string | null = null;
@ -125,7 +132,7 @@ export default class ModalService {
console.log("MEMBERS:", members);
// We take all the addresses except our own
const service = await Services.getInstance();
const localAddress = await service.getDeviceAddress();
const localAddress = service.getDeviceAddress();
for (const member of members) {
if (member.sp_addresses) {
for (const address of member.sp_addresses) {
@ -164,92 +171,55 @@ export default class ModalService {
if (this.modal) this.modal.style.display = 'none';
}
async confirmPairing() {
const service = await Services.getInstance();
if (this.modal) this.modal.style.display = 'none';
async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise<boolean> {
// Create modal element
const modalElement = document.createElement('div');
modalElement.id = 'confirmation-modal';
modalElement.innerHTML = `
<div class="modal-overlay">
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ''}>
<h2>${options.title}</h2>
<div class="modal-body">
${options.content}
</div>
<div class="modal-footer">
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || 'Annuler'}</button>
<button id="confirm-button" class="btn btn-primary">${options.confirmText || 'Confirmer'}</button>
</div>
</div>
</div>
`;
if (service.device1) {
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;
// Add modal to document
document.body.appendChild(modalElement);
// Return promise that resolves with user choice
return new Promise((resolve) => {
const confirmButton = modalElement.querySelector('#confirm-button');
const cancelButton = modalElement.querySelector('#cancel-button');
const modalOverlay = modalElement.querySelector('.modal-overlay');
const cleanup = () => {
modalElement.remove();
};
confirmButton?.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelButton?.addEventListener('click', () => {
cleanup();
resolve(false);
});
modalOverlay?.addEventListener('click', (e) => {
if (e.target === modalOverlay) {
cleanup();
resolve(false);
}
} else {
throw new Error('No currentPcdCommitment');
}
// We send confirmation that we validate the change
try {
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
await this.injectWaitingModal();
const waitingModal = document.getElementById('waiting-modal');
if (waitingModal) waitingModal.style.display = 'flex';
if (!service.device2Ready) {
while (!service.device2Ready) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log("Device 2 is ready - Device 1 can now proceed");
}
service.pairDevice(this.paired_addresses, this.processId);
this.paired_addresses = [];
this.processId = null;
this.stateId = null;
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('chat');
service.resetState();
} catch (e) {
throw e;
}
// try {
// service.pairDevice(this.paired_addresses);
// } catch (e) {
// throw e;
// }
} else {
console.log("Device 2 detected");
// if (this.stateId && this.processId) {
// try {
// // Device B shouldn't do this again
// const createPrdUpdateReturn = service.createPrdUpdate(this.processId, this.stateId);
// await service.handleApiReturn(createPrdUpdateReturn);
// } catch (e) {
// throw e;
// }
// } else {
// throw new Error('No currentPcdCommitment');
// }
// We send confirmation that we validate the change
try {
const approveChangeReturn = await service.approveChange(this.processId!, this.stateId!);
await service.handleApiReturn(approveChangeReturn);
} catch (e) {
throw e;
}
service.pairDevice(this.paired_addresses, this.processId!);
this.paired_addresses = [];
this.processId = null;
this.stateId = null;
const newDevice = service.dumpDeviceFromMemory();
console.log(newDevice);
await service.saveDeviceInDatabase(newDevice);
navigate('chat');
}
});
});
}
async closeConfirmationModal() {

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ export async function storeData(servers: string[], key: string, value: Blob, ttl
}
return response;
} catch (error) {
if (error?.response?.status === 409) {
if (axios.isAxiosError(error) && error.response?.status === 409) {
return null;
}
console.error('Error storing data:', error);
@ -58,7 +58,7 @@ interface TestResponse {
}
export async function testData(servers: string[], key: string): Promise<Record<string, boolean | null> | null> {
const res = {};
const res: Record<string, boolean | null> = {};
for (const server of servers) {
res[server] = null;
try {

87
src/services/token.ts Normal file
View File

@ -0,0 +1,87 @@
import * as jose from 'jose';
interface TokenPair {
accessToken: string;
refreshToken: string;
}
export default class TokenService {
private static instance: TokenService;
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
private readonly ACCESS_TOKEN_EXPIRATION = '30s';
private readonly REFRESH_TOKEN_EXPIRATION = '7d';
private readonly encoder = new TextEncoder();
private constructor() {}
static async getInstance(): Promise<TokenService> {
if (!TokenService.instance) {
TokenService.instance = new TokenService();
}
return TokenService.instance;
}
async generateSessionToken(origin: string): Promise<TokenPair> {
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
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 = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
const { payload } = await jose.jwtVerify(token, secret);
return payload.origin === origin;
} catch (error: any) {
if (error?.code === 'ERR_JWT_EXPIRED') {
console.log('Token expiré');
return false;
}
console.error('Erreur de validation du token:', error);
return false;
}
}
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> {
try {
// Vérifier si le refresh token est valide
const isValid = await this.validateToken(refreshToken, origin);
if (!isValid) {
return null;
}
// Vérifier le type du token
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
const { payload } = await jose.jwtVerify(refreshToken, secret);
if (payload.type !== 'refresh') {
return null;
}
// Générer un nouveau access token
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
.sign(secret);
return newAccessToken;
} catch (error) {
console.error('Erreur lors du refresh du token:', error);
return null;
}
}
}

View File

@ -0,0 +1,24 @@
export function splitPrivateData(data: Record<string, any>, privateFields: string[]): { privateData: Record<string, any>, publicData: Record<string, any> } {
const privateData: Record<string, any> = {};
const publicData: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
if (privateFields.includes(key)) {
privateData[key] = value;
} else {
publicData[key] = value;
}
}
return { privateData, publicData };
}
export function isValid32ByteHex(value: string): boolean {
// Check if string is exactly 64 characters (32 bytes in hex)
if (value.length !== 64) {
return false;
}
// Check if string only contains valid hex characters
return /^[0-9a-fA-F]{64}$/.test(value);
}

View File

@ -105,6 +105,7 @@ export function initAddressInput() {
const emojiDisplay = container.querySelector('#emoji-display-2');
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
const actionButton = container.querySelector('#actionButton') as HTMLButtonElement;
addSubscription(addressInput, 'input', async () => {
let address = addressInput.value;
@ -141,12 +142,6 @@ export function initAddressInput() {
}
});
if (okButton) {
addSubscription(okButton, 'click', () => {
onOkButtonClick();
});
}
if (createButton) {
addSubscription(createButton, 'click', () => {
onCreateButtonClick();
@ -154,64 +149,44 @@ export function initAddressInput() {
}
}
async function onOkButtonClick() {
const container = getCorrectDOM('login-4nk-component') as HTMLElement
const secondDeviceAddress = (container.querySelector('#addressInput') as HTMLInputElement).value;
try {
// Connect to target, if necessary
await prepareAndSendPairingTx(secondDeviceAddress);
} catch (e) {
console.error(`onOkButtonClick error: ${e}`);
}
}
async function onCreateButtonClick() {
try {
await prepareAndSendPairingTx();
const service = await Services.getInstance();
await service.confirmPairing();
} catch (e) {
console.error(`onCreateButtonClick error: ${e}`);
}
}
export async function prepareAndSendPairingTx(promptName: boolean = false) {
export async function prepareAndSendPairingTx(): Promise<void> {
const service = await Services.getInstance();
// Device 1 wait Device 2
// service.device1 = true;
try {
await service.checkConnections([]);
} catch (e) {
throw e;
}
// Prompt the user for a username.
let userName;
if (promptName) {
userName = prompt("Please enter your user name:");
} else {
userName = "";
}
// Create the process after a delay.
setTimeout(async () => {
try {
const relayAddress = service.getAllRelays();
// Pass the userName as an additional parameter.
const createPairingProcessReturn = await service.createPairingProcess(
userName,
"",
[],
relayAddress[0].spAddress,
1,
userName
);
if (!createPairingProcessReturn.updated_process) {
throw new Error('createPairingProcess returned an empty new process'); // This should never happen
throw new Error('createPairingProcess returned an empty new process');
}
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
await service.handleApiReturn(createPairingProcessReturn);
}, 1000);
} catch (err) {
console.error(err);
}
}
export async function generateQRCode(spAddress: string) {

7
start-dev.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
# Démarrer nginx en arrière-plan
nginx
# Démarrer le serveur de développement Vite
npm run start

9
tsconfig.build.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist",
"module": "commonjs"
},
"exclude": ["node_modules", "dist"]
}

View File

@ -26,4 +26,4 @@
},
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
"exclude": ["node_modules"]
}
}

View File

@ -35,7 +35,7 @@ export default defineConfig({
target: 'esnext',
minify: false,
rollupOptions: {
input: './src/router.ts',
input: './src/index.ts',
output: {
entryFileNames: 'index.js',
},
@ -57,7 +57,7 @@ export default defineConfig({
fs: {
cachedChecks: false,
},
port: 3001,
port: 3003,
proxy: {
'/storage': {
target: 'https://demo.4nkweb.com',