Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

506
web/frontend/public/totp.js Normal file
View 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;
}