diff --git a/README.md b/README.md index 28f58ec..b741462 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ A powerful, secure, and feature-rich password generator built with Python. Featu - **Pattern Detection** - Warns about repeated/sequential characters ### Web Interface -- Beautiful dark-themed UI +- Responsive, accessible interface for desktop and mobile - Random password and passphrase modes -- Real-time strength indicators -- One-click copy to clipboard +- Clear strength and entropy indicators +- One-click copy with inline validation feedback ## Installation diff --git a/templates/index.html b/templates/index.html index 63008a5..5be5fe8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,494 +1,842 @@ - + - - -PassGen - + + + + PassGen β€” Private password generator + -
-
- -

PassGen

-

Secure password generator

-
- -
-
- - + + +
+
+
+

Private credential utility

+

Make a password worth keeping.

+
+

Choose the shape, set the strength, and copy the result. Every value is generated with cryptographically secure randomness and is not persisted.

+
+ +
+
+
+

Configure

+ 01 / 02 +
+ +
+ + +
-
- + + -
-
-
- -
- - 20 +
+
+ +
+ + 20 +
+ +
+ Character sets +
+ + + + + +
+
-
- - -
-
-
- - - - - -
-
-
+
+ + + + function strengthSegments(strength) { + const counts = { Strong: 4, Good: 3, Fair: 2, Weak: 1 }; + const track = document.createElement('span'); + track.className = 'strength-track'; + track.setAttribute('aria-label', `${strength} strength`); + const activeCount = counts[strength] || 1; + for (let index = 0; index < 4; index += 1) { + const segment = document.createElement('span'); + segment.className = `strength-segment strength-${strength.toLowerCase()}`; + if (index < activeCount) segment.classList.add('is-on'); + track.appendChild(segment); + } + return track; + } + + async function copyPassword(password, copyButton) { + try { + await navigator.clipboard.writeText(password); + copyButton.textContent = 'Copied'; + copyButton.classList.add('copied'); + window.setTimeout(() => { + copyButton.textContent = 'Copy'; + copyButton.classList.remove('copied'); + }, 1600); + } catch { + const value = copyButton.previousElementSibling; + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(value); + selection.removeAllRanges(); + selection.addRange(range); + showError('Clipboard access was blocked. The password is selected; copy it manually.'); + } + } + + function validateResults(data) { + const strengths = new Set(['Strong', 'Good', 'Fair', 'Weak']); + if (!data || typeof data !== 'object' || !Array.isArray(data.passwords) + || data.passwords.length < 1 || data.passwords.length > 20) { + throw new Error('The generator returned an unexpected response.'); + } + return data.passwords.map((item) => { + if (!item || typeof item.password !== 'string' || !strengths.has(item.strength) + || !['string', 'number'].includes(typeof item.entropy)) { + throw new Error('The generator returned an unexpected response.'); + } + return { ...item, entropy: String(item.entropy) }; + }); + } + + function renderResults(passwords) { + results.replaceChildren(); + emptyState.hidden = passwords.length > 0; + if (!passwords.length) return; + + const heading = document.createElement('h3'); + heading.className = 'results-heading'; + heading.textContent = `${passwords.length} generated ${passwords.length === 1 ? 'credential' : 'credentials'}`; + results.appendChild(heading); + + passwords.forEach((password, index) => { + const item = document.createElement('article'); + item.className = 'result-item'; + item.style.animationDelay = `${Math.min(index * 35, 210)}ms`; + + const topLine = document.createElement('div'); + topLine.className = 'result-topline'; + + const number = document.createElement('span'); + number.className = 'result-number'; + number.textContent = String(index + 1).padStart(2, '0'); + + const badge = document.createElement('span'); + badge.className = `badge badge-${password.strength.toLowerCase()}`; + badge.textContent = password.strength; + topLine.append(number, badge); + + const passwordLine = document.createElement('div'); + passwordLine.className = 'password-line'; + + const value = document.createElement('code'); + value.className = 'password-value'; + value.textContent = password.password; + + const copyButton = document.createElement('button'); + copyButton.className = 'copy-button'; + copyButton.type = 'button'; + copyButton.textContent = 'Copy'; + copyButton.setAttribute('aria-label', `Copy generated credential ${index + 1}`); + copyButton.addEventListener('click', () => copyPassword(password.password, copyButton)); + passwordLine.append(value, copyButton); + + const meta = document.createElement('div'); + meta.className = 'result-meta'; + const entropy = document.createElement('span'); + entropy.textContent = `${password.entropy} bits entropy`; + meta.append(entropy, strengthSegments(password.strength)); + + item.append(topLine, passwordLine, meta); + results.appendChild(item); + }); + } + - \ No newline at end of file + diff --git a/tests/test_web.py b/tests/test_web.py index 01c8750..13ec031 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -7,6 +7,40 @@ client = TestClient(app, raise_server_exceptions=False) +def test_home_uses_accessible_generator_controls(): + response = client.get("/") + + assert response.status_code == 200 + html = response.text + assert 'role="tablist"' in html + assert html.count('