feat: intégration complète de ihm_client dans l'infrastructure 4NK_node - Ajout du service ihm_client au docker-compose.yml - Configuration des variables d'environnement pour la communication avec les SDK relays - Ajout du volume ihm_client_logs - Création des scripts de démarrage start-ihm-client.sh et start-4nk-node-with-ui.sh - Interface utilisateur accessible sur http://localhost:8080
Some checks failed
CI - 4NK Node / Code Quality (push) Failing after 31s
CI - 4NK Node / Unit Tests (push) Failing after 29s
CI - 4NK Node / Integration Tests (push) Successful in 26s
CI - 4NK Node / Security Tests (push) Failing after 28s
CI - 4NK Node / Docker Build & Test (push) Failing after 10s
CI - 4NK Node / Documentation Tests (push) Successful in 4s
CI - 4NK Node / Performance Tests (push) Successful in 33s
CI - 4NK Node / Notify (push) Failing after 2s
Some checks failed
CI - 4NK Node / Code Quality (push) Failing after 31s
CI - 4NK Node / Unit Tests (push) Failing after 29s
CI - 4NK Node / Integration Tests (push) Successful in 26s
CI - 4NK Node / Security Tests (push) Failing after 28s
CI - 4NK Node / Docker Build & Test (push) Failing after 10s
CI - 4NK Node / Documentation Tests (push) Successful in 4s
CI - 4NK Node / Performance Tests (push) Successful in 33s
CI - 4NK Node / Notify (push) Failing after 2s
This commit is contained in:
parent
425fb36dc9
commit
7610ebcb69
@ -218,6 +218,34 @@ services:
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
ihm_client:
|
||||
build:
|
||||
context: ./ihm_client
|
||||
dockerfile: Dockerfile
|
||||
container_name: 4nk-ihm-client
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
- SDK_RELAY_WS_URL=ws://sdk_relay_1:8090
|
||||
- SDK_RELAY_HTTP_URL=http://sdk_relay_1:8091
|
||||
- BITCOIN_RPC_URL=http://bitcoin:18443
|
||||
- BLINDBIT_URL=http://blindbit:8000
|
||||
volumes:
|
||||
- ihm_client_logs:/var/log/nginx
|
||||
networks:
|
||||
- btcnet
|
||||
depends_on:
|
||||
- sdk_relay_1
|
||||
- sdk_relay_2
|
||||
- sdk_relay_3
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--timeout=5", "--spider", "http://localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
bitcoin_data:
|
||||
name: 4nk_node_bitcoin_data
|
||||
@ -229,6 +257,8 @@ volumes:
|
||||
name: 4nk_node_sdk_relay_2_data
|
||||
sdk_relay_3_data:
|
||||
name: 4nk_node_sdk_relay_3_data
|
||||
ihm_client_logs:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
btcnet:
|
||||
|
53
ihm_client/Dockerfile
Normal file
53
ihm_client/Dockerfile
Normal file
@ -0,0 +1,53 @@
|
||||
# Dockerfile optimisé pour l'intégration dans 4NK_node
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Installation des dépendances système
|
||||
RUN apk update && apk add --no-cache \
|
||||
git \
|
||||
build-base \
|
||||
python3 \
|
||||
make \
|
||||
g++
|
||||
|
||||
# Copie des fichiers de dépendances
|
||||
COPY package*.json ./
|
||||
|
||||
# Installation des dépendances
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copie du code source
|
||||
COPY . .
|
||||
|
||||
# Build de l'application
|
||||
RUN npm run build
|
||||
|
||||
# Image de production
|
||||
FROM nginx:alpine
|
||||
|
||||
# Installation de Node.js pour les scripts de démarrage
|
||||
RUN apk update && apk add --no-cache nodejs npm
|
||||
|
||||
# Copie des fichiers buildés
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /app/package*.json /app/
|
||||
|
||||
# Copie de la configuration nginx optimisée pour 4NK_node
|
||||
COPY nginx.4nk-node.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Script de démarrage
|
||||
COPY start-4nk-node.sh /start-4nk-node.sh
|
||||
RUN chmod +x /start-4nk-node.sh
|
||||
|
||||
# Exposition des ports
|
||||
EXPOSE 80 3003
|
||||
|
||||
# Variables d'environnement pour 4NK_node
|
||||
ENV SDK_RELAY_WS_URL=ws://sdk_relay_1:8090
|
||||
ENV SDK_RELAY_HTTP_URL=http://sdk_relay_1:8091
|
||||
ENV BITCOIN_RPC_URL=http://bitcoin:18443
|
||||
ENV BLINDBIT_URL=http://blindbit:8000
|
||||
|
||||
# Point d'entrée
|
||||
CMD ["/start-4nk-node.sh"]
|
26
ihm_client/index.html
Executable file
26
ihm_client/index.html
Executable file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="author" content="4NK">
|
||||
<meta name="description" content="4NK Web5 Platform">
|
||||
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="./style/4nk.css">
|
||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
||||
<title>4NK Application</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-container"></div>
|
||||
<div id="containerId" class="container">
|
||||
<!-- 4NK Web5 Solution -->
|
||||
</div>
|
||||
<!-- <script type="module" src="/src/index.ts"></script> -->
|
||||
<script type="module">
|
||||
import { init } from '/src/router.ts';
|
||||
(async () => {
|
||||
await init();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
96
ihm_client/nginx.conf
Normal file
96
ihm_client/nginx.conf
Normal file
@ -0,0 +1,96 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gestion des fichiers statiques
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Headers de sécurité
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
|
||||
# Proxy vers sdk_relay WebSocket
|
||||
location /ws/ {
|
||||
proxy_pass http://sdk_relay_1: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_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
}
|
||||
|
||||
# Proxy vers sdk_relay HTTP API
|
||||
location /api/ {
|
||||
proxy_pass http://sdk_relay_1: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;
|
||||
|
||||
# Gestion des requêtes OPTIONS
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
|
||||
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With";
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy vers Bitcoin Core RPC (si nécessaire)
|
||||
location /bitcoin/ {
|
||||
proxy_pass http://bitcoin:18443/;
|
||||
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;
|
||||
|
||||
# Authentification basique pour Bitcoin RPC
|
||||
auth_basic "Bitcoin RPC";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
}
|
||||
|
||||
# Proxy vers Blindbit (si nécessaire)
|
||||
location /blindbit/ {
|
||||
proxy_pass http://blindbit:8000/;
|
||||
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;
|
||||
}
|
||||
|
||||
# Cache pour les assets statiques
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Gestion des erreurs
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# Logs
|
||||
access_log /var/log/nginx/ihm_client_access.log;
|
||||
error_log /var/log/nginx/ihm_client_error.log;
|
||||
}
|
51
ihm_client/package.json
Executable file
51
ihm_client/package.json
Executable file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "sdk_client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build_wasm": "./scripts/setup-remote-deps.sh",
|
||||
"cleanup_deps": "./scripts/cleanup-deps.sh",
|
||||
"start": "vite --host 0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"deploy": "sudo cp -r dist/* /var/www/html/",
|
||||
"prettify": "prettier --config ./.prettierrc --write \"src/**/*{.ts,.html,.css,.js}\"",
|
||||
"build:dist": "tsc -p tsconfig.build.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/elements": "^19.0.1",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"axios": "^1.11.0",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"jose": "^6.0.13",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"sweetalert2": "^11.22.4",
|
||||
"vite-plugin-copy": "^0.1.6",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-wasm": "^3.3.0"
|
||||
}
|
||||
}
|
BIN
ihm_client/public/assets/4nk_image.png
Executable file
BIN
ihm_client/public/assets/4nk_image.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
BIN
ihm_client/public/assets/4nk_revoke.jpg
Executable file
BIN
ihm_client/public/assets/4nk_revoke.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
ihm_client/public/assets/bgd.webp
Executable file
BIN
ihm_client/public/assets/bgd.webp
Executable file
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
BIN
ihm_client/public/assets/camera.jpg
Executable file
BIN
ihm_client/public/assets/camera.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
34
ihm_client/public/assets/home.js
Executable file
34
ihm_client/public/assets/home.js
Executable file
@ -0,0 +1,34 @@
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById(tab.getAttribute('data-tab')).classList.add('active');
|
||||
});
|
||||
});
|
||||
function toggleMenu() {
|
||||
var menu = document.getElementById('menu');
|
||||
if (menu.style.display === 'block') {
|
||||
menu.style.display = 'none';
|
||||
} else {
|
||||
menu.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
//// Modal
|
||||
function openModal() {
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('modal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
BIN
ihm_client/public/assets/qr_code.png
Executable file
BIN
ihm_client/public/assets/qr_code.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
877
ihm_client/public/style/4nk.css
Executable file
877
ihm_client/public/style/4nk.css
Executable file
@ -0,0 +1,877 @@
|
||||
: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;
|
||||
}
|
||||
|
||||
/* Confirmation Modal Styles */
|
||||
#confirmation-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-confirmation {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-confirmation h3 {
|
||||
margin-bottom: 15px;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.modal-confirmation p {
|
||||
margin: 8px 0;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-footer button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-confirmation h3 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.modal-confirmation p {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
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%;
|
||||
}
|
1507
ihm_client/public/style/account.css
Executable file
1507
ihm_client/public/style/account.css
Executable file
File diff suppressed because it is too large
Load Diff
597
ihm_client/public/style/chat.css
Executable file
597
ihm_client/public/style/chat.css
Executable file
@ -0,0 +1,597 @@
|
||||
/* Styles de base */
|
||||
:root {
|
||||
--primary-color: #3A506B;
|
||||
/* Bleu métallique */
|
||||
--secondary-color: #B0BEC5;
|
||||
/* Gris acier */
|
||||
--accent-color: #D68C45;
|
||||
/* Cuivre */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/* 4NK NAVBAR */
|
||||
|
||||
.brand-logo {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #37474F;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
/* Icônes de la barre de navigation */
|
||||
.nav-right-icons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-container {
|
||||
position: relative;
|
||||
/* Conserve la position pour le notification-board */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
/* Position absolue pour le placer par rapport au container */
|
||||
top: 40px;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
width: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
/* Scroll si les notifications dépassent la taille */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
/* Définit la priorité d'affichage au-dessus des autres éléments */
|
||||
display: none;
|
||||
/* Par défaut, la notification est masquée */
|
||||
}
|
||||
|
||||
.notification-item{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: 35px;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
display: none;
|
||||
/* S'affiche seulement lorsqu'il y a des notifications */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Par défaut, le menu est masqué */
|
||||
#menu {
|
||||
display: none;
|
||||
/* Menu caché par défaut */
|
||||
transition: display 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.burger-menu {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Icône burger */
|
||||
#burger-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Ajustement pour la barre de navigation fixe */
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 90vh;
|
||||
margin-top: 9vh;
|
||||
margin-left: -1%;
|
||||
text-align: left;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
|
||||
/* Liste des groupes */
|
||||
|
||||
.group-list {
|
||||
width: 25%;
|
||||
background-color: #1f2c3d;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
border-right: 2px solid #2c3e50;
|
||||
flex-shrink: 0;
|
||||
padding-right: 10px;
|
||||
height: 91vh;
|
||||
}
|
||||
.group-list ul {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
padding-right: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.group-list li {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background-color: #273646;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.group-list li:hover {
|
||||
background-color: #34495e;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.group-list .member-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.group-list .member-container button {
|
||||
margin-left: 40px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
border-radius: 50px;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: -25px;
|
||||
}
|
||||
|
||||
.group-list .member-container button:hover {
|
||||
background: var(--accent-color)
|
||||
}
|
||||
|
||||
|
||||
/* Zone de chat */
|
||||
.chat-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
}
|
||||
|
||||
/* En-tête du chat */
|
||||
.chat-header {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #f1f1f1;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
}
|
||||
.message-container .message {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-container .message.user {
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 70%;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background:var(--secondary-color);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* Messages de l'utilisateur */
|
||||
.message.user {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.7;
|
||||
margin-left: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* Amélioration de l'esthétique des messages */
|
||||
/* .message.user:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -10px;
|
||||
border: 10px solid transparent;
|
||||
border-left-color: #3498db;
|
||||
} */
|
||||
|
||||
/* Zone de saisie */
|
||||
.input-area {
|
||||
padding: 10px;
|
||||
background-color: #bdc3c7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
/* Alignement vertical */
|
||||
}
|
||||
|
||||
.input-area input[type="text"] {
|
||||
flex: 1;
|
||||
/* Prend l'espace restant */
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.input-area .attachment-icon {
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area button {
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
background-color: #2980b9;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-area button:hover {
|
||||
background-color: #1f608d;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
margin: 20px 0px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: 0px solid var(--primary-color);
|
||||
margin-right: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Signature */
|
||||
.signature-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color:#f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
margin: 1% 0% 0.5% 1%;
|
||||
transition: all 1s ease 0.1s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.signature-area.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.signature-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-left: 4%;
|
||||
}
|
||||
|
||||
.signature-content {
|
||||
padding: 10px;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
margin: 1%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.signature-description {
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signature-description li {
|
||||
margin: 1% 0% 1% 0%;
|
||||
list-style: none;
|
||||
padding: 2%;
|
||||
border-radius: 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--secondary-color);
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 2%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.signature-description li .member-list {
|
||||
margin-left: -30%;
|
||||
}
|
||||
|
||||
.signature-description li .member-list li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-description li .member-list li:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.signature-documents {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
margin: 0% 10% 0% 10%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.signature-documents-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 15%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#request-document-button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
margin-left: 5%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#request-document-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#close-signature {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 2%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: -3%;
|
||||
margin-top: -5%;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#close-signature:hover {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* REQUEST MODAL */
|
||||
.request-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--secondary-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.modal-members {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-members ul li{
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
background: var(--background-color-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.remove-file {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.remove-file:hover {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
#message-input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 768px) {
|
||||
.group-list {
|
||||
display: none;
|
||||
/* Masquer la liste des groupes sur les petits écrans */
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--primary-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--secondary-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color);
|
||||
}
|
1664
ihm_client/public/style/signature.css
Executable file
1664
ihm_client/public/style/signature.css
Executable file
File diff suppressed because it is too large
Load Diff
818
ihm_client/src/4nk.css
Normal file
818
ihm_client/src/4nk.css
Normal file
@ -0,0 +1,818 @@
|
||||
:host {
|
||||
--primary-color: #3a506b;
|
||||
/* Bleu métallique */
|
||||
--secondary-color: #b0bec5;
|
||||
/* Gris acier */
|
||||
--accent-color: #d68c45;
|
||||
/* Cuivre */
|
||||
font-family: Arial, sans-serif;
|
||||
height: 100vh;
|
||||
font-size: 16px;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
background-image: url(../assets/bgd.webp);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-blend-mode: soft-light;
|
||||
height: 100vh;
|
||||
}
|
||||
.message {
|
||||
margin: 30px 0;
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message strong {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/** Modal Css */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 55%;
|
||||
height: 30%;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.confirmation-box {
|
||||
/* margin-top: 20px; */
|
||||
align-content: center;
|
||||
width: 70%;
|
||||
height: 20%;
|
||||
/* padding: 20px; */
|
||||
font-size: 1.5em;
|
||||
color: #333333;
|
||||
top: 5%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
background: radial-gradient(circle, white, var(--primary-color));
|
||||
/* background-color: #CFD8DC; */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #37474f;
|
||||
height: 9vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-shadow:
|
||||
0px 8px 10px -5px rgba(0, 0, 0, 0.2),
|
||||
0px 16px 24px 2px rgba(0, 0, 0, 0.14),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.nav-right-icons {
|
||||
display: flex;
|
||||
.notification-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.notification-bell,
|
||||
.burger-menu {
|
||||
z-index: 3;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -0.7rem;
|
||||
left: -0.8rem;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 2.5px 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.notification-board {
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
min-height: 8rem;
|
||||
background-color: white;
|
||||
right: 0.5rem;
|
||||
display: none;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
|
||||
.notification-element {
|
||||
padding: 0.8rem 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.notification-element:not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
align-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 10px;
|
||||
grid-auto-rows: 10vh 15vh 1fr;
|
||||
}
|
||||
.title-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 2;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
.tab-container {
|
||||
display: none;
|
||||
}
|
||||
.page-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.process-container {
|
||||
grid-column: 3 / 6;
|
||||
grid-row: 3;
|
||||
|
||||
.card {
|
||||
min-width: 40vw;
|
||||
}
|
||||
}
|
||||
.separator {
|
||||
width: 2px;
|
||||
background-color: #78909c;
|
||||
height: 80%;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.process-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 3;
|
||||
}
|
||||
.container {
|
||||
grid-auto-rows: 10vh 15vh 15vh 1fr;
|
||||
}
|
||||
.tab-container {
|
||||
grid-column: 1 / 8;
|
||||
grid-row: 3;
|
||||
}
|
||||
.page-container {
|
||||
grid-column: 2 / 7;
|
||||
grid-row: 4;
|
||||
}
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e0e4d6;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: #6200ea;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.tab.active {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80%;
|
||||
}
|
||||
.modal-content {
|
||||
width: 80%;
|
||||
height: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.emoji-display {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#emoji-display-2 {
|
||||
margin-top: 30px;
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#okButton {
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #d0d0d7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pairing-request {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif;
|
||||
font-size: 14px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
margin-bottom: 2em;
|
||||
cursor: pointer;
|
||||
background-color: #d0d0d7;
|
||||
color: white;
|
||||
border-style: none;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.camera-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
/* height: 200px; */
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #3700b3;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 60vh;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: 0.4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
padding: 20px;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3.4rem;
|
||||
right: 1rem;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-content a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.qr-code-scanner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* QR READER */
|
||||
#qr-reader div {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
#qr-reader div img {
|
||||
top: 15px;
|
||||
right: 25px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* INPUT CSS **/
|
||||
.input-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #eceff1;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 36vw;
|
||||
padding: 10px 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
border-bottom: 2px solid #6200ea;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
position: absolute;
|
||||
margin-top: -0.5em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 10px 0;
|
||||
font-size: 1rem;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
color 0.3s,
|
||||
font-size 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus + .input-label,
|
||||
.input-field:not(:placeholder-shown) + .input-label {
|
||||
transform: translateY(-20px);
|
||||
font-size: 0.8em;
|
||||
color: #6200ea;
|
||||
}
|
||||
|
||||
.input-underline {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #6200ea;
|
||||
transition:
|
||||
width 0.3s,
|
||||
left 0.3s;
|
||||
}
|
||||
|
||||
.input-field:focus ~ .input-underline {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content span {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdown-content span:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/** AUTOCOMPLETE **/
|
||||
|
||||
select[data-multi-select-plugin] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multi-select-component {
|
||||
width: 36vw;
|
||||
padding: 5px 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
-o-transition:
|
||||
border-color ease-in-out 0.15s,
|
||||
box-shadow ease-in-out 0.15s;
|
||||
transition:
|
||||
border-color ease-in-out 0.15s,
|
||||
box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
.multi-select-component:focus-within {
|
||||
box-shadow: inset 0px 0px 0px 2px #78abfe;
|
||||
}
|
||||
|
||||
.multi-select-component .btn-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.multiselect-native-select .multiselect-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-processes {
|
||||
background-color: white;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.selected-wrapper {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #ededed;
|
||||
white-space: nowrap;
|
||||
margin: 1px 5px 5px 0;
|
||||
height: 22px;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-label {
|
||||
max-width: 514px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.selected-wrapper .selected-close {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.49rem;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
padding-right: 4px;
|
||||
opacity: 0.2;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search-container .selected-input {
|
||||
background: none;
|
||||
border: 0;
|
||||
height: 20px;
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.search-container .selected-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown-icon.active {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.search-container .dropdown-icon {
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 0 !important;
|
||||
/* needed */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
/* SVG background image */
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
|
||||
background-position: center;
|
||||
background-size: 10px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.search-container ul {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
z-index: 3;
|
||||
margin-top: 29px;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.search-container ul :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-container ul li {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 8px 29px 2px 12px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
min-height: 31px;
|
||||
}
|
||||
|
||||
.search-container ul li:first-child {
|
||||
border-top: 1px solid #ccc;
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
.search-container ul li:last-child {
|
||||
border-radius: 4px 0px 0 0;
|
||||
}
|
||||
|
||||
.search-container ul li:hover.not-cursor {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search-container ul li:hover {
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
border-color: #adadad;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Adding scrool to select options */
|
||||
.autocomplete-list {
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/**************************************** Process page card ******************************************************/
|
||||
.process-card {
|
||||
min-width: 300px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 40vh;
|
||||
max-height: 60vh;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.process-card-content {
|
||||
text-align: left;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
left: 2vw;
|
||||
width: 90%;
|
||||
.process-title {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.process-element {
|
||||
padding: 0.4rem 0;
|
||||
&:hover {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
.selected-process-zone {
|
||||
background-color: rgba(26, 28, 24, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.process-card-description {
|
||||
padding: 20px;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.process-card-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**************************************** Select Member Home Page ******************************************************/
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
direction: ltr;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.custom-select option {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-select option:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-select::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
36
ihm_client/src/components/header/header.html
Executable file
36
ihm_client/src/components/header/header.html
Executable file
@ -0,0 +1,36 @@
|
||||
<div class="nav-wrapper">
|
||||
<div id="profile-header-container"></div>
|
||||
<div class="brand-logo">4NK</div>
|
||||
<div class="nav-right-icons">
|
||||
<div class="notification-container">
|
||||
<div class="bell-icon">
|
||||
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path
|
||||
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="notification-badge"></div>
|
||||
<div id="notification-board" class="notification-board">
|
||||
<div class="no-notification">No notifications available</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="burger-menu">
|
||||
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
|
||||
</svg>
|
||||
|
||||
<div class="menu-content" id="menu">
|
||||
<!-- <a onclick="unpair()">Revoke</a> -->
|
||||
<a onclick="importJSON()">Import</a>
|
||||
<a onclick="createBackUp()">Export</a>
|
||||
<a onclick="navigate('account')">Account</a>
|
||||
<a onclick="navigate('chat')">Chat</a>
|
||||
<a onclick="navigate('signature')">Signatures</a>
|
||||
<a onclick="navigate('process')">Process</a>
|
||||
<a onclick="disconnect()">Disconnect</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
220
ihm_client/src/components/header/header.ts
Executable file
220
ihm_client/src/components/header/header.ts
Executable file
@ -0,0 +1,220 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
import { INotification } from '../../models/notification.model';
|
||||
import { currentRoute, navigate } from '../../router';
|
||||
import Services from '../../services/service';
|
||||
import { BackUp } from '~/models/backup.model';
|
||||
|
||||
let notifications = [];
|
||||
|
||||
export async function unpair() {
|
||||
const service = await Services.getInstance();
|
||||
await service.unpairDevice();
|
||||
await 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() || [];
|
||||
await 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');
|
||||
|
||||
await 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;
|
14
ihm_client/src/components/login-modal/login-modal.html
Executable file
14
ihm_client/src/components/login-modal/login-modal.html
Executable file
@ -0,0 +1,14 @@
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="confirmation-box">
|
||||
<div class="message">
|
||||
Attempting to pair device with address
|
||||
<strong>{{device1}}</strong>
|
||||
with device with address
|
||||
<strong>{{device2}}</strong>
|
||||
</div>
|
||||
<div>Awaiting pairing validation...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
ihm_client/src/components/login-modal/login-modal.js
Executable file
13
ihm_client/src/components/login-modal/login-modal.js
Executable file
@ -0,0 +1,13 @@
|
||||
import Routing from '/src/services/routing.service.ts';
|
||||
|
||||
const router = await Routing.getInstance();
|
||||
export async function confirmLogin() {
|
||||
router.confirmLogin();
|
||||
}
|
||||
|
||||
export async function closeLoginModal() {
|
||||
router.closeLoginModal();
|
||||
}
|
||||
|
||||
window.confirmLogin = confirmLogin;
|
||||
window.closeLoginModal = closeLoginModal;
|
16
ihm_client/src/components/modal/confirmation-modal.html
Executable file
16
ihm_client/src/components/modal/confirmation-modal.html
Executable file
@ -0,0 +1,16 @@
|
||||
<div id="modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to pair device?<br />
|
||||
Attempting to pair device with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
with device with address <br />
|
||||
<strong>{{device2}}</strong>
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
ihm_client/src/components/modal/confirmation-modal.ts
Executable file
13
ihm_client/src/components/modal/confirmation-modal.ts
Executable file
@ -0,0 +1,13 @@
|
||||
import ModalService from '../../services/modal.service';
|
||||
|
||||
const modalService = await ModalService.getInstance();
|
||||
// export async function confirm() {
|
||||
// modalService.confirmPairing();
|
||||
// }
|
||||
|
||||
export async function closeConfirmationModal() {
|
||||
modalService.closeConfirmationModal();
|
||||
}
|
||||
|
||||
(window as any).confirm = confirm;
|
||||
(window as any).closeConfirmationModal = closeConfirmationModal;
|
14
ihm_client/src/components/modal/creation-modal.html
Normal file
14
ihm_client/src/components/modal/creation-modal.html
Normal file
@ -0,0 +1,14 @@
|
||||
<div id="creation-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Do you want to create a 4NK member?<br />
|
||||
Attempting to create a member with address <br />
|
||||
<strong>{{device1}}</strong> <br />
|
||||
</div>
|
||||
<div class="confirmation-box">
|
||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
ihm_client/src/components/modal/waiting-modal.html
Normal file
8
ihm_client/src/components/modal/waiting-modal.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div id="waiting-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Login</div>
|
||||
<div class="message">
|
||||
Waiting for Device 2...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,73 @@
|
||||
import QrScanner from 'qr-scanner';
|
||||
import Services from '../../services/service';
|
||||
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
|
||||
|
||||
export default class QrScannerComponent extends HTMLElement {
|
||||
videoElement: any;
|
||||
wrapper: any;
|
||||
qrScanner: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.style.position = 'relative';
|
||||
this.wrapper.style.width = '150px';
|
||||
this.wrapper.style.height = '150px';
|
||||
|
||||
this.videoElement = document.createElement('video');
|
||||
this.videoElement.style.width = '100%';
|
||||
document.body?.append(this.wrapper);
|
||||
this.wrapper.prepend(this.videoElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.initializeScanner();
|
||||
}
|
||||
|
||||
async initializeScanner() {
|
||||
if (!this.videoElement) {
|
||||
console.error('Video element not found!');
|
||||
return;
|
||||
}
|
||||
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
|
||||
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
|
||||
highlightScanRegion: true,
|
||||
highlightCodeOutline: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await QrScanner.hasCamera();
|
||||
this.qrScanner.start();
|
||||
this.videoElement.style = 'height: 200px; width: 200px';
|
||||
this.shadowRoot?.appendChild(this.wrapper);
|
||||
} catch (e) {
|
||||
console.error('No camera found or error starting the QR scanner', e);
|
||||
}
|
||||
}
|
||||
|
||||
async onQrCodeScanned(result: any) {
|
||||
console.log(`QR Code detected:`, result);
|
||||
const data = result.data;
|
||||
const scannedUrl = new URL(data);
|
||||
|
||||
// Extract the 'sp_address' parameter
|
||||
const spAddress = scannedUrl.searchParams.get('sp_address');
|
||||
if (spAddress) {
|
||||
// Call the sendPairingTx function with the extracted sp_address
|
||||
try {
|
||||
await prepareAndSendPairingTx();
|
||||
} 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);
|
@ -0,0 +1,70 @@
|
||||
.validation-modal {
|
||||
display: block; /* Show the modal for demo purposes */
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding-top: 60px;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
height: fit-content;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.validation-box {
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.expansion-panel-header {
|
||||
background-color: #e0e0e0;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.expansion-panel-body {
|
||||
display: none;
|
||||
background-color: #fafafa;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.expansion-panel-body pre {
|
||||
background-color: #f6f8fa;
|
||||
padding: 10px;
|
||||
border-left: 4px solid #d1d5da;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.diff {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.diff-side {
|
||||
width: 48%;
|
||||
padding: 10px;
|
||||
}
|
||||
.diff-old {
|
||||
background-color: #fee;
|
||||
border: 1px solid #f00;
|
||||
}
|
||||
.diff-new {
|
||||
background-color: #e6ffe6;
|
||||
border: 1px solid #0f0;
|
||||
}
|
||||
.radio-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
11
ihm_client/src/components/validation-modal/validation-modal.html
Executable file
11
ihm_client/src/components/validation-modal/validation-modal.html
Executable file
@ -0,0 +1,11 @@
|
||||
<div id="validation-modal" class="validation-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">Validate Process {{processId}}</div>
|
||||
<div class="validation-box">
|
||||
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button onclick="validate()">Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
56
ihm_client/src/components/validation-modal/validation-modal.ts
Executable file
56
ihm_client/src/components/validation-modal/validation-modal.ts
Executable file
@ -0,0 +1,56 @@
|
||||
import ModalService from '~/services/modal.service';
|
||||
|
||||
async function validate() {
|
||||
console.log('==> VALIDATE');
|
||||
const modalservice = await ModalService.getInstance();
|
||||
modalservice.closeValidationModal();
|
||||
}
|
||||
|
||||
export async function initValidationModal(processDiffs: any) {
|
||||
console.log("🚀 ~ initValidationModal ~ processDiffs:", processDiffs)
|
||||
for(const diff of processDiffs.diffs) {
|
||||
let diffs = ''
|
||||
for(const value of diff) {
|
||||
diffs+= `
|
||||
<div class="radio-buttons">
|
||||
<label>
|
||||
<input type="radio" name="validation1" value="old" />
|
||||
Keep Old
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="validation1" value="new" />
|
||||
Keep New
|
||||
</label>
|
||||
</div>
|
||||
<div class="diff">
|
||||
<div class="diff-side diff-old">
|
||||
<pre>-${value.previous_value}</pre>
|
||||
</div>
|
||||
<div class="diff-side diff-new">
|
||||
<pre>+${value.new_value}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const state = `
|
||||
<div class="expansion-panel">
|
||||
<div class="expansion-panel-header">State ${diff[0].new_state_merkle_root}</div>
|
||||
<div class="expansion-panel-body">
|
||||
${diffs}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const box = document.querySelector('.validation-box')
|
||||
if(box) box.innerHTML += state
|
||||
}
|
||||
document.querySelectorAll('.expansion-panel-header').forEach((header) => {
|
||||
header.addEventListener('click', function (event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const body = target.nextElementSibling as HTMLElement;
|
||||
if (body?.style) body.style.display = body.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).validate = validate;
|
@ -0,0 +1,42 @@
|
||||
<div id="validation-rule-modal" style="
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
">
|
||||
<div style="
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
">
|
||||
<h2 style="font-size: 1.2rem; font-weight: bold; margin-bottom: 1rem;">
|
||||
Add Validation Rule
|
||||
</h2>
|
||||
|
||||
<label style="display: block; margin-bottom: 0.5rem;">
|
||||
Quorum:
|
||||
<input id="vr-quorum" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||
</label>
|
||||
|
||||
<label style="display: block; margin-bottom: 0.5rem;">
|
||||
Min Sig Member:
|
||||
<input id="vr-minsig" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||
</label>
|
||||
|
||||
<label style="display: block; margin-bottom: 1rem;">
|
||||
Fields (comma-separated):
|
||||
<input id="vr-fields" type="text" placeholder="e.g. field1, field2" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
||||
</label>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
|
||||
<button id="vr-cancel" style="padding: 0.5rem 1rem;">Cancel</button>
|
||||
<button id="vr-submit" style="padding: 0.5rem 1rem; background-color: #4f46e5; color: white; border: none; border-radius: 0.375rem;">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,61 @@
|
||||
export interface ValidationRule {
|
||||
quorum: number;
|
||||
fields: string[];
|
||||
min_sig_member: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and injects the modal HTML into the document if not already loaded.
|
||||
*/
|
||||
export async function loadValidationRuleModal(templatePath: string = '/src/components/validation-rule-modal/validation-rule-modal.html') {
|
||||
if (document.getElementById('validation-rule-modal')) return;
|
||||
|
||||
const res = await fetch(templatePath);
|
||||
const html = await res.text();
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
const modal = tempDiv.querySelector('#validation-rule-modal');
|
||||
if (!modal) {
|
||||
throw new Error('Modal HTML missing #validation-rule-modal');
|
||||
}
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the modal and lets the user input a ValidationRule.
|
||||
* Calls the callback with the constructed rule on submit.
|
||||
*/
|
||||
export function showValidationRuleModal(onSubmit: (rule: ValidationRule) => void) {
|
||||
const modal = document.getElementById('validation-rule-modal')!;
|
||||
const quorumInput = document.getElementById('vr-quorum') as HTMLInputElement;
|
||||
const minsigInput = document.getElementById('vr-minsig') as HTMLInputElement;
|
||||
const fieldsInput = document.getElementById('vr-fields') as HTMLInputElement;
|
||||
|
||||
const cancelBtn = document.getElementById('vr-cancel')!;
|
||||
const submitBtn = document.getElementById('vr-submit')!;
|
||||
|
||||
// Reset values
|
||||
quorumInput.value = '';
|
||||
minsigInput.value = '';
|
||||
fieldsInput.value = '';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
modal.style.display = 'none';
|
||||
};
|
||||
|
||||
submitBtn.onclick = () => {
|
||||
const rule: ValidationRule = {
|
||||
quorum: parseInt(quorumInput.value),
|
||||
min_sig_member: parseInt(minsigInput.value),
|
||||
fields: fieldsInput.value.split(',').map(f => f.trim()).filter(Boolean),
|
||||
};
|
||||
|
||||
modal.style.display = 'none';
|
||||
onSubmit(rule);
|
||||
};
|
||||
}
|
10
ihm_client/src/decs.d.ts
vendored
Normal file
10
ihm_client/src/decs.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
declare class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor();
|
||||
connectedCallback(): void;
|
||||
fetchData(): Promise<void>;
|
||||
set callback(fn: any);
|
||||
get callback(): any;
|
||||
render(): void;
|
||||
}
|
||||
export { AccountComponent };
|
3
ihm_client/src/index.ts
Executable file
3
ihm_client/src/index.ts
Executable file
@ -0,0 +1,3 @@
|
||||
export { default as Services } from './services/service';
|
||||
export { default as Database } from './services/database.service';
|
||||
export { MessageType } from './models/process.model';
|
22
ihm_client/src/interface/groupInterface.ts
Normal file
22
ihm_client/src/interface/groupInterface.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { DocumentSignature } from '~/models/signature.models';
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
roles: Array<{
|
||||
name: string;
|
||||
members: Array<{ id: string | number; name: string }>;
|
||||
documents?: Array<any>;
|
||||
}>;
|
||||
commonDocuments: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
visibility: string;
|
||||
description: string;
|
||||
createdAt?: string | null;
|
||||
deadline?: string | null;
|
||||
signatures?: DocumentSignature[];
|
||||
status?: string;
|
||||
}>;
|
||||
}
|
7
ihm_client/src/interface/memberInterface.ts
Normal file
7
ihm_client/src/interface/memberInterface.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface Member {
|
||||
id: string | number;
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
processRoles?: Array<{ processId: number | string; role: string }>;
|
||||
}
|
30
ihm_client/src/main.ts
Normal file
30
ihm_client/src/main.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { SignatureComponent } from './pages/signature/signature-component';
|
||||
import { SignatureElement } from './pages/signature/signature';
|
||||
/*import { ChatComponent } from './pages/chat/chat-component';
|
||||
import { ChatElement } from './pages/chat/chat';*/
|
||||
import { AccountComponent } from './pages/account/account-component';
|
||||
import { AccountElement } from './pages/account/account';
|
||||
|
||||
export { SignatureComponent, SignatureElement, AccountComponent, AccountElement };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'signature-component': SignatureComponent;
|
||||
'signature-element': SignatureElement;
|
||||
/*'chat-component': ChatComponent;
|
||||
'chat-element': ChatElement;*/
|
||||
'account-component': AccountComponent;
|
||||
'account-element': AccountElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration pour le mode indépendant
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
||||
// Initialiser les composants si nécessaire
|
||||
customElements.define('signature-component', SignatureComponent);
|
||||
customElements.define('signature-element', SignatureElement);
|
||||
/*customElements.define('chat-component', ChatComponent);
|
||||
customElements.define('chat-element', ChatElement);*/
|
||||
customElements.define('account-component', AccountComponent);
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
272
ihm_client/src/mocks/mock-account/constAccountMock.ts
Normal file
272
ihm_client/src/mocks/mock-account/constAccountMock.ts
Normal file
@ -0,0 +1,272 @@
|
||||
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
pairing: 'pairingRows',
|
||||
wallet: 'walletRows',
|
||||
process: 'processRows',
|
||||
data: 'dataRows',
|
||||
};
|
||||
|
||||
// Initialiser le stockage des lignes par défaut dans le localStorage
|
||||
export const defaultRows = [
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
|
||||
column2: '🎊😑🎄😩',
|
||||
column3: 'Laptop',
|
||||
},
|
||||
{
|
||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
|
||||
column2: '🎏🎕😧🌥',
|
||||
column3: 'Phone',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockNotifications: { [key: string]: Notification[] } = {};
|
||||
|
||||
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
|
||||
|
||||
export const mockDataRows = [
|
||||
{
|
||||
column1: 'User Project',
|
||||
column2: 'private',
|
||||
column3: 'User',
|
||||
column4: '6 months',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #123',
|
||||
processName: 'User Process',
|
||||
zone: 'A',
|
||||
},
|
||||
{
|
||||
column1: 'Process Project',
|
||||
column2: 'private',
|
||||
column3: 'Process',
|
||||
column4: '1 year',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #456',
|
||||
processName: 'Process Management',
|
||||
zone: 'B',
|
||||
},
|
||||
{
|
||||
column1: 'Member Project',
|
||||
column2: 'private',
|
||||
column3: 'Member',
|
||||
column4: '3 months',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #789',
|
||||
processName: 'Member Process',
|
||||
zone: 'C',
|
||||
},
|
||||
{
|
||||
column1: 'Peer Project',
|
||||
column2: 'public',
|
||||
column3: 'Peer',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #101',
|
||||
processName: 'Peer Process',
|
||||
zone: 'D',
|
||||
},
|
||||
{
|
||||
column1: 'Payment Project',
|
||||
column2: 'confidential',
|
||||
column3: 'Payment',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #102',
|
||||
processName: 'Payment Process',
|
||||
zone: 'E',
|
||||
},
|
||||
{
|
||||
column1: 'Deposit Project',
|
||||
column2: 'private',
|
||||
column3: 'Deposit',
|
||||
column4: '6 months',
|
||||
column5: 'Terms accepted',
|
||||
column6: 'Contract #103',
|
||||
processName: 'Deposit Process',
|
||||
zone: 'F',
|
||||
},
|
||||
{
|
||||
column1: 'Artefact Project',
|
||||
column2: 'public',
|
||||
column3: 'Artefact',
|
||||
column4: '1 year',
|
||||
column5: 'GDPR compliant',
|
||||
column6: 'Contract #104',
|
||||
processName: 'Artefact Process',
|
||||
zone: 'G',
|
||||
},
|
||||
{
|
||||
column1: 'Resolve Project',
|
||||
column2: 'private',
|
||||
column3: 'Resolve',
|
||||
column4: '2 years',
|
||||
column5: 'IP rights',
|
||||
column6: 'Contract #105',
|
||||
processName: 'Resolve Process',
|
||||
zone: 'H',
|
||||
},
|
||||
{
|
||||
column1: 'Backup Project',
|
||||
column2: 'public',
|
||||
column3: 'Backup',
|
||||
column4: '1 year',
|
||||
column5: 'NDA signed',
|
||||
column6: 'Contract #106',
|
||||
processName: 'Backup Process',
|
||||
zone: 'I',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockProcessRows = [
|
||||
{
|
||||
process: 'User Project',
|
||||
role: 'User',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
|
||||
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Member Project',
|
||||
role: 'Member',
|
||||
notification: {
|
||||
messages: [
|
||||
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
|
||||
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
|
||||
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Peer Project',
|
||||
role: 'Peer',
|
||||
notification: {
|
||||
unread: 2,
|
||||
total: 4,
|
||||
messages: [
|
||||
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
|
||||
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
||||
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
|
||||
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
||||
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Deposit Project',
|
||||
role: 'Deposit',
|
||||
notification: {
|
||||
unread: 1,
|
||||
total: 10,
|
||||
messages: [
|
||||
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
|
||||
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Artefact Project',
|
||||
role: 'Artefact',
|
||||
notification: {
|
||||
unread: 0,
|
||||
total: 3,
|
||||
messages: [
|
||||
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
|
||||
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
process: 'Resolve Project',
|
||||
role: 'Resolve',
|
||||
notification: {
|
||||
unread: 5,
|
||||
total: 12,
|
||||
messages: [
|
||||
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
|
||||
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
|
||||
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
||||
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
||||
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockContracts = {
|
||||
'Contract #123': {
|
||||
title: 'User Project Agreement',
|
||||
date: '2024-01-15',
|
||||
parties: ['Company XYZ', 'User Team'],
|
||||
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
|
||||
content: 'This agreement establishes the terms and conditions for user project management.',
|
||||
},
|
||||
'Contract #456': {
|
||||
title: 'Process Management Contract',
|
||||
date: '2024-02-01',
|
||||
parties: ['Company XYZ', 'Process Team'],
|
||||
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
|
||||
content: 'This contract defines the process management standards and procedures.',
|
||||
},
|
||||
'Contract #789': {
|
||||
title: 'Member Access Agreement',
|
||||
date: '2024-03-15',
|
||||
parties: ['Company XYZ', 'Member Team'],
|
||||
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
|
||||
content: 'This agreement outlines the terms for member access and privileges.',
|
||||
},
|
||||
'Contract #101': {
|
||||
title: 'Peer Collaboration Agreement',
|
||||
date: '2024-04-01',
|
||||
parties: ['Company XYZ', 'Peer Network'],
|
||||
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
|
||||
content: 'This contract establishes peer collaboration and networking guidelines.',
|
||||
},
|
||||
'Contract #102': {
|
||||
title: 'Payment Processing Agreement',
|
||||
date: '2024-05-01',
|
||||
parties: ['Company XYZ', 'Payment Team'],
|
||||
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
|
||||
content: 'This agreement defines payment processing terms and conditions.',
|
||||
},
|
||||
'Contract #103': {
|
||||
title: 'Deposit Management Contract',
|
||||
date: '2024-06-01',
|
||||
parties: ['Company XYZ', 'Deposit Team'],
|
||||
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
|
||||
content: 'This contract outlines deposit management procedures and security measures.',
|
||||
},
|
||||
'Contract #104': {
|
||||
title: 'Artefact Handling Agreement',
|
||||
date: '2024-07-01',
|
||||
parties: ['Company XYZ', 'Artefact Team'],
|
||||
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
|
||||
content: 'This agreement establishes artefact handling and preservation guidelines.',
|
||||
},
|
||||
'Contract #105': {
|
||||
title: 'Resolution Protocol Agreement',
|
||||
date: '2024-08-01',
|
||||
parties: ['Company XYZ', 'Resolution Team'],
|
||||
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
|
||||
content: 'This contract defines the resolution process and protocol standards.',
|
||||
},
|
||||
'Contract #106': {
|
||||
title: 'Backup Service Agreement',
|
||||
date: '2024-09-01',
|
||||
parties: ['Company XYZ', 'Backup Team'],
|
||||
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
|
||||
content: 'This agreement outlines backup service terms and recovery procedures.',
|
||||
},
|
||||
};
|
45
ihm_client/src/mocks/mock-account/interfacesAccountMock.ts
Normal file
45
ihm_client/src/mocks/mock-account/interfacesAccountMock.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export interface Row {
|
||||
column1: string;
|
||||
column2: string;
|
||||
column3: string;
|
||||
}
|
||||
|
||||
// Types supplémentaires nécessaires
|
||||
export interface Contract {
|
||||
title: string;
|
||||
date: string;
|
||||
parties: string[];
|
||||
terms: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface WalletRow {
|
||||
column1: string; // Label
|
||||
column2: string; // Wallet
|
||||
column3: string; // Type
|
||||
}
|
||||
|
||||
export interface DataRow {
|
||||
column1: string; // Name
|
||||
column2: string; // Visibility
|
||||
column3: string; // Role
|
||||
column4: string; // Duration
|
||||
column5: string; // Legal
|
||||
column6: string; // Contract
|
||||
processName: string;
|
||||
zone: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
message: string;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
// Déplacer l'interface en dehors de la classe, au début du fichier
|
||||
export interface NotificationMessage {
|
||||
id: number;
|
||||
read: boolean;
|
||||
date: string;
|
||||
message: string;
|
||||
}
|
52
ihm_client/src/mocks/mock-chat/groupsMock.js
Executable file
52
ihm_client/src/mocks/mock-chat/groupsMock.js
Executable file
@ -0,0 +1,52 @@
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Group 🚀 ',
|
||||
roles: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 1, name: 'Member 1' },
|
||||
{ id: 2, name: 'Member 2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Role 2',
|
||||
members: [
|
||||
{ id: 3, name: 'Member 3' },
|
||||
{ id: 4, name: 'Member 4' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group ₿',
|
||||
roles: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 5, name: 'Member 5' },
|
||||
{ id: 6, name: 'Member 6' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 🪙',
|
||||
roles: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Role 1',
|
||||
members: [
|
||||
{ id: 7, name: 'Member 7' },
|
||||
{ id: 8, name: 'Member 8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
64
ihm_client/src/mocks/mock-chat/messagesMock.js
Executable file
64
ihm_client/src/mocks/mock-chat/messagesMock.js
Executable file
@ -0,0 +1,64 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
471
ihm_client/src/mocks/mock-signature/groupsMock.js
Executable file
471
ihm_client/src/mocks/mock-signature/groupsMock.js
Executable file
@ -0,0 +1,471 @@
|
||||
// Définir les rôles autorisés
|
||||
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
||||
|
||||
const VISIBILITY_LEVELS = {
|
||||
PUBLIC: 'public',
|
||||
CONFIDENTIAL: 'confidential',
|
||||
PRIVATE: 'private',
|
||||
};
|
||||
|
||||
const DOCUMENT_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
PENDING: 'pending',
|
||||
IN_REVIEW: 'in_review',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
EXPIRED: 'expired',
|
||||
};
|
||||
|
||||
// Fonction pour créer un rôle
|
||||
function createRole(name, members) {
|
||||
if (!VALID_ROLES.includes(name)) {
|
||||
throw new Error(`Role "${name}" is not valid.`);
|
||||
}
|
||||
return { name, members };
|
||||
}
|
||||
|
||||
export const groupsMock = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Processus 1',
|
||||
description: 'Description du processus 1',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 101,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: 'Urgency A',
|
||||
description: "Document vierge pour le plan d'urgence A",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
name: 'Urgency B',
|
||||
description: "Document vierge pour le plan d'urgence B",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
name: 'Urgency C',
|
||||
description: "Document vierge pour le plan d'urgence C",
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
name: 'Document à signer',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'User',
|
||||
members: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Document User A',
|
||||
description: 'Description du document User A.',
|
||||
visibility: 'public',
|
||||
createdAt: '2024-01-01',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 1, name: 'Alice' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 2, name: 'Bob' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Document User B',
|
||||
description: 'Document vierge pour le rôle User',
|
||||
visibility: 'confidential',
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Document User C',
|
||||
description: 'Document vierge pour validation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Document User D',
|
||||
description: 'Document vierge pour approbation utilisateur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Process',
|
||||
members: [
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Document Process A',
|
||||
description: 'Description du document Process A.',
|
||||
visibility: 'confidential',
|
||||
createdAt: '2024-01-10',
|
||||
deadline: '2024-03-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-12',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Document Process B',
|
||||
description: 'Document vierge pour processus interne',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Document Process C',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Process D',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Process E',
|
||||
description: 'Document vierge pour validation processus',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.PENDING,
|
||||
createdAt: '2024-01-15',
|
||||
deadline: '2024-02-01',
|
||||
signatures: [
|
||||
{
|
||||
member: { id: 3, name: 'Charlie' },
|
||||
signed: true,
|
||||
signedAt: '2024-01-15',
|
||||
},
|
||||
{
|
||||
member: { id: 4, name: 'David' },
|
||||
signed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Backup',
|
||||
members: [
|
||||
{ id: 15, name: 'Oscar' },
|
||||
{ id: 16, name: 'Patricia' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 11,
|
||||
name: 'Document Backup A',
|
||||
description: 'Document vierge pour sauvegarde',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Processus 2',
|
||||
description: 'Description du processus 2',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 201,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 203,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 204,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 205,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Artefact',
|
||||
members: [
|
||||
{ id: 17, name: 'Quinn' },
|
||||
{ id: 18, name: 'Rachel' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 12,
|
||||
name: 'Document Artefact A',
|
||||
description: 'Document vierge pour artefact',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Document Artefact B',
|
||||
description: 'Document vierge pour validation artefact',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Resolve',
|
||||
members: [
|
||||
{ id: 19, name: 'Sam' },
|
||||
{ id: 20, name: 'Tom' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 14,
|
||||
name: 'Document Resolve A',
|
||||
description: 'Document vierge pour résolution',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Processus 3',
|
||||
description: 'Description du processus 3',
|
||||
commonDocuments: [
|
||||
{
|
||||
id: 301,
|
||||
name: 'Règlement intérieur',
|
||||
description: 'Document vierge pour le règlement intérieur',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
name: 'Charte de confidentialité',
|
||||
description: 'Document vierge pour la charte de confidentialité',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
name: 'Procédures générales',
|
||||
description: 'Document vierge pour les procédures générales',
|
||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
roles: [
|
||||
{
|
||||
name: 'Deposit',
|
||||
members: [
|
||||
{ id: 21, name: 'Uma' },
|
||||
{ id: 22, name: 'Victor' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 15,
|
||||
name: 'Document Deposit A',
|
||||
description: 'Document vierge pour dépôt',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Document Deposit B',
|
||||
description: 'Document vierge pour validation dépôt',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Payment',
|
||||
members: [
|
||||
{ id: 23, name: 'Walter' },
|
||||
{ id: 24, name: 'Xena' },
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
id: 17,
|
||||
name: 'Document Payment B',
|
||||
description: 'Document vierge pour paiement',
|
||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Document Payment C',
|
||||
description: 'Document vierge pour validation paiement',
|
||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
||||
status: DOCUMENT_STATUS.DRAFT,
|
||||
createdAt: null,
|
||||
deadline: null,
|
||||
signatures: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
105
ihm_client/src/mocks/mock-signature/membersMocks.js
Executable file
105
ihm_client/src/mocks/mock-signature/membersMocks.js
Executable file
@ -0,0 +1,105 @@
|
||||
export const membersMock = [
|
||||
// Processus 1
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice',
|
||||
avatar: 'A',
|
||||
email: 'alice@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob',
|
||||
avatar: 'B',
|
||||
email: 'bob@company.com',
|
||||
processRoles: [{ processId: 1, role: 'User' }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Charlie',
|
||||
avatar: 'C',
|
||||
email: 'charlie@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David',
|
||||
avatar: 'D',
|
||||
email: 'david@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Process' }],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Oscar',
|
||||
avatar: 'O',
|
||||
email: 'oscar@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Patricia',
|
||||
avatar: 'P',
|
||||
email: 'patricia@company.com',
|
||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
||||
},
|
||||
|
||||
// Processus 2
|
||||
{
|
||||
id: 17,
|
||||
name: 'Quinn',
|
||||
avatar: 'Q',
|
||||
email: 'quinn@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'Rachel',
|
||||
avatar: 'R',
|
||||
email: 'rachel@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Sam',
|
||||
avatar: 'S',
|
||||
email: 'sam@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Tom',
|
||||
avatar: 'T',
|
||||
email: 'tom@company.com',
|
||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
||||
},
|
||||
|
||||
// Processus 3
|
||||
{
|
||||
id: 21,
|
||||
name: 'Uma',
|
||||
avatar: 'U',
|
||||
email: 'uma@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Victor',
|
||||
avatar: 'V',
|
||||
email: 'victor@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Walter',
|
||||
avatar: 'W',
|
||||
email: 'walter@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: 'Xena',
|
||||
avatar: 'X',
|
||||
email: 'xena@company.com',
|
||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
||||
},
|
||||
];
|
64
ihm_client/src/mocks/mock-signature/messagesMock.ts
Executable file
64
ihm_client/src/mocks/mock-signature/messagesMock.ts
Executable file
@ -0,0 +1,64 @@
|
||||
export const messagesMock = [
|
||||
{
|
||||
memberId: 1, // Conversations avec Mmber 1
|
||||
messages: [
|
||||
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 2, // Conversations avec Member 2
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 3, // Conversations avec Member 3
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 4, // Conversations avec Member 4
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 5, // Conversations avec Member 5
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 6, // Conversations avec Member 6
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 7, // Conversations avec Member 7
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
memberId: 8, // Conversations avec Member 8
|
||||
messages: [
|
||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
||||
],
|
||||
},
|
||||
];
|
7
ihm_client/src/models/backup.model.ts
Normal file
7
ihm_client/src/models/backup.model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
||||
|
||||
export interface BackUp {
|
||||
device: Device,
|
||||
secrets: SecretsStore,
|
||||
processes: Record<string, Process>,
|
||||
}
|
30
ihm_client/src/models/notification.model.ts
Executable file
30
ihm_client/src/models/notification.model.ts
Executable file
@ -0,0 +1,30 @@
|
||||
export interface INotification {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
sendToNotificationPage?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// Quelles sont les données utiles pour le user ???
|
||||
export interface IUser {
|
||||
id: string;
|
||||
information?: any;
|
||||
}
|
||||
|
||||
// Quelles sont les données utiles pour les messages ???
|
||||
export interface IMessage {
|
||||
id: string;
|
||||
message: any;
|
||||
}
|
||||
|
||||
export interface UserDiff {
|
||||
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
|
||||
field: string;
|
||||
previous_value: string;
|
||||
new_value: string;
|
||||
notify_user: boolean;
|
||||
need_validation: boolean;
|
||||
// validated: bool,
|
||||
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
|
||||
}
|
65
ihm_client/src/models/process.model.ts
Executable file
65
ihm_client/src/models/process.model.ts
Executable file
@ -0,0 +1,65 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
59
ihm_client/src/models/signature.models.ts
Executable file
59
ihm_client/src/models/signature.models.ts
Executable file
@ -0,0 +1,59 @@
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
roles: {
|
||||
id?: number;
|
||||
name: string;
|
||||
members: { id: string | number; name: string }[];
|
||||
documents?: {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
visibility: string;
|
||||
createdAt: string | null;
|
||||
deadline: string | null;
|
||||
signatures: DocumentSignature[];
|
||||
status?: string;
|
||||
files?: Array<{ name: string; url: string }>;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
sender: string;
|
||||
text?: string;
|
||||
time: string;
|
||||
type: 'text' | 'file';
|
||||
fileName?: string;
|
||||
fileData?: string;
|
||||
}
|
||||
|
||||
export interface MemberMessages {
|
||||
memberId: string;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface DocumentSignature {
|
||||
signed: boolean;
|
||||
member: {
|
||||
name: string;
|
||||
};
|
||||
signedAt?: string;
|
||||
}
|
||||
|
||||
export interface RequestParams {
|
||||
processId: number;
|
||||
processName: string;
|
||||
roleId: number;
|
||||
roleName: string;
|
||||
documentId: number;
|
||||
documentName: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
memberId: string;
|
||||
text: string;
|
||||
time: string;
|
||||
}
|
62
ihm_client/src/pages/account/account-component.ts
Normal file
62
ihm_client/src/pages/account/account-component.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { AccountElement } from './account';
|
||||
import accountCss from '../../../public/style/account.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class AccountComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
accountElement: AccountElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
this.fetchData();
|
||||
|
||||
if (!customElements.get('account-element')) {
|
||||
customElements.define('account-element', AccountElement);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
||||
const data = await (window as any).myService?.getProcesses();
|
||||
} else {
|
||||
const service = await Services.getInstance();
|
||||
const data = await service.getProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = accountCss;
|
||||
|
||||
const accountElement = document.createElement('account-element');
|
||||
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(accountElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AccountComponent };
|
||||
customElements.define('account-component', AccountComponent);
|
10
ihm_client/src/pages/account/account.html
Executable file
10
ihm_client/src/pages/account/account.html
Executable file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Account</title>
|
||||
</head>
|
||||
<body>
|
||||
<account-component></account-component>
|
||||
<script type="module" src="./account.ts"></script>
|
||||
</body>
|
||||
</html>
|
1589
ihm_client/src/pages/account/account.ts
Executable file
1589
ihm_client/src/pages/account/account.ts
Executable file
File diff suppressed because it is too large
Load Diff
321
ihm_client/src/pages/account/document-validation.ts
Normal file
321
ihm_client/src/pages/account/document-validation.ts
Normal file
@ -0,0 +1,321 @@
|
||||
import type { ProcessState } from '../../../pkg/sdk_client';
|
||||
import Services from '../../services/service';
|
||||
|
||||
interface State {
|
||||
file: File | null;
|
||||
fileHash: string | null;
|
||||
certificate: ProcessState | null;
|
||||
commitmentHashes: string[];
|
||||
}
|
||||
|
||||
export interface Vin {
|
||||
txid: string; // The txid of the previous transaction (being spent)
|
||||
vout: number; // The output index in the previous tx
|
||||
prevout: {
|
||||
scriptpubkey: string;
|
||||
scriptpubkey_asm: string;
|
||||
scriptpubkey_type: string;
|
||||
scriptpubkey_address: string;
|
||||
value: number;
|
||||
};
|
||||
scriptsig: string;
|
||||
scriptsig_asm: string;
|
||||
witness: string[];
|
||||
is_coinbase: boolean;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
export interface TransactionInfo {
|
||||
txid: string;
|
||||
version: number;
|
||||
locktime: number;
|
||||
vin: Vin[];
|
||||
vout: any[];
|
||||
size: number;
|
||||
weight: number;
|
||||
fee: number;
|
||||
status: {
|
||||
confirmed: boolean;
|
||||
block_height: number;
|
||||
block_hash: string;
|
||||
block_time: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function getDocumentValidation(container: HTMLElement) {
|
||||
const state: State = {
|
||||
file: null,
|
||||
fileHash: null,
|
||||
certificate: null,
|
||||
commitmentHashes: []
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
container.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
gap: 2rem;
|
||||
`;
|
||||
|
||||
function createDropButton(
|
||||
label: string,
|
||||
onDrop: (file: File, updateVisuals: (file: File) => void) => void,
|
||||
accept: string = '*/*'
|
||||
): HTMLElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = `
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
border: 2px dashed #888;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
background: #f8f8f8;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = label;
|
||||
|
||||
const filename = document.createElement('div');
|
||||
filename.style.cssText = `
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.5rem;
|
||||
color: #444;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
wrapper.appendChild(title);
|
||||
wrapper.appendChild(filename);
|
||||
|
||||
const updateVisuals = (file: File) => {
|
||||
wrapper.style.borderColor = 'green';
|
||||
wrapper.style.background = '#e6ffed';
|
||||
filename.textContent = file.name;
|
||||
};
|
||||
|
||||
// === Hidden file input ===
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = accept;
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (file) {
|
||||
onDrop(file, updateVisuals);
|
||||
fileInput.value = ''; // reset so same file can be re-selected
|
||||
}
|
||||
};
|
||||
|
||||
// === Handle drag-and-drop ===
|
||||
wrapper.ondragover = e => {
|
||||
e.preventDefault();
|
||||
wrapper.style.background = '#e0e0e0';
|
||||
};
|
||||
|
||||
wrapper.ondragleave = () => {
|
||||
wrapper.style.background = '#f8f8f8';
|
||||
};
|
||||
|
||||
wrapper.ondrop = e => {
|
||||
e.preventDefault();
|
||||
wrapper.style.background = '#f8f8f8';
|
||||
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (file) {
|
||||
onDrop(file, updateVisuals);
|
||||
}
|
||||
};
|
||||
|
||||
// === Handle click to open file manager ===
|
||||
wrapper.onclick = () => {
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const fileDropButton = createDropButton('Drop file', async (file, updateVisuals) => {
|
||||
try {
|
||||
state.file = file;
|
||||
updateVisuals(file);
|
||||
console.log('Loaded file:', state.file);
|
||||
checkReady();
|
||||
} catch (err) {
|
||||
alert('Failed to drop the file.');
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const certDropButton = createDropButton('Drop certificate', async (file, updateVisuals) => {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
if (
|
||||
typeof json === 'object' &&
|
||||
json !== null &&
|
||||
typeof json.pcd_commitment === 'object' &&
|
||||
typeof json.state_id === 'string'
|
||||
) {
|
||||
state.certificate = json as ProcessState;
|
||||
|
||||
state.commitmentHashes = Object.values(json.pcd_commitment).map((h: any) =>
|
||||
(h as string).toLowerCase()
|
||||
);
|
||||
|
||||
updateVisuals(file);
|
||||
console.log('Loaded certificate, extracted hashes:', state.commitmentHashes);
|
||||
checkReady();
|
||||
} else {
|
||||
alert('Invalid certificate structure.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to parse certificate JSON.');
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
const buttonRow = document.createElement('div');
|
||||
buttonRow.style.display = 'flex';
|
||||
buttonRow.style.gap = '2rem';
|
||||
buttonRow.appendChild(fileDropButton);
|
||||
buttonRow.appendChild(certDropButton);
|
||||
|
||||
container.appendChild(buttonRow);
|
||||
|
||||
async function checkReady() {
|
||||
if (state.file && state.certificate && state.commitmentHashes.length > 0) {
|
||||
// We take the commited_in and all pcd_commitment keys to reconstruct all the possible hash
|
||||
const fileBlob = {
|
||||
type: state.file.type,
|
||||
data: new Uint8Array(await state.file.arrayBuffer())
|
||||
};
|
||||
const service = await Services.getInstance();
|
||||
const commitedIn = state.certificate.commited_in;
|
||||
if (!commitedIn) return;
|
||||
const [prevTxid, prevTxVout] = commitedIn.split(':');
|
||||
const processId = state.certificate.commited_in;
|
||||
const stateId = state.certificate.state_id;
|
||||
const process = await service.getProcess(processId);
|
||||
if (!process) return;
|
||||
|
||||
// Get the transaction that comes right after the commited_in
|
||||
const nextState = service.getNextStateAfterId(process, stateId);
|
||||
|
||||
if (!nextState) {
|
||||
alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [outspentTxId, _] = nextState.commited_in.split(':');
|
||||
console.log(outspentTxId);
|
||||
|
||||
// Check that the commitment transaction exists, and that it commits to the state id
|
||||
|
||||
const txInfo = await fetchTransaction(outspentTxId);
|
||||
if (!txInfo) {
|
||||
console.error(`Validation error: Can't fetch new state commitment transaction`);
|
||||
alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// We must check that this transaction indeed spend the commited_in we have in the certificate
|
||||
let found = false;
|
||||
for (const vin of txInfo.vin) {
|
||||
if (vin.txid === prevTxid) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
|
||||
alert('❌ Validation failed: Unconsistent commitment transactions history.');
|
||||
return;
|
||||
}
|
||||
|
||||
// set found back to false for next check
|
||||
found = false;
|
||||
|
||||
// is the state_id commited in the transaction?
|
||||
for (const vout of txInfo.vout) {
|
||||
console.log(vout);
|
||||
if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
|
||||
found = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (vout.scriptpubkey_asm) {
|
||||
const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
|
||||
if (hash) {
|
||||
if (hash !== stateId) {
|
||||
console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
|
||||
alert('❌ Validation failed: Transaction does not commit to that state.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
alert('❌ Validation failed: Transaction does not contain data.');
|
||||
return;
|
||||
}
|
||||
|
||||
// set found back to false for next check
|
||||
found = false;
|
||||
|
||||
for (const label of Object.keys(state.certificate.pcd_commitment)) {
|
||||
// Compute the hash for this label
|
||||
console.log(`Computing hash with label ${label}`)
|
||||
const fileHex = service.getHashForFile(commitedIn, label, fileBlob);
|
||||
console.log(`Found hash ${fileHex}`);
|
||||
found = state.commitmentHashes.includes(fileHex);
|
||||
if (found) break;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
alert('✅ Validation successful: file hash found in pcd_commitment.');
|
||||
} else {
|
||||
alert('❌ Validation failed: file hash NOT found in pcd_commitment.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTransaction(txid: string): Promise<TransactionInfo> {
|
||||
const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const outspend: TransactionInfo = await response.json();
|
||||
return outspend;
|
||||
}
|
||||
|
||||
function extractHexFromScriptAsm(scriptAsm: string): string | null {
|
||||
const parts = scriptAsm.trim().split(/\s+/);
|
||||
const last = parts[parts.length - 1];
|
||||
|
||||
// Basic validation: must be 64-char hex (32 bytes)
|
||||
if (/^[0-9a-fA-F]{64}$/.test(last)) {
|
||||
return last.toLowerCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
196
ihm_client/src/pages/account/key-value-section.ts
Normal file
196
ihm_client/src/pages/account/key-value-section.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import type { ValidationRule, RoleDefinition } from '../../../pkg/sdk_client';
|
||||
import { showValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
||||
|
||||
export function createKeyValueSection(title: string, id: string, isRoleSection = false) {
|
||||
const section = document.createElement('div');
|
||||
section.id = id;
|
||||
section.style.cssText = 'margin-bottom: 2rem; background: #fff; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);';
|
||||
|
||||
const titleEl = document.createElement('h2');
|
||||
titleEl.textContent = title;
|
||||
titleEl.style.cssText = 'font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;';
|
||||
section.appendChild(titleEl);
|
||||
|
||||
const rowContainer = document.createElement('div');
|
||||
section.appendChild(rowContainer);
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.textContent = '+ Add Row';
|
||||
addBtn.style.cssText = `
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #888;
|
||||
border-radius: 0.375rem;
|
||||
background-color: #f9f9f9;
|
||||
cursor: pointer;
|
||||
`;
|
||||
section.appendChild(addBtn);
|
||||
|
||||
const roleRowStates: {
|
||||
roleNameInput: HTMLInputElement;
|
||||
membersInput: HTMLInputElement;
|
||||
storagesInput: HTMLInputElement;
|
||||
validationRules: ValidationRule[];
|
||||
}[] = [];
|
||||
type fileBlob = {
|
||||
type: string,
|
||||
data: Uint8Array
|
||||
};
|
||||
const nonRoleRowStates: {
|
||||
keyInput: HTMLInputElement,
|
||||
valueInput: HTMLInputElement,
|
||||
fileInput: HTMLInputElement,
|
||||
fileBlob: fileBlob | null
|
||||
}[] = [];
|
||||
|
||||
const inputStyle = 'flex: 1; height: 2.5rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem;';
|
||||
|
||||
const createRow = () => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 0.5rem; align-items: center;';
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.textContent = '🗑️';
|
||||
deleteBtn.style.cssText = 'background: none; border: none; font-size: 1.2rem; cursor: pointer;';
|
||||
deleteBtn.onclick = () => {
|
||||
row.remove();
|
||||
updateDeleteButtons();
|
||||
};
|
||||
|
||||
if (isRoleSection) {
|
||||
const roleName = document.createElement('input');
|
||||
const members = document.createElement('input');
|
||||
const storages = document.createElement('input');
|
||||
|
||||
roleName.placeholder = 'Role name';
|
||||
members.placeholder = 'members';
|
||||
storages.placeholder = 'storages';
|
||||
[roleName, members, storages].forEach(input => {
|
||||
input.type = 'text';
|
||||
input.style.cssText = inputStyle;
|
||||
});
|
||||
|
||||
const ruleButton = document.createElement('button');
|
||||
ruleButton.textContent = 'Add Validation Rule';
|
||||
ruleButton.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||
|
||||
const rules: ValidationRule[] = [];
|
||||
ruleButton.onclick = () => {
|
||||
showValidationRuleModal(rule => {
|
||||
rules.push(rule);
|
||||
ruleButton.textContent = `Rules (${rules.length})`;
|
||||
});
|
||||
};
|
||||
|
||||
row.appendChild(roleName);
|
||||
row.appendChild(members);
|
||||
row.appendChild(storages);
|
||||
row.appendChild(ruleButton);
|
||||
row.appendChild(deleteBtn);
|
||||
|
||||
roleRowStates.push({ roleNameInput: roleName, membersInput: members, storagesInput: storages, validationRules: rules });
|
||||
} else {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.onchange = async () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const uint8 = new Uint8Array(buffer);
|
||||
|
||||
rowState.fileBlob = {
|
||||
type: file.type,
|
||||
data: uint8,
|
||||
};
|
||||
|
||||
valueInput.value = `📄 ${file.name}`;
|
||||
valueInput.disabled = true;
|
||||
attachBtn.textContent = `📎 ${file.name}`;
|
||||
};
|
||||
|
||||
const attachBtn = document.createElement('button');
|
||||
attachBtn.textContent = '📎 Attach';
|
||||
attachBtn.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||
attachBtn.onclick = () => fileInput.click();
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
const valueInput = document.createElement('input');
|
||||
|
||||
const rowState = {
|
||||
keyInput,
|
||||
valueInput,
|
||||
fileInput,
|
||||
fileBlob: null as fileBlob | null
|
||||
};
|
||||
nonRoleRowStates.push(rowState);
|
||||
|
||||
keyInput.placeholder = 'Key';
|
||||
valueInput.placeholder = 'Value';
|
||||
[keyInput, valueInput].forEach(input => {
|
||||
input.type = 'text';
|
||||
input.style.cssText = inputStyle;
|
||||
});
|
||||
|
||||
row.appendChild(keyInput);
|
||||
row.appendChild(valueInput);
|
||||
|
||||
row.appendChild(attachBtn);
|
||||
row.appendChild(fileInput);
|
||||
|
||||
row.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
rowContainer.appendChild(row);
|
||||
updateDeleteButtons();
|
||||
};
|
||||
|
||||
const updateDeleteButtons = () => {
|
||||
const rows = Array.from(rowContainer.children);
|
||||
rows.forEach(row => {
|
||||
const btn = row.querySelector('button:last-child') as HTMLButtonElement;
|
||||
if (rows.length === 1) {
|
||||
btn.disabled = true;
|
||||
btn.style.visibility = 'hidden';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.style.visibility = 'visible';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
createRow();
|
||||
addBtn.addEventListener('click', createRow);
|
||||
|
||||
return {
|
||||
element: section,
|
||||
getData: () => {
|
||||
if (isRoleSection) {
|
||||
const data: Record<string, RoleDefinition> = {};
|
||||
for (const row of roleRowStates) {
|
||||
const key = row.roleNameInput.value.trim();
|
||||
if (!key) continue;
|
||||
data[key] = {
|
||||
members: row.membersInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
||||
storages: row.storagesInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
||||
validation_rules: row.validationRules
|
||||
};
|
||||
}
|
||||
return data;
|
||||
} else {
|
||||
const data: Record<string, string | fileBlob> = {};
|
||||
for (const row of nonRoleRowStates) {
|
||||
const key = row.keyInput.value.trim();
|
||||
if (!key) continue;
|
||||
if (row.fileBlob) {
|
||||
data[key] = row.fileBlob;
|
||||
} else {
|
||||
data[key] = row.valueInput.value.trim();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
93
ihm_client/src/pages/account/process-creation.ts
Normal file
93
ihm_client/src/pages/account/process-creation.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { createKeyValueSection } from './key-value-section';
|
||||
import { loadValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
||||
import Services from '../../services/service';
|
||||
import type { RoleDefinition } from '../../../pkg/sdk_client';
|
||||
|
||||
export async function getProcessCreation(container: HTMLElement) {
|
||||
await loadValidationRuleModal();
|
||||
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div class="parameter-header">Process Creation</div>`;
|
||||
const privateSec = createKeyValueSection('Private Data', 'private-section');
|
||||
const publicSec = createKeyValueSection('Public Data', 'public-section');
|
||||
const rolesSec = createKeyValueSection('Roles', 'roles-section', true);
|
||||
|
||||
container.appendChild(privateSec.element);
|
||||
container.appendChild(publicSec.element);
|
||||
container.appendChild(rolesSec.element);
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'Create Process';
|
||||
btn.style.cssText = `
|
||||
display: block;
|
||||
margin: 2rem auto 0;
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
btn.onclick = async () => {
|
||||
const privateData = privateSec.getData();
|
||||
const publicData = publicSec.getData();
|
||||
const roles = rolesSec.getData() as Record<string, RoleDefinition>;
|
||||
|
||||
console.log('Private:', privateData);
|
||||
console.log('Public:', publicData);
|
||||
console.log('Roles:', roles);
|
||||
|
||||
const service = await Services.getInstance();
|
||||
|
||||
const createProcessResult = await service.createProcess(privateData, publicData, roles);
|
||||
const processId = createProcessResult.updated_process!.process_id;
|
||||
const stateId = createProcessResult.updated_process!.current_process.states[0].state_id;
|
||||
await service.handleApiReturn(createProcessResult);
|
||||
|
||||
// Now we want to validate the update and register the first state of our new process
|
||||
const updateProcessResult = await service.createPrdUpdate(processId, stateId);
|
||||
await service.handleApiReturn(createProcessResult);
|
||||
|
||||
const approveChangeResult = await service.approveChange(processId, stateId);
|
||||
await service.handleApiReturn(approveChangeResult);
|
||||
if (approveChangeResult) {
|
||||
const process = await service.getProcess(processId);
|
||||
let newState = process ? service.getStateFromId(process, stateId) : null;
|
||||
if (!newState) return;
|
||||
for (const label of Object.keys(newState.keys)) {
|
||||
const hash = newState.pcd_commitment[label];
|
||||
const encryptedData = await service.getBlobFromDb(hash);
|
||||
if (!encryptedData) continue;
|
||||
const filename = `${label}-${hash.slice(0,8)}.bin`;
|
||||
|
||||
const blob = new Blob([encryptedData], { type: "application/octet-stream" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||
}
|
||||
|
||||
// await service.generateProcessPdf(processId, newState);
|
||||
|
||||
// Add processId to the state we export
|
||||
// newState n'a pas de propriété process_id, on utilise commited_in
|
||||
// newState['process_id'] = processId;
|
||||
const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `process_${processId}_${stateId}.json`;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url); // Clean up
|
||||
}
|
||||
};
|
||||
|
||||
container.appendChild(btn);
|
||||
}
|
66
ihm_client/src/pages/account/process.ts
Normal file
66
ihm_client/src/pages/account/process.ts
Normal file
@ -0,0 +1,66 @@
|
||||
export function createProcessTab(container: HTMLElement, processes: { name: string, publicData: Record<string, any> }[]): HTMLElement {
|
||||
container.id = 'process-tab';
|
||||
container.style.display = 'block';
|
||||
container.style.cssText = 'padding: 1.5rem;';
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.textContent = 'Processes';
|
||||
title.style.cssText = 'font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;';
|
||||
container.appendChild(title);
|
||||
|
||||
processes.forEach(proc => {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = 'margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; background: #fff;';
|
||||
|
||||
const nameEl = document.createElement('h3');
|
||||
nameEl.textContent = proc.name;
|
||||
nameEl.style.cssText = 'font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem;';
|
||||
card.appendChild(nameEl);
|
||||
|
||||
const dataList = document.createElement('div');
|
||||
for (const [key, value] of Object.entries(proc.publicData)) {
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = 'margin-bottom: 0.5rem;';
|
||||
|
||||
const label = document.createElement('strong');
|
||||
label.textContent = key + ': ';
|
||||
item.appendChild(label);
|
||||
|
||||
// Let's trim the quotes
|
||||
const trimmed = value.replace(/^'|'$/g, '');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (_) {
|
||||
parsed = trimmed;
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = '💾 Save as JSON';
|
||||
saveBtn.style.cssText = 'margin-left: 0.5rem; padding: 0.25rem 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
||||
saveBtn.onclick = () => {
|
||||
const blob = new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${proc.name}_${key}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
item.appendChild(saveBtn);
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = String(parsed);
|
||||
item.appendChild(span);
|
||||
}
|
||||
|
||||
dataList.appendChild(item);
|
||||
}
|
||||
|
||||
card.appendChild(dataList);
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
49
ihm_client/src/pages/chat/chat-component.ts
Normal file
49
ihm_client/src/pages/chat/chat-component.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/*import { ChatElement } from './chat';
|
||||
import chatCss from '../../../public/style/chat.css?raw';
|
||||
import Services from '../../services/service.js';
|
||||
|
||||
class ChatComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
chatElement: ChatElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT');
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs');
|
||||
this.render();
|
||||
|
||||
if (!customElements.get('chat-element')) {
|
||||
customElements.define('chat-element', ChatElement);
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot) {
|
||||
// Créer l'élément chat-element
|
||||
const chatElement = document.createElement('chat-element');
|
||||
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
|
||||
this.shadowRoot.appendChild(chatElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatComponent };
|
||||
customElements.define('chat-component', ChatComponent);*/
|
14
ihm_client/src/pages/chat/chat.html
Executable file
14
ihm_client/src/pages/chat/chat.html
Executable file
@ -0,0 +1,14 @@
|
||||
<!--
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Chat</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<chat-component></chat-component>
|
||||
<script type="module" src="./chat.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1738
ihm_client/src/pages/chat/chat.ts
Executable file
1738
ihm_client/src/pages/chat/chat.ts
Executable file
File diff suppressed because it is too large
Load Diff
49
ihm_client/src/pages/home/home-component.ts
Normal file
49
ihm_client/src/pages/home/home-component.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import loginHtml from './home.html?raw';
|
||||
import loginScript from './home.ts?raw';
|
||||
import loginCss from '../../4nk.css?raw';
|
||||
import { initHomePage } from './home';
|
||||
|
||||
export class LoginComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK LOGIN PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initHomePage();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${loginCss}
|
||||
</style>${loginHtml}
|
||||
<script type="module">
|
||||
${loginScript}
|
||||
</scipt>
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('login-4nk-component')) {
|
||||
customElements.define('login-4nk-component', LoginComponent);
|
||||
}
|
42
ihm_client/src/pages/home/home.html
Executable file
42
ihm_client/src/pages/home/home.html
Executable file
@ -0,0 +1,42 @@
|
||||
<div class="title-container">
|
||||
<h1>Create Account / New Session</h1>
|
||||
</div>
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="tab1">Create an account</div>
|
||||
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-container">
|
||||
<div id="tab1" class="card tab-content active">
|
||||
<div class="card-description">Create an account :</div>
|
||||
<div class="pairing-request"></div>
|
||||
<!-- <div class="card-image qr-code">
|
||||
<img src="assets/qr_code.png" alt="QR Code" width="150" height="150" />
|
||||
</div> -->
|
||||
<button id="createButton" class="create-btn"></button>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div id="tab2" class="card tab-content">
|
||||
<div class="card-description">Add a device for an existing member :</div>
|
||||
<div class="card-image camera-card">
|
||||
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
|
||||
<button id="scan-btn" onclick="scanDevice()">Scan</button>
|
||||
<div class="qr-code-scanner">
|
||||
<div id="qr-reader" style="width: 200px; display: contents"></div>
|
||||
<div id="qr-reader-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Or</p>
|
||||
<!-- <input type="text" id="addressInput" placeholder="Paste address" />
|
||||
<div id="emoji-display-2"></div> -->
|
||||
<div class="card-description">Chose a member :</div>
|
||||
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
|
||||
<!-- Options -->
|
||||
</select>
|
||||
|
||||
<button id="okButton" style="display: none">OK</button>
|
||||
</div>
|
||||
</div>
|
96
ihm_client/src/pages/home/home.ts
Executable file
96
ihm_client/src/pages/home/home.ts
Executable file
@ -0,0 +1,96 @@
|
||||
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';
|
||||
import { navigate, registerAllListeners } from '../../router';
|
||||
|
||||
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 = await 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;
|
51
ihm_client/src/pages/process-element/process-component.ts
Normal file
51
ihm_client/src/pages/process-element/process-component.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import processHtml from './process-element.html?raw';
|
||||
import processScript from './process-element.ts?raw';
|
||||
import processCss from '../../4nk.css?raw';
|
||||
import { initProcessElement } from './process-element';
|
||||
|
||||
export class ProcessListComponent extends HTMLElement {
|
||||
_callback: any;
|
||||
id: string = '';
|
||||
zone: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACK PROCESS LIST PAGE');
|
||||
this.render();
|
||||
setTimeout(() => {
|
||||
initProcessElement(this.id, this.zone);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shadowRoot)
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${processCss}
|
||||
</style>${processHtml}
|
||||
<script type="module">
|
||||
${processScript}
|
||||
</scipt>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('process-4nk-component')) {
|
||||
customElements.define('process-4nk-component', ProcessListComponent);
|
||||
}
|
5
ihm_client/src/pages/process-element/process-element.html
Executable file
5
ihm_client/src/pages/process-element/process-element.html
Executable file
@ -0,0 +1,5 @@
|
||||
<div class="title-container">
|
||||
<h1>Process {{processTitle}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="process-container"></div>
|
50
ihm_client/src/pages/process-element/process-element.ts
Executable file
50
ihm_client/src/pages/process-element/process-element.ts
Executable file
@ -0,0 +1,50 @@
|
||||
import { interpolate } from '../../utils/html.utils';
|
||||
import Services from '../../services/service';
|
||||
import type { 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;
|
||||
}
|
49
ihm_client/src/pages/process/process-list-component.ts
Normal file
49
ihm_client/src/pages/process/process-list-component.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// import processHtml from './process.html?raw';
|
||||
// import processScript from './process.ts?raw';
|
||||
// import processCss from '../../4nk.css?raw';
|
||||
// import { init } from './process';
|
||||
|
||||
// export class ProcessListComponent extends HTMLElement {
|
||||
// _callback: any;
|
||||
// constructor() {
|
||||
// super();
|
||||
// this.attachShadow({ mode: 'open' });
|
||||
// }
|
||||
|
||||
// connectedCallback() {
|
||||
// console.log('CALLBACK PROCESS LIST PAGE');
|
||||
// this.render();
|
||||
// setTimeout(() => {
|
||||
// init();
|
||||
// }, 500);
|
||||
// }
|
||||
|
||||
// set callback(fn) {
|
||||
// if (typeof fn === 'function') {
|
||||
// this._callback = fn;
|
||||
// } else {
|
||||
// console.error('Callback is not a function');
|
||||
// }
|
||||
// }
|
||||
|
||||
// get callback() {
|
||||
// return this._callback;
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// if (this.shadowRoot)
|
||||
// this.shadowRoot.innerHTML = `
|
||||
// <style>
|
||||
// ${processCss}
|
||||
// </style>${processHtml}
|
||||
// <script type="module">
|
||||
// ${processScript}
|
||||
// </scipt>
|
||||
|
||||
// `;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!customElements.get('process-list-4nk-component')) {
|
||||
// customElements.define('process-list-4nk-component', ProcessListComponent);
|
||||
// }
|
19
ihm_client/src/pages/process/process.html
Executable file
19
ihm_client/src/pages/process/process.html
Executable file
@ -0,0 +1,19 @@
|
||||
<!-- <div class="title-container">
|
||||
<h1>Process Selection</h1>
|
||||
</div>
|
||||
|
||||
<div class="process-container">
|
||||
<div class="process-card">
|
||||
<div class="process-card-description">
|
||||
<div class="input-container">
|
||||
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
|
||||
<label for="autocomplete" class="input-label">Filter processes :</label>
|
||||
<div class="selected-processes"></div>
|
||||
</div>
|
||||
<div class="process-card-content"></div>
|
||||
</div>
|
||||
<div class="process-card-action">
|
||||
<a class="btn" onclick="goToProcessPage()">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
520
ihm_client/src/pages/process/process.ts
Executable file
520
ihm_client/src/pages/process/process.ts
Executable file
@ -0,0 +1,520 @@
|
||||
// import { addSubscription } from '../../utils/subscription.utils';
|
||||
// import Services from '../../services/service';
|
||||
// import { getCorrectDOM } from '~/utils/html.utils';
|
||||
// import { Process } from 'pkg/sdk_client';
|
||||
// import chatStyle from '../../../public/style/chat.css?inline';
|
||||
// import { Database } from '../../services/database.service';
|
||||
|
||||
// // Initialize function, create initial tokens with itens that are already selected by the user
|
||||
// export async function init() {
|
||||
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
// const element = container.querySelector('select') as HTMLSelectElement;
|
||||
// // Create div that wroaps all the elements inside (select, elements selected, search div) to put select inside
|
||||
// const wrapper = document.createElement('div');
|
||||
// if (wrapper) addSubscription(wrapper, 'click', clickOnWrapper);
|
||||
// wrapper.classList.add('multi-select-component');
|
||||
// wrapper.classList.add('input-field');
|
||||
|
||||
// // Create elements of search
|
||||
// const search_div = document.createElement('div');
|
||||
// search_div.classList.add('search-container');
|
||||
// const input = document.createElement('input');
|
||||
// input.classList.add('selected-input');
|
||||
// input.setAttribute('autocomplete', 'off');
|
||||
// input.setAttribute('tabindex', '0');
|
||||
// if (input) {
|
||||
// addSubscription(input, 'keyup', inputChange);
|
||||
// addSubscription(input, 'keydown', deletePressed);
|
||||
// addSubscription(input, 'click', openOptions);
|
||||
// }
|
||||
|
||||
// const dropdown_icon = document.createElement('a');
|
||||
// dropdown_icon.classList.add('dropdown-icon');
|
||||
|
||||
// if (dropdown_icon) addSubscription(dropdown_icon, 'click', clickDropdown);
|
||||
// const autocomplete_list = document.createElement('ul');
|
||||
// autocomplete_list.classList.add('autocomplete-list');
|
||||
// search_div.appendChild(input);
|
||||
// search_div.appendChild(autocomplete_list);
|
||||
// search_div.appendChild(dropdown_icon);
|
||||
|
||||
// // set the wrapper as child (instead of the element)
|
||||
// element.parentNode?.replaceChild(wrapper, element);
|
||||
// // set element as child of wrapper
|
||||
// wrapper.appendChild(element);
|
||||
// wrapper.appendChild(search_div);
|
||||
|
||||
// addPlaceholder(wrapper);
|
||||
// }
|
||||
|
||||
// function removePlaceholder(wrapper: HTMLElement) {
|
||||
// const input_search = wrapper.querySelector('.selected-input');
|
||||
// input_search?.removeAttribute('placeholder');
|
||||
// }
|
||||
|
||||
// function addPlaceholder(wrapper: HTMLElement) {
|
||||
// const input_search = wrapper.querySelector('.selected-input');
|
||||
// const tokens = wrapper.querySelectorAll('.selected-wrapper');
|
||||
// if (!tokens.length && !(document.activeElement === input_search)) input_search?.setAttribute('placeholder', '---------');
|
||||
// }
|
||||
|
||||
// // Listener of user search
|
||||
// function inputChange(e: Event) {
|
||||
// const target = e.target as HTMLInputElement;
|
||||
// const wrapper = target?.parentNode?.parentNode;
|
||||
// const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
// const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
|
||||
// const input_val = target?.value;
|
||||
|
||||
// if (input_val) {
|
||||
// dropdown?.classList.add('active');
|
||||
// populateAutocompleteList(select, input_val.trim());
|
||||
// } else {
|
||||
// dropdown?.classList.remove('active');
|
||||
// const event = new Event('click');
|
||||
// dropdown?.dispatchEvent(event);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Listen for clicks on the wrapper, if click happens focus on the input
|
||||
// function clickOnWrapper(e: Event) {
|
||||
// const wrapper = e.target as HTMLElement;
|
||||
// if (wrapper.tagName == 'DIV') {
|
||||
// const input_search = wrapper.querySelector('.selected-input');
|
||||
// const dropdown = wrapper.querySelector('.dropdown-icon');
|
||||
// if (!dropdown?.classList.contains('active')) {
|
||||
// const event = new Event('click');
|
||||
// dropdown?.dispatchEvent(event);
|
||||
// }
|
||||
// (input_search as HTMLInputElement)?.focus();
|
||||
// removePlaceholder(wrapper);
|
||||
// }
|
||||
// }
|
||||
|
||||
// function openOptions(e: Event) {
|
||||
// const input_search = e.target as HTMLElement;
|
||||
// const wrapper = input_search?.parentElement?.parentElement;
|
||||
// const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
// if (!dropdown?.classList.contains('active')) {
|
||||
// const event = new Event('click');
|
||||
// dropdown?.dispatchEvent(event);
|
||||
// }
|
||||
// e.stopPropagation();
|
||||
// }
|
||||
|
||||
// // Function that create a token inside of a wrapper with the given value
|
||||
// function createToken(wrapper: HTMLElement, value: any) {
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
// const search = wrapper.querySelector('.search-container');
|
||||
// const inputInderline = container.querySelector('.selected-processes');
|
||||
// // Create token wrapper
|
||||
// const token = document.createElement('div');
|
||||
// token.classList.add('selected-wrapper');
|
||||
// const token_span = document.createElement('span');
|
||||
// token_span.classList.add('selected-label');
|
||||
// token_span.innerText = value;
|
||||
// const close = document.createElement('a');
|
||||
// close.classList.add('selected-close');
|
||||
// close.setAttribute('tabindex', '-1');
|
||||
// close.setAttribute('data-option', value);
|
||||
// close.setAttribute('data-hits', '0');
|
||||
// close.innerText = 'x';
|
||||
// if (close) addSubscription(close, 'click', removeToken);
|
||||
// token.appendChild(token_span);
|
||||
// token.appendChild(close);
|
||||
// inputInderline?.appendChild(token);
|
||||
// }
|
||||
|
||||
// // Listen for clicks in the dropdown option
|
||||
// function clickDropdown(e: Event) {
|
||||
// const dropdown = e.target as HTMLElement;
|
||||
// const wrapper = dropdown?.parentNode?.parentNode;
|
||||
// const input_search = wrapper?.querySelector('.selected-input') as HTMLInputElement;
|
||||
// const select = wrapper?.querySelector('select') as HTMLSelectElement;
|
||||
// dropdown.classList.toggle('active');
|
||||
|
||||
// if (dropdown.classList.contains('active')) {
|
||||
// removePlaceholder(wrapper as HTMLElement);
|
||||
// input_search?.focus();
|
||||
|
||||
// if (!input_search?.value) {
|
||||
// populateAutocompleteList(select, '', true);
|
||||
// } else {
|
||||
// populateAutocompleteList(select, input_search.value);
|
||||
// }
|
||||
// } else {
|
||||
// clearAutocompleteList(select);
|
||||
// addPlaceholder(wrapper as HTMLElement);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Clears the results of the autocomplete list
|
||||
// function clearAutocompleteList(select: HTMLSelectElement) {
|
||||
// const wrapper = select.parentNode;
|
||||
|
||||
// const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
// if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
// }
|
||||
|
||||
// async function populateAutocompleteList(select: HTMLSelectElement, query: string, dropdown = false) {
|
||||
// const { autocomplete_options } = getOptions(select);
|
||||
|
||||
// let options_to_show = [];
|
||||
|
||||
// const service = await Services.getInstance();
|
||||
// const mineArray: string[] = await service.getMyProcesses();
|
||||
// const allProcesses = await service.getProcesses();
|
||||
// const allArray: string[] = Object.keys(allProcesses).filter(x => !mineArray.includes(x));
|
||||
|
||||
// const wrapper = select.parentNode;
|
||||
// const input_search = wrapper?.querySelector('.search-container');
|
||||
// const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
// if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
|
||||
// const addProcessToList = (processId:string, isMine: boolean) => {
|
||||
// const li = document.createElement('li');
|
||||
// li.innerText = processId;
|
||||
// li.setAttribute("data-value", processId);
|
||||
|
||||
// if (isMine) {
|
||||
// li.classList.add("my-process");
|
||||
// li.style.cssText = `color: var(--accent-color)`;
|
||||
// }
|
||||
|
||||
// if (li) addSubscription(li, 'click', selectOption);
|
||||
// autocomplete_list?.appendChild(li);
|
||||
// };
|
||||
|
||||
// mineArray.forEach(processId => addProcessToList(processId, true));
|
||||
// allArray.forEach(processId => addProcessToList(processId, false));
|
||||
|
||||
// if (mineArray.length === 0 && allArray.length === 0) {
|
||||
// const li = document.createElement('li');
|
||||
// li.classList.add('not-cursor');
|
||||
// li.innerText = 'No options found';
|
||||
// autocomplete_list?.appendChild(li);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Listener to autocomplete results when clicked set the selected property in the select option
|
||||
// function selectOption(e: any) {
|
||||
// console.log('🎯 Click event:', e);
|
||||
// console.log('🎯 Target value:', e.target.dataset.value);
|
||||
|
||||
// const wrapper = e.target.parentNode.parentNode.parentNode;
|
||||
// const select = wrapper.querySelector('select');
|
||||
// const input_search = wrapper.querySelector('.selected-input');
|
||||
// const option = wrapper.querySelector(`select option[value="${e.target.dataset.value}"]`);
|
||||
|
||||
// console.log('🎯 Selected option:', option);
|
||||
// console.log('🎯 Process ID:', option?.getAttribute('data-process-id'));
|
||||
|
||||
// if (e.target.dataset.value.includes('messaging')) {
|
||||
// const messagingNumber = parseInt(e.target.dataset.value.split(' ')[1]);
|
||||
// const processId = select.getAttribute(`data-messaging-id-${messagingNumber}`);
|
||||
|
||||
// console.log('🚀 Dispatching newMessagingProcess event:', {
|
||||
// processId,
|
||||
// processName: `Messaging Process ${processId}`
|
||||
// });
|
||||
|
||||
// // Dispatch l'événement avant la navigation
|
||||
// document.dispatchEvent(new CustomEvent('newMessagingProcess', {
|
||||
// detail: {
|
||||
// processId: processId,
|
||||
// processName: `Messaging Process ${processId}`
|
||||
// }
|
||||
// }));
|
||||
|
||||
// // Navigation vers le chat
|
||||
// const navigateEvent = new CustomEvent('navigate', {
|
||||
// detail: {
|
||||
// page: 'chat',
|
||||
// processId: processId || ''
|
||||
// }
|
||||
// });
|
||||
// document.dispatchEvent(navigateEvent);
|
||||
// return;
|
||||
// }
|
||||
// option.setAttribute('selected', '');
|
||||
// createToken(wrapper, e.target.dataset.value);
|
||||
// if (input_search.value) {
|
||||
// input_search.value = '';
|
||||
// }
|
||||
|
||||
// showSelectedProcess(e.target.dataset.value);
|
||||
|
||||
// input_search.focus();
|
||||
|
||||
// e.target.remove();
|
||||
// const autocomplete_list = wrapper.querySelector('.autocomplete-list');
|
||||
|
||||
// if (!autocomplete_list.children.length) {
|
||||
// const li = document.createElement('li');
|
||||
// li.classList.add('not-cursor');
|
||||
// li.innerText = 'No options found';
|
||||
// autocomplete_list.appendChild(li);
|
||||
// }
|
||||
|
||||
// const event = new Event('keyup');
|
||||
// input_search.dispatchEvent(event);
|
||||
// e.stopPropagation();
|
||||
// }
|
||||
|
||||
// // function that returns a list with the autcomplete list of matches
|
||||
// function autocomplete(query: string, options: any) {
|
||||
// // No query passed, just return entire list
|
||||
// if (!query) {
|
||||
// return options;
|
||||
// }
|
||||
// let options_return = [];
|
||||
|
||||
// for (let i = 0; i < options.length; i++) {
|
||||
// if (query.toLowerCase() === options[i].slice(0, query.length).toLowerCase()) {
|
||||
// options_return.push(options[i]);
|
||||
// }
|
||||
// }
|
||||
// return options_return;
|
||||
// }
|
||||
|
||||
// // Returns the options that are selected by the user and the ones that are not
|
||||
// function getOptions(select: HTMLSelectElement) {
|
||||
// // Select all the options available
|
||||
// const all_options = Array.from(select.querySelectorAll('option')).map((el) => el.value);
|
||||
|
||||
// // Get the options that are selected from the user
|
||||
// const options_selected = Array.from(select.querySelectorAll('option:checked')).map((el: any) => el.value);
|
||||
|
||||
// // Create an autocomplete options array with the options that are not selected by the user
|
||||
// const autocomplete_options: any[] = [];
|
||||
// all_options.forEach((option) => {
|
||||
// if (!options_selected.includes(option)) {
|
||||
// autocomplete_options.push(option);
|
||||
// }
|
||||
// });
|
||||
|
||||
// autocomplete_options.sort();
|
||||
|
||||
// return {
|
||||
// options_selected,
|
||||
// autocomplete_options,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Listener for when the user wants to remove a given token.
|
||||
// function removeToken(e: Event) {
|
||||
// // Get the value to remove
|
||||
// const target = e.target as HTMLSelectElement;
|
||||
// const value_to_remove = target.dataset.option;
|
||||
// const wrapper = target.parentNode?.parentNode?.parentNode;
|
||||
// const input_search = wrapper?.querySelector('.selected-input');
|
||||
// const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
// // Get the options in the select to be unselected
|
||||
// const option_to_unselect = wrapper?.querySelector(`select option[value="${value_to_remove}"]`);
|
||||
// option_to_unselect?.removeAttribute('selected');
|
||||
// // Remove token attribute
|
||||
// (target.parentNode as any)?.remove();
|
||||
// dropdown?.classList.remove('active');
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
// const process = container.querySelector('#' + target.dataset.option);
|
||||
// process?.remove();
|
||||
// }
|
||||
|
||||
// // Listen for 2 sequence of hits on the delete key, if this happens delete the last token if exist
|
||||
// function deletePressed(e: Event) {
|
||||
// const input_search = e.target as HTMLInputElement;
|
||||
// const wrapper = input_search?.parentNode?.parentNode;
|
||||
// const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
|
||||
// const tokens = wrapper?.querySelectorAll('.selected-wrapper');
|
||||
|
||||
// if (tokens?.length) {
|
||||
// const last_token_x = tokens[tokens.length - 1].querySelector('a');
|
||||
// let hits = +(last_token_x?.dataset?.hits || 0);
|
||||
|
||||
// if (key == 8 || key == 46) {
|
||||
// if (!input_search.value) {
|
||||
// if (hits > 1) {
|
||||
// // Trigger delete event
|
||||
// const event = new Event('click');
|
||||
// last_token_x?.dispatchEvent(event);
|
||||
// } else {
|
||||
// if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// // Dismiss on outside click
|
||||
// addSubscription(document, 'click', () => {
|
||||
// // get select that has the options available
|
||||
// const select = document.querySelectorAll('[data-multi-select-plugin]');
|
||||
// for (let i = 0; i < select.length; i++) {
|
||||
// if (event) {
|
||||
// var isClickInside = select[i].parentElement?.parentElement?.contains(event.target as Node);
|
||||
|
||||
// if (!isClickInside) {
|
||||
// const wrapper = select[i].parentElement?.parentElement;
|
||||
// const dropdown = wrapper?.querySelector('.dropdown-icon');
|
||||
// const autocomplete_list = wrapper?.querySelector('.autocomplete-list');
|
||||
// //the click was outside the specifiedElement, do something
|
||||
// dropdown?.classList.remove('active');
|
||||
// if (autocomplete_list) autocomplete_list.innerHTML = '';
|
||||
// addPlaceholder(wrapper as HTMLElement);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// async function showSelectedProcess(elem: MouseEvent) {
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
// if (elem) {
|
||||
// const cardContent = container.querySelector('.process-card-content');
|
||||
|
||||
// const processes = await getProcesses();
|
||||
// const process = processes.find((process: any) => process[1].title === elem);
|
||||
// if (process) {
|
||||
// const processDiv = document.createElement('div');
|
||||
// processDiv.className = 'process';
|
||||
// processDiv.id = process[0];
|
||||
// const titleDiv = document.createElement('div');
|
||||
// titleDiv.className = 'process-title';
|
||||
// titleDiv.innerHTML = `${process[1].title} : ${process[1].description}`;
|
||||
// processDiv.appendChild(titleDiv);
|
||||
// for (const zone of process.zones) {
|
||||
// const zoneElement = document.createElement('div');
|
||||
// zoneElement.className = 'process-element';
|
||||
// const zoneId = process[1].title + '-' + zone.id;
|
||||
// zoneElement.setAttribute('zone-id', zoneId);
|
||||
// zoneElement.setAttribute('process-title', process[1].title);
|
||||
// zoneElement.setAttribute('process-id', `${process[0]}_${zone.id}`);
|
||||
// zoneElement.innerHTML = `${zone.title}: ${zone.description}`;
|
||||
// addSubscription(zoneElement, 'click', select);
|
||||
// processDiv.appendChild(zoneElement);
|
||||
// }
|
||||
// if (cardContent) cardContent.appendChild(processDiv);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// function select(event: Event) {
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
// const target = event.target as HTMLElement;
|
||||
// const oldSelectedProcess = container.querySelector('.selected-process-zone');
|
||||
// oldSelectedProcess?.classList.remove('selected-process-zone');
|
||||
// if (target) {
|
||||
// target.classList.add('selected-process-zone');
|
||||
// }
|
||||
// const name = target.getAttribute('zone-id');
|
||||
// console.log('🚀 ~ select ~ name:', name);
|
||||
// }
|
||||
|
||||
// function goToProcessPage() {
|
||||
// const container = getCorrectDOM('process-list-4nk-component') as HTMLElement;
|
||||
|
||||
// const target = container.querySelector('.selected-process-zone');
|
||||
// console.log('🚀 ~ goToProcessPage ~ event:', target);
|
||||
// if (target) {
|
||||
// const process = target?.getAttribute('process-id');
|
||||
|
||||
// console.log('=======================> going to process page', process);
|
||||
// // navigate('process-element/' + process);
|
||||
// document.querySelector('process-list-4nk-component')?.dispatchEvent(
|
||||
// new CustomEvent('processSelected', {
|
||||
// detail: {
|
||||
// process: process,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// (window as any).goToProcessPage = goToProcessPage;
|
||||
|
||||
// async function createMessagingProcess(): Promise<void> {
|
||||
// console.log('Creating messaging process');
|
||||
// const service = await Services.getInstance();
|
||||
// const otherMembers = [
|
||||
// {
|
||||
// sp_addresses: [
|
||||
// "tsp1qqd7snxfh44am8f7a3x36znkh4v0dcagcgakfux488ghsg0tny7degq4gd9q4n4us0cyp82643f2p4jgcmtwknadqwl3waf9zrynl6n7lug5tg73a",
|
||||
// "tsp1qqvd8pak9fyz55rxqj90wxazqzwupf2egderc96cn84h3l84z8an9vql85scudrmwvsnltfuy9ungg7pxnhys2ft5wnf2gyr3n4ukvezygswesjuc"
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// sp_addresses: [
|
||||
// "tsp1qqgl5vawdey6wnnn2sfydcejsr06uzwsjlfa6p6yr8u4mkqwezsnvyqlazuqmxhxd8crk5eq3wfvdwv4k3tn68mkj2nj72jj39d2ngauu4unfx0q7",
|
||||
// "tsp1qqthmj56gj8vvkjzwhcmswftlrf6ye7ukpks2wra92jkehqzrvx7m2q570q5vv6zj6dnxvussx2h8arvrcfwz9sp5hpdzrfugmmzz90pmnganxk28"
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// sp_addresses: [
|
||||
// "tsp1qqwjtxr9jye7d40qxrsmd6h02egdwel6mfnujxzskgxvxphfya4e6qqjq4tsdmfdmtnmccz08ut24q8y58qqh4lwl3w8pvh86shlmavrt0u3smhv2",
|
||||
// "tsp1qqwn7tf8q2jhmfh8757xze53vg2zc6x5u6f26h3wyty9mklvcy0wnvqhhr4zppm5uyyte4y86kljvh8r0tsmkmszqqwa3ecf2lxcs7q07d56p8sz5"
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
// await service.checkConnections(otherMembers);
|
||||
// const relayAddress = service.getAllRelays().pop();
|
||||
// if (!relayAddress) {
|
||||
// throw new Error('Empty relay address list');
|
||||
// }
|
||||
// const feeRate = 1;
|
||||
// setTimeout(async () => {
|
||||
// const createProcessReturn = await service.createMessagingProcess(otherMembers, relayAddress.spAddress, feeRate);
|
||||
// const updatedProcess = createProcessReturn.updated_process.current_process;
|
||||
// if (!updatedProcess) {
|
||||
// console.error('Failed to retrieved new messaging process');
|
||||
// return;
|
||||
// }
|
||||
// const processId = updatedProcess.states[0].commited_in;
|
||||
// const stateId = updatedProcess.states[0].state_id;
|
||||
// await service.handleApiReturn(createProcessReturn);
|
||||
// const createPrdReturn = await service.createPrdUpdate(processId, stateId);
|
||||
// await service.handleApiReturn(createPrdReturn);
|
||||
// const approveChangeReturn = await service.approveChange(processId, stateId);
|
||||
// await service.handleApiReturn(approveChangeReturn);
|
||||
// }, 500)
|
||||
// }
|
||||
|
||||
// async function getDescription(processId: string, process: Process): Promise<string | null> {
|
||||
// const service = await Services.getInstance();
|
||||
// // Get the `commited_in` value of the last state and remove it from the array
|
||||
// const currentCommitedIn = process.states.pop()?.commited_in;
|
||||
|
||||
// if (currentCommitedIn === undefined) {
|
||||
// return null; // No states available
|
||||
// }
|
||||
|
||||
// // Find the last state where `commited_in` is different
|
||||
// let lastDifferentState = process.states.findLast(
|
||||
// state => state.commited_in !== currentCommitedIn
|
||||
// );
|
||||
|
||||
// if (!lastDifferentState) {
|
||||
// // It means that we only have one state that is not commited yet, that can happen with process we just created
|
||||
// // let's assume that the right description is in the last concurrent state and not handle the (arguably rare) case where we have multiple concurrent states on a creation
|
||||
// lastDifferentState = process.states.pop();
|
||||
// }
|
||||
|
||||
// // Take the description out of the state, if any
|
||||
// const description = lastDifferentState!.pcd_commitment['description'];
|
||||
// if (description) {
|
||||
// const userDiff = await service.getDiffByValue(description);
|
||||
// if (userDiff) {
|
||||
// console.log("Successfully retrieved userDiff:", userDiff);
|
||||
// return userDiff.new_value;
|
||||
// } else {
|
||||
// console.log("Failed to retrieve a non-null userDiff.");
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
58
ihm_client/src/pages/signature/signature-component.ts
Normal file
58
ihm_client/src/pages/signature/signature-component.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { SignatureElement } from './signature';
|
||||
import signatureCss from '../../../public/style/signature.css?raw'
|
||||
import Services from '../../services/service.js'
|
||||
|
||||
class SignatureComponent extends HTMLElement {
|
||||
_callback: any
|
||||
signatureElement: SignatureElement | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log('INIT')
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.signatureElement = this.shadowRoot?.querySelector('signature-element') || null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('CALLBACKs')
|
||||
this.render();
|
||||
this.fetchData();
|
||||
|
||||
if (!customElements.get('signature-element')) {
|
||||
customElements.define('signature-element', SignatureElement);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
||||
const data = await (window as any).myService?.getProcesses();
|
||||
} else {
|
||||
const service = await Services.getInstance()
|
||||
const data = await service.getProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
set callback(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this._callback = fn;
|
||||
} else {
|
||||
console.error('Callback is not a function');
|
||||
}
|
||||
}
|
||||
|
||||
get callback() {
|
||||
return this._callback;
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.shadowRoot) {
|
||||
const signatureElement = document.createElement('signature-element');
|
||||
this.shadowRoot.innerHTML = `<style>${signatureCss}</style>`;
|
||||
this.shadowRoot.appendChild(signatureElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SignatureComponent }
|
||||
customElements.define('signature-component', SignatureComponent);
|
12
ihm_client/src/pages/signature/signature.html
Executable file
12
ihm_client/src/pages/signature/signature.html
Executable file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Signatures</title>
|
||||
</head>
|
||||
<body>
|
||||
<signature-component></signature-component>
|
||||
<script type="module" src="./signature.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1758
ihm_client/src/pages/signature/signature.ts
Executable file
1758
ihm_client/src/pages/signature/signature.ts
Executable file
File diff suppressed because it is too large
Load Diff
945
ihm_client/src/router.ts
Executable file
945
ihm_client/src/router.ts
Executable file
@ -0,0 +1,945 @@
|
||||
import '../public/style/4nk.css';
|
||||
import { initHeader } from './components/header/header';
|
||||
/*import { initChat } from '../src/pages/chat/chat';*/
|
||||
import Database from './services/database.service';
|
||||
import Services from './services/service';
|
||||
import TokenService from './services/token';
|
||||
import { cleanSubscriptions } from './utils/subscription.utils';
|
||||
import { LoginComponent } from './pages/home/home-component';
|
||||
import { prepareAndSendPairingTx } from './utils/sp-address.utils';
|
||||
import ModalService from './services/modal.service';
|
||||
import { MessageType } from './models/process.model';
|
||||
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
|
||||
import type { MerkleProofResult } from 'pkg/sdk_client';
|
||||
|
||||
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;
|
||||
const db = await Database.getInstance();
|
||||
db.registerServiceWorker('/src/service-workers/database.worker.js');
|
||||
const device = await services.getDeviceFromDatabase();
|
||||
console.log('🚀 ~ setTimeout ~ device:', device);
|
||||
|
||||
if (!device) {
|
||||
await services.createNewDevice();
|
||||
} else {
|
||||
services.restoreDevice(device);
|
||||
}
|
||||
|
||||
// If we create a new device, we most probably don't have anything in db, but just in case
|
||||
await services.restoreProcessesFromDB();
|
||||
await services.restoreSecretsFromDB();
|
||||
|
||||
// We connect to all relays now
|
||||
await services.connectAllRelays();
|
||||
|
||||
await services.updateDeviceBlockHeight();
|
||||
|
||||
// We register all the event listeners if we run in an iframe
|
||||
if (window.self !== window.top) {
|
||||
await registerAllListeners();
|
||||
}
|
||||
|
||||
if (services.isPaired()) {
|
||||
await navigate('process');
|
||||
} else {
|
||||
await navigate('home');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await navigate('home');
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerAllListeners() {
|
||||
const services = await Services.getInstance();
|
||||
const tokenService = await TokenService.getInstance();
|
||||
|
||||
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
error: errorMsg,
|
||||
messageId
|
||||
},
|
||||
origin
|
||||
);
|
||||
}
|
||||
|
||||
// --- Handler functions ---
|
||||
const handleRequestLink = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.REQUEST_LINK) {
|
||||
return;
|
||||
}
|
||||
const modalService = await ModalService.getInstance();
|
||||
const result = await modalService.showConfirmationModal({
|
||||
title: 'Confirmation de liaison',
|
||||
content: `
|
||||
<div class="modal-confirmation">
|
||||
<h3>Liaison avec ${event.origin}</h3>
|
||||
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
|
||||
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
|
||||
<p>Voulez-vous continuer ?</p>
|
||||
</div>
|
||||
`,
|
||||
confirmText: 'Ajouter un service',
|
||||
cancelText: 'Annuler'
|
||||
}, true);
|
||||
|
||||
if (!result) {
|
||||
const errorMsg = 'Failed to pair device: User refused to link';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await tokenService.generateSessionToken(event.origin);
|
||||
const acceptedMsg = {
|
||||
type: MessageType.LINK_ACCEPTED,
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
messageId: event.data.messageId
|
||||
};
|
||||
window.parent.postMessage(
|
||||
acceptedMsg,
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to generate tokens: ${error}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreatePairing = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.CREATE_PAIRING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (services.isPaired()) {
|
||||
const errorMsg = 'Device already paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
console.log('🚀 Starting pairing process');
|
||||
await prepareAndSendPairingTx();
|
||||
await services.confirmPairing();
|
||||
|
||||
const pairingId = services.getPairingProcessId();
|
||||
|
||||
if (!pairingId) {
|
||||
throw new Error('Failed to get pairing process id');
|
||||
}
|
||||
|
||||
// Send success response
|
||||
const successMsg = {
|
||||
type: MessageType.PAIRING_CREATED,
|
||||
pairingId,
|
||||
messageId: event.data.messageId
|
||||
};
|
||||
window.parent.postMessage(successMsg, event.origin);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to create pairing process: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetMyProcesses = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.GET_MY_PROCESSES) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const myProcesses = await services.getMyProcesses();
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.GET_MY_PROCESSES,
|
||||
myProcesses,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to get processes: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetProcesses = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.GET_PROCESSES) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenService = await TokenService.getInstance();
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = event.data;
|
||||
|
||||
// Validate the session token
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const processes = await services.getProcesses();
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESSES_RETRIEVED,
|
||||
processes,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to get processes: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
/// We got a state for some process and return as many clear attributes as we can
|
||||
const handleDecryptState = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.RETRIEVE_DATA) {
|
||||
return;
|
||||
}
|
||||
const tokenService = await TokenService.getInstance();
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { processId, stateId, accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
// Retrieve the state for the process
|
||||
const process = await services.getProcess(processId);
|
||||
if (!process) {
|
||||
throw new Error('Can\'t find process');
|
||||
}
|
||||
const state = services.getStateFromId(process, stateId);
|
||||
|
||||
let res: Record<string, any> = {};
|
||||
if (state) {
|
||||
// Decrypt all the data we have the key for
|
||||
for (const attribute of Object.keys(state.pcd_commitment)) {
|
||||
if (attribute === 'roles' || state.public_data[attribute]) {
|
||||
continue;
|
||||
}
|
||||
const decryptedAttribute = await services.decryptAttribute(processId, state, attribute);
|
||||
if (decryptedAttribute) {
|
||||
res[attribute] = decryptedAttribute;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown state for process', processId);
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.DATA_RETRIEVED,
|
||||
data: res,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to retrieve data: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidateToken = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.VALIDATE_TOKEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessToken = event.data.accessToken;
|
||||
const refreshToken = event.data.refreshToken;
|
||||
if (!accessToken || !refreshToken) {
|
||||
errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId);
|
||||
}
|
||||
|
||||
const isValid = await tokenService.validateToken(accessToken, event.origin);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.VALIDATE_TOKEN,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
isValid: isValid,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
};
|
||||
|
||||
const handleRenewToken = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.RENEW_TOKEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshToken = event.data.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token provided');
|
||||
}
|
||||
|
||||
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
|
||||
|
||||
if (!newAccessToken) {
|
||||
throw new Error('Failed to refresh token');
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.RENEW_TOKEN,
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: refreshToken,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to renew token: ${error}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetPairingId = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.GET_PAIRING_ID) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const userPairingId = services.getPairingProcessId();
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.GET_PAIRING_ID,
|
||||
userPairingId,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to get pairing id: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateProcess = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.CREATE_PROCESS) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { processData, privateFields, roles, accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const { privateData, publicData } = splitPrivateData(processData, privateFields);
|
||||
|
||||
const createProcessReturn = await services.createProcess(privateData, publicData, roles);
|
||||
if (!createProcessReturn.updated_process) {
|
||||
throw new Error('Empty updated_process in createProcessReturn');
|
||||
}
|
||||
const processId = createProcessReturn.updated_process.process_id;
|
||||
const process = createProcessReturn.updated_process.current_process;
|
||||
const stateId = process.states[0].state_id;
|
||||
await services.handleApiReturn(createProcessReturn);
|
||||
|
||||
const res = {
|
||||
processId,
|
||||
process,
|
||||
processData,
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESS_CREATED,
|
||||
processCreated: res,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to create process: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotifyUpdate = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.NOTIFY_UPDATE) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { processId, stateId, accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
if (!isValid32ByteHex(stateId)) {
|
||||
throw new Error('Invalid state id');
|
||||
}
|
||||
|
||||
const res = await services.createPrdUpdate(processId, stateId);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.UPDATE_NOTIFIED,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to notify update for process: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidateState = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.VALIDATE_STATE) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { processId, stateId, accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const res = await services.approveChange(processId, stateId);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.STATE_VALIDATED,
|
||||
validatedProcess: res.updated_process,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to validate process: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateProcess = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.UPDATE_PROCESS) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
|
||||
try {
|
||||
// privateFields is only used if newData contains new fields
|
||||
// roles can be empty meaning that roles from the last commited state are kept
|
||||
const { processId, newData, privateFields, roles, accessToken } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
// Check if the new data is already in the process or if it's a new field
|
||||
const process = await services.getProcess(processId);
|
||||
if (!process) {
|
||||
throw new Error('Process not found');
|
||||
}
|
||||
let lastState = services.getLastCommitedState(process);
|
||||
if (!lastState) {
|
||||
const firstState = process.states[0];
|
||||
const roles = firstState.roles;
|
||||
if (services.rolesContainsUs(roles)) {
|
||||
const approveChangeRes= await services.approveChange(processId, firstState.state_id);
|
||||
await services.handleApiReturn(approveChangeRes);
|
||||
const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id);
|
||||
await services.handleApiReturn(prdUpdateRes);
|
||||
} else {
|
||||
if (firstState.validation_tokens.length > 0) {
|
||||
// Try to send it again anyway
|
||||
const res = await services.createPrdUpdate(processId, firstState.state_id);
|
||||
await services.handleApiReturn(res);
|
||||
}
|
||||
}
|
||||
// Wait a couple seconds
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
lastState = services.getLastCommitedState(process);
|
||||
if (!lastState) {
|
||||
throw new Error('Process doesn\'t have a commited state yet');
|
||||
}
|
||||
}
|
||||
const lastStateIndex = services.getLastCommitedStateIndex(process);
|
||||
if (lastStateIndex === null) {
|
||||
throw new Error('Process doesn\'t have a commited state yet');
|
||||
} // Shouldn't happen
|
||||
|
||||
const privateData: Record<string, any> = {};
|
||||
const publicData: Record<string, any> = {};
|
||||
|
||||
for (const field of Object.keys(newData)) {
|
||||
// Public data are carried along each new state
|
||||
// So the first thing we can do is check if the new data is public data
|
||||
if (lastState.public_data[field]) {
|
||||
// Add it to public data
|
||||
publicData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's not a public data, it may be either a private data update, or a new field (public of private)
|
||||
// Caller gave us a list of new private fields, if we see it here this is a new private field
|
||||
if (privateFields.includes(field)) {
|
||||
// Add it to private data
|
||||
privateData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now it can be an update of private data or a new public data
|
||||
// We check that the field exists in previous states private data
|
||||
for (let i = lastStateIndex; i >= 0; i--) {
|
||||
const state = process.states[i];
|
||||
if (state.pcd_commitment[field]) {
|
||||
// We don't even check if it's a public field, we would have seen it in the last state
|
||||
privateData[field] = newData[field];
|
||||
break;
|
||||
} else {
|
||||
// This attribute was not modified in that state, we go back to the previous state
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (privateData[field]) continue;
|
||||
|
||||
// We've get back all the way to the first state without seeing it, it's a new public field
|
||||
publicData[field] = newData[field];
|
||||
}
|
||||
|
||||
// We'll let the wasm check if roles are consistent
|
||||
|
||||
const res = await services.updateProcess(process, privateData, publicData, roles);
|
||||
await services.handleApiReturn(res);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PROCESS_UPDATED,
|
||||
updatedProcess: res.updated_process,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to update process: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDecodePublicData = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) return;
|
||||
|
||||
if (!services.isPaired()) {
|
||||
const errorMsg = 'Device not paired';
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { accessToken, encodedData } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const decodedData = services.decodeValue(encodedData);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.PUBLIC_DATA_DECODED,
|
||||
decodedData,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to decode data: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleHashValue = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.HASH_VALUE) return;
|
||||
|
||||
console.log('handleHashValue', event.data);
|
||||
|
||||
try {
|
||||
const { accessToken, commitedIn, label, fileBlob } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const hash = services.getHashForFile(commitedIn, label, fileBlob);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.VALUE_HASHED,
|
||||
hash,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to hash value: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetMerkleProof = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.GET_MERKLE_PROOF) return;
|
||||
|
||||
try {
|
||||
const { accessToken, processState, attributeName } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
const proof = services.getMerkleProofForFile(processState, attributeName);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.MERKLE_PROOF_RETRIEVED,
|
||||
proof,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to get merkle proof: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidateMerkleProof = async (event: MessageEvent) => {
|
||||
if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return;
|
||||
|
||||
try {
|
||||
const { accessToken, merkleProof, documentHash } = event.data;
|
||||
|
||||
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||
throw new Error('Invalid or expired session token');
|
||||
}
|
||||
|
||||
// Try to parse the proof
|
||||
// We will validate it's a MerkleProofResult in the wasm
|
||||
let parsedMerkleProof: MerkleProofResult;
|
||||
try {
|
||||
parsedMerkleProof= JSON.parse(merkleProof);
|
||||
} catch (e) {
|
||||
throw new Error('Provided merkleProof is not a valid json object');
|
||||
}
|
||||
|
||||
const res = services.validateMerkleProof(parsedMerkleProof, documentHash);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.MERKLE_PROOF_VALIDATED,
|
||||
isValid: res,
|
||||
messageId: event.data.messageId
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to get merkle proof: ${e}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
window.removeEventListener('message', handleMessage);
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
async function handleMessage(event: MessageEvent) {
|
||||
try {
|
||||
switch (event.data.type) {
|
||||
case MessageType.REQUEST_LINK:
|
||||
await handleRequestLink(event);
|
||||
break;
|
||||
case MessageType.CREATE_PAIRING:
|
||||
await handleCreatePairing(event);
|
||||
break;
|
||||
case MessageType.GET_MY_PROCESSES:
|
||||
await handleGetMyProcesses(event);
|
||||
break;
|
||||
case MessageType.GET_PROCESSES:
|
||||
await handleGetProcesses(event);
|
||||
break;
|
||||
case MessageType.RETRIEVE_DATA:
|
||||
await handleDecryptState(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_TOKEN:
|
||||
await handleValidateToken(event);
|
||||
break;
|
||||
case MessageType.RENEW_TOKEN:
|
||||
await handleRenewToken(event);
|
||||
break;
|
||||
case MessageType.GET_PAIRING_ID:
|
||||
await handleGetPairingId(event);
|
||||
break;
|
||||
case MessageType.CREATE_PROCESS:
|
||||
await handleCreateProcess(event);
|
||||
break;
|
||||
case MessageType.NOTIFY_UPDATE:
|
||||
await handleNotifyUpdate(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_STATE:
|
||||
await handleValidateState(event);
|
||||
break;
|
||||
case MessageType.UPDATE_PROCESS:
|
||||
await handleUpdateProcess(event);
|
||||
break;
|
||||
case MessageType.DECODE_PUBLIC_DATA:
|
||||
await handleDecodePublicData(event);
|
||||
break;
|
||||
case MessageType.HASH_VALUE:
|
||||
await handleHashValue(event);
|
||||
break;
|
||||
case MessageType.GET_MERKLE_PROOF:
|
||||
await handleGetMerkleProof(event);
|
||||
break;
|
||||
case MessageType.VALIDATE_MERKLE_PROOF:
|
||||
await handleValidateMerkleProof(event);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unhandled message type: ${event.data.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `Error handling message: ${error}`;
|
||||
errorResponse(errorMsg, event.origin, event.data.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: MessageType.LISTENING
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
async function cleanPage() {
|
||||
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 || '');
|
||||
}
|
||||
}
|
||||
}));
|
13
ihm_client/src/scanner.js
Executable file
13
ihm_client/src/scanner.js
Executable file
@ -0,0 +1,13 @@
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// handle the scanned code as you like, for example:
|
||||
console.log(`Code matched = ${decodedText}`, decodedResult);
|
||||
}
|
||||
|
||||
function onScanFailure(error) {
|
||||
// handle scan failure, usually better to ignore and keep scanning.
|
||||
// for example:
|
||||
console.warn(`Code scan error = ${error}`);
|
||||
}
|
||||
|
||||
let html5QrcodeScanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: { width: 250, height: 250 } }, /* verbose= */ false);
|
||||
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
|
8
ihm_client/src/service-workers/cache.worker.js
Normal file
8
ihm_client/src/service-workers/cache.worker.js
Normal file
@ -0,0 +1,8 @@
|
||||
const addResourcesToCache = async (resources) => {
|
||||
const cache = await caches.open('v1');
|
||||
await cache.addAll(resources);
|
||||
};
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(addResourcesToCache(['/', '/index.html', '/style.css', '/app.js', '/image-list.js', '/star-wars-logo.jpg', '/gallery/bountyHunters.jpg', '/gallery/myLittleVader.jpg', '/gallery/snowTroopers.jpg']));
|
||||
});
|
281
ihm_client/src/service-workers/database.worker.js
Executable file
281
ihm_client/src/service-workers/database.worker.js
Executable file
@ -0,0 +1,281 @@
|
||||
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 });
|
||||
}
|
||||
} else if (data.type === 'BATCH_WRITING') {
|
||||
const { storeName, objects } = data.payload;
|
||||
const db = await openDatabase();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
|
||||
for (const { key, object } of objects) {
|
||||
if (key) {
|
||||
await store.put(object, key);
|
||||
} else {
|
||||
await store.put(object);
|
||||
}
|
||||
}
|
||||
|
||||
await tx.done;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
454
ihm_client/src/services/database.service.ts
Executable file
454
ihm_client/src/services/database.service.ts
Executable file
@ -0,0 +1,454 @@
|
||||
import Services from './service';
|
||||
|
||||
export class Database {
|
||||
private static instance: Database;
|
||||
private db: IDBDatabase | null = null;
|
||||
private dbName: string = '4nk';
|
||||
private dbVersion: number = 1;
|
||||
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
||||
private messageChannel: MessageChannel | null = null;
|
||||
private messageChannelForGet: MessageChannel | null = null;
|
||||
private serviceWorkerCheckIntervalId: number | null = null;
|
||||
private storeDefinitions = {
|
||||
AnkLabels: {
|
||||
name: 'labels',
|
||||
options: { keyPath: 'emoji' },
|
||||
indices: [],
|
||||
},
|
||||
AnkWallet: {
|
||||
name: 'wallet',
|
||||
options: { keyPath: 'pre_id' },
|
||||
indices: [],
|
||||
},
|
||||
AnkProcess: {
|
||||
name: 'processes',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
AnkSharedSecrets: {
|
||||
name: 'shared_secrets',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
AnkUnconfirmedSecrets: {
|
||||
name: 'unconfirmed_secrets',
|
||||
options: { autoIncrement: true },
|
||||
indices: [],
|
||||
},
|
||||
AnkPendingDiffs: {
|
||||
name: 'diffs',
|
||||
options: { keyPath: 'value_commitment' },
|
||||
indices: [
|
||||
{ name: 'byStateId', keyPath: 'state_id', options: { unique: false } },
|
||||
{ name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } },
|
||||
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } },
|
||||
],
|
||||
},
|
||||
AnkData: {
|
||||
name: 'data',
|
||||
options: {},
|
||||
indices: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Private constructor to prevent direct instantiation from outside
|
||||
private constructor() {}
|
||||
|
||||
// Method to access the singleton instance of Database
|
||||
public static async getInstance(): Promise<Database> {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
await Database.instance.init();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
// Initialize the database
|
||||
private async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
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 () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('Database error:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getDb(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
await this.init();
|
||||
}
|
||||
return this.db!;
|
||||
}
|
||||
|
||||
public getStoreList(): { [key: string]: string } {
|
||||
const objectList: { [key: string]: string } = {};
|
||||
Object.keys(this.storeDefinitions).forEach((key) => {
|
||||
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
|
||||
});
|
||||
return objectList;
|
||||
}
|
||||
|
||||
public async registerServiceWorker(path: string) {
|
||||
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
||||
console.log('registering worker at', path);
|
||||
|
||||
try {
|
||||
// Get existing service worker registrations
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
if (registrations.length === 0) {
|
||||
// No existing workers: register a new one.
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
|
||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||
} else if (registrations.length === 1) {
|
||||
// One existing worker: update it (restart it) without unregistering.
|
||||
this.serviceWorkerRegistration = registrations[0];
|
||||
await this.serviceWorkerRegistration.update();
|
||||
console.log('Service Worker updated');
|
||||
} else {
|
||||
// More than one existing worker: unregister them all and register a new one.
|
||||
console.log('Multiple Service Worker(s) detected. Unregistering all...');
|
||||
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||
console.log('All previous Service Workers unregistered.');
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
|
||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
||||
}
|
||||
|
||||
await this.checkForUpdates();
|
||||
|
||||
// Set up a global message listener for responses from the service worker.
|
||||
navigator.serviceWorker.addEventListener('message', async (event) => {
|
||||
console.log('Received message from service worker:', event.data);
|
||||
await this.handleServiceWorkerMessage(event.data);
|
||||
});
|
||||
|
||||
// Set up a periodic check to ensure the service worker is active and to send a SCAN message.
|
||||
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
||||
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
|
||||
const service = await Services.getInstance();
|
||||
const payload = await service.getMyProcesses();
|
||||
if (payload && payload.length != 0) {
|
||||
activeWorker?.postMessage({ type: 'SCAN', payload });
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to wait for service worker activation
|
||||
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
|
||||
return new Promise((resolve) => {
|
||||
if (registration.active) {
|
||||
resolve(registration.active);
|
||||
} else {
|
||||
const listener = () => {
|
||||
if (registration.active) {
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', listener);
|
||||
resolve(registration.active);
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.addEventListener('controllerchange', listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates() {
|
||||
if (this.serviceWorkerRegistration) {
|
||||
// Check for updates to the service worker
|
||||
try {
|
||||
await this.serviceWorkerRegistration.update();
|
||||
|
||||
// If there's a new worker waiting, activate it immediately
|
||||
if (this.serviceWorkerRegistration.waiting) {
|
||||
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for service worker updates:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleServiceWorkerMessage(message: any) {
|
||||
switch (message.type) {
|
||||
case 'TO_DOWNLOAD':
|
||||
await this.handleDownloadList(message.data);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown message type received from service worker:', message);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadList(downloadList: string[]): Promise<void> {
|
||||
// Download the missing data
|
||||
let requestedStateId: string[] = [];
|
||||
const service = await Services.getInstance();
|
||||
for (const hash of downloadList) {
|
||||
const diff = await service.getDiffByValue(hash);
|
||||
if (!diff) {
|
||||
// This should never happen
|
||||
console.warn(`Missing a diff for hash ${hash}`);
|
||||
continue;
|
||||
}
|
||||
const processId = diff.process_id;
|
||||
const stateId = diff.state_id;
|
||||
const roles = diff.roles;
|
||||
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);
|
||||
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: string[] = [];
|
||||
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);
|
||||
if (diff === null) {
|
||||
continue;
|
||||
}
|
||||
const processId = diff!.process_id;
|
||||
const stateId = diff!.state_id;
|
||||
const roles = diff!.roles;
|
||||
if (!requestedStateId.includes(stateId)) {
|
||||
await service.requestDataFromPeers(processId, [stateId], [roles]);
|
||||
requestedStateId.push(stateId);
|
||||
}
|
||||
}
|
||||
} 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}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.serviceWorkerRegistration) {
|
||||
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
|
||||
}
|
||||
|
||||
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
if (event.data.status === 'success') {
|
||||
resolve();
|
||||
} else {
|
||||
const error = event.data.message;
|
||||
reject(new Error(error || 'Unknown error occurred while adding objects'));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
activeWorker?.postMessage(
|
||||
{
|
||||
type: 'BATCH_WRITING',
|
||||
payload,
|
||||
},
|
||||
[messageChannel.port2],
|
||||
);
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to send message to service worker: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getObject(storeName: string, key: string): Promise<any | null> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
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>> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const result: Record<string, any> = {};
|
||||
const cursor = store.openCursor();
|
||||
|
||||
cursor.onsuccess = (event) => {
|
||||
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
|
||||
const cursor = request.result;
|
||||
if (cursor) {
|
||||
result[cursor.key as string] = cursor.value;
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
cursor.onerror = () => {
|
||||
reject(cursor.error);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data from IndexedDB:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteObject(storeName: string, key: string): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const getRequest = store.delete(key);
|
||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async clearStore(storeName: string): Promise<void> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
const store = tx.objectStore(storeName);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const clearRequest = store.clear();
|
||||
clearRequest.onsuccess = () => resolve(clearRequest.result);
|
||||
clearRequest.onerror = () => reject(clearRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Request a store by index
|
||||
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
|
||||
const db = await this.getDb();
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const store = tx.objectStore(storeName);
|
||||
const index = store.index(indexName);
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const getAllRequest = index.getAll(request);
|
||||
getAllRequest.onsuccess = () => {
|
||||
const allItems = getAllRequest.result;
|
||||
const filtered = allItems.filter(item => item.state_id === request);
|
||||
resolve(filtered);
|
||||
};
|
||||
getAllRequest.onerror = () => reject(getAllRequest.error);
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
240
ihm_client/src/services/modal.service.ts
Executable file
240
ihm_client/src/services/modal.service.ts
Executable file
@ -0,0 +1,240 @@
|
||||
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 type { Member, RoleDefinition } from 'pkg/sdk_client';
|
||||
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
||||
import { interpolate } from '~/utils/html.utils';
|
||||
|
||||
interface ConfirmationModalOptions {
|
||||
title: string;
|
||||
content: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export default class ModalService {
|
||||
private static instance: ModalService;
|
||||
private stateId: string | null = null;
|
||||
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 memberOutPoints;
|
||||
if (roleDefinition['pairing']) {
|
||||
const owner = roleDefinition['pairing'];
|
||||
memberOutPoints = owner.members;
|
||||
} else {
|
||||
throw new Error('No "pairing" role');
|
||||
}
|
||||
|
||||
if (memberOutPoints.length != 1) {
|
||||
throw new Error('Must have exactly 1 member');
|
||||
}
|
||||
|
||||
console.log("MEMBER OUTPOINTS:", memberOutPoints);
|
||||
// We take all the addresses except our own
|
||||
const service = await Services.getInstance();
|
||||
const localAddress = service.getDeviceAddress();
|
||||
|
||||
// Récupérer les objets Member à partir des OutPoint
|
||||
const members: Member[] = [];
|
||||
for (const outPoint of memberOutPoints) {
|
||||
// Ici, vous devriez récupérer l'objet Member correspondant à l'OutPoint
|
||||
// Pour l'instant, on crée un objet Member vide
|
||||
const member: Member = { sp_addresses: [] };
|
||||
members.push(member);
|
||||
}
|
||||
|
||||
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 showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise<boolean> {
|
||||
// Create modal element
|
||||
const modalElement = document.createElement('div');
|
||||
modalElement.id = 'confirmation-modal';
|
||||
modalElement.innerHTML = `
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ''}>
|
||||
<h2>${options.title}</h2>
|
||||
<div class="modal-body">
|
||||
${options.content}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || 'Annuler'}</button>
|
||||
<button id="confirm-button" class="btn btn-primary">${options.confirmText || 'Confirmer'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add modal to document
|
||||
document.body.appendChild(modalElement);
|
||||
|
||||
// Return promise that resolves with user choice
|
||||
return new Promise((resolve) => {
|
||||
const confirmButton = modalElement.querySelector('#confirm-button');
|
||||
const cancelButton = modalElement.querySelector('#cancel-button');
|
||||
const modalOverlay = modalElement.querySelector('.modal-overlay');
|
||||
|
||||
const cleanup = () => {
|
||||
modalElement.remove();
|
||||
};
|
||||
|
||||
confirmButton?.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
cancelButton?.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
modalOverlay?.addEventListener('click', (e) => {
|
||||
if (e.target === modalOverlay) {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async closeConfirmationModal() {
|
||||
const service = await Services.getInstance();
|
||||
await service.unpairDevice();
|
||||
if (this.modal) this.modal.style.display = 'none';
|
||||
}
|
||||
}
|
1752
ihm_client/src/services/service.ts
Executable file
1752
ihm_client/src/services/service.ts
Executable file
File diff suppressed because it is too large
Load Diff
81
ihm_client/src/services/storage.service.ts
Normal file
81
ihm_client/src/services/storage.service.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
export async function storeData(servers: string[], key: string, value: Blob, ttl: number | null): Promise<AxiosResponse | null> {
|
||||
for (const server of servers) {
|
||||
try {
|
||||
// Append key and ttl as query parameters
|
||||
const url = new URL(`${server}/store`);
|
||||
url.searchParams.append('key', key);
|
||||
if (ttl !== null) {
|
||||
url.searchParams.append('ttl', ttl.toString());
|
||||
}
|
||||
|
||||
// Send the encrypted ArrayBuffer as the raw request body.
|
||||
const response = await axios.post(url.toString(), value, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
});
|
||||
console.log('Data stored successfully:', key);
|
||||
if (response.status !== 200) {
|
||||
console.error('Received response status', response.status);
|
||||
continue;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 409) {
|
||||
return null;
|
||||
}
|
||||
console.error('Error storing data:', error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function retrieveData(servers: string[], key: string): Promise<ArrayBuffer | null> {
|
||||
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;
|
||||
}
|
||||
// console.log('Retrieved data:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving 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: Record<string, boolean | null> = {};
|
||||
for (const server of servers) {
|
||||
res[server] = null;
|
||||
try {
|
||||
const response = await axios.get(`${server}/test/${key}`);
|
||||
if (response.status !== 200) {
|
||||
console.error(`${server}: Test response status: ${response.status}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data: TestResponse = response.data;
|
||||
|
||||
res[server] = data.value;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
87
ihm_client/src/services/token.ts
Normal file
87
ihm_client/src/services/token.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as jose from 'jose';
|
||||
|
||||
interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export default class TokenService {
|
||||
private static instance: TokenService;
|
||||
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
|
||||
private readonly ACCESS_TOKEN_EXPIRATION = '30s';
|
||||
private readonly REFRESH_TOKEN_EXPIRATION = '7d';
|
||||
private readonly encoder = new TextEncoder();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static async getInstance(): Promise<TokenService> {
|
||||
if (!TokenService.instance) {
|
||||
TokenService.instance = new TokenService();
|
||||
}
|
||||
return TokenService.instance;
|
||||
}
|
||||
|
||||
async generateSessionToken(origin: string): Promise<TokenPair> {
|
||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
||||
|
||||
const accessToken = await new jose.SignJWT({ origin, type: 'access' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
|
||||
const refreshToken = await new jose.SignJWT({ origin, type: 'refresh' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.REFRESH_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
async validateToken(token: string, origin: string): Promise<boolean> {
|
||||
try {
|
||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
||||
const { payload } = await jose.jwtVerify(token, secret);
|
||||
|
||||
return payload.origin === origin;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ERR_JWT_EXPIRED') {
|
||||
console.log('Token expiré');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error('Erreur de validation du token:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> {
|
||||
try {
|
||||
// Vérifier si le refresh token est valide
|
||||
const isValid = await this.validateToken(refreshToken, origin);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Vérifier le type du token
|
||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
||||
const { payload } = await jose.jwtVerify(refreshToken, secret);
|
||||
if (payload.type !== 'refresh') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Générer un nouveau access token
|
||||
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
||||
.sign(secret);
|
||||
|
||||
return newAccessToken;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du refresh du token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
35
ihm_client/src/types/raw-imports.d.ts
vendored
Normal file
35
ihm_client/src/types/raw-imports.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
// Déclarations pour les imports de fichiers raw
|
||||
declare module '*.html?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css?inline' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.js?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.ts?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
// Déclaration pour import.meta.env
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_JWT_SECRET_KEY: string;
|
||||
// Ajoutez d'autres variables d'environnement si nécessaire
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
258
ihm_client/src/types/sdk_client.d.ts
vendored
Normal file
258
ihm_client/src/types/sdk_client.d.ts
vendored
Normal file
@ -0,0 +1,258 @@
|
||||
// Types TypeScript pour sdk_client basé sur l'API Rust
|
||||
// Ce fichier définit les interfaces TypeScript correspondant aux types Rust exportés par wasm-bindgen
|
||||
|
||||
declare module 'pkg/sdk_client' {
|
||||
// Types de base
|
||||
export type DiffStatus = "None" | "Rejected" | "Validated";
|
||||
export type AnkFlag = "NewTx" | "Faucet" | "Cipher" | "Commit" | "Handshake" | "Sync" | "Unknown";
|
||||
export type PrdType = "None" | "Connect" | "Message" | "Update" | "List" | "Response" | "Confirm" | "TxProposal" | "Request";
|
||||
export type SyncType = "StateSync" | "ProcessSync" | "MemberSync" | "TxSync" | "BlockSync" | "PeerSync" | "RelaySync" | "HealthSync" | "MetricsSync" | "ConfigSync" | "CapabilitySync";
|
||||
export type HealthStatus = "Healthy" | "Warning" | "Critical" | "Offline";
|
||||
export type OutPoint = string;
|
||||
export type SilentPaymentAddress = string;
|
||||
export type AnkSharedSecretHash = string;
|
||||
export type OutPointProcessMap = Record<OutPoint, Process>;
|
||||
export type OutPointMemberMap = Record<OutPoint, Member>;
|
||||
export type TsUnsignedTransaction = any; // SilentPaymentUnsignedTransaction
|
||||
export type PcdCommitments = Record<string, string>;
|
||||
export type Pcd = Record<string, number[]>;
|
||||
export type Roles = Record<string, RoleDefinition>;
|
||||
// Types de base
|
||||
export interface Device {
|
||||
sp_wallet: SpWallet;
|
||||
pairing_process_commitment: OutPoint | null;
|
||||
paired_member: Member;
|
||||
}
|
||||
|
||||
export interface SpWallet {
|
||||
// Structure simplifiée pour SpWallet
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
sp_addresses: string[];
|
||||
}
|
||||
|
||||
export interface Process {
|
||||
states: ProcessState[];
|
||||
}
|
||||
|
||||
export interface ProcessState {
|
||||
commited_in: OutPoint;
|
||||
pcd_commitment: Record<string, string>;
|
||||
state_id: string;
|
||||
keys: Record<string, string>;
|
||||
validation_tokens: Proof[];
|
||||
public_data: Pcd;
|
||||
roles: Record<string, RoleDefinition>;
|
||||
}
|
||||
|
||||
export interface RoleDefinition {
|
||||
members: OutPoint[];
|
||||
validation_rules: ValidationRule[];
|
||||
storages: string[];
|
||||
}
|
||||
|
||||
export interface ValidationRule {
|
||||
quorum: number;
|
||||
fields: string[];
|
||||
min_sig_member: number;
|
||||
}
|
||||
|
||||
export interface SecretsStore {
|
||||
shared_secrets: Record<SilentPaymentAddress, AnkSharedSecretHash>;
|
||||
unconfirmed_secrets: AnkSharedSecretHash[];
|
||||
}
|
||||
|
||||
export interface MerkleProofResult {
|
||||
proof: string;
|
||||
root: string;
|
||||
attribute: string;
|
||||
attribute_index: number;
|
||||
total_leaves_count: number;
|
||||
}
|
||||
|
||||
export interface encryptWithNewKeyResult {
|
||||
cipher: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface UserDiff {
|
||||
process_id: string;
|
||||
state_id: string;
|
||||
value_commitment: string;
|
||||
field: string;
|
||||
roles: Roles;
|
||||
description: string | null;
|
||||
notify_user: boolean;
|
||||
need_validation: boolean;
|
||||
validation_status: DiffStatus;
|
||||
}
|
||||
|
||||
export interface UpdatedProcess {
|
||||
process_id: OutPoint;
|
||||
current_process: Process;
|
||||
diffs: UserDiff[];
|
||||
encrypted_data: Record<string, string>;
|
||||
validated_state: number[] | null;
|
||||
}
|
||||
|
||||
export interface ApiReturn {
|
||||
secrets: SecretsStore | null;
|
||||
updated_process: UpdatedProcess | null;
|
||||
new_tx_to_send: NewTxMessage | null;
|
||||
ciphers_to_send: string[];
|
||||
commit_to_send: CommitMessage | null;
|
||||
push_to_storage: string[];
|
||||
partial_tx: TsUnsignedTransaction | null;
|
||||
}
|
||||
|
||||
export interface NewTxMessage {
|
||||
transaction: string;
|
||||
tweak_data: string | null;
|
||||
error: AnkError | null;
|
||||
}
|
||||
|
||||
export interface CommitMessage {
|
||||
process_id: OutPoint;
|
||||
pcd_commitment: PcdCommitments;
|
||||
roles: Roles;
|
||||
public_data: Pcd;
|
||||
validation_tokens: Proof[];
|
||||
error: AnkError | null;
|
||||
}
|
||||
|
||||
export interface HandshakeMessage {
|
||||
sp_address: string;
|
||||
peers_list: OutPointMemberMap;
|
||||
processes_list: OutPointProcessMap;
|
||||
chain_tip: number;
|
||||
}
|
||||
|
||||
export interface FaucetMessage {
|
||||
sp_address: string;
|
||||
commitment: string;
|
||||
error: AnkError | null;
|
||||
}
|
||||
|
||||
export interface Prd {
|
||||
prd_type: PrdType;
|
||||
process_id: OutPoint;
|
||||
sender: Member;
|
||||
keys: Record<string, number[]>;
|
||||
pcd_commitments: PcdCommitments;
|
||||
validation_tokens: Proof[];
|
||||
roles: Roles;
|
||||
public_data: Pcd;
|
||||
payload: string;
|
||||
proof: Proof | null;
|
||||
}
|
||||
|
||||
export interface AnkError {
|
||||
// Structure simplifiée pour AnkError
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Proof {
|
||||
// Structure simplifiée pour Proof
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Fonctions exportées par wasm-bindgen
|
||||
export function setup(): void;
|
||||
export function get_address(): string;
|
||||
export function get_member(): Member;
|
||||
export function restore_device(device: Device): void;
|
||||
export function create_device_from_sp_wallet(sp_wallet: string): string;
|
||||
export function create_new_device(birthday: number, network_str: string): string;
|
||||
export function is_paired(): boolean;
|
||||
export function pair_device(process_id: string, sp_addresses: string[]): void;
|
||||
export function unpair_device(): void;
|
||||
export function dump_wallet(): string;
|
||||
export function reset_process_cache(): void;
|
||||
export function dump_process_cache(): string;
|
||||
export function set_process_cache(processes: any): void;
|
||||
export function add_to_process_cache(process_id: string, process: string): void;
|
||||
export function reset_shared_secrets(): void;
|
||||
export function set_shared_secrets(secrets: string): void;
|
||||
export function get_pairing_process_id(): string;
|
||||
export function dump_device(): Device;
|
||||
export function dump_neutered_device(): Device;
|
||||
export function reset_device(): void;
|
||||
export function get_txid(transaction: string): string;
|
||||
export function parse_new_tx(new_tx_msg: string, block_height: number, members_list: OutPointMemberMap): ApiReturn;
|
||||
export function parse_cipher(cipher_msg: string, members_list: OutPointMemberMap): ApiReturn;
|
||||
export function get_outputs(): any;
|
||||
export function get_available_amount(): bigint;
|
||||
export function create_transaction(addresses: string[], fee_rate: number): ApiReturn;
|
||||
export function sign_transaction(partial_tx: TsUnsignedTransaction): ApiReturn;
|
||||
export function create_new_process(
|
||||
private_data: Pcd,
|
||||
roles: Roles,
|
||||
public_data: Pcd,
|
||||
relay_address: string,
|
||||
fee_rate: number,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function update_process(
|
||||
process: Process,
|
||||
new_attributes: Pcd,
|
||||
roles: Roles,
|
||||
new_public_data: Pcd,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function request_data(
|
||||
process_id: string,
|
||||
state_ids_str: string[],
|
||||
roles: any,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function create_update_message(
|
||||
process: Process,
|
||||
state_id: string,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function validate_state(
|
||||
process: Process,
|
||||
state_id: string,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function refuse_state(
|
||||
process: Process,
|
||||
state_id: string,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function evaluate_state(
|
||||
process: Process,
|
||||
state_id: string,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function create_response_prd(
|
||||
process: Process,
|
||||
state_id: string,
|
||||
members_list: OutPointMemberMap
|
||||
): ApiReturn;
|
||||
export function create_faucet_msg(): string;
|
||||
export function get_storages(process_outpoint: string): string[];
|
||||
export function is_child_role(parent_roles: string, child_roles: string): void;
|
||||
export function decrypt_data(key: Uint8Array, data: Uint8Array): Uint8Array;
|
||||
export function encode_binary(data: any): Pcd;
|
||||
export function encode_json(json_data: any): Pcd;
|
||||
export function decode_value(value: Uint8Array): any;
|
||||
export function hash_value(value: any, commited_in: string, label: string): string;
|
||||
export function get_merkle_proof(process_state: ProcessState, attribute_name: string): MerkleProofResult;
|
||||
export function validate_merkle_proof(proof_result: MerkleProofResult, hash: string): boolean;
|
||||
}
|
||||
|
||||
// Types pour les imports relatifs
|
||||
declare module '../../../pkg/sdk_client' {
|
||||
export * from 'pkg/sdk_client';
|
||||
}
|
||||
|
||||
declare module '../../pkg/sdk_client' {
|
||||
export * from 'pkg/sdk_client';
|
||||
}
|
||||
|
||||
declare module '../pkg/sdk_client' {
|
||||
export * from 'pkg/sdk_client';
|
||||
}
|
4
ihm_client/src/utils/document.utils.ts
Normal file
4
ihm_client/src/utils/document.utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export function getCorrectDOM(componentTag: string): Node {
|
||||
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
|
||||
return dom;
|
||||
}
|
8
ihm_client/src/utils/html.utils.ts
Executable file
8
ihm_client/src/utils/html.utils.ts
Executable file
@ -0,0 +1,8 @@
|
||||
export function interpolate(template: string, data: { [key: string]: string }) {
|
||||
return template.replace(/{{(.*?)}}/g, (_, key) => data[key.trim()]);
|
||||
}
|
||||
|
||||
export function getCorrectDOM(componentTag: string): Node {
|
||||
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
|
||||
return dom;
|
||||
}
|
53
ihm_client/src/utils/messageMock.ts
Executable file
53
ihm_client/src/utils/messageMock.ts
Executable file
@ -0,0 +1,53 @@
|
||||
import { messagesMock as initialMessagesMock } from '../mocks/mock-signature/messagesMock.js';
|
||||
|
||||
// Store singleton for messages
|
||||
class MessageStore {
|
||||
private readonly STORAGE_KEY = 'chat_messages';
|
||||
private messages: any[] = [];
|
||||
|
||||
constructor() {
|
||||
this.messages = this.loadFromLocalStorage() || [];
|
||||
}
|
||||
|
||||
private loadFromLocalStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
} catch (error) {
|
||||
console.error('Error loading messages:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
setMessages(messages: any[]) {
|
||||
this.messages = messages;
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
|
||||
private saveToLocalStorage() {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.messages));
|
||||
} catch (error) {
|
||||
console.error('Error saving messages:', error);
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(memberId: string | number, message: any) {
|
||||
const memberMessages = this.messages.find((m) => String(m.memberId) === String(memberId));
|
||||
if (memberMessages) {
|
||||
memberMessages.messages.push(message);
|
||||
} else {
|
||||
this.messages.push({
|
||||
memberId: String(memberId),
|
||||
messages: [message],
|
||||
});
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
export const messageStore = new MessageStore();
|
96
ihm_client/src/utils/notification.store.ts
Executable file
96
ihm_client/src/utils/notification.store.ts
Executable file
@ -0,0 +1,96 @@
|
||||
interface INotification {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
time?: string;
|
||||
memberId?: string;
|
||||
}
|
||||
|
||||
class NotificationStore {
|
||||
private static instance: NotificationStore;
|
||||
private notifications: INotification[] = [];
|
||||
|
||||
private constructor() {
|
||||
this.loadFromLocalStorage();
|
||||
}
|
||||
|
||||
static getInstance(): NotificationStore {
|
||||
if (!NotificationStore.instance) {
|
||||
NotificationStore.instance = new NotificationStore();
|
||||
}
|
||||
return NotificationStore.instance;
|
||||
}
|
||||
|
||||
addNotification(notification: INotification) {
|
||||
this.notifications.push(notification);
|
||||
this.saveToLocalStorage();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
removeNotification(index: number) {
|
||||
this.notifications.splice(index, 1);
|
||||
this.saveToLocalStorage();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
getNotifications(): INotification[] {
|
||||
return this.notifications;
|
||||
}
|
||||
|
||||
private saveToLocalStorage() {
|
||||
localStorage.setItem('notifications', JSON.stringify(this.notifications));
|
||||
}
|
||||
|
||||
private loadFromLocalStorage() {
|
||||
const stored = localStorage.getItem('notifications');
|
||||
if (stored) {
|
||||
this.notifications = JSON.parse(stored);
|
||||
}
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
const badge = document.querySelector('.notification-badge') as HTMLElement;
|
||||
const board = document.querySelector('.notification-board') as HTMLElement;
|
||||
|
||||
if (badge) {
|
||||
badge.textContent = this.notifications.length.toString();
|
||||
badge.style.display = this.notifications.length > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (board) {
|
||||
this.renderNotificationBoard(board);
|
||||
}
|
||||
}
|
||||
|
||||
private renderNotificationBoard(board: HTMLElement) {
|
||||
board.innerHTML = '';
|
||||
|
||||
if (this.notifications.length === 0) {
|
||||
board.innerHTML = '<div class="no-notification">No notifications available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.forEach((notif, index) => {
|
||||
const notifElement = document.createElement('div');
|
||||
notifElement.className = 'notification-item';
|
||||
notifElement.innerHTML = `
|
||||
<div>${notif.title}</div>
|
||||
<div>${notif.description}</div>
|
||||
${notif.time ? `<div>${notif.time}</div>` : ''}
|
||||
`;
|
||||
notifElement.onclick = () => {
|
||||
if (notif.memberId) {
|
||||
window.loadMemberChat(notif.memberId);
|
||||
}
|
||||
this.removeNotification(index);
|
||||
};
|
||||
board.appendChild(notifElement);
|
||||
});
|
||||
}
|
||||
|
||||
public refreshNotifications() {
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
export const notificationStore = NotificationStore.getInstance();
|
24
ihm_client/src/utils/service.utils.ts
Normal file
24
ihm_client/src/utils/service.utils.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export function splitPrivateData(data: Record<string, any>, privateFields: string[]): { privateData: Record<string, any>, publicData: Record<string, any> } {
|
||||
const privateData: Record<string, any> = {};
|
||||
const publicData: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (privateFields.includes(key)) {
|
||||
privateData[key] = value;
|
||||
} else {
|
||||
publicData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { privateData, publicData };
|
||||
}
|
||||
|
||||
export function isValid32ByteHex(value: string): boolean {
|
||||
// Check if string is exactly 64 characters (32 bytes in hex)
|
||||
if (value.length !== 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if string only contains valid hex characters
|
||||
return /^[0-9a-fA-F]{64}$/.test(value);
|
||||
}
|
216
ihm_client/src/utils/sp-address.utils.ts
Executable file
216
ihm_client/src/utils/sp-address.utils.ts
Executable file
@ -0,0 +1,216 @@
|
||||
import Services from '../services/service';
|
||||
import { getCorrectDOM } from './html.utils';
|
||||
import { addSubscription } from './subscription.utils';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
//Copy Address
|
||||
export async function copyToClipboard(fullAddress: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullAddress);
|
||||
alert('Adresse copiée dans le presse-papiers !');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy the address: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
//Generate emojis list
|
||||
export function generateEmojiList(): string[] {
|
||||
const emojiRanges = [
|
||||
[0x1f600, 0x1f64f],
|
||||
[0x1f300, 0x1f5ff],
|
||||
[0x1f680, 0x1f6ff],
|
||||
[0x1f700, 0x1f77f],
|
||||
];
|
||||
|
||||
const emojiList: string[] = [];
|
||||
for (const range of emojiRanges) {
|
||||
const [start, end] = range;
|
||||
for (let i = start; i <= end && emojiList.length < 256; i++) {
|
||||
emojiList.push(String.fromCodePoint(i));
|
||||
}
|
||||
if (emojiList.length >= 256) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return emojiList.slice(0, 256);
|
||||
}
|
||||
|
||||
//Adress to emojis
|
||||
export async function addressToEmoji(text: string): Promise<string> {
|
||||
//Adress to Hash
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
|
||||
const hash = new Uint8Array(hashBuffer);
|
||||
const bytes = hash.slice(-4);
|
||||
|
||||
//Hash slice to emojis
|
||||
const emojiList = generateEmojiList();
|
||||
const emojis = Array.from(bytes)
|
||||
.map((byte) => emojiList[byte])
|
||||
.join('');
|
||||
return emojis;
|
||||
}
|
||||
|
||||
//Get emojis from other device
|
||||
async function emojisPairingRequest() {
|
||||
try {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||
|
||||
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
|
||||
const sp_adress: string | null = urlParams.get('sp_address');
|
||||
|
||||
if (!sp_adress) {
|
||||
// console.error("No 'sp_adress' parameter found in the URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis = await addressToEmoji(sp_adress);
|
||||
const emojiDisplay = container?.querySelector('.pairing-request');
|
||||
|
||||
if (emojiDisplay) {
|
||||
emojiDisplay.textContent = '(Request from: ' + emojis + ')';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Display address emojis and other device emojis
|
||||
export async function displayEmojis(text: string) {
|
||||
console.log('🚀 ~ Services ~ adressToEmoji');
|
||||
try {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
||||
const emojis = await addressToEmoji(text);
|
||||
const emojiDisplay = container?.querySelector('.emoji-display');
|
||||
|
||||
if (emojiDisplay) {
|
||||
emojiDisplay.textContent = emojis;
|
||||
}
|
||||
|
||||
emojisPairingRequest();
|
||||
|
||||
initAddressInput();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Other address
|
||||
export function initAddressInput() {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||
const addressInput = container.querySelector('#addressInput') as HTMLInputElement;
|
||||
const emojiDisplay = container.querySelector('#emoji-display-2');
|
||||
const okButton = container.querySelector('#okButton') as HTMLButtonElement;
|
||||
const createButton = container.querySelector('#createButton') as HTMLButtonElement;
|
||||
const actionButton = container.querySelector('#actionButton') as HTMLButtonElement;
|
||||
addSubscription(addressInput, 'input', async () => {
|
||||
let address = addressInput.value;
|
||||
|
||||
// Vérifie si l'adresse est une URL
|
||||
try {
|
||||
const url = new URL(address);
|
||||
// Si c'est une URL valide, extraire le paramètre sp_address
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
const extractedAddress = urlParams.get('sp_address') || ''; // Prend sp_address ou une chaîne vide
|
||||
|
||||
if (extractedAddress) {
|
||||
address = extractedAddress;
|
||||
addressInput.value = address; // Met à jour l'input pour afficher uniquement l'adresse extraite
|
||||
}
|
||||
} catch (e) {
|
||||
// Si ce n'est pas une URL valide, on garde l'adresse originale
|
||||
console.log("Ce n'est pas une URL valide, on garde l'adresse originale.");
|
||||
}
|
||||
if (address) {
|
||||
const emojis = await addressToEmoji(address);
|
||||
if (emojiDisplay) {
|
||||
emojiDisplay.innerHTML = emojis;
|
||||
}
|
||||
if (okButton) {
|
||||
okButton.style.display = 'inline-block';
|
||||
}
|
||||
} else {
|
||||
if (emojiDisplay) {
|
||||
emojiDisplay.innerHTML = '';
|
||||
}
|
||||
if (okButton) {
|
||||
okButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (createButton) {
|
||||
addSubscription(createButton, 'click', () => {
|
||||
onCreateButtonClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateButtonClick() {
|
||||
try {
|
||||
await prepareAndSendPairingTx();
|
||||
const service = await Services.getInstance();
|
||||
await service.confirmPairing();
|
||||
} catch (e) {
|
||||
console.error(`onCreateButtonClick error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareAndSendPairingTx(): Promise<void> {
|
||||
const service = await Services.getInstance();
|
||||
|
||||
try {
|
||||
await service.checkConnections([]);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const relayAddress = service.getAllRelays();
|
||||
const createPairingProcessReturn = await service.createPairingProcess(
|
||||
"",
|
||||
[],
|
||||
);
|
||||
|
||||
if (!createPairingProcessReturn.updated_process) {
|
||||
throw new Error('createPairingProcess returned an empty new process');
|
||||
}
|
||||
|
||||
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
|
||||
service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id);
|
||||
|
||||
await service.handleApiReturn(createPairingProcessReturn);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateQRCode(spAddress: string) {
|
||||
try {
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||
const currentUrl = 'https://' + window.location.host;
|
||||
const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress);
|
||||
const qrCode = container?.querySelector('.qr-code img');
|
||||
qrCode?.setAttribute('src', url);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateCreateBtn() {
|
||||
try{
|
||||
//Generate CreateBtn
|
||||
const container = getCorrectDOM('login-4nk-component') as HTMLElement
|
||||
const createBtn = container?.querySelector('.create-btn');
|
||||
if (createBtn) {
|
||||
createBtn.textContent = 'CREATE';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
}
|
19
ihm_client/src/utils/subscription.utils.ts
Executable file
19
ihm_client/src/utils/subscription.utils.ts
Executable file
@ -0,0 +1,19 @@
|
||||
let subscriptions: { element: Element | Document; event: any; eventHandler: EventListenerOrEventListenerObject }[] = [];
|
||||
|
||||
export function cleanSubscriptions(): void {
|
||||
console.log('🚀 ~ cleanSubscriptions ~ sub:', subscriptions);
|
||||
for (const sub of subscriptions) {
|
||||
const el = sub.element;
|
||||
const eventHandler = sub.eventHandler;
|
||||
if (el) {
|
||||
el.removeEventListener(sub.event, eventHandler);
|
||||
}
|
||||
}
|
||||
subscriptions = [];
|
||||
}
|
||||
|
||||
export function addSubscription(element: Element | Document, event: any, eventHandler: EventListenerOrEventListenerObject): void {
|
||||
if (!element) return;
|
||||
subscriptions.push({ element, event, eventHandler });
|
||||
element.addEventListener(event, eventHandler);
|
||||
}
|
89
ihm_client/src/websockets.ts
Executable file
89
ihm_client/src/websockets.ts
Executable file
@ -0,0 +1,89 @@
|
||||
import type { AnkFlag } from 'pkg/sdk_client';
|
||||
import Services from './services/service';
|
||||
|
||||
let ws: WebSocket;
|
||||
let messageQueue: string[] = [];
|
||||
export async function initWebsocket(url: string) {
|
||||
ws = new WebSocket(url);
|
||||
|
||||
if (ws !== null) {
|
||||
ws.onopen = async (event) => {
|
||||
console.log('WebSocket connection established');
|
||||
|
||||
while (messageQueue.length > 0) {
|
||||
const message = messageQueue.shift();
|
||||
if (message) {
|
||||
ws.send(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for messages
|
||||
ws.onmessage = (event) => {
|
||||
const msgData = event.data;
|
||||
|
||||
// console.log("Received text message: ", msgData);
|
||||
(async () => {
|
||||
if (typeof msgData === 'string') {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(msgData);
|
||||
const services = await Services.getInstance();
|
||||
switch (parsedMessage.flag) {
|
||||
case 'Handshake':
|
||||
await services.handleHandshakeMsg(url, parsedMessage.content);
|
||||
break;
|
||||
case 'NewTx':
|
||||
await services.parseNewTx(parsedMessage.content);
|
||||
break;
|
||||
case 'Cipher':
|
||||
await services.parseCipher(parsedMessage.content);
|
||||
break;
|
||||
case 'Commit':
|
||||
// Basically if we see this it means we have an error
|
||||
await services.handleCommitError(parsedMessage.content);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Received an invalid message:', error);
|
||||
}
|
||||
} else {
|
||||
console.error('Received a non-string message');
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// Listen for possible errors
|
||||
ws.onerror = (event) => {
|
||||
console.error('WebSocket error:', event);
|
||||
};
|
||||
|
||||
// Listen for when the connection is closed
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket is closed now.');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Method to send messages
|
||||
export function sendMessage(flag: AnkFlag, message: string): void {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const networkMessage = {
|
||||
flag: flag,
|
||||
content: message,
|
||||
};
|
||||
console.log('Sending message of type:', flag);
|
||||
ws.send(JSON.stringify(networkMessage));
|
||||
} else {
|
||||
console.error('WebSocket is not open. ReadyState:', ws.readyState);
|
||||
messageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function getUrl(): string {
|
||||
return ws.url;
|
||||
}
|
||||
|
||||
// Method to close the WebSocket connection
|
||||
export function close(): void {
|
||||
ws.close();
|
||||
}
|
124
ihm_client/start.sh
Normal file
124
ihm_client/start.sh
Normal file
@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Démarrage de l'interface utilisateur 4NK..."
|
||||
|
||||
# Variables d'environnement avec valeurs par défaut
|
||||
SDK_RELAY_WS_URL=${SDK_RELAY_WS_URL:-"ws://sdk_relay_1:8090"}
|
||||
SDK_RELAY_HTTP_URL=${SDK_RELAY_HTTP_URL:-"http://sdk_relay_1:8091"}
|
||||
BITCOIN_RPC_URL=${BITCOIN_RPC_URL:-"http://bitcoin:18443"}
|
||||
BLINDBIT_URL=${BLINDBIT_URL:-"http://blindbit:8000"}
|
||||
|
||||
# Fonction pour attendre qu'un service soit disponible
|
||||
wait_for_service() {
|
||||
local service_name=$1
|
||||
local service_url=$2
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
echo "⏳ Attente du service $service_name ($service_url)..."
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if wget --quiet --tries=1 --timeout=5 --spider "$service_url" 2>/dev/null; then
|
||||
echo "✅ Service $service_name disponible"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Tentative $attempt/$max_attempts - Service $service_name non disponible"
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
echo "❌ Service $service_name non disponible après $max_attempts tentatives"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Fonction pour vérifier la connectivité WebSocket
|
||||
check_websocket() {
|
||||
local service_name=$1
|
||||
local ws_url=$2
|
||||
local max_attempts=10
|
||||
local attempt=1
|
||||
|
||||
echo "🔌 Vérification WebSocket $service_name ($ws_url)..."
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if nc -z $(echo $ws_url | sed 's|ws://||' | sed 's|wss://||' | cut -d: -f1) $(echo $ws_url | cut -d: -f3) 2>/dev/null; then
|
||||
echo "✅ WebSocket $service_name accessible"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Tentative $attempt/$max_attempts - WebSocket $service_name non accessible"
|
||||
sleep 3
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
echo "⚠️ WebSocket $service_name non accessible (continuera sans)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Vérification des services critiques
|
||||
echo "🔍 Vérification des services 4NK_node..."
|
||||
|
||||
# Attendre sdk_relay HTTP (critique)
|
||||
if ! wait_for_service "sdk_relay HTTP" "$SDK_RELAY_HTTP_URL/health"; then
|
||||
echo "❌ Service sdk_relay HTTP critique non disponible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Vérifier sdk_relay WebSocket (optionnel)
|
||||
check_websocket "sdk_relay WebSocket" "$SDK_RELAY_WS_URL"
|
||||
|
||||
# Vérifier Bitcoin Core (optionnel)
|
||||
if ! wait_for_service "Bitcoin Core" "$BITCOIN_RPC_URL" 2>/dev/null; then
|
||||
echo "⚠️ Bitcoin Core non disponible (optionnel)"
|
||||
fi
|
||||
|
||||
# Vérifier Blindbit (optionnel)
|
||||
if ! wait_for_service "Blindbit" "$BLINDBIT_URL" 2>/dev/null; then
|
||||
echo "⚠️ Blindbit non disponible (optionnel)"
|
||||
fi
|
||||
|
||||
# Génération de la configuration dynamique
|
||||
echo "⚙️ Génération de la configuration dynamique..."
|
||||
|
||||
# Créer un fichier de configuration JavaScript pour l'application
|
||||
cat > /usr/share/nginx/html/config.js << EOF
|
||||
window.ENV_CONFIG = {
|
||||
SDK_RELAY_WS_URL: '$SDK_RELAY_WS_URL',
|
||||
SDK_RELAY_HTTP_URL: '$SDK_RELAY_HTTP_URL',
|
||||
BITCOIN_RPC_URL: '$BITCOIN_RPC_URL',
|
||||
BLINDBIT_URL: '$BLINDBIT_URL',
|
||||
ENVIRONMENT: '4nk-node'
|
||||
};
|
||||
EOF
|
||||
|
||||
# Démarrage de nginx
|
||||
echo "🌐 Démarrage de nginx..."
|
||||
nginx -g "daemon off;" &
|
||||
|
||||
# Attendre que nginx soit prêt
|
||||
sleep 2
|
||||
|
||||
# Vérifier que nginx fonctionne
|
||||
if ! wget --quiet --tries=1 --timeout=5 --spider http://localhost 2>/dev/null; then
|
||||
echo "❌ Nginx n'a pas démarré correctement"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Interface utilisateur 4NK démarrée avec succès"
|
||||
echo " 📍 URL: http://localhost"
|
||||
echo " 🔌 WebSocket: $SDK_RELAY_WS_URL"
|
||||
echo " 🌐 API: $SDK_RELAY_HTTP_URL"
|
||||
|
||||
# Maintenir le conteneur en vie
|
||||
while true; do
|
||||
sleep 30
|
||||
|
||||
# Vérification périodique de la santé
|
||||
if ! wget --quiet --tries=1 --timeout=5 --spider http://localhost 2>/dev/null; then
|
||||
echo "❌ Nginx ne répond plus, redémarrage..."
|
||||
nginx -s reload
|
||||
fi
|
||||
done
|
29
ihm_client/tsconfig.json
Executable file
29
ihm_client/tsconfig.json
Executable file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext", "webworker"],
|
||||
"types": [],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
81
ihm_client/vite.config.ts
Executable file
81
ihm_client/vite.config.ts
Executable file
@ -0,0 +1,81 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue'; // or react from '@vitejs/plugin-react' if using React
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
import {createHtmlPlugin} from 'vite-plugin-html';
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
// import pluginTerminal from 'vite-plugin-terminal';
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
include: ['qrcode']
|
||||
},
|
||||
plugins: [
|
||||
vue(), // or react() if using React
|
||||
wasm(),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
template: 'index.html',
|
||||
}),
|
||||
typescript({
|
||||
sourceMap: false,
|
||||
declaration: true,
|
||||
declarationDir: "dist/types",
|
||||
rootDir: "src",
|
||||
outDir: "dist",
|
||||
}),
|
||||
// pluginTerminal({
|
||||
// console: 'terminal',
|
||||
// output: ['terminal', 'console']
|
||||
// })
|
||||
],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
target: 'esnext',
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
input: './src/index.ts',
|
||||
output: {
|
||||
entryFileNames: 'index.js',
|
||||
},
|
||||
},
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/router.ts'),
|
||||
name: 'ihm-service',
|
||||
formats: ['es'],
|
||||
fileName: (format) => `ihm-service.${format}.js`,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
},
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
cachedChecks: false,
|
||||
},
|
||||
port: 3003,
|
||||
proxy: {
|
||||
'/storage': {
|
||||
target: 'https://demo.4nkweb.com',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on('error', (err, _req, _res) => {
|
||||
console.log('proxy error', err);
|
||||
});
|
||||
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||
console.log('Sending Request:', req.method, req.url);
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||
console.log('Received Response:', proxyRes.statusCode, req.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
77
start-4nk-node-with-ui.sh
Executable file
77
start-4nk-node-with-ui.sh
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Démarrage de l'infrastructure 4NK_node avec interface utilisateur..."
|
||||
|
||||
# Vérifier que nous sommes dans le bon répertoire
|
||||
if [[ ! -f "docker-compose.yml" ]]; then
|
||||
echo "❌ Ce script doit être exécuté depuis le répertoire 4NK_node"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Arrêter et nettoyer les conteneurs existants
|
||||
echo "🧹 Nettoyage des conteneurs existants..."
|
||||
docker-compose down
|
||||
|
||||
# Démarrer tous les services
|
||||
echo "📦 Démarrage de tous les services..."
|
||||
docker-compose up -d
|
||||
|
||||
# Attendre que les services critiques soient prêts
|
||||
echo "⏳ Attente du démarrage des services critiques..."
|
||||
sleep 30
|
||||
|
||||
# Vérifier la santé des services
|
||||
echo "🔍 Vérification de la santé des services..."
|
||||
|
||||
# Bitcoin
|
||||
if docker-compose ps bitcoin | grep -q "Up"; then
|
||||
echo "✅ Bitcoin démarré"
|
||||
else
|
||||
echo "❌ Bitcoin n'est pas démarré"
|
||||
docker-compose logs bitcoin
|
||||
fi
|
||||
|
||||
# Blindbit
|
||||
if docker-compose ps blindbit | grep -q "Up"; then
|
||||
echo "✅ Blindbit démarré"
|
||||
else
|
||||
echo "❌ Blindbit n'est pas démarré"
|
||||
docker-compose logs blindbit
|
||||
fi
|
||||
|
||||
# SDK Relays
|
||||
for i in {1..3}; do
|
||||
if docker-compose ps "sdk_relay_$i" | grep -q "Up"; then
|
||||
echo "✅ SDK Relay $i démarré"
|
||||
else
|
||||
echo "❌ SDK Relay $i n'est pas démarré"
|
||||
docker-compose logs "sdk_relay_$i"
|
||||
fi
|
||||
done
|
||||
|
||||
# Interface utilisateur
|
||||
if docker-compose ps ihm_client | grep -q "Up"; then
|
||||
echo "✅ Interface utilisateur démarrée"
|
||||
else
|
||||
echo "❌ Interface utilisateur n'est pas démarrée"
|
||||
docker-compose logs ihm_client
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 Infrastructure 4NK_node démarrée avec succès !"
|
||||
echo ""
|
||||
echo "📍 URLs d'accès :"
|
||||
echo " 🌐 Interface utilisateur: http://localhost:8080"
|
||||
echo " 🔗 Bitcoin RPC: http://localhost:18443"
|
||||
echo " 🔗 Blindbit: http://localhost:8000"
|
||||
echo " 🔗 SDK Relay 1: http://localhost:8091"
|
||||
echo " 🔗 SDK Relay 2: http://localhost:8093"
|
||||
echo " 🔗 SDK Relay 3: http://localhost:8095"
|
||||
echo ""
|
||||
echo "🔍 Commandes utiles :"
|
||||
echo " 📋 Statut des services: docker-compose ps"
|
||||
echo " 📋 Logs d'un service: docker-compose logs <service_name>"
|
||||
echo " 📋 Arrêter l'infrastructure: docker-compose down"
|
||||
echo " 📋 Redémarrer un service: docker-compose restart <service_name>"
|
30
start-ihm-client.sh
Executable file
30
start-ihm-client.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Démarrage de l'interface utilisateur 4NK..."
|
||||
|
||||
# Vérifier que nous sommes dans le bon répertoire
|
||||
if [[ ! -f "docker-compose.yml" ]]; then
|
||||
echo "❌ Ce script doit être exécuté depuis le répertoire 4NK_node"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Démarrer uniquement le service ihm_client
|
||||
echo "📦 Démarrage du service ihm_client..."
|
||||
docker-compose up -d ihm_client
|
||||
|
||||
# Attendre que le service soit prêt
|
||||
echo "⏳ Attente du démarrage..."
|
||||
sleep 10
|
||||
|
||||
# Vérifier la santé du service
|
||||
if docker-compose ps ihm_client | grep -q "Up"; then
|
||||
echo "✅ Interface utilisateur démarrée avec succès"
|
||||
echo " 📍 URL: http://localhost:8080"
|
||||
echo " 🔍 Logs: docker logs 4nk-ihm-client"
|
||||
else
|
||||
echo "❌ Échec du démarrage de l'interface utilisateur"
|
||||
docker-compose logs ihm_client
|
||||
exit 1
|
||||
fi
|
Loading…
x
Reference in New Issue
Block a user