**Motivations :** - Implémentation du système de sélection de mode de sécurité - Séparation claire entre les données de sécurité et les données du wallet - Suppression des duplications entre 'encrypted-pbkdf2-key' et 'pbkdf2-key' - Architecture modulaire pour la gestion des credentials **Modifications :** - Ajout du composant security-mode-selector pour la sélection du mode de sécurité - Création des pages séquentielles : security-setup, wallet-setup, birthday-setup - Implémentation des services de credentials (encryption, storage, webauthn) - Ajout du service security-mode pour la gestion des modes de sécurité - Correction du stockage des clés PBKDF2 avec le securityMode dynamique - Suppression des méthodes redondantes dans StorageService - Nettoyage des appels redondants dans secure-credentials.service.ts **Pages affectées :** - src/components/security-mode-selector/ (nouveau composant) - src/pages/security-setup/ (nouvelle page) - src/pages/wallet-setup/ (nouvelle page) - src/pages/birthday-setup/ (nouvelle page) - src/services/credentials/ (nouveaux services) - src/services/security-mode.service.ts (nouveau service) - src/services/secure-credentials.service.ts (modifié) - src/services/database.service.ts (modifié) - src/router.ts (modifié) - src/pages/home/home.ts (modifié)
1161 lines
43 KiB
TypeScript
Executable File
1161 lines
43 KiB
TypeScript
Executable File
import Services from '../../services/service';
|
|
import { addSubscription } from '../../utils/subscription.utils';
|
|
import { displayEmojis, generateCreateBtn, addressToEmoji, prepareAndSendPairingTx } from '../../utils/sp-address.utils';
|
|
import { getCorrectDOM } from '../../utils/html.utils';
|
|
import { IframePairingComponent } from '../../components/iframe-pairing/iframe-pairing';
|
|
|
|
// Extend WindowEventMap to include custom events
|
|
declare global {
|
|
interface WindowEventMap {
|
|
'pairing-words-generated': CustomEvent;
|
|
'pairing-status-update': CustomEvent;
|
|
'pairing-success': CustomEvent;
|
|
'pairing-error': CustomEvent;
|
|
}
|
|
}
|
|
|
|
|
|
let isInitializing = false;
|
|
|
|
export async function initHomePage(): Promise<void> {
|
|
if (isInitializing) {
|
|
console.log('⚠️ Home page already initializing, skipping...');
|
|
return;
|
|
}
|
|
|
|
isInitializing = true;
|
|
console.log('INIT-HOME');
|
|
|
|
// No loading spinner - let the interface load naturally
|
|
|
|
// Initialize iframe pairing, content menu, and communication only if in iframe
|
|
if (window.parent !== window) {
|
|
initIframePairing();
|
|
initContentMenu();
|
|
initIframeCommunication();
|
|
}
|
|
|
|
// Set up iframe pairing button listeners
|
|
setupIframePairingButtons();
|
|
|
|
// Set up main pairing interface (avec protection contre les appels multiples)
|
|
if (!isMainPairingSetup) {
|
|
setupMainPairing();
|
|
}
|
|
|
|
// Set up account actions
|
|
setupAccountActions();
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
try {
|
|
console.log('🔧 Getting services instance...');
|
|
const service = await Services.getInstance();
|
|
|
|
// D'abord vérifier la sécurité avant de créer le wallet
|
|
console.log('🔐 Checking security configuration...');
|
|
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
|
|
const secureCredentialsService = SecureCredentialsService.getInstance();
|
|
|
|
const hasCredentials = await secureCredentialsService.hasCredentials();
|
|
|
|
if (!hasCredentials) {
|
|
console.log('🔐 No security credentials found, user must configure security first...');
|
|
// Afficher le sélecteur de mode de sécurité
|
|
await handleMainPairing();
|
|
return;
|
|
}
|
|
|
|
// Check if wallet exists, create if not
|
|
console.log('🔍 Checking for existing wallet...');
|
|
const existingDevice = await service.getDeviceFromDatabase();
|
|
|
|
if (!existingDevice) {
|
|
console.log('📱 No wallet found, creating new device...');
|
|
const spAddress = await service.createNewDevice();
|
|
console.log('✅ New device created with address:', spAddress);
|
|
|
|
// Verify wallet was created successfully
|
|
const verifyDevice = await service.getDeviceFromDatabase();
|
|
if (!verifyDevice) {
|
|
throw new Error('Failed to create wallet - device not found after creation');
|
|
}
|
|
console.log('✅ Wallet creation verified');
|
|
} else {
|
|
console.log('📱 Existing wallet found');
|
|
console.log('🔍 Wallet details:', {
|
|
hasSpendKey: !!existingDevice.sp_wallet?.spend_key,
|
|
hasScanKey: !!existingDevice.sp_wallet?.scan_key,
|
|
birthday: existingDevice.sp_wallet?.birthday
|
|
});
|
|
}
|
|
|
|
// Trigger WebAuthn authentication
|
|
console.log('🔐 Triggering WebAuthn authentication...');
|
|
await handleMainPairing();
|
|
|
|
// Attendre que les credentials soient réellement disponibles avant de continuer
|
|
console.log('⏳ Waiting for credentials to be fully available...');
|
|
await waitForCredentialsAvailability();
|
|
console.log('✅ Credentials confirmed as available, proceeding...');
|
|
|
|
// After WebAuthn, get device address and setup UI
|
|
console.log('🔧 Getting device address...');
|
|
|
|
try {
|
|
const spAddress = await service.getDeviceAddress();
|
|
console.log('🔧 Generating create button...');
|
|
generateCreateBtn();
|
|
console.log('🔧 Displaying emojis...');
|
|
displayEmojis(spAddress);
|
|
} catch (error) {
|
|
console.error('❌ Failed to get device address:', error);
|
|
if (error.message.includes('Wallet keys not available')) {
|
|
console.error('❌ Wallet keys not available - authentication failed');
|
|
throw new Error('Authentication failed - wallet keys not available');
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
console.log('✅ Home page initialization completed');
|
|
} catch (error) {
|
|
console.error('❌ Error initializing home page:', error);
|
|
throw error;
|
|
} finally {
|
|
isInitializing = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Initialize iframe pairing component
|
|
let iframePairing: IframePairingComponent | null = null;
|
|
|
|
export function initIframePairing() {
|
|
if (!iframePairing) {
|
|
iframePairing = new IframePairingComponent();
|
|
iframePairing.createHiddenIframe();
|
|
|
|
// Listen for pairing events
|
|
window.addEventListener('pairing-words-generated', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('✅ 4 words generated via iframe:', customEvent.detail.words);
|
|
// Update the UI with the generated words
|
|
const creatorWordsElement = document.querySelector('#creator-words');
|
|
if (creatorWordsElement) {
|
|
creatorWordsElement.textContent = customEvent.detail.words;
|
|
creatorWordsElement.className = 'words-content active';
|
|
}
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_WORDS_GENERATED',
|
|
data: { words: customEvent.detail.words },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pairing-status-update', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('📊 Pairing status update:', customEvent.detail.status);
|
|
// Update status indicators
|
|
const statusElement = document.querySelector(`#${customEvent.detail.type}-status span`);
|
|
if (statusElement) {
|
|
statusElement.textContent = customEvent.detail.status;
|
|
}
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_STATUS_UPDATE',
|
|
data: { status: customEvent.detail.status, type: customEvent.detail.type },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pairing-success', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.log('✅ Pairing successful:', customEvent.detail.message);
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_SUCCESS',
|
|
data: { message: customEvent.detail.message },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
|
|
// Handle successful pairing
|
|
setTimeout(() => {
|
|
window.location.href = '/account';
|
|
}, 2000);
|
|
});
|
|
|
|
window.addEventListener('pairing-error', (event: Event) => {
|
|
const customEvent = event as CustomEvent;
|
|
console.error('❌ Pairing error:', customEvent.detail.error);
|
|
|
|
// Send message to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'PAIRING_4WORDS_ERROR',
|
|
data: { error: customEvent.detail.error },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
|
|
// Handle pairing error
|
|
alert(`Pairing error: ${customEvent.detail.error}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize content menu (only in iframe mode)
|
|
export function initContentMenu() {
|
|
// Only add menu buttons if we're in an iframe
|
|
if (window.parent !== window) {
|
|
// Add iframe mode class to body
|
|
document.body.classList.add('iframe-mode');
|
|
|
|
// Add menu buttons to title container
|
|
const titleContainer = document.querySelector('.title-container');
|
|
if (titleContainer) {
|
|
const menuHtml = `
|
|
<div class="content-menu">
|
|
<button class="menu-btn active" data-page="home">🏠 Home</button>
|
|
<button class="menu-btn" data-page="account">👤 Account</button>
|
|
<button class="menu-btn" data-page="settings">⚙️ Settings</button>
|
|
<button class="menu-btn" data-page="help">❓ Help</button>
|
|
</div>
|
|
`;
|
|
titleContainer.insertAdjacentHTML('beforeend', menuHtml);
|
|
}
|
|
|
|
const menuButtons = document.querySelectorAll('.menu-btn');
|
|
|
|
menuButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
// Remove active class from all buttons
|
|
menuButtons.forEach(btn => btn.classList.remove('active'));
|
|
// Add active class to clicked button
|
|
button.classList.add('active');
|
|
|
|
const page = button.getAttribute('data-page');
|
|
console.log(`Menu clicked: ${page}`);
|
|
|
|
// Send message to parent window
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'MENU_NAVIGATION',
|
|
data: { page },
|
|
},
|
|
'*'
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize iframe communication
|
|
export function initIframeCommunication() {
|
|
// Listen for messages from parent window
|
|
window.addEventListener('message', event => {
|
|
// Filter out browser extension messages first
|
|
if (
|
|
event.data.source === 'react-devtools-content-script' ||
|
|
event.data.hello === true ||
|
|
!event.data.type ||
|
|
event.data.type.startsWith('Pass::') ||
|
|
event.data.type === 'PassClientScriptReady'
|
|
) {
|
|
return; // Ignore browser extension messages
|
|
}
|
|
|
|
// Security check - in production, verify event.origin
|
|
console.log('📨 Received message from parent:', event.data);
|
|
|
|
const { type, data } = event.data;
|
|
|
|
switch (type) {
|
|
case 'TEST_MESSAGE':
|
|
console.log('🧪 Test message received:', data.message);
|
|
// Send response back to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'TEST_RESPONSE',
|
|
data: { response: 'Hello from 4NK iframe!' },
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'PAIRING_4WORDS_CREATE':
|
|
console.log('🔐 Parent requested pairing creation');
|
|
createPairingViaIframe();
|
|
break;
|
|
|
|
case 'PAIRING_4WORDS_JOIN':
|
|
console.log('🔗 Parent requested pairing join with words:', data.words);
|
|
joinPairingViaIframe(data.words);
|
|
break;
|
|
|
|
case 'LISTENING':
|
|
console.log('👂 Parent is listening for messages');
|
|
break;
|
|
|
|
case 'IFRAME_READY':
|
|
console.log('✅ Iframe is ready and initialized');
|
|
break;
|
|
|
|
default:
|
|
console.log('❓ Unknown message type from parent:', type);
|
|
}
|
|
});
|
|
|
|
// Notify parent that iframe is ready
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'IFRAME_READY',
|
|
data: { service: '4nk-pairing' },
|
|
},
|
|
'*'
|
|
);
|
|
console.log('📡 Notified parent that iframe is ready');
|
|
}
|
|
}
|
|
|
|
// Enhanced pairing functions using iframe
|
|
export async function createPairingViaIframe() {
|
|
if (!iframePairing) {
|
|
initIframePairing();
|
|
}
|
|
|
|
try {
|
|
await iframePairing!.createPairing();
|
|
} catch (error) {
|
|
console.error('Error creating pairing via iframe:', error);
|
|
alert(`Error creating pairing: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
export async function joinPairingViaIframe(words: string) {
|
|
if (!iframePairing) {
|
|
initIframePairing();
|
|
}
|
|
|
|
try {
|
|
await iframePairing!.joinPairing(words);
|
|
} catch (error) {
|
|
console.error('Error joining pairing via iframe:', error);
|
|
alert(`Error joining pairing: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// Set up button listeners for iframe pairing
|
|
export function setupIframePairingButtons() {
|
|
// Create button listener
|
|
const createButton = document.getElementById('createButton');
|
|
if (createButton) {
|
|
createButton.addEventListener('click', async () => {
|
|
console.log('🔐 Create button clicked - using iframe pairing');
|
|
await createPairingViaIframe();
|
|
});
|
|
}
|
|
|
|
// Join button listener
|
|
const joinButton = document.getElementById('joinButton');
|
|
const wordsInput = document.getElementById('wordsInput') as HTMLInputElement;
|
|
|
|
if (joinButton && wordsInput) {
|
|
// Enable join button when words are entered
|
|
wordsInput.addEventListener('input', () => {
|
|
const words = wordsInput.value.trim();
|
|
(joinButton as HTMLButtonElement).disabled = !words;
|
|
});
|
|
|
|
joinButton.addEventListener('click', async () => {
|
|
const words = wordsInput.value.trim();
|
|
if (words) {
|
|
console.log('🔗 Join button clicked - using iframe pairing with words:', words);
|
|
await joinPairingViaIframe(words);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Copy words button listener
|
|
const copyWordsBtn = document.getElementById('copyWordsBtn');
|
|
if (copyWordsBtn) {
|
|
copyWordsBtn.addEventListener('click', () => {
|
|
const creatorWordsElement = document.querySelector('#creator-words');
|
|
if (creatorWordsElement && creatorWordsElement.textContent) {
|
|
navigator.clipboard
|
|
.writeText(creatorWordsElement.textContent)
|
|
.then(() => {
|
|
console.log('✅ Words copied to clipboard');
|
|
// Show feedback
|
|
const originalText = copyWordsBtn.textContent;
|
|
copyWordsBtn.textContent = '✅ Copied!';
|
|
setTimeout(() => {
|
|
copyWordsBtn.textContent = originalText;
|
|
}, 2000);
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to copy words:', err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Variable pour éviter les appels multiples à setupMainPairing
|
|
let isMainPairingSetup = false;
|
|
|
|
// Main Pairing Interface - Automatic WebAuthn trigger
|
|
export function setupMainPairing(): void {
|
|
// Protection contre les appels multiples
|
|
if (isMainPairingSetup) {
|
|
console.log('🔐 Main pairing already setup, skipping...');
|
|
return;
|
|
}
|
|
|
|
isMainPairingSetup = true;
|
|
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container.querySelector('#main-status') as HTMLElement;
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--info-color)">⏳ Waiting for user to validate secure key access...</span>';
|
|
}
|
|
|
|
console.log('🔐 Main pairing setup - waiting for user interaction');
|
|
}
|
|
|
|
function setupUserInteractionListener(): void {
|
|
let hasTriggered = false;
|
|
|
|
const triggerWebAuthn = async (event: Event) => {
|
|
if (hasTriggered) return;
|
|
hasTriggered = true;
|
|
|
|
console.log('🔐 User interaction detected:', event.type, 'triggering WebAuthn...');
|
|
await handleMainPairing();
|
|
};
|
|
|
|
// Listen for any user interaction with more specific events
|
|
document.addEventListener('click', triggerWebAuthn, { once: true, passive: true });
|
|
document.addEventListener('keydown', triggerWebAuthn, { once: true, passive: true });
|
|
document.addEventListener('touchstart', triggerWebAuthn, { once: true, passive: true });
|
|
document.addEventListener('mousedown', triggerWebAuthn, { once: true, passive: true });
|
|
|
|
console.log('🔐 User interaction listeners set up');
|
|
}
|
|
|
|
/**
|
|
* Affiche le sélecteur de mode de sécurisation
|
|
*/
|
|
async function showSecurityModeSelector(): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
// Créer le conteneur pour le sélecteur
|
|
const selectorContainer = document.createElement('div');
|
|
selectorContainer.id = 'security-mode-selector-container';
|
|
selectorContainer.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 10000;
|
|
backdrop-filter: blur(5px);
|
|
`;
|
|
|
|
// Créer le contenu du sélecteur
|
|
const selectorContent = document.createElement('div');
|
|
selectorContent.style.cssText = `
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
max-width: 600px;
|
|
width: 90%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
`;
|
|
|
|
selectorContent.innerHTML = `
|
|
<div style="text-align: center; margin-bottom: 30px;">
|
|
<h2 style="color: #2c3e50; margin-bottom: 10px;">🔐 Mode de Sécurisation</h2>
|
|
<p style="color: #7f8c8d;">Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
|
|
</div>
|
|
|
|
<div style="display: grid; gap: 15px; margin-bottom: 30px;">
|
|
<div class="security-option" data-mode="proton-pass" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">🔒</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Proton Pass</span>
|
|
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Utilise Proton Pass pour l'authentification biométrique</div>
|
|
</div>
|
|
|
|
<div class="security-option" data-mode="os" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">🖥️</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Authentificateur OS</span>
|
|
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Utilise l'authentificateur intégré de votre système</div>
|
|
</div>
|
|
|
|
<div class="security-option" data-mode="browser" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">🌐</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Navigateur</span>
|
|
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Moyennement sécurisé</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Utilise les fonctionnalités de sécurité du navigateur</div>
|
|
</div>
|
|
|
|
<div class="security-option" data-mode="2fa" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">📱</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Application 2FA</span>
|
|
<span style="background: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">⚠️ Non sécurisé</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair avec authentification 2FA</div>
|
|
</div>
|
|
|
|
<div class="security-option" data-mode="password" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">🔑</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Mot de Passe</span>
|
|
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">⚠️ Non sauvegardé</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Chiffrement par mot de passe (non sauvegardé, non récupérable)</div>
|
|
</div>
|
|
|
|
<div class="security-option" data-mode="none" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-size: 20px; margin-right: 10px;">🚨</span>
|
|
<span style="font-weight: 600; color: #2c3e50;">Aucune Sécurité</span>
|
|
<span style="background: #f5c6cb; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">DANGEREUX</span>
|
|
</div>
|
|
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair sans aucune protection</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center;">
|
|
<button id="confirm-security-mode" style="background: #27ae60; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
|
|
Confirmer le Mode de Sécurisation
|
|
</button>
|
|
<button id="cancel-security-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
|
|
Annuler
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
selectorContainer.appendChild(selectorContent);
|
|
document.body.appendChild(selectorContainer);
|
|
|
|
let selectedMode: string | null = null;
|
|
|
|
// Gestion des événements
|
|
const options = selectorContent.querySelectorAll('.security-option');
|
|
const confirmBtn = selectorContent.querySelector('#confirm-security-mode') as HTMLButtonElement;
|
|
const cancelBtn = selectorContent.querySelector('#cancel-security-mode') as HTMLButtonElement;
|
|
|
|
// Sélection d'un mode
|
|
options.forEach(option => {
|
|
option.addEventListener('click', () => {
|
|
options.forEach(opt => opt.style.borderColor = '#e1e8ed');
|
|
option.style.borderColor = '#27ae60';
|
|
option.style.background = '#f8fff8';
|
|
selectedMode = option.getAttribute('data-mode');
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.style.opacity = '1';
|
|
});
|
|
|
|
// Effet hover
|
|
option.addEventListener('mouseenter', () => {
|
|
if (option.style.borderColor !== '#27ae60') {
|
|
option.style.borderColor = '#3498db';
|
|
}
|
|
});
|
|
option.addEventListener('mouseleave', () => {
|
|
if (option.style.borderColor !== '#27ae60') {
|
|
option.style.borderColor = '#e1e8ed';
|
|
}
|
|
});
|
|
});
|
|
|
|
// Confirmation
|
|
confirmBtn.addEventListener('click', async () => {
|
|
if (selectedMode) {
|
|
console.log(`🔐 Security mode selected: ${selectedMode}`);
|
|
|
|
// Vérifier si le mode nécessite une confirmation
|
|
const { SecurityModeService } = await import('../../services/security-mode.service');
|
|
const securityModeService = SecurityModeService.getInstance();
|
|
const modeConfig = securityModeService.getSecurityModeConfig(selectedMode as any);
|
|
|
|
if (modeConfig.requiresConfirmation) {
|
|
// Afficher une alerte de sécurité
|
|
const confirmed = await showSecurityWarning(modeConfig);
|
|
if (!confirmed) {
|
|
return; // L'utilisateur a annulé
|
|
}
|
|
}
|
|
|
|
// Définir le mode de sécurisation
|
|
await securityModeService.setSecurityMode(selectedMode as any);
|
|
|
|
// Fermer le sélecteur
|
|
document.body.removeChild(selectorContainer);
|
|
|
|
// Réinitialiser les flags pour permettre la relance
|
|
isPairingInProgress = false;
|
|
pairingAttempts = 0;
|
|
|
|
// Relancer l'authentification avec le mode sélectionné
|
|
await handleMainPairing();
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
// Annulation
|
|
cancelBtn.addEventListener('click', () => {
|
|
console.log('❌ Security mode selection cancelled');
|
|
document.body.removeChild(selectorContainer);
|
|
resolve();
|
|
});
|
|
|
|
// Fermer avec Escape
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
document.body.removeChild(selectorContainer);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
resolve();
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEscape);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Affiche une alerte de sécurité pour les modes non recommandés
|
|
*/
|
|
async function showSecurityWarning(modeConfig: any): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const warningContainer = document.createElement('div');
|
|
warningContainer.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 10001;
|
|
backdrop-filter: blur(5px);
|
|
`;
|
|
|
|
const warningContent = document.createElement('div');
|
|
warningContent.style.cssText = `
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
`;
|
|
|
|
warningContent.innerHTML = `
|
|
<div style="text-align: center; margin-bottom: 20px;">
|
|
<h3 style="color: #e74c3c; margin-bottom: 10px;">⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
|
|
<p>${modeConfig.description}</p>
|
|
</div>
|
|
|
|
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin-bottom: 20px;">
|
|
<h5 style="color: #721c24; margin-top: 0;">⚠️ Risques identifiés :</h5>
|
|
<ul style="color: #721c24; margin-bottom: 0;">
|
|
${modeConfig.warnings.map((warning: string) => `<li>${warning}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
|
|
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 20px;">
|
|
<p style="color: #856404; margin: 0;">
|
|
<strong>Recommandation :</strong>
|
|
${modeConfig.securityLevel === 'low'
|
|
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
|
|
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
|
<label style="display: flex; align-items: center; cursor: pointer; font-weight: 500;">
|
|
<input type="checkbox" id="understand-risks" style="margin-right: 10px; transform: scale(1.2);">
|
|
Je comprends les risques et souhaite continuer
|
|
</label>
|
|
</div>
|
|
|
|
<div style="text-align: center;">
|
|
<button id="confirm-risky-mode" style="background: #e74c3c; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
|
|
Continuer Malgré les Risques
|
|
</button>
|
|
<button id="cancel-risky-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
|
|
Choisir un Autre Mode
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
warningContainer.appendChild(warningContent);
|
|
document.body.appendChild(warningContainer);
|
|
|
|
const understandCheckbox = warningContent.querySelector('#understand-risks') as HTMLInputElement;
|
|
const confirmBtn = warningContent.querySelector('#confirm-risky-mode') as HTMLButtonElement;
|
|
const cancelBtn = warningContent.querySelector('#cancel-risky-mode') as HTMLButtonElement;
|
|
|
|
// Gestion de la checkbox
|
|
understandCheckbox.addEventListener('change', () => {
|
|
confirmBtn.disabled = !understandCheckbox.checked;
|
|
confirmBtn.style.opacity = understandCheckbox.checked ? '1' : '0.5';
|
|
});
|
|
|
|
// Confirmation
|
|
confirmBtn.addEventListener('click', () => {
|
|
document.body.removeChild(warningContainer);
|
|
resolve(true);
|
|
});
|
|
|
|
// Annulation
|
|
cancelBtn.addEventListener('click', () => {
|
|
document.body.removeChild(warningContainer);
|
|
resolve(false);
|
|
});
|
|
|
|
// Fermer avec Escape
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
document.body.removeChild(warningContainer);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
resolve(false);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEscape);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Attend que les credentials soient réellement disponibles
|
|
*/
|
|
// Variable pour éviter les appels multiples à waitForCredentialsAvailability
|
|
let isWaitingForCredentials = false;
|
|
|
|
async function waitForCredentialsAvailability(): Promise<void> {
|
|
// Protection contre les appels multiples
|
|
if (isWaitingForCredentials) {
|
|
console.log('🔍 Already waiting for credentials, skipping...');
|
|
return;
|
|
}
|
|
|
|
isWaitingForCredentials = true;
|
|
|
|
try {
|
|
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
|
|
|
|
let attempts = 0;
|
|
const maxAttempts = 20;
|
|
const delayMs = 1000;
|
|
|
|
while (attempts < maxAttempts) {
|
|
attempts++;
|
|
console.log(`🔍 Vérification de la disponibilité des credentials (tentative ${attempts}/${maxAttempts})...`);
|
|
|
|
try {
|
|
// Vérifier que les credentials sont réellement disponibles et accessibles
|
|
const credentials = await secureCredentialsService.retrieveCredentials('');
|
|
|
|
if (credentials && credentials.spendKey && credentials.scanKey) {
|
|
console.log('✅ Credentials confirmés comme disponibles');
|
|
return;
|
|
} else {
|
|
throw new Error('Credentials incomplets');
|
|
}
|
|
} catch (error) {
|
|
console.warn(`⚠️ Credentials pas encore disponibles (tentative ${attempts}):`, error);
|
|
|
|
if (attempts < maxAttempts) {
|
|
console.log(`⏳ Attente de ${delayMs}ms avant la prochaine tentative...`);
|
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error('Credentials non disponibles après toutes les tentatives');
|
|
} finally {
|
|
isWaitingForCredentials = false;
|
|
}
|
|
}
|
|
|
|
// Variables pour éviter les appels multiples
|
|
let isPairingInProgress = false;
|
|
let pairingAttempts = 0;
|
|
const MAX_PAIRING_ATTEMPTS = 1;
|
|
|
|
async function handleMainPairing(): Promise<void> {
|
|
// Protection renforcée contre les appels multiples
|
|
if (isPairingInProgress) {
|
|
console.log('🔐 Pairing already in progress, skipping...');
|
|
return;
|
|
}
|
|
|
|
if (pairingAttempts >= MAX_PAIRING_ATTEMPTS) {
|
|
console.log('🔐 Maximum pairing attempts reached, skipping...');
|
|
return;
|
|
}
|
|
|
|
isPairingInProgress = true;
|
|
pairingAttempts++;
|
|
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container.querySelector('#main-status') as HTMLElement;
|
|
|
|
try {
|
|
// Vérifier si un mode de sécurisation est déjà sélectionné
|
|
const { SecurityModeService } = await import('../../services/security-mode.service');
|
|
const securityModeService = SecurityModeService.getInstance();
|
|
|
|
let currentMode: string | null = null;
|
|
try {
|
|
currentMode = await securityModeService.getCurrentMode();
|
|
} catch (error) {
|
|
// Ignorer les erreurs de base de données lors du premier lancement
|
|
console.log('🔐 No security mode configured yet (first launch)');
|
|
currentMode = null;
|
|
}
|
|
|
|
if (!currentMode) {
|
|
// Aucun mode sélectionné, afficher le sélecteur
|
|
console.log('🔐 No security mode selected, showing selector...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Please select your security mode...</span>';
|
|
}
|
|
|
|
// Réinitialiser le flag avant d'afficher le sélecteur
|
|
isPairingInProgress = false;
|
|
await showSecurityModeSelector();
|
|
return; // La fonction sera rappelée après sélection du mode
|
|
}
|
|
|
|
// Mode sélectionné, continuer avec l'authentification
|
|
console.log(`🔐 Using security mode: ${currentMode}`);
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Authenticating with selected security mode...</span>';
|
|
}
|
|
|
|
// Import and trigger authentication with selected mode
|
|
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
|
|
|
|
// Check if we have existing credentials (regardless of wallet existence)
|
|
console.log('🔍 Checking for existing credentials...');
|
|
const hasCredentials = await secureCredentialsService.hasCredentials();
|
|
|
|
if (hasCredentials) {
|
|
console.log('🔓 Existing credentials found, decrypting...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Decrypting existing credentials...</span>';
|
|
}
|
|
|
|
try {
|
|
// This will trigger authentication for decryption of existing credentials
|
|
console.log('🔐 Starting credentials decryption process...');
|
|
const decryptedCredentials = await secureCredentialsService.retrieveCredentials('');
|
|
|
|
if (!decryptedCredentials) {
|
|
throw new Error('Failed to decrypt existing credentials - no data returned');
|
|
}
|
|
|
|
console.log('✅ Credentials decryption completed successfully');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>';
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Credentials decryption failed:', error);
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to decrypt credentials. Please try again.</span>';
|
|
}
|
|
throw error; // Arrêter le processus si le déchiffrement échoue
|
|
}
|
|
} else {
|
|
console.log('🔐 No existing credentials, creating new ones...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Setting up secure authentication...</span>';
|
|
}
|
|
|
|
try {
|
|
// This will trigger authentication for creation of new credentials
|
|
console.log('🔐 Starting credentials creation process...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
|
|
}
|
|
|
|
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
|
|
|
|
if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
|
|
throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
|
|
}
|
|
|
|
console.log('✅ Credentials creation completed successfully');
|
|
|
|
// Store the credentials in IndexedDB
|
|
console.log('💾 Storing credentials in IndexedDB...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
|
|
}
|
|
|
|
await secureCredentialsService.storeCredentials(credentialData, '');
|
|
console.log('✅ Credentials stored successfully');
|
|
|
|
// Decrypt and make keys available to SDK
|
|
console.log('🔓 Decrypting credentials for SDK access...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
|
|
}
|
|
|
|
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
|
|
|
|
if (!retrievedCredentials) {
|
|
throw new Error('Failed to retrieve stored credentials - decryption failed');
|
|
}
|
|
|
|
console.log('✅ Credentials decrypted and available');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Secure authentication ready</span>';
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Credentials creation/encryption/decryption failed:', error);
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to create/encrypt/decrypt credentials. Please try again.</span>';
|
|
}
|
|
throw error; // Arrêter le processus si la génération/chiffrement/déchiffrement échoue
|
|
}
|
|
}
|
|
|
|
// Ensure WebAuthn process is completely finished
|
|
console.log('🔐 WebAuthn process completed, waiting for final confirmation...');
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional wait to ensure completion
|
|
|
|
// Wait longer to ensure credentials are fully processed and stored
|
|
console.log('⏳ Waiting for credentials to be fully processed...');
|
|
await new Promise(resolve => setTimeout(resolve, 5000)); // Increased wait time to 5 seconds
|
|
|
|
// Verify credentials are available before proceeding with retry mechanism
|
|
let credentialsReady = false;
|
|
let attempts = 0;
|
|
const maxAttempts = 10; // Increased attempts
|
|
const delayMs = 2000; // Increased delay between attempts
|
|
|
|
while (!credentialsReady && attempts < maxAttempts) {
|
|
attempts++;
|
|
console.log(`🔍 Checking credentials availability (attempt ${attempts}/${maxAttempts})...`);
|
|
|
|
try {
|
|
// Vérifier que les credentials sont réellement disponibles
|
|
const credentials = await secureCredentialsService.retrieveCredentials('');
|
|
if (!credentials || !credentials.spendKey || !credentials.scanKey) {
|
|
throw new Error('Credentials not properly available');
|
|
}
|
|
credentialsReady = true;
|
|
console.log('✅ Credentials verified as available');
|
|
} catch (error) {
|
|
console.warn(`⚠️ Credentials not ready on attempt ${attempts}:`, error);
|
|
if (attempts < maxAttempts) {
|
|
console.log(`⏳ Waiting ${delayMs}ms before next attempt...`);
|
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Si les credentials ne sont toujours pas disponibles après tous les essais, arrêter le processus
|
|
if (!credentialsReady) {
|
|
console.error('❌ Credentials not available after all attempts - stopping process');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Authentication failed - credentials not available</span>';
|
|
}
|
|
throw new Error('Credentials not available after maximum retry attempts');
|
|
}
|
|
|
|
console.log('✅ Credentials verified, proceeding with pairing...');
|
|
|
|
// Now proceed with pairing process
|
|
console.log('🚀 Starting pairing process...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>🚀 Starting secure pairing process...</span>';
|
|
}
|
|
|
|
// Attendre que les credentials soient réellement disponibles avant de continuer
|
|
await waitForCredentialsAvailability();
|
|
|
|
await prepareAndSendPairingTx();
|
|
|
|
} catch (error) {
|
|
// If WebAuthn fails due to no user gesture, wait for real interaction
|
|
if (error instanceof Error && error.message && error.message.includes('WebAuthn authentication was cancelled or timed out')) {
|
|
console.log('🔐 WebAuthn requires user interaction, waiting...');
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--info-color)">⏳ Waiting for user to validate secure key access...</span>';
|
|
}
|
|
|
|
// Set up listener for real user interaction
|
|
setupUserInteractionListener();
|
|
} else {
|
|
console.error('Pairing failed:', error);
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Pairing failed: ' + (error as Error).message + '</span>';
|
|
}
|
|
throw error;
|
|
}
|
|
} finally {
|
|
// Réinitialiser les flags pour permettre de nouveaux appels
|
|
isPairingInProgress = false;
|
|
// Ne pas réinitialiser pairingAttempts ici pour éviter les boucles infinies
|
|
}
|
|
}
|
|
|
|
|
|
// Account Actions
|
|
export function setupAccountActions(): void {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
|
|
const deleteAccountButton = container.querySelector('#deleteAccountButton') as HTMLButtonElement;
|
|
|
|
if (deleteAccountButton) {
|
|
deleteAccountButton.addEventListener('click', async () => {
|
|
await handleDeleteAccount();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handleDeleteAccount(): Promise<void> {
|
|
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
|
|
const mainStatus = container.querySelector('#main-status') as HTMLElement;
|
|
|
|
// Confirmation dialog
|
|
const confirmed = confirm(
|
|
'⚠️ WARNING: This will permanently delete your account and all associated data.\n\n' +
|
|
'This action cannot be undone!\n\n' +
|
|
'Are you sure you want to delete your account?'
|
|
);
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
// Double confirmation
|
|
const doubleConfirmed = confirm(
|
|
'🚨 FINAL WARNING: You are about to permanently delete your account.\n\n' +
|
|
'All your data, credentials, and pairings will be lost forever.\n\n' +
|
|
'Type "DELETE" to confirm (case sensitive):'
|
|
);
|
|
|
|
if (doubleConfirmed) {
|
|
const userInput = prompt('Type "DELETE" to confirm account deletion:');
|
|
if (userInput !== 'DELETE') {
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--warning-color)">❌ Account deletion cancelled - confirmation text did not match</span>';
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<div class="spinner"></div><span>Deleting account and all data...</span>';
|
|
}
|
|
|
|
// Get services
|
|
const service = await Services.getInstance();
|
|
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
|
|
|
|
// Delete all credentials
|
|
await secureCredentialsService.deleteCredentials();
|
|
|
|
// Clear all local storage
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
|
|
// Clear IndexedDB
|
|
if ('indexedDB' in window) {
|
|
const databases = await indexedDB.databases();
|
|
for (const db of databases) {
|
|
if (db.name) {
|
|
indexedDB.deleteDatabase(db.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear service worker caches
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
await Promise.all(
|
|
cacheNames.map(cacheName => caches.delete(cacheName))
|
|
);
|
|
}
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Account and all data deleted successfully</span>';
|
|
}
|
|
|
|
// Reload the page to start fresh
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
console.error('Account deletion failed:', error);
|
|
|
|
if (mainStatus) {
|
|
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to delete account</span>';
|
|
}
|
|
}
|
|
}
|