Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Add Password - Secure Password Manager</title> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #2563eb; | |
| --primary-hover: #1d4ed8; | |
| --success-color: #10b981; | |
| --warning-color: #f59e0b; | |
| --danger-color: #ef4444; | |
| --text-primary: #1f2937; | |
| --text-secondary: #4b5563; | |
| --bg-card: #ffffff; | |
| --border-color: #e5e7eb; | |
| --transition: all 0.3s ease; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| background: #f3f4f6; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| header { | |
| background: var(--bg-card); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| .header-content { | |
| width: 100%; | |
| } | |
| .header-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .header-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| h1 { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin: 0; | |
| } | |
| nav ul { | |
| display: flex; | |
| gap: 1rem; | |
| padding: 0; | |
| margin: 1rem 0 0; | |
| list-style: none; | |
| } | |
| nav a { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.5rem; | |
| transition: var(--transition); | |
| } | |
| nav a:hover, nav a.active { | |
| color: var(--primary-color); | |
| background: #f0f9ff; | |
| } | |
| .card { | |
| background: var(--bg-card); | |
| border-radius: 1rem; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| transition: var(--transition); | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| label { | |
| display: block; | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| margin-bottom: 0.5rem; | |
| } | |
| input[type="text"], | |
| input[type="password"] { | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| border: 2px solid var(--border-color); | |
| border-radius: 0.5rem; | |
| font-size: 1rem; | |
| transition: var(--transition); | |
| } | |
| input[type="text"]:focus, | |
| input[type="password"]:focus { | |
| border-color: var(--primary-color); | |
| outline: none; | |
| box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); | |
| } | |
| .password-input-group { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .toggle-button { | |
| position: absolute; | |
| right: 7.5rem; | |
| padding: 0.5rem; | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| } | |
| .generate-button { | |
| position: absolute; | |
| right: 0.5rem; | |
| padding: 0.5rem 1rem; | |
| background: none; | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.5rem; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| transition: var(--transition); | |
| } | |
| .generate-button:hover { | |
| border-color: var(--primary-color); | |
| color: var(--primary-color); | |
| } | |
| .password-strength-area { | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| background: #f8fafc; | |
| border-radius: 0.5rem; | |
| } | |
| .strength-meter-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .strength-label { | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| min-width: 4rem; | |
| } | |
| .strength-bar { | |
| flex-grow: 1; | |
| height: 0.5rem; | |
| background: #e5e7eb; | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| } | |
| .strength-indicator { | |
| height: 100%; | |
| width: 0; | |
| transition: width 0.3s ease, background-color 0.3s ease; | |
| } | |
| .strength-indicator.very-weak { background: var(--danger-color); } | |
| .strength-indicator.weak { background: #f97316; } | |
| .strength-indicator.medium { background: var(--warning-color); } | |
| .strength-indicator.strong { background: #84cc16; } | |
| .strength-indicator.very-strong { background: var(--success-color); } | |
| .generator-options { | |
| margin-top: 1.5rem; | |
| padding: 1.5rem; | |
| background: #f8fafc; | |
| border-radius: 0.5rem; | |
| border: 1px solid var(--border-color); | |
| } | |
| .generator-options h4 { | |
| margin: 0 0 1rem; | |
| color: var(--text-primary); | |
| } | |
| .length-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .length-control input[type="range"] { | |
| flex-grow: 1; | |
| } | |
| .length-display { | |
| min-width: 2.5rem; | |
| text-align: center; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .char-options { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .option-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .option-group input[type="checkbox"] { | |
| width: 1rem; | |
| height: 1rem; | |
| border-radius: 0.25rem; | |
| border: 2px solid var(--border-color); | |
| cursor: pointer; | |
| } | |
| .option-group label { | |
| margin: 0; | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0.75rem 1.5rem; | |
| background: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 0.5rem; | |
| font-size: 1rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .btn:hover { | |
| background: var(--primary-hover); | |
| transform: translateY(-1px); | |
| } | |
| .message { | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| font-size: 0.875rem; | |
| } | |
| .message.success { | |
| background: #dcfce7; | |
| color: #166534; | |
| } | |
| .message.error { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| } | |
| .breach-status-area { | |
| margin-top: 1rem; | |
| padding: 0.75rem; | |
| border-radius: 0.5rem; | |
| background: #f8fafc; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .breach-label { | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| } | |
| .breach-indicator { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 1rem; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| } | |
| .breach-indicator.safe { | |
| background: #dcfce7; | |
| color: #166534; | |
| } | |
| .breach-indicator.pwned { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| } | |
| .breach-indicator.checking { | |
| background: #dbeafe; | |
| color: #1e40af; | |
| } | |
| #theme-toggle { | |
| padding: 0.5rem; | |
| background: none; | |
| border: 1px solid var(--border-color); | |
| border-radius: 0.5rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| #theme-toggle:hover { | |
| border-color: var(--primary-color); | |
| } | |
| .logout-link { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: 0.875rem; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .logout-link:hover { | |
| color: var(--primary-color); | |
| } | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 1rem; | |
| } | |
| .card { | |
| padding: 1.5rem; | |
| } | |
| .header-top { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 1rem; | |
| } | |
| nav ul { | |
| flex-direction: column; | |
| } | |
| nav a { | |
| display: block; | |
| } | |
| .char-options { | |
| grid-template-columns: 1fr; | |
| } | |
| .password-input-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .toggle-button, | |
| .generate-button { | |
| position: static; | |
| margin-top: 0.5rem; | |
| } | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg-card: #1f2937; | |
| --text-primary: #f3f4f6; | |
| --text-secondary: #9ca3af; | |
| --border-color: #374151; | |
| } | |
| body { | |
| background: #111827; | |
| } | |
| input[type="text"], | |
| input[type="password"] { | |
| background: #374151; | |
| color: #f3f4f6; | |
| } | |
| .password-strength-area, | |
| .generator-options, | |
| .breach-status-area { | |
| background: #374151; | |
| } | |
| .card { | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2); | |
| } | |
| .message.success { | |
| background: #064e3b; | |
| color: #6ee7b7; | |
| } | |
| .message.error { | |
| background: #7f1d1d; | |
| color: #fecaca; | |
| } | |
| .breach-indicator.safe { | |
| background: #064e3b; | |
| color: #6ee7b7; | |
| } | |
| .breach-indicator.pwned { | |
| background: #7f1d1d; | |
| color: #fecaca; | |
| } | |
| .breach-indicator.checking { | |
| background: #1e3a8a; | |
| color: #93c5fd; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="header-content"> | |
| <div class="header-top"> | |
| <h1>Add New Password</h1> | |
| <div class="header-controls"> | |
| <button id="theme-toggle" title="Toggle light/dark theme"> | |
| <span class="icon-sun">☀️</span> | |
| <span class="icon-moon" style="display:none;">🌙</span> | |
| </button> | |
| {% if current_user.is_authenticated %} | |
| <a href="{{ url_for('logout') }}" class="logout-link"> | |
| <span class="user-email">{{ current_user.email }}</span> | |
| <span class="logout-text">Logout</span> | |
| </a> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <nav> | |
| <ul> | |
| <li><a href="{{ url_for('add_password_page') }}" class="active">Add Password</a></li> | |
| <li><a href="{{ url_for('storage') }}">View Passwords</a></li> | |
| <li><a href="{{ url_for('analyse') }}">Analyse Passwords</a></li> | |
| </ul> | |
| </nav> | |
| </div> | |
| </header> | |
| <main> | |
| <section class="card"> | |
| <h2>Add New Credential</h2> | |
| <form id="password-form"> | |
| <div class="form-group"> | |
| <label for="service">Service/Website</label> | |
| <input type="text" id="service" name="service" required placeholder="Enter service or website name"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="username">Username/Email</label> | |
| <input type="text" id="username" name="username" required placeholder="Enter username or email"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="password">Password</label> | |
| <div class="password-input-group"> | |
| <input type="password" id="password" name="password" required autocomplete="new-password" placeholder="Enter password"> | |
| <button type="button" id="toggle-password" class="toggle-button" title="Show/hide password">Show</button> | |
| <button type="button" id="generate-password-btn" class="generate-button">Generate</button> | |
| </div> | |
| </div> | |
| <div id="password-strength-area" class="password-strength-area" style="display: none;"> | |
| <div class="strength-meter-container"> | |
| <span class="strength-label">Strength:</span> | |
| <div class="strength-bar"> | |
| <div id="strength-indicator" class="strength-indicator"></div> | |
| </div> | |
| </div> | |
| <div id="password-strength-feedback"></div> | |
| <div id="password-breach-status" class="breach-status-area" style="display: none;"> | |
| <span class="breach-label">Breach Check:</span> | |
| <span id="breach-status-indicator" class="breach-indicator checking">Checking...</span> | |
| </div> | |
| </div> | |
| <div id="generator-options" class="generator-options" style="display: none;"> | |
| <h4>Password Generator Options</h4> | |
| <div class="length-control"> | |
| <label for="gen-length">Length:</label> | |
| <input type="range" id="gen-length" name="gen-length" min="8" max="64" value="16"> | |
| <span class="length-display" id="gen-length-value">16</span> | |
| </div> | |
| <div class="char-options"> | |
| <div class="option-group"> | |
| <input type="checkbox" id="gen-lowercase" name="gen-lowercase" checked> | |
| <label for="gen-lowercase">Lowercase (a-z)</label> | |
| </div> | |
| <div class="option-group"> | |
| <input type="checkbox" id="gen-uppercase" name="gen-uppercase" checked> | |
| <label for="gen-uppercase">Uppercase (A-Z)</label> | |
| </div> | |
| <div class="option-group"> | |
| <input type="checkbox" id="gen-digits" name="gen-digits" checked> | |
| <label for="gen-digits">Digits (0-9)</label> | |
| </div> | |
| <div class="option-group"> | |
| <input type="checkbox" id="gen-symbols" name="gen-symbols" checked> | |
| <label for="gen-symbols">Symbols (!@#...)</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <button type="submit" class="btn">Encrypt & Save</button> | |
| </div> | |
| </form> | |
| <div id="message" class="message"></div> | |
| </section> | |
| </main> | |
| <footer> | |
| <p>Secure Password Manager - End-to-End Encrypted Password Management</p> | |
| </footer> | |
| </div> | |
| <script src="{{ url_for('static', filename='js/zxcvbn.js') }}"></script> | |
| <script src="{{ url_for('static', filename='js/crypto-helpers.js') }}"></script> | |
| <script> | |
| // --- E2EE Helper Functions (Encryption/Decryption - Keep for saving) --- | |
| function base64UrlDecode(b64url){let b64=b64url.replace(/-/g,'+').replace(/_/g,'/');while(b64.length%4){b64+='=';}return base64ToArrayBuffer(b64);} | |
| // Ensure base64ToArrayBuffer and arrayBufferToBase64 are loaded from crypto-helpers.js | |
| // async function getEncryptionKeyRawBytes(){ ... } // Keep this function if needed elsewhere, but primarily use sessionStorage key | |
| async function getEncryptionKeyRawBytesFromSession(){ const k=sessionStorage.getItem('encryptionKey'); if(!k){console.error("Key missing.");alert("Key missing. Login.");window.location.href="{{ url_for('login') }}"; return null;} try{ const rawKey = base64UrlDecode(k); console.log("Key from session (B64URL Decoded):", arrayBufferToBase64(rawKey)); return rawKey; } catch(e){console.error("Key decode fail:",e);alert("Invalid key. Login.");window.location.href="{{ url_for('login') }}";return null;}} | |
| // Ensure encryptData is loaded from crypto-helpers.js | |
| // --- Theme Toggler --- | |
| const themeToggleBtn = document.getElementById('theme-toggle'); | |
| const sunIcon = themeToggleBtn?.querySelector('.icon-sun'); | |
| const moonIcon = themeToggleBtn?.querySelector('.icon-moon'); | |
| const currentTheme = localStorage.getItem('theme') || 'light'; // Default to light | |
| function applyTheme(theme) { | |
| document.body.setAttribute('data-theme', theme); | |
| localStorage.setItem('theme', theme); | |
| if (sunIcon && moonIcon) { | |
| if (theme === 'dark') { | |
| sunIcon.style.display = 'none'; | |
| moonIcon.style.display = 'inline'; | |
| } else { | |
| sunIcon.style.display = 'inline'; | |
| moonIcon.style.display = 'none'; | |
| } | |
| } | |
| } | |
| applyTheme(currentTheme); // Apply initial theme on load | |
| if (themeToggleBtn) { | |
| themeToggleBtn.addEventListener('click', () => { | |
| const newTheme = document.body.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; | |
| applyTheme(newTheme); | |
| }); | |
| } | |
| // --- Strength & Breach Meter Helpers --- | |
| function getStrengthClassFromScore(score) { | |
| const classes = ['very-weak', 'weak', 'medium', 'strong', 'very-strong']; | |
| return classes[score] || 'very-weak'; | |
| } | |
| function formatBackendFeedback(feedbackArray) { | |
| let html = '<ul>'; | |
| if (!feedbackArray || feedbackArray.length === 0) { | |
| html += '<li class="suggestion">Analysis complete.</li>'; | |
| } else { | |
| feedbackArray.forEach(fb => { | |
| let itemClass = 'suggestion'; // Default | |
| if (fb.toLowerCase().includes('warning:') || fb.toLowerCase().includes('issue:')) { | |
| itemClass = 'warning'; | |
| } | |
| html += `<li class="${itemClass}">${escapeHtml(fb)}</li>`; | |
| }); | |
| } | |
| html += '</ul>'; | |
| return html; | |
| } | |
| // Ensure escapeHtml is loaded from crypto-helpers.js | |
| // --- DOMContentLoaded Event Listener --- | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Ensure key is in sessionStorage from Flask session or redirect | |
| const flaskProvidedKey = "{{ session.get('encryption_key', 'null') }}"; | |
| if (flaskProvidedKey && flaskProvidedKey !== 'null' && !sessionStorage.getItem('encryptionKey')) { | |
| sessionStorage.setItem('encryptionKey', flaskProvidedKey); | |
| console.log("Encryption key loaded into sessionStorage from Flask."); | |
| } else if (!sessionStorage.getItem('encryptionKey')) { | |
| console.warn("Encryption key missing from Flask session and sessionStorage. Redirecting to login."); | |
| window.location.href = "{{ url_for('login') }}"; | |
| return; // Stop script execution if no key | |
| } | |
| // --- Get DOM Elements --- | |
| const form = document.getElementById('password-form'); | |
| const messageEl = document.getElementById('message'); | |
| const togglePasswordBtn = document.getElementById('toggle-password'); | |
| const passwordInput = document.getElementById('password'); | |
| const submitButton = form.querySelector('button[type="submit"]'); | |
| const generatePasswordBtn = document.getElementById('generate-password-btn'); | |
| const genLengthSlider = document.getElementById('gen-length'); | |
| const genLengthValueSpan = document.getElementById('gen-length-value'); | |
| const genLowercaseCheckbox = document.getElementById('gen-lowercase'); | |
| const genUppercaseCheckbox = document.getElementById('gen-uppercase'); | |
| const genDigitsCheckbox = document.getElementById('gen-digits'); | |
| const genSymbolsCheckbox = document.getElementById('gen-symbols'); | |
| // Strength & Breach Display Elements | |
| const strengthArea = document.getElementById('password-strength-area'); | |
| const strengthIndicator = document.getElementById('strength-indicator'); | |
| const strengthTextLabel = document.getElementById('strength-text-label'); | |
| const strengthFeedbackDiv = document.getElementById('password-strength-feedback'); | |
| const breachStatusArea = document.getElementById('password-breach-status'); | |
| const breachStatusIndicator = document.getElementById('breach-status-indicator'); | |
| // --- Debounce Function --- | |
| let debounceTimerStrength; | |
| function debounce(func, delay) { | |
| return function(...args) { | |
| clearTimeout(debounceTimerStrength); | |
| debounceTimerStrength = setTimeout(() => { | |
| func.apply(this, args); | |
| }, delay); | |
| }; | |
| } | |
| // --- Function to Update UI for Both Strength and Breach --- | |
| async function updatePasswordAnalysisUI(password) { | |
| if (!password) { | |
| // Hide strength meter and breach status if password is empty | |
| strengthArea.style.display = 'none'; | |
| strengthIndicator.className = 'strength-indicator'; | |
| strengthTextLabel.textContent = 'Strength:'; | |
| strengthFeedbackDiv.innerHTML = ''; | |
| strengthArea.className = 'password-strength-area'; | |
| breachStatusArea.style.display = 'none'; // Hide breach area | |
| breachStatusIndicator.textContent = 'Checking...'; | |
| breachStatusIndicator.className = 'breach-indicator'; // Reset breach style | |
| return; | |
| } | |
| // --- Show elements and indicate loading --- | |
| strengthArea.style.display = 'block'; // Show combined area | |
| breachStatusArea.style.display = 'flex'; // Show breach area (use flex for alignment) | |
| strengthTextLabel.textContent = 'Strength: Checking...'; | |
| strengthIndicator.className = 'strength-indicator'; // Reset bar | |
| strengthFeedbackDiv.innerHTML = '<ul><li>Checking...</li></ul>'; | |
| strengthArea.className = 'password-strength-area'; // Reset background | |
| breachStatusIndicator.textContent = 'Checking...'; | |
| breachStatusIndicator.className = 'breach-indicator loading'; // Style for loading | |
| // --- Perform Checks Concurrently --- | |
| const strengthPromise = fetch("{{ url_for('strength_check_api') }}", { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ password: password }) | |
| }); | |
| // Use the FREE HIBP check directly from crypto-helpers.js | |
| const breachPromise = checkHIBPPassword(password); // Assumes checkHIBPPassword is loaded | |
| // --- Process Strength Results --- | |
| try { | |
| const response = await strengthPromise; | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({ error: 'Unknown server error' })); | |
| throw new Error(errorData.error || `Server error: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| const score = result.score; // Score 0-4 | |
| const strengthClass = getStrengthClassFromScore(score); | |
| const assessment = result.assessment || "Unknown"; | |
| strengthIndicator.className = `strength-indicator ${strengthClass}`; | |
| strengthTextLabel.textContent = `Strength: ${assessment}`; | |
| strengthFeedbackDiv.innerHTML = formatBackendFeedback(result.feedback); | |
| strengthArea.className = `password-strength-area strength-${score}`; | |
| } catch (error) { | |
| console.error('Strength Check Error:', error); | |
| strengthTextLabel.textContent = 'Strength: Error'; | |
| strengthFeedbackDiv.innerHTML = `<ul><li class="warning">Error checking strength: ${escapeHtml(error.message)}</li></ul>`; | |
| strengthArea.className = 'password-strength-area strength-0'; // Show error style | |
| } | |
| // --- Process Breach Results --- | |
| try { | |
| const breachResult = await breachPromise; | |
| if (breachResult.error) { | |
| breachStatusIndicator.textContent = `Error: ${escapeHtml(breachResult.error)}`; | |
| breachStatusIndicator.className = 'breach-indicator error'; | |
| } else if (breachResult.isPwned) { | |
| breachStatusIndicator.textContent = `Compromised! Found in ${breachResult.count} breach${breachResult.count > 1 ? 'es' : ''}.`; | |
| breachStatusIndicator.className = 'breach-indicator pwned'; | |
| } else { | |
| breachStatusIndicator.textContent = 'Not found in known breaches.'; | |
| breachStatusIndicator.className = 'breach-indicator safe'; | |
| } | |
| } catch (error) { // Should not happen if checkHIBPPassword handles errors, but for safety | |
| console.error("Error processing breach result:", error); | |
| breachStatusIndicator.textContent = 'Error checking breach status.'; | |
| breachStatusIndicator.className = 'breach-indicator error'; | |
| } | |
| } | |
| // --- Password Input Listener --- | |
| passwordInput.addEventListener('input', debounce(function() { | |
| if (typeof checkHIBPPassword === 'function' && typeof zxcvbn === 'function') { // Check helpers are loaded | |
| updatePasswordAnalysisUI(this.value); | |
| } else { | |
| console.error("Required analysis functions (checkHIBPPassword or zxcvbn) not found. Ensure scripts are loaded correctly."); | |
| // Basic fallback or visual error indication could go here | |
| strengthTextLabel.textContent = 'Strength: Error'; | |
| strengthArea.style.display = 'block'; | |
| strengthFeedbackDiv.innerHTML = `<ul><li class="warning">Error: Analysis script missing.</li></ul>`; | |
| breachStatusIndicator.textContent = 'Error: Checker missing.'; | |
| breachStatusIndicator.className = 'breach-indicator error'; | |
| breachStatusArea.style.display = 'flex'; | |
| } | |
| }, 600)); // Debounce API calls by 600ms | |
| // --- Generator Logic --- | |
| genLengthSlider.addEventListener('input', function() { genLengthValueSpan.textContent = this.value; }); | |
| togglePasswordBtn.addEventListener('click', function() { const t=passwordInput.type==='password'?'text':'password'; passwordInput.type=t; this.textContent=t==='password'?'Show':'Hide'; }); | |
| generatePasswordBtn.addEventListener('click', async function() { | |
| messageEl.textContent = ''; messageEl.className = 'message'; | |
| generatePasswordBtn.disabled = true; generatePasswordBtn.textContent = '...'; | |
| const options = { | |
| length: parseInt(genLengthSlider.value, 10), | |
| use_lowercase: genLowercaseCheckbox.checked, | |
| use_uppercase: genUppercaseCheckbox.checked, | |
| use_digits: genDigitsCheckbox.checked, | |
| use_symbols: genSymbolsCheckbox.checked | |
| }; | |
| try { | |
| // Fetch generated password from backend | |
| const response = await fetch("{{ url_for('generate_password_api') }}", { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(options) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok && result.password) { | |
| passwordInput.value = result.password; | |
| passwordInput.type = 'text'; // Show generated password briefly | |
| togglePasswordBtn.textContent = 'Hide'; | |
| // *** Trigger the analysis immediately after generation *** | |
| if (typeof checkHIBPPassword === 'function' && typeof zxcvbn === 'function') { | |
| updatePasswordAnalysisUI(result.password); // Call analysis function | |
| } else { | |
| console.error("Analysis functions not available after generation."); | |
| } | |
| // Hide after a delay | |
| setTimeout(() => { | |
| if (passwordInput.type === 'text') { | |
| passwordInput.type = 'password'; | |
| togglePasswordBtn.textContent = 'Show'; | |
| } | |
| }, 2500); // Show for 2.5 seconds | |
| } else { | |
| throw new Error(result.error || 'Failed to generate password.'); | |
| } | |
| } catch (error) { | |
| messageEl.textContent = `Generation Error: ${escapeHtml(error.message)}`; | |
| messageEl.className = 'message error'; | |
| console.error('Generate Password Error:', error); | |
| } finally { | |
| generatePasswordBtn.disabled = false; | |
| generatePasswordBtn.textContent = 'Generate'; | |
| } | |
| }); | |
| // --- Form Submission Logic --- | |
| form.addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| messageEl.textContent = ''; messageEl.className = 'message'; | |
| submitButton.disabled = true; submitButton.textContent = 'Saving...'; | |
| const service = document.getElementById('service').value.trim(); | |
| const username = document.getElementById('username').value.trim(); | |
| const password = document.getElementById('password').value; // Get password from input | |
| if (!service || !username || !password) { | |
| messageEl.textContent = 'Service, Username, and Password are required.'; | |
| messageEl.className = 'message error'; | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Encrypt & Save'; | |
| return; | |
| } | |
| try { | |
| // Get the raw key bytes from session storage | |
| const keyBuffer = await getEncryptionKeyRawBytesFromSession(); | |
| if (!keyBuffer) { | |
| // Error handled within getEncryptionKeyRawBytesFromSession (alert/redirect) | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Encrypt & Save'; | |
| return; | |
| } | |
| // Encrypt the data | |
| const dataToEncrypt = { service: service, username: username, password: password }; | |
| const encryptedB64Data = await encryptData(keyBuffer, dataToEncrypt); // Assumes encryptData is loaded | |
| // Send to backend | |
| const response = await fetch("{{ url_for('add_credential') }}", { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| encrypted_data: encryptedB64Data, | |
| service_hint: service // Use service name as hint | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| messageEl.textContent = 'Credential encrypted and saved successfully!'; | |
| messageEl.className = 'message success'; | |
| form.reset(); // Clear the form | |
| // Reset generator options to default | |
| genLengthSlider.value = 16; | |
| genLengthValueSpan.textContent = '16'; | |
| genLowercaseCheckbox.checked = true; | |
| genUppercaseCheckbox.checked = true; | |
| genDigitsCheckbox.checked = true; | |
| genSymbolsCheckbox.checked = true; | |
| passwordInput.type = 'password'; | |
| togglePasswordBtn.textContent = 'Show'; | |
| // Reset strength & breach display on successful save | |
| strengthArea.style.display = 'none'; // Hide the whole area | |
| breachStatusArea.style.display = 'none'; | |
| strengthIndicator.className = 'strength-indicator'; | |
| strengthTextLabel.textContent = 'Strength:'; | |
| strengthFeedbackDiv.innerHTML = ''; | |
| strengthArea.className = 'password-strength-area'; | |
| breachStatusIndicator.textContent = 'Checking...'; | |
| breachStatusIndicator.className = 'breach-indicator'; | |
| } else { | |
| messageEl.textContent = `Error: ${escapeHtml(result.message || 'Failed to save credential.')}`; | |
| messageEl.className = 'message error'; | |
| } | |
| } catch (error) { | |
| messageEl.textContent = `An error occurred: ${escapeHtml(error.message)}`; | |
| messageEl.className = 'message error'; | |
| console.error('Save Credential Error:', error); | |
| // Log specific crypto errors if they occur | |
| if (error.message.includes("encrypt") || error.message.includes("Base64")) { | |
| console.error("Potential encryption or encoding error during save."); | |
| } | |
| } finally { | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Encrypt & Save'; | |
| } | |
| }); // End form submit listener | |
| }); // End DOMContentLoaded | |
| </script> | |
| </body> | |
| </html> |