506
web/frontend/public/totp.js
Normal file
506
web/frontend/public/totp.js
Normal file
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* TOTP Client-Side Module
|
||||
* Handles Two-Factor Authentication setup, verification, and management
|
||||
*/
|
||||
|
||||
const TOTP = {
|
||||
/**
|
||||
* Initialize TOTP setup process
|
||||
*/
|
||||
async setupTOTP() {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
showNotification('Please log in first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/totp/setup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
showNotification(data.error || 'Failed to setup 2FA', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show setup modal with QR code
|
||||
this.showSetupModal(data.qrCodeDataURL, data.secret);
|
||||
} catch (error) {
|
||||
console.error('TOTP setup error:', error);
|
||||
showNotification('Failed to setup 2FA', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display TOTP setup modal with QR code
|
||||
*/
|
||||
showSetupModal(qrCodeDataURL, secret) {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'totpSetupModal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content totp-modal">
|
||||
<div class="modal-header">
|
||||
<h2>
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
Enable Two-Factor Authentication
|
||||
</h2>
|
||||
<button class="close-btn" onclick="TOTP.closeSetupModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="totp-setup-steps">
|
||||
<div class="step active" id="step1">
|
||||
<h3>Step 1: Scan QR Code</h3>
|
||||
<p>Open your authenticator app (Google Authenticator, Microsoft Authenticator, Authy, etc.) and scan this QR code:</p>
|
||||
|
||||
<div class="qr-code-container">
|
||||
<img src="${qrCodeDataURL}" alt="QR Code" class="qr-code-image">
|
||||
</div>
|
||||
|
||||
<div class="manual-entry">
|
||||
<p>Can't scan the code? Enter this key manually:</p>
|
||||
<div class="secret-key">
|
||||
<code id="secretKey">${secret}</code>
|
||||
<button class="btn-copy" onclick="TOTP.copySecret('${secret}')">
|
||||
<i class="fas fa-copy"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="TOTP.showVerifyStep()">
|
||||
Next: Verify Code
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="step" id="step2" style="display: none;">
|
||||
<h3>Step 2: Verify Your Code</h3>
|
||||
<p>Enter the 6-digit code from your authenticator app:</p>
|
||||
|
||||
<div class="verify-input-container">
|
||||
<input
|
||||
type="text"
|
||||
id="verifyCode"
|
||||
maxlength="6"
|
||||
pattern="[0-9]{6}"
|
||||
placeholder="000000"
|
||||
autocomplete="off"
|
||||
class="totp-code-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="TOTP.showScanStep()">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="TOTP.verifySetup()">
|
||||
<i class="fas fa-check"></i> Verify & Enable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" id="step3" style="display: none;">
|
||||
<h3>Step 3: Save Backup Codes</h3>
|
||||
<div class="warning-box">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p><strong>Important:</strong> Save these backup codes in a safe place.
|
||||
You can use them to access your account if you lose your authenticator device.</p>
|
||||
</div>
|
||||
|
||||
<div class="backup-codes" id="backupCodes">
|
||||
<!-- Backup codes will be inserted here -->
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="TOTP.downloadBackupCodes()">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="TOTP.printBackupCodes()">
|
||||
<i class="fas fa-print"></i> Print
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="TOTP.copyBackupCodes()">
|
||||
<i class="fas fa-copy"></i> Copy All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="confirm-save">
|
||||
<label>
|
||||
<input type="checkbox" id="confirmSaved">
|
||||
I have saved my backup codes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="TOTP.finishSetup()" disabled id="finishBtn">
|
||||
<i class="fas fa-check-circle"></i> Finish Setup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Enable finish button when checkbox is checked
|
||||
document.getElementById('confirmSaved')?.addEventListener('change', (e) => {
|
||||
document.getElementById('finishBtn').disabled = !e.target.checked;
|
||||
});
|
||||
|
||||
// Auto-focus on verify code input when shown
|
||||
document.getElementById('verifyCode')?.addEventListener('input', (e) => {
|
||||
e.target.value = e.target.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
},
|
||||
|
||||
showScanStep() {
|
||||
document.getElementById('step1').style.display = 'block';
|
||||
document.getElementById('step2').style.display = 'none';
|
||||
document.getElementById('step1').classList.add('active');
|
||||
document.getElementById('step2').classList.remove('active');
|
||||
},
|
||||
|
||||
showVerifyStep() {
|
||||
document.getElementById('step1').style.display = 'none';
|
||||
document.getElementById('step2').style.display = 'block';
|
||||
document.getElementById('step1').classList.remove('active');
|
||||
document.getElementById('step2').classList.add('active');
|
||||
setTimeout(() => document.getElementById('verifyCode')?.focus(), 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify TOTP code and complete setup
|
||||
*/
|
||||
async verifySetup() {
|
||||
const code = document.getElementById('verifyCode')?.value;
|
||||
|
||||
if (!code || code.length !== 6) {
|
||||
showNotification('Please enter a 6-digit code', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
|
||||
|
||||
const response = await fetch('/api/auth/totp/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
showNotification(data.error || 'Invalid code', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show backup codes
|
||||
this.showBackupCodes(data.backupCodes);
|
||||
|
||||
document.getElementById('step2').style.display = 'none';
|
||||
document.getElementById('step3').style.display = 'block';
|
||||
document.getElementById('step2').classList.remove('active');
|
||||
document.getElementById('step3').classList.add('active');
|
||||
|
||||
showNotification('Two-factor authentication enabled!', 'success');
|
||||
} catch (error) {
|
||||
console.error('TOTP verification error:', error);
|
||||
showNotification('Failed to verify code', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display backup codes
|
||||
*/
|
||||
showBackupCodes(codes) {
|
||||
const container = document.getElementById('backupCodes');
|
||||
if (!container) {return;}
|
||||
|
||||
this.backupCodes = codes; // Store for later use
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="backup-codes-grid">
|
||||
${codes.map((code, index) => `
|
||||
<div class="backup-code-item">
|
||||
<span class="code-number">${index + 1}.</span>
|
||||
<code>${code}</code>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy secret to clipboard
|
||||
*/
|
||||
copySecret(secret) {
|
||||
navigator.clipboard.writeText(secret).then(() => {
|
||||
showNotification('Secret key copied to clipboard', 'success');
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy secret key', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Download backup codes as text file
|
||||
*/
|
||||
downloadBackupCodes() {
|
||||
if (!this.backupCodes) {return;}
|
||||
|
||||
const content = `Backup Central - Two-Factor Authentication Backup Codes
|
||||
Generated: ${new Date().toLocaleString()}
|
||||
|
||||
IMPORTANT: Keep these codes in a safe place!
|
||||
Each code can only be used once.
|
||||
|
||||
${this.backupCodes.map((code, i) => `${i + 1}. ${code}`).join('\n')}
|
||||
|
||||
If you lose access to your authenticator app, you can use one of these codes to log in.
|
||||
`;
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `backup-codes-${Date.now()}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showNotification('Backup codes downloaded', 'success');
|
||||
},
|
||||
|
||||
/**
|
||||
* Print backup codes
|
||||
*/
|
||||
printBackupCodes() {
|
||||
if (!this.backupCodes) {return;}
|
||||
|
||||
const printWindow = window.open('', '', 'width=600,height=400');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Backup Codes</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; }
|
||||
h1 { font-size: 18px; }
|
||||
.code { margin: 10px 0; font-size: 14px; }
|
||||
@media print {
|
||||
button { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Backup Central - 2FA Backup Codes</h1>
|
||||
<p>Generated: ${new Date().toLocaleString()}</p>
|
||||
<hr>
|
||||
${this.backupCodes.map((code, i) => `<div class="code">${i + 1}. ${code}</div>`).join('')}
|
||||
<hr>
|
||||
<p><strong>Keep these codes in a safe place!</strong></p>
|
||||
<button onclick="window.print()">Print</button>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy all backup codes to clipboard
|
||||
*/
|
||||
copyBackupCodes() {
|
||||
if (!this.backupCodes) {return;}
|
||||
|
||||
const text = this.backupCodes.map((code, i) => `${i + 1}. ${code}`).join('\n');
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showNotification('Backup codes copied to clipboard', 'success');
|
||||
}).catch(() => {
|
||||
showNotification('Failed to copy backup codes', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Complete setup and close modal
|
||||
*/
|
||||
finishSetup() {
|
||||
this.closeSetupModal();
|
||||
showNotification('Two-factor authentication is now active!', 'success');
|
||||
|
||||
// Refresh 2FA status in UI
|
||||
if (typeof loadTOTPStatus === 'function') {
|
||||
loadTOTPStatus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close setup modal
|
||||
*/
|
||||
closeSetupModal() {
|
||||
const modal = document.getElementById('totpSetupModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable TOTP for current user
|
||||
*/
|
||||
async disableTOTP() {
|
||||
const confirmed = confirm('Are you sure you want to disable two-factor authentication?\n\nThis will make your account less secure.');
|
||||
if (!confirmed) {return;}
|
||||
|
||||
const password = prompt('Enter your password to confirm:');
|
||||
if (!password) {return;}
|
||||
|
||||
const code = prompt('Enter your current 2FA code:');
|
||||
if (!code || code.length !== 6) {
|
||||
showNotification('Invalid code', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
|
||||
|
||||
const response = await fetch('/api/auth/totp/disable', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password, code })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
showNotification(data.error || 'Failed to disable 2FA', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showNotification('Two-factor authentication disabled', 'success');
|
||||
|
||||
// Refresh 2FA status in UI
|
||||
if (typeof loadTOTPStatus === 'function') {
|
||||
loadTOTPStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Disable TOTP error:', error);
|
||||
showNotification('Failed to disable 2FA', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Regenerate backup codes
|
||||
*/
|
||||
async regenerateBackupCodes() {
|
||||
const confirmed = confirm('This will invalidate your old backup codes.\n\nAre you sure you want to generate new backup codes?');
|
||||
if (!confirmed) {return;}
|
||||
|
||||
const code = prompt('Enter your current 2FA code to confirm:');
|
||||
if (!code || code.length !== 6) {
|
||||
showNotification('Invalid code', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken');
|
||||
|
||||
const response = await fetch('/api/auth/totp/regenerate-backup-codes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
showNotification(data.error || 'Failed to regenerate codes', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show new backup codes
|
||||
this.showBackupCodesModal(data.backupCodes);
|
||||
} catch (error) {
|
||||
console.error('Regenerate codes error:', error);
|
||||
showNotification('Failed to regenerate backup codes', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show backup codes in a modal (for regeneration)
|
||||
*/
|
||||
showBackupCodesModal(codes) {
|
||||
this.backupCodes = codes;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'backupCodesModal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>New Backup Codes</h2>
|
||||
<button class="close-btn" onclick="TOTP.closeBackupCodesModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="warning-box">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p><strong>Important:</strong> Save these new backup codes. Your old codes are no longer valid.</p>
|
||||
</div>
|
||||
<div class="backup-codes" id="newBackupCodes">
|
||||
<div class="backup-codes-grid">
|
||||
${codes.map((code, index) => `
|
||||
<div class="backup-code-item">
|
||||
<span class="code-number">${index + 1}.</span>
|
||||
<code>${code}</code>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="TOTP.downloadBackupCodes()">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="TOTP.printBackupCodes()">
|
||||
<i class="fas fa-print"></i> Print
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="TOTP.copyBackupCodes()">
|
||||
<i class="fas fa-copy"></i> Copy All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
showNotification('New backup codes generated', 'success');
|
||||
},
|
||||
|
||||
closeBackupCodesModal() {
|
||||
const modal = document.getElementById('backupCodesModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TOTP;
|
||||
}
|
||||
Reference in New Issue
Block a user