Compare commits

...

275 Commits

Author SHA1 Message Date
Sosthene
1025c907b5 Fix init bugs 2025-12-03 10:43:44 +01:00
77019896e5 refactor(data.worker): streamline logging and comments in scanMissingData function for improved clarity and resource management 2025-12-03 10:06:52 +01:00
81025dca42 refactor(database.service): simplify service worker message handling by removing unnecessary comment and obsolete method for improved clarity 2025-12-03 10:06:41 +01:00
6dd8ce730f refactor(service): standardize import statements, improve code organization, and enhance clarity in Services class methods 2025-12-03 10:06:15 +01:00
d78dc14a2b refactor(network.service): implement event-driven mechanism for relay readiness, replacing polling with promise-based handling for improved efficiency and clarity 2025-12-02 00:54:14 +01:00
355d5ea18d refactor(storage.service): replace axios with fetch for data storage and retrieval, improve error handling, and enhance code clarity 2025-12-02 00:53:53 +01:00
8580070dc4 refactor(core.worker): standardize import statements, enhance code organization, and improve clarity in CoreBackend class methods 2025-12-02 00:47:52 +01:00
12b8c100c9 refactor(main): remove obsolete database initialization and streamline comments for improved clarity in bootstrap function 2025-12-02 00:37:16 +01:00
7c6f5c8739 refactor(utils): remove unnecessary console logs to enhance code clarity in emoji display function 2025-12-02 00:36:07 +01:00
50903c7e39 refactor(service): standardize import statements and improve code organization by relocating BackUp type import 2025-12-02 00:25:48 +01:00
da76c1ac3e refactor(iframe-controller): enhance async handling, improve error checks, and remove unnecessary comments for better code clarity 2025-12-02 00:25:41 +01:00
23fe47f69f refactor(sp-address.utils): standardize import statements and improve string handling for better code consistency 2025-12-02 00:20:48 +01:00
a9976ca624 refactor(network.service): improve code clarity by removing commented-out lines, standardizing import statements, and adding polling mechanism for relay address retrieval 2025-12-02 00:20:39 +01:00
4f86b26890 refactor(process-list): streamline code by removing commented-out lines, optimizing async calls, and enhancing readability in ProcessListPage component 2025-12-02 00:11:06 +01:00
662f3820d5 refactor(architecture): implement multi-worker architecture by introducing core and network workers, enhancing service communication, and removing obsolete websocket service 2025-12-02 00:10:54 +01:00
8fdd756d86 feat(dependencies): add comlink library to package.json for improved worker communication 2025-12-02 00:10:21 +01:00
f7b9129401 feat(services): add wallet and process services to the Services class and comment out pairing confirmation logic 2025-11-28 10:44:28 +01:00
09b1c29788 refactor(services): remove commented-out code and improve code clarity in iframe-controller, storage, and wallet services 2025-11-28 10:44:17 +01:00
96ee5b03e6 refactor(iframe-controller): standardize import statements, enhance logging, and improve error handling in IframeController service 2025-11-28 00:13:51 +01:00
8a87fe38c5 refactor(data.worker): optimize database access in service worker by implementing direct IndexedDB functions and enhancing message handling 2025-11-28 00:13:42 +01:00
bbbca27009 refactor(token): streamline token management by implementing secret key caching and enhancing token generation methods 2025-11-28 00:13:30 +01:00
e1d220596e refactor(home, process): standardize import statements, enhance code readability, and improve error handling in Home and ProcessList components 2025-11-28 00:13:14 +01:00
d45cf7c530 refactor(database): remove obsolete database.worker.js and update database service to utilize Web Worker for IndexedDB operations 2025-11-27 23:20:53 +01:00
90376e364a fix(crypto): cast uint8Array to any in hexToBlob method to resolve TypeScript type issues 2025-11-26 13:17:06 +01:00
0970d1b0da refactor(utils): remove unused copyToClipboard and generateQRCode functions from sp-address.utils.ts 2025-11-26 13:16:58 +01:00
6f508b4b8b feat(vite): add allowedHosts configuration for server access 2025-11-26 13:16:49 +01:00
584735ca02 refactor: remove Prettier; update project metadata, package dependencies, and improve index.html; added author and contributors name 2025-11-26 13:16:33 +01:00
4aa4079ec8 Update README.md to reflect new project structure, installation instructions, and key features for the 4NK Client SDK 2025-11-26 12:52:49 +01:00
04996ceaa5 refactor(core): découpage du service principal en modules métier et centralisation de la configuration globale 2025-11-26 12:40:36 +01:00
3eeef3fc9a refactor: remove unused modal components and related files 2025-11-25 10:42:02 +01:00
d072eb0831 fix: resolve TypeScript strict mode errors in service.ts 2025-11-25 10:38:24 +01:00
225dd27c2c Improved websocket to ensure the reconnection 2025-11-25 09:58:14 +01:00
8512b90d36 Added withToken func to remove the duplicated code 2025-11-25 09:57:57 +01:00
740a29688d Updated vite & ts config 2025-11-25 00:20:33 +01:00
86b488b492 Deleted all the unused files, deadCode & refactoring all the app structure 2025-11-25 00:10:06 +01:00
592759a9b6 Delete secrets flag 2025-11-24 15:42:15 +01:00
95e7044e0a fix(service): Remplacer le polling des clés par un listener pour éviter une race condition 2025-11-15 12:08:32 +01:00
f610a1bfa6 Moved the handleUpdateProcess complexe logic in service 2025-11-15 11:52:44 +01:00
6137b99d56 remove the flag waiting to ensure the reconnexion from the iframe 2025-11-14 10:33:38 +01:00
08bc930675 Add a waiting of __PAIRING_READY flag in handleGetPairingId 2025-11-14 10:21:55 +01:00
2ab9bd2976 fix: Refactoring prepareAndSendPairingTx to ensure auto-pairing workflow 2025-11-14 09:35:18 +01:00
99e7793fbb Fix: Add __PAIRING_READY flag to make handleRequestLink await auto-pairing 2025-11-12 16:18:27 +01:00
8c827944a2 Improved the handleRequestLink function to ensure the iframe authentication 2025-11-12 16:17:25 +01:00
de7a55e7bc revert 3a61ffe7a670e9b7011d41e91b3c3ab864715a93
revert Automated the login with the iframe
2025-11-12 14:25:14 +00:00
bd0c40241f Added a forgotten , 2025-11-12 12:36:39 +01:00
c09fa6f2f8 Add beautiful loader step for the connexion with the iframe 2025-11-12 12:30:19 +01:00
3a61ffe7a6 Automated the login with the iframe 2025-11-12 12:26:17 +01:00
b473ddeefe Improved decrypAttribute func 2025-11-12 09:26:06 +01:00
6419e4e1c9 Fix: an used processId value was undefined 2025-11-10 13:04:08 +01:00
f75c87bd09 Deleted some logs with processId undefined 2025-11-10 13:01:39 +01:00
bd0e3b9114 Grouped the logs for decryptAttribute func 2025-11-07 19:54:49 +01:00
129b7cf32e Deleted an unused func 2025-11-07 19:53:21 +01:00
61c4c7c831 Removed the prdUpdate and approvechange while createProcess 2025-11-07 19:52:51 +01:00
8cc016c2a5 Adding secretsAreCompromised flag 2025-11-05 01:59:23 +01:00
af3503db86 Added blindbit location in nginx conf 2025-11-05 01:38:44 +01:00
26b5eaaf33 Delete unused file 2025-11-05 00:23:13 +01:00
bbb0c12506 Refactoring, upgrading & adding logs in router.ts 2025-11-04 23:04:03 +01:00
614569f5aa Updated the process and process-element page 2025-11-04 23:03:24 +01:00
465a4a3c18 Updated the home page 2025-11-04 23:03:02 +01:00
dddbe04a2d Commented all files in /src/pages/account 2025-11-04 23:02:39 +01:00
f78ed88cb1 updated the import of __dirname 2025-11-04 23:01:06 +01:00
696fc5833c Added "type": "module" in package.json for vite 2025-11-04 23:00:40 +01:00
059f3e2e33 Added the nginx conf for dev2.4nkweb.com 2025-11-04 22:59:42 +01:00
9d30e84bd2 Edited the port for the storage for the dev nginx conf 2025-11-04 22:59:19 +01:00
7ea4ef1920 Add some debug methods for pairing workflow
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 44s
2025-10-31 14:10:26 +01:00
79633ed923 Add logs for handleCreatePairing
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m35s
2025-10-10 11:33:40 +02:00
412c855777 Add promise to wait for relay availability to make createProcess functional
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m23s
2025-10-08 11:10:03 +02:00
d9daa00b32 Improved .gitignore
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m29s
2025-10-08 10:16:36 +02:00
31b88865d7 Deleted package-lock.json 2025-10-08 10:16:21 +02:00
cd4a971d8d Updated .env.exemple 2025-10-08 10:16:10 +02:00
e74ce0aabc Replaced demo by dev3 url
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m10s
2025-10-07 14:08:03 +02:00
Sosthene
0d473cf3d1 [bug] Update device blockheight right after initialization to prevent race condition
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m25s
2025-09-13 08:12:30 +02:00
Sosthene
457994c506 Updating testData in storage
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m15s
2025-09-13 07:34:43 +02:00
Sosthene
5fc485e233 Fix checkConnections, also check more aggressively 2025-09-13 07:34:43 +02:00
Sosthene
0d934e7b6e [bug] coerce potention undefined to null as return value of getObject() 2025-09-13 07:34:43 +02:00
02d28d46bb Add .env.exemple file
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m8s
2025-09-12 12:33:06 +02:00
723f4d5d85 Add .env to gitignore
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m11s
2025-09-12 12:30:27 +02:00
omaroughriss
6f9fa60e2f Use env variables to config urls
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m2s
2025-09-10 17:41:01 +02:00
omaroughriss
e729e32b35 Add handshakeMsg log
Some checks failed
Build and Push to Registry / build-and-push (push) Has been cancelled
2025-09-10 16:24:06 +02:00
omaroughriss
e4681f91e4 Merge branch 'dev' of https://git.4nkweb.com/4nk/ihm_client into dev
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m4s
2025-09-10 15:25:07 +02:00
omaroughriss
6363ec1189 Update relay handshake verification to use peer list or sp adresses 2025-09-10 15:25:05 +02:00
Sosthene
c8ac815e2b Refactor handleHandshakeMsg
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m11s
2025-09-08 22:00:36 +02:00
Sosthene
ef31cba983 Improve checkConnections to prevent no shared secrets 2025-09-08 22:00:36 +02:00
Omar Oughriss
47c7d31249 Rename workflow
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m18s
2025-09-08 16:53:44 +02:00
Omar Oughriss
ede8d95fd1 Add "dev" tagged image
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m17s
2025-09-08 16:44:33 +02:00
Omar Oughriss
0fc7b6e4c3 Update Dockerfile to use branche dev of sdk_client 2025-09-08 16:44:02 +02:00
Sosthene
3f64369852 [bug] Fix wrong api calls for storeData 2025-09-08 15:56:34 +02:00
Sosthene
e8c2d1a05a Get the storage urls from diffs 2025-09-08 15:56:14 +02:00
Sosthene
63ee4ce719 Merge branch 'bug_scan_blocks_update' into dev 2025-09-05 07:59:09 +02:00
Sosthene
e0e186f4f4 Refactor pairing logic
Account for the fact that we need the pairing id earlier now
2025-09-05 07:57:57 +02:00
Sosthene
bfca596e8b Adapt storage to new api 2025-09-05 07:57:50 +02:00
Sosthene
acb9739a80 [bug] Now save device after scan_blocks 2025-09-02 13:30:52 +02:00
Sosthene
c422881cd1 [bug] Failed to update tips 2025-09-02 13:30:52 +02:00
Sosthene
19da967605 Remove unnecessary async 2025-08-25 01:32:24 +02:00
Sosthene
d4223ce604 [bug] Correct args for parse_cipher 2025-08-25 01:31:35 +02:00
Sosthene
420979e63e [bug] Prevent duplicated addresses when sharing secrets 2025-08-25 01:30:58 +02:00
Sosthene
1c92a40984 [bug] remove test code 2025-08-23 16:10:11 +02:00
Sosthene
046eef18e6 Merge branch 'scan_blocks' into dev 2025-08-23 16:05:03 +02:00
Sosthene
2ba7be8dbb Call updateDeviceBlockHeight from router 2025-08-23 16:04:34 +02:00
Sosthene
77d9c1ad43 Add updateDeviceBlockHeight 2025-08-23 16:03:49 +02:00
Sosthene
3ce412d814 Keep currentBlockHeight in services 2025-08-23 16:02:31 +02:00
Sosthene
7100eda272 [bug] update parse_new_tx 2025-08-23 16:02:06 +02:00
Sosthene
1a3a2dbef1 Remove pdf code 2025-08-23 16:00:35 +02:00
Sosthene
76a1d38e09 Merge branch 'add_create_pairing' into dev 2025-08-23 15:59:17 +02:00
Sosthene
8a0a8e2df2 Add handleCreatePairing 2025-08-23 15:58:30 +02:00
Sosthene
48194dd2de Add CREATE_PAIRING MessageType 2025-08-23 15:55:39 +02:00
Sosthene
8e9d7f0c76 remove wasm processes cache 2025-08-23 15:54:22 +02:00
Sosthene
eda7102ded Merge branch 'cicd' into dev 2025-08-23 15:53:47 +02:00
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
111 changed files with 6207 additions and 23575 deletions

3
.env
View File

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

4
.env.exemple Normal file
View File

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

44
.github/workflows/dev.yml vendored Normal file
View File

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

98
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,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 # Installation des dépendances nécessaires pour la compilation
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 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 # Installation de wasm-pack
USER node RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Configuration SSH basique
RUN mkdir -p /root/.ssh && \
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# On se place dans le bon répertoire parent
WORKDIR /build
# Copie du projet ihm_client
COPY . ihm_client/
# Clonage du sdk_client au même niveau que ihm_client en utilisant la clé SSH montée
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
# Build du WebAssembly avec accès SSH pour les dépendances
WORKDIR /build/sdk_client
RUN --mount=type=ssh wasm-pack build --out-dir ../ihm_client/pkg --target bundler --dev
FROM node:20-alpine
WORKDIR /app WORKDIR /app
CMD ["npm", "start"] # Installation des dépendances nécessaires
# "--disable-host-check", "--host", "0.0.0.0", "--ssl", "--ssl-cert", "/ssl/certs/site.crt", "--ssl-key", "/ssl/private/site.dec.key"] RUN 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"]

128
README.md
View File

@ -1,52 +1,90 @@
# ihm_client # 4NK Client SDK (Iframe & Standalone)
Une application **Web5 décentralisée** construite avec **TypeScript**, **Vite**, **Web Components** et **WebAssembly (Rust)**.
Cette application est conçue pour fonctionner de manière autonome ou intégrée via Iframe en tant que SDK pour des applications tierces.
## 🏗 Architecture
## HOW TO START Le projet suit une architecture modulaire stricte (**Domain-Driven Design**) :
1 - clone sdk_common, commit name "doc pcd" from 28.10.2024 - **`src/services/core`** : Services bas niveau (Réseau `network.service.ts`, SDK WASM `sdk.service.ts`).
2 - clone sdk_client, commit name "Ignore messages" from 17.10.2024 - **`src/services/domain`** : Logique métier (Wallet `wallet.service.ts`, Process `process.service.ts`, Crypto `crypto.service.ts`).
3 - clone ihm_client_test3 - **`src/services/service.ts`** : Façade principale (Singleton) orchestrant les sous-services.
4 - cargo build in sdk_common - **`src/services/iframe-controller.service.ts`** : Contrôleur dédié à la communication API via `postMessage` (mode Iframe).
5 - cargo run in sdk_client - **`src/service-workers`** : Gestionnaires d'arrière-plan modulaires (Base de données, Cache).
6 - npm run build_wasm in ihm_client_test3 - **`src/pages` & `src/components`** : Interface utilisateur basée sur des Web Components natifs (Shadow DOM) et un design system "Glassmorphism".
7 - npm run start in ihm_client_test3 - **`src/config`** : Configuration centralisée (`constants.ts`).
## USER STORIES ## 🚀 Démarrage Rapide
1 - I can login with my adress device ### Prérequis
2 - I can login with QR code - **Node.js** (v20+)
3 - J'accède à la page Process après ma connexion - **Rust & Cargo** (pour la compilation WASM)
4 - Dans l'interface Process, je peux sélectionner un processus avec sa zone - **Nginx** (pour la production ou le reverse-proxy local)
5 - Je reçois des notifications dans la page Process
6 - Dans le menu, je peux importer mes données au format JSON
7 - Dans le menu, je peux accèder à la page Account
8 - Dans la page Account, je peux cliquer sur mon profil via la bulle de profil en haut à gauche et une popup de profil s'ouvre
9 - Dans la popup de profil, je peux voir mes informations personnelles et modifier certaines d'entre elles dont le nom, le prénom
10 - Dans la popup de profil, je peux changer ma photo de profil
11 - Dans la popup de profil, je peux fermer la popup en cliquant sur le bouton "X" en haut à droite de la popup
12 - Dans la popup de profil, je peux cliquer sur le bouton "Export User Data", ce qui me génère un fichier JSON
13 - Dans la popup de profil, je peux cliquer sur le bouton "Delete Account", ce qui me demande à valider mon choix
14 - Dans la popup de profil, je peux cliquer sur le bouton "Logout", ce qui me déconnecte
15 - Dans la popup de profil, je peux cliquer sur le bouton "Export Recovery", ce qui me demandera de confirmer mon choix ou d'annuler, si je confirme, je dois retenir et écrire les 4 mots de récupération, le bouton ne sera plus accessible après cela
16 - Dans l'onglet Pairing de la page Account, je peux ajouter un nouveau "device" en cliquant sur le bouton "Add Device"
17 - Dans l'onglet Pairing de la page Account, je peux supprimer un "device" en cliquant sur l'emoji de la poubelle à côté du device que je souhaite supprimer
18 - Dans l'onglet Pairing de la page Account, je peux cliquer sur le bouton "Scan QR Code" pour scanner le QR Code d'un nouveau device
19 - Dans l'onglet Pairing de la page Account, je peux renommer un "Device" en cliquant sur son nom et en modifiant le nom
20 - Dans l'onglet Wallet de la page Account, je peux ajouter un nouveau "wallet" en cliquant sur le bouton "Add a line"
21 - Dans l'onglet Process de la page Account, je peux voir les Process disponibles et voir leur notifications en cliquant sur sur la sonnette à côté du processus
22 - Dans l'onglet Data de la page Account, je peux voir les données importées
23 - Je peux voir le contrat associé à une Data en cliquant sur le contrat dans la ligne de la Data
24 - Dans le menu je peux accèder à la page Chat
25 - Dans la page Chat, je peux voir les Processus
26 - Dans les Processus, je peux voir utilisateurs assignés à un rôle
27 - Dans les Processus, je peux envoyer des messages et des documents en cliquant sur le nom d'un utilisateur en en envoyant "send"
28 - Dans le menu je peux accèder à la page "Signatures"
29 - Je peux voir les documents à signer et vierge en cliquand sur l'emoji ⚙️ à côté du processus
30 - En cliquand sur l'onglet d'un processus, je peux voir les rôles assignés à un utilisateur en cliquant dessus
31 - En cliquand sur l'emoji 📁 à côté d'un rôle, je peux voir les documents associés à ce rôle
32 - Dans la vue des documents associés à un rôle, je peux créer un évènement de nouvelle signature pour tous les rôles associés à ce Processus, avec le bouton "New Request"
33 - En cliquant sur le bouton "New Request", une nouvelle fenêtre s'ouvre pour me permettre de rentrer la description, la visibilité, la date d'échéance, importer des documents, voir les signataires et "Request"
34 - Dans le menu, je peux me déconnecter avec le bouton "Disconnect"
## TO DO ### Installation
1. **Compiler le module WASM :**
```bash
npm run build_wasm
```
*Note : Cette commande compile le code Rust situé dans `../sdk_client` vers `./pkg`.*
2. **Installer les dépendances JS :**
```bash
npm install
```
3. **Lancer en développement :**
```bash
npm run start
```
L'application sera accessible sur `http://localhost:3003`.
## 📦 Build & Production
Pour créer une version de production optimisée dans le dossier `dist/` :
```bash
npm run build
```
### Déploiement Nginx
Utilisez les fichiers de configuration fournis à la racine :
- **`nginx.dev.conf`** : Pour le développement local (proxy vers Vite).
- **`nginx.prod.conf`** : Pour la production (SSL, Headers de sécurité, Service-Worker-Allowed).
## 🔧 Configuration
La configuration de l'application est centralisée dans `src/config/constants.ts`.
Vous pouvez surcharger les URLs via un fichier `.env` à la racine :
| Variable | Description |
| :--- | :--- |
| `VITE_BASEURL` | URL de base de l'infrastructure. |
| `VITE_BOOTSTRAPURL` | URL du relais WebSocket. |
| `VITE_STORAGEURL` | URL du stockage distant. |
| `VITE_BLINDBITURL` | URL de l'indexeur Bitcoin. |
## 🧪 Fonctionnalités Clés
### 1\. Auto-Healing WebSocket
Le service réseau maintient la connexion active avec un système de "Heartbeat" et de reconnexion exponentielle automatique. Aucune action utilisateur requise en cas de coupure réseau.
### 2\. Iframe Persistence & Performance
En mode Iframe, l'application charge son état en mémoire et ne nécessite **aucun rafraîchissement**.
- Stratégie **Cache-First** : Les requêtes `GET_PROCESSES` répondent instantanément via le cache mémoire.
- **Verrou d'initialisation** : Empêche les conflits si l'iframe est rechargée par erreur.
### 3\. Service Worker Modulaire
Le fichier `src/service-workers/sw.ts` agit comme un point d'entrée maître, important dynamiquement la logique de base de données (`database.ts`). Il inclut une logique de nettoyage automatique des anciens workers ("Zombie Killer").
### 4\. UX Moderne
Interface "Glassmorphism" responsive utilisant CSS Grid pour un Layout (App Shell) stable et des Web Components natifs pour l'isolation des styles (Shadow DOM).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -1,26 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="author" content="4NK"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="4NK Web5 Platform"> <meta name="author" content="Nicolas Cantu, Sosthene, Omar, Titouan">
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style/4nk.css">
<script src="https://unpkg.com/html5-qrcode"></script>
<title>4NK Application</title> <title>4NK Application</title>
<link rel="stylesheet" href="/src/assets/styles/style.css" />
</head> </head>
<body> <body>
<div id="header-container"></div> <app-layout>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution --> <div id="header-slot" slot="header"></div>
</div>
<!-- <script type="module" src="/src/index.ts"></script> --> <div id="app-container" slot="content" class="container"></div>
<script type="module">
import { init } from '/src/router.ts'; </app-layout>
(async () => {
await init(); <script type="module" src="/src/main.ts"></script>
})();
</script>
</body> </body>
</html> </html>

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:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /api/ {
proxy_pass http://localhost:8091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS headers
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
}
}

99
nginx.prod.conf Normal file
View File

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

7185
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

202
public/data.worker.js Normal file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,597 +0,0 @@
/* Styles de base */
:root {
--primary-color: #3A506B;
/* Bleu métallique */
--secondary-color: #B0BEC5;
/* Gris acier */
--accent-color: #D68C45;
/* Cuivre */
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
/* 4NK NAVBAR */
.brand-logo {
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
.nav-wrapper {
position: fixed;
background: radial-gradient(circle, white, var(--primary-color));
display: flex;
justify-content: space-between;
align-items: center;
color: #37474F;
height: 9vh;
width: 100vw;
left: 0;
top: 0;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
}
/* Icônes de la barre de navigation */
.nav-right-icons {
display: flex;
}
.notification-bell,
.burger-menu {
height: 20px;
width: 20px;
margin-right: 1rem;
cursor: pointer;
}
.notification-container {
position: relative;
/* Conserve la position pour le notification-board */
display: inline-flex;
align-items: center;
}
.notification-board {
position: absolute;
/* Position absolue pour le placer par rapport au container */
top: 40px;
right: 0;
background-color: white;
border: 1px solid #ccc;
padding: 10px;
width: 200px;
max-height: 300px;
overflow-y: auto;
/* Scroll si les notifications dépassent la taille */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
/* Définit la priorité d'affichage au-dessus des autres éléments */
display: none;
/* Par défaut, la notification est masquée */
}
.notification-item{
cursor: pointer;
}
.notification-badge {
position: absolute;
top: -18px;
right: 35px;
background-color: red;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
display: none;
/* S'affiche seulement lorsqu'il y a des notifications */
z-index: 10;
}
/* Par défaut, le menu est masqué */
#menu {
display: none;
/* Menu caché par défaut */
transition: display 0.3s ease-in-out;
}
.burger-menu {
cursor: pointer;
}
/* Icône burger */
#burger-icon {
cursor: pointer;
}
.menu-content {
display: none;
position: absolute;
top: 3.4rem;
right: 1rem;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
overflow: hidden;
}
.menu-content a {
display: block;
padding: 10px 20px;
text-decoration: none;
color: #333;
border-bottom: 1px solid #e0e0e0;
&:hover {
background-color: rgba(26, 28, 24, .08);
}
}
.menu-content a:last-child {
border-bottom: none;
}
/* Ajustement pour la barre de navigation fixe */
.container {
display: flex;
flex: 1;
height: 90vh;
margin-top: 9vh;
margin-left: -1%;
text-align: left;
width: 100vw;
}
/* Liste des groupes */
.group-list {
width: 25%;
background-color: #1f2c3d;
color: white;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
border-right: 2px solid #2c3e50;
flex-shrink: 0;
padding-right: 10px;
height: 91vh;
}
.group-list ul {
cursor: pointer;
list-style: none;
padding: 0;
padding-right: 10px;
margin-left: 20px;
}
.group-list li {
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
background-color: #273646;
cursor: pointer;
transition: background-color 0.3s, box-shadow 0.3s;
}
.group-list li:hover {
background-color: #34495e;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.group-list .member-container {
position: relative;
}
.group-list .member-container button {
margin-left: 40px;
padding: 5px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
border-radius: 50px;
position: absolute;
top: -25px;
right: -25px;
}
.group-list .member-container button:hover {
background: var(--accent-color)
}
/* Zone de chat */
.chat-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
}
/* En-tête du chat */
.chat-header {
background-color: #34495e;
color: white;
padding: 15px;
font-size: 20px;
font-weight: bold;
border-radius: 10px 10px 0 0;
text-align: center;
}
/* Messages */
.messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f1f1f1;
border-top: 1px solid #ddd;
}
.message-container {
display: flex;
margin: 8px;
}
.message-container .message {
align-self: flex-start;
}
.message-container .message.user {
align-self: flex-end;
margin-left: auto;
color: white;
}
.message {
max-width: 70%;
padding: 10px;
border-radius: 12px;
background:var(--secondary-color);
margin: 2px 0;
}
/* Messages de l'utilisateur */
.message.user {
background: #2196f3;
color: white;
}
.message-time {
font-size: 0.7em;
opacity: 0.7;
margin-left: 0px;
margin-top: 5px;
}
/* Amélioration de l'esthétique des messages */
/* .message.user:before {
content: '';
position: absolute;
top: 10px;
right: -10px;
border: 10px solid transparent;
border-left-color: #3498db;
} */
/* Zone de saisie */
.input-area {
padding: 10px;
background-color: #bdc3c7;
display: flex;
align-items: center;
border-radius: 10px;
margin: 1%;
/* Alignement vertical */
}
.input-area input[type="text"] {
flex: 1;
/* Prend l'espace restant */
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.input-area .attachment-icon {
margin: 0 10px;
cursor: pointer;
display: flex;
align-items: center;
}
.input-area button {
padding: 10px;
margin-left: 10px;
background-color: #2980b9;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.input-area button:hover {
background-color: #1f608d;
}
.tabs {
display: flex;
margin: 20px 0px;
gap: 10px;
}
.tabs button {
padding: 10px 20px;
cursor: pointer;
background: var(--primary-color);
color: white;
border: 0px solid var(--primary-color);
margin-right: 5px;
border-radius: 10px;
}
.tabs button:hover {
background: var(--secondary-color);
color: var(--primary-color);
}
/* Signature */
.signature-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
background-color:#f1f1f1;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
margin: 1% 0% 0.5% 1%;
transition: all 1s ease 0.1s;
visibility: visible;
}
.signature-area.hidden {
opacity: 0;
visibility: hidden;
display: none;
pointer-events: none;
}
.signature-header {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color);
color: white;
border-radius: 10px 10px 0 0;
padding-left: 4%;
}
.signature-content {
padding: 10px;
background-color: var(--secondary-color);
color: var(--primary-color);
height: 100%;
border-radius: 10px;
margin: 1%;
display: flex;
flex-direction: column;
align-items: center;
}
.signature-description {
height: 20%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-description li {
margin: 1% 0% 1% 0%;
list-style: none;
padding: 2%;
border-radius: 10px;
background-color: var(--primary-color);
color: var(--secondary-color);
width: 20%;
text-align: center;
cursor: pointer;
font-weight: bold;
margin-right: 2%;
overflow: auto;
}
.signature-description li .member-list {
margin-left: -30%;
}
.signature-description li .member-list li {
width: 100%;
}
.signature-description li .member-list li:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
.signature-documents {
height: 80%;
width: 100%;
margin: 0% 10% 0% 10%;
overflow: auto;
display: flex;
}
.signature-documents-header {
display: flex;
width: 100%;
height: 15%;
align-items: center;
}
#request-document-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
margin-left: 5%;
font-weight: bold;
}
#request-document-button:hover {
background-color: var(--accent-color);
font-weight: bold;
}
#close-signature {
cursor: pointer;
align-items: center;
margin-left: auto;
margin-right: 2%;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
border: none;
padding: -3%;
margin-top: -5%;
font-size: 1em;
font-weight: bold;
}
#close-signature:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
/* REQUEST MODAL */
.request-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: var(--secondary-color);
padding: 20px;
border-radius: 8px;
position: relative;
min-width: 300px;
}
.close-modal {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 1.5em;
cursor: pointer;
font-weight: bold;
}
.close-modal:hover {
color: var(--accent-color);
}
.modal-members {
display: flex;
justify-content: space-between;
}
.modal-members ul li{
list-style: none;
}
.file-upload-container {
margin: 10px 0;
}
.file-list {
margin-top: 10px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
margin: 5px 0;
background: var(--background-color-secondary);
border-radius: 4px;
}
.remove-file {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0 5px;
}
.remove-file:hover {
color: var(--error-color);
}
#message-input {
width: 100%;
height: 50px;
resize: none;
padding: 10px;
box-sizing: border-box;
overflow: auto;
max-width: 100%;
border-radius: 10px;
}
/* Responsive */
@media screen and (max-width: 768px) {
.group-list {
display: none;
/* Masquer la liste des groupes sur les petits écrans */
}
.chat-area {
margin: 0;
}
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: var(--primary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}

File diff suppressed because it is too large Load Diff

View File

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

69
src/App.ts Normal file
View File

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

133
src/assets/styles/style.css Normal file
View File

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

242
src/components/header/Header.ts Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

31
src/config/constants.ts Normal file
View File

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

10
src/decs.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +0,0 @@
export interface IProcess {
id: number;
name: string;
description: string;
icon?: string;
zoneList: IZone[];
}
export interface IZone {
id: number;
name: string;
path: string;
// Est-ce que la zone a besoin d'une icone ?
icon?: string;
}
export interface INotification {
id: number;
title: string;
description: string;
sendToNotificationPage?: boolean;
path?: string;
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

275
src/pages/home/Home.ts Normal file
View File

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

View File

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

View File

@ -1,42 +1,51 @@
<div class="title-container"> <div class="home-layout">
<h1>Create Account / New Session</h1>
</div>
<div class="tab-container"> <div id="iframe-loader" class="loader-overlay">
<div class="tabs"> <div class="loader-content glass-panel">
<div class="tab active" data-tab="tab1">Create an account</div> <div class="spinner"></div>
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div> <div id="loader-steps-container">
<p class="loader-step active">Démarrage du système...</p>
</div>
</div> </div>
</div> </div>
<div class="page-container"> <div id="main-content" class="auth-container" style="display: none;">
<div id="tab1" class="card tab-content active">
<div class="card-description">Create an account :</div>
<div class="pairing-request"></div>
<!-- <div class="card-image qr-code">
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
</div> -->
<button id="createButton" class="create-btn"></button>
</div>
<div class="separator"></div>
<div id="tab2" class="card tab-content">
<div class="card-description">Add a device for an existing member :</div>
<div class="card-image camera-card">
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
<button id="scan-btn" onclick="scanDevice()">Scan</button>
<div class="qr-code-scanner">
<div id="qr-reader" style="width: 200px; display: contents"></div>
<div id="qr-reader-results"></div>
</div>
</div>
<p>Or</p>
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
<div id="emoji-display-2"></div> -->
<div class="card-description">Chose a member :</div>
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
<!-- Options -->
</select>
<button id="okButton" style="display: none">OK</button> <div class="auth-card glass-panel">
<div class="auth-header">
<h1>Bienvenue</h1>
<p class="subtitle">Connectez votre appareil ou créez un compte</p>
</div>
<div class="tabs-nav">
<button class="tab-btn active" data-tab="tab2">Connexion</button>
<button class="tab-btn" data-tab="tab1">Nouveau Compte</button>
</div>
<div id="tab2" class="tab-content active">
<div class="input-group">
<label>Sélectionner un membre</label>
<div class="select-wrapper">
<select id="memberSelect"></select>
</div>
</div>
<div class="my-address-display">
<span>Mon ID :</span>
<span class="emoji-display">...</span>
</div>
<button id="okButton" class="btn w-full mt-4">Se Connecter</button>
</div>
<div id="tab1" class="tab-content">
<div class="qr-section">
<div class="qr-code">
<img src="" alt="Scan QR" />
</div>
<p class="pairing-request"></p>
</div>
<button id="createButton" class="btn w-full mt-4">Créer un compte</button>
</div>
</div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

310
src/pages/process/ProcessList.ts Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

64
src/router/index.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

50
src/types/index.ts Normal file
View File

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

View File

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

View File

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

View File

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

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