ihm_client/src/router.ts

892 lines
29 KiB
TypeScript
Executable File

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