04/17/2026 update launch button, password generation, and password checker
This commit is contained in:
+205
-2
@@ -255,6 +255,7 @@ const Vault = (() => {
|
||||
|
||||
const showCopyUser = item.item_type === 'password';
|
||||
const showCopyPass = ['password','card','bank','ssn'].includes(item.item_type);
|
||||
const showLaunch = item.item_type === 'password' && item.plain?.url;
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="item-icon">${icon}</div>
|
||||
@@ -263,12 +264,21 @@ const Vault = (() => {
|
||||
<span class="item-sub">${escHtml(subText)}</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
${showLaunch ? `<button class="btn-icon" title="Open URL" data-action="launch">🌐</button>` : ''}
|
||||
${showCopyUser ? `<button class="btn-icon" title="Copy username" data-action="copy-user">👤</button>` : ''}
|
||||
${showCopyPass ? `<button class="btn-icon" title="Copy secret" data-action="copy-pass">📋</button>` : ''}
|
||||
<button class="btn-icon" title="Edit" data-action="edit">✏️</button>
|
||||
<button class="btn-icon" title="Delete" data-action="delete">🗑️</button>
|
||||
</div>`;
|
||||
|
||||
if (showLaunch) {
|
||||
li.querySelector('[data-action="launch"]').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
let url = item.plain?.url || '';
|
||||
if (url && !/^https?:\/\//i.test(url)) url = 'https://' + url;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}
|
||||
if (showCopyUser) {
|
||||
li.querySelector('[data-action="copy-user"]').addEventListener('click', e => {
|
||||
e.stopPropagation(); copyToClipboard(item.plain?.username || '', 'Username copied');
|
||||
@@ -853,6 +863,191 @@ const Vault = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Password Generator ────────────────────────────────────────────────
|
||||
|
||||
const GEN_CHARSETS = {
|
||||
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
lower: 'abcdefghijklmnopqrstuvwxyz',
|
||||
digits: '0123456789',
|
||||
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?',
|
||||
};
|
||||
|
||||
function generatePassword(length = 20, opts = { upper: true, lower: true, digits: true, symbols: true }) {
|
||||
let pool = '';
|
||||
const required = [];
|
||||
if (opts.upper) { pool += GEN_CHARSETS.upper; required.push(GEN_CHARSETS.upper); }
|
||||
if (opts.lower) { pool += GEN_CHARSETS.lower; required.push(GEN_CHARSETS.lower); }
|
||||
if (opts.digits) { pool += GEN_CHARSETS.digits; required.push(GEN_CHARSETS.digits); }
|
||||
if (opts.symbols) { pool += GEN_CHARSETS.symbols; required.push(GEN_CHARSETS.symbols); }
|
||||
if (!pool) pool = GEN_CHARSETS.lower;
|
||||
|
||||
const arr = new Uint32Array(length + required.length);
|
||||
window.crypto.getRandomValues(arr);
|
||||
|
||||
// Guarantee at least one char from each required charset
|
||||
const chars = required.map((cs, i) => cs[arr[i] % cs.length]);
|
||||
for (let i = required.length; i < length + required.length; i++) {
|
||||
chars.push(pool[arr[i] % pool.length]);
|
||||
}
|
||||
// Fisher-Yates shuffle using crypto random
|
||||
const shuffle = new Uint32Array(chars.length);
|
||||
window.crypto.getRandomValues(shuffle);
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = shuffle[i] % (i + 1);
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.slice(0, length).join('');
|
||||
}
|
||||
|
||||
function scorePassword(pw) {
|
||||
if (!pw) return { score: 0, label: '', cls: '' };
|
||||
let score = 0;
|
||||
if (pw.length >= 8) score++;
|
||||
if (pw.length >= 12) score++;
|
||||
if (pw.length >= 16) score++;
|
||||
if (/[A-Z]/.test(pw)) score++;
|
||||
if (/[a-z]/.test(pw)) score++;
|
||||
if (/[0-9]/.test(pw)) score++;
|
||||
if (/[^A-Za-z0-9]/.test(pw)) score++;
|
||||
// clamp to 5 bands
|
||||
const band = Math.min(5, Math.ceil(score / 7 * 5));
|
||||
const labels = ['', 'Very Weak', 'Weak', 'Fair', 'Strong', 'Very Strong'];
|
||||
const clses = ['', 'strength-1','strength-2','strength-3','strength-4','strength-5'];
|
||||
return { score: band, label: labels[band], cls: clses[band] };
|
||||
}
|
||||
|
||||
// Attach strength meter + generate button to the password field in the item modal
|
||||
function initPasswordFieldEnhancements() {
|
||||
const pwInput = document.getElementById('field-password');
|
||||
if (!pwInput) return;
|
||||
|
||||
// Strength bar below the password field
|
||||
if (!document.getElementById('field-password-strength')) {
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'field-password-strength';
|
||||
bar.className = 'password-strength';
|
||||
pwInput.closest('.input-with-toggle').insertAdjacentElement('afterend', bar);
|
||||
}
|
||||
|
||||
pwInput.addEventListener('input', () => {
|
||||
const { label, cls } = scorePassword(pwInput.value);
|
||||
const bar = document.getElementById('field-password-strength');
|
||||
bar.textContent = pwInput.value ? label : '';
|
||||
bar.className = 'password-strength ' + (pwInput.value ? cls : '');
|
||||
});
|
||||
|
||||
// Generate button — add only once
|
||||
if (!document.getElementById('btn-generate-pass')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.id = 'btn-generate-pass';
|
||||
btn.className = 'btn-secondary btn-sm';
|
||||
btn.textContent = '⚡ Generate';
|
||||
btn.style.marginTop = '6px';
|
||||
pwInput.closest('.form-group').appendChild(btn);
|
||||
}
|
||||
|
||||
document.getElementById('btn-generate-pass').onclick = () => {
|
||||
const pw = generatePassword(20);
|
||||
pwInput.value = pw;
|
||||
pwInput.type = 'text'; // reveal so user can see what was generated
|
||||
pwInput.dispatchEvent(new Event('input')); // trigger strength bar
|
||||
};
|
||||
}
|
||||
|
||||
function openPasswordGeneratorModal() {
|
||||
// Build modal content dynamically — keeps the HTML lean
|
||||
let modal = document.getElementById('gen-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'gen-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.innerHTML = `
|
||||
<div class="modal" style="max-width:420px">
|
||||
<div class="modal-header">
|
||||
<h3>⚡ Password Generator</h3>
|
||||
<button class="btn-icon btn-close" id="btn-close-gen-modal" aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:8px">
|
||||
<label>Length: <strong id="gen-length-display">20</strong></label>
|
||||
<input type="range" id="gen-length" min="8" max="64" value="20" style="width:100%;margin-top:4px">
|
||||
</div>
|
||||
<div class="gen-options" style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px">
|
||||
<label class="gen-opt"><input type="checkbox" id="gen-upper" checked> Uppercase</label>
|
||||
<label class="gen-opt"><input type="checkbox" id="gen-lower" checked> Lowercase</label>
|
||||
<label class="gen-opt"><input type="checkbox" id="gen-digits" checked> Numbers</label>
|
||||
<label class="gen-opt"><input type="checkbox" id="gen-symbols" checked> Symbols</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Generated Password</label>
|
||||
<div class="input-with-toggle" style="margin-bottom:4px">
|
||||
<input type="text" id="gen-output" readonly style="font-family:monospace;letter-spacing:.5px;padding-right:40px">
|
||||
<button type="button" class="btn-show-pass" id="btn-gen-copy" title="Copy" style="font-size:14px">📋</button>
|
||||
</div>
|
||||
<div id="gen-strength" class="password-strength"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" id="btn-close-gen-modal2">Close</button>
|
||||
<button type="button" class="btn-primary" id="btn-regenerate">↺ Regenerate</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const lengthSlider = document.getElementById('gen-length');
|
||||
const lengthDisplay = document.getElementById('gen-length-display');
|
||||
const output = document.getElementById('gen-output');
|
||||
const strengthEl = document.getElementById('gen-strength');
|
||||
|
||||
function refreshGen() {
|
||||
const pw = generatePassword(parseInt(lengthSlider.value), {
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
symbols: document.getElementById('gen-symbols').checked,
|
||||
});
|
||||
output.value = pw;
|
||||
const { label, cls } = scorePassword(pw);
|
||||
strengthEl.textContent = label;
|
||||
strengthEl.className = 'password-strength ' + cls;
|
||||
}
|
||||
|
||||
lengthSlider.addEventListener('input', () => {
|
||||
lengthDisplay.textContent = lengthSlider.value;
|
||||
refreshGen();
|
||||
});
|
||||
['gen-upper','gen-lower','gen-digits','gen-symbols'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('change', refreshGen);
|
||||
});
|
||||
document.getElementById('btn-regenerate').addEventListener('click', refreshGen);
|
||||
document.getElementById('btn-gen-copy').addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(output.value).then(() => showToast('Password copied'));
|
||||
});
|
||||
const closeGen = () => modal.classList.remove('open');
|
||||
document.getElementById('btn-close-gen-modal').addEventListener('click', closeGen);
|
||||
document.getElementById('btn-close-gen-modal2').addEventListener('click', closeGen);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeGen(); });
|
||||
|
||||
refreshGen();
|
||||
}
|
||||
modal.classList.add('open');
|
||||
// Regenerate fresh password each time modal opens
|
||||
const lengthSlider = document.getElementById('gen-length');
|
||||
document.getElementById('gen-length-display').textContent = lengthSlider.value;
|
||||
const pw = generatePassword(parseInt(lengthSlider.value), {
|
||||
upper: document.getElementById('gen-upper').checked,
|
||||
lower: document.getElementById('gen-lower').checked,
|
||||
digits: document.getElementById('gen-digits').checked,
|
||||
symbols: document.getElementById('gen-symbols').checked,
|
||||
});
|
||||
document.getElementById('gen-output').value = pw;
|
||||
const { label, cls } = scorePassword(pw);
|
||||
const strengthEl = document.getElementById('gen-strength');
|
||||
strengthEl.textContent = label;
|
||||
strengthEl.className = 'password-strength ' + cls;
|
||||
}
|
||||
|
||||
// ── Account Settings ──────────────────────────────────────────────────────
|
||||
|
||||
let _pendingMfaSecret = null;
|
||||
@@ -1123,6 +1318,10 @@ const Vault = (() => {
|
||||
}
|
||||
document.getElementById('item-modal').classList.add('open');
|
||||
document.getElementById('field-name').focus();
|
||||
if ((mode === 'add' && (document.getElementById('field-type').value || 'password') === 'password') ||
|
||||
(mode === 'edit' && (item?.item_type || 'password') === 'password')) {
|
||||
initPasswordFieldEnhancements();
|
||||
}
|
||||
}
|
||||
|
||||
function setVal(id, val) { const el = document.getElementById(id); if (el) el.value = val || ''; }
|
||||
@@ -1303,7 +1502,10 @@ const Vault = (() => {
|
||||
document.getElementById('btn-logout')?.addEventListener('click', handleLogout);
|
||||
|
||||
document.getElementById('sort-select')?.addEventListener('change', e => { _sortOrder = e.target.value; applyCurrentFilter(); });
|
||||
document.getElementById('field-type')?.addEventListener('change', e => switchModalType(e.target.value));
|
||||
document.getElementById('field-type')?.addEventListener('change', e => {
|
||||
switchModalType(e.target.value);
|
||||
if (e.target.value === 'password') initPasswordFieldEnhancements();
|
||||
});
|
||||
document.getElementById('btn-toggle-pass')?.addEventListener('click', () => { const el = document.getElementById('field-password'); el.type = el.type === 'password' ? 'text' : 'password'; });
|
||||
document.getElementById('btn-toggle-ssn')?.addEventListener('click', () => { const el = document.getElementById('field-ssn-number'); el.type = el.type === 'password' ? 'text' : 'password'; });
|
||||
|
||||
@@ -1317,6 +1519,7 @@ const Vault = (() => {
|
||||
document.getElementById('sidebar-security')?.addEventListener('click', () => switchView('security'));
|
||||
document.getElementById('sidebar-sharing')?.addEventListener('click', () => switchView('sharing'));
|
||||
document.getElementById('sidebar-emergency')?.addEventListener('click', () => switchView('emergency'));
|
||||
document.getElementById('sidebar-generator')?.addEventListener('click', () => openPasswordGeneratorModal());
|
||||
|
||||
document.querySelectorAll('[data-type-filter]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
@@ -1332,7 +1535,7 @@ const Vault = (() => {
|
||||
});
|
||||
|
||||
document.getElementById('item-modal')?.addEventListener('click', e => { if (e.target.id === 'item-modal') closeModal(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); closeModal('share-modal'); closeModal('emergency-modal'); closeModal('settings-modal'); closeModal('detail-modal'); } });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); closeModal('share-modal'); closeModal('emergency-modal'); closeModal('settings-modal'); closeModal('detail-modal'); closeModal('gen-modal'); } });
|
||||
|
||||
// Detail modal (shared item view)
|
||||
const closeDetailModal = () => closeModal('detail-modal');
|
||||
|
||||
Reference in New Issue
Block a user