507 lines
18 KiB
JavaScript
507 lines
18 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|