From f95461aee2c717ec876d9a5c8d30ab29f7b9e944 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Tue, 19 May 2026 09:31:33 -0400 Subject: [PATCH] 05/19 Enhance codes --- app/static/js/vault.js | 109 +++++++++++++++++++--------------- extension/popup/popup.css | 22 +++++-- extension/popup/popup.js | 119 +++++++++++++++++++++++--------------- 3 files changed, 151 insertions(+), 99 deletions(-) diff --git a/app/static/js/vault.js b/app/static/js/vault.js index 0c1f3a9..6987ed1 100644 --- a/app/static/js/vault.js +++ b/app/static/js/vault.js @@ -471,8 +471,8 @@ const Vault = (() => { _sortOrder === "folder" ? Object.keys(groups).sort() : ["(No folder)", ..._folders.map((f) => f.name)].filter( - (k) => groups[k], - ); + (k) => groups[k], + ); Object.keys(groups).forEach((k) => { if (!keys.includes(k)) keys.push(k); }); @@ -540,7 +540,7 @@ const Vault = (() => { case "card": subText = item.plain.card_number ? "•••• " + - String(item.plain.card_number).replace(/\s/g, "").slice(-4) + String(item.plain.card_number).replace(/\s/g, "").slice(-4) : ""; break; case "bank": @@ -920,9 +920,9 @@ const Vault = (() => { 0, Math.round( 100 - - (weak.length / total) * 40 - - (reused.length / total) * 30 - - (old.length / total) * 15, + (weak.length / total) * 40 - + (reused.length / total) * 30 - + (old.length / total) * 15, ), ); const cls = @@ -943,24 +943,22 @@ const Vault = (() => { `; sectionsEl.innerHTML = ""; - const makeSection = (title, icon, items, desc) => { + const makeSection = (title, icon, items, desc, renderItem) => { if (!items.length) return; const sec = document.createElement("div"); sec.className = "sec-section"; + const defaultRender = (i) => `
  • + ${escHtml(i.name)} + ${escHtml(i.plain?.username || "")} + +
  • `; + const renderFn = renderItem || defaultRender; sec.innerHTML = `
    ${icon}
    ${title} (${items.length})
    ${desc}
    `; sec.querySelectorAll("[data-sec-edit]").forEach((btn) => { btn.addEventListener("click", () => { @@ -995,6 +993,20 @@ const Vault = (() => { "🕐", old, "Weak or reused passwords not changed in over 180 days.", + (i) => { + const ref = i.plain?.password_changed_at || i.created_at; + const daysAgo = ref + ? Math.floor((Date.now() - new Date(ref).getTime()) / 86400000) + : null; + const ageLabel = daysAgo !== null + ? `Last changed ${daysAgo} day${daysAgo !== 1 ? "s" : ""} ago` + : "Age unknown"; + return `
  • + ${escHtml(i.name)} + ${escHtml(ageLabel)} + +
  • `; + }, ); // ── Missing 2FA warning ────────────────────────────────────────────────── @@ -1060,16 +1072,16 @@ const Vault = (() => { `; hibpSection.querySelectorAll("[data-sec-edit]").forEach((btn) => { btn.addEventListener("click", () => { @@ -1423,6 +1435,9 @@ const Vault = (() => { payload = _importRows; } else { // Plaintext CSV rows — encrypt each one now. + // Use the import timestamp as password_changed_at — best available + // approximation since CSV exports don't carry a change date. + const importedAt = new Date().toISOString(); payload = await Promise.all( _importRows.map(async (row) => { const plain = { @@ -1430,6 +1445,7 @@ const Vault = (() => { username: row.username || "", password: row.password, notes: row.notes || "", + password_changed_at: importedAt, }; const { enc_data, iv } = await Crypto.encryptItem( vaultKey, @@ -1607,8 +1623,7 @@ const Vault = (() => { ${escHtml(s.item_name)} From ${escHtml(s.owner_email)} - ${ - !s.accepted + ${!s.accepted ? `` : `` - } + } `, ) .join(""); @@ -2155,10 +2170,9 @@ const Vault = (() => { `; @@ -2487,8 +2501,8 @@ const Vault = (() => { const typeLabel = transports.includes("internal") ? "📱 Device" : transports.some((t) => ["usb", "nfc", "ble", "smart-card"].includes(t)) - ? "🔑 Security key" - : "🔑 Passkey"; + ? "🔑 Security key" + : "🔑 Passkey"; return `
    ${escHtml(c.name)} @@ -2751,16 +2765,15 @@ const Vault = (() => {

    ${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}

    - ${ - isFirstTime - ? `

    + ${isFirstTime + ? `

    These codes let you sign in if you lose access to your authenticator app. Save them now — they will not be shown again.

    ` - : `

    + : `

    Your previous codes have been invalidated. Save these new codes securely.

    ` - } + }
    ${codesHtml}
    @@ -3269,14 +3282,14 @@ const Vault = (() => { const pool = !_activeFilter ? _items : _activeFilter.type === "itemType" - ? _items.filter((i) => i.item_type === _activeFilter.value) - : _activeFilter.type === "folder" - ? _items.filter((i) => i.folder_id === _activeFilter.value) - : _activeFilter.type === "tag" - ? _items.filter((i) => - (i.plain?.tags || []).includes(_activeFilter.value), - ) - : _items; + ? _items.filter((i) => i.item_type === _activeFilter.value) + : _activeFilter.type === "folder" + ? _items.filter((i) => i.folder_id === _activeFilter.value) + : _activeFilter.type === "tag" + ? _items.filter((i) => + (i.plain?.tags || []).includes(_activeFilter.value), + ) + : _items; renderItemList( pool.filter( (item) => @@ -3427,7 +3440,7 @@ const Vault = (() => { if ( (mode === "add" && (document.getElementById("field-type").value || "password") === - "password") || + "password") || (mode === "edit" && (item?.item_type || "password") === "password") ) { initPasswordFieldEnhancements(); @@ -3684,7 +3697,7 @@ const Vault = (() => { "X-CSRFToken": csrfToken(), }, body: JSON.stringify({ refresh_token: refreshToken }), - }).catch(() => {}); + }).catch(() => { }); VaultSession.clear(); SharingSession.clear(); sessionStorage.removeItem("access_token"); @@ -3789,11 +3802,11 @@ const Vault = (() => { // Auto-clear clipboard after 30 seconds — industry-standard hygiene. if (_clipboardClearTimer) clearTimeout(_clipboardClearTimer); _clipboardClearTimer = setTimeout(() => { - navigator.clipboard.writeText("").catch(() => {}); + navigator.clipboard.writeText("").catch(() => { }); _clipboardClearTimer = null; }, 30_000); }) - .catch(() => {}); + .catch(() => { }); } function escHtml(str) { diff --git a/extension/popup/popup.css b/extension/popup/popup.css index b610a29..12691a9 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -932,6 +932,7 @@ body { background: #e0f2fe; color: #0369a1; } + /* ── Three-dot flyout menu ───────────────────────────────────────────────── */ .pk-flyout { @@ -940,7 +941,7 @@ body { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; - box-shadow: 0 4px 16px rgba(0,0,0,0.13); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.13); min-width: 150px; padding: 4px 0; display: flex; @@ -972,9 +973,20 @@ body { color: #6b7280; } +.pk-flyout-info { + padding: 6px 12px 7px; + font-size: 11px; + color: #9ca3af; + border-top: 1px solid #f3f4f6; + margin-top: 2px; + user-select: none; +} + /* ── Collapsible folder groups ────────────────────────────────────────────── */ -.pk-group { margin-bottom: 2px; } +.pk-group { + margin-bottom: 2px; +} .pk-group-header { display: flex; @@ -988,7 +1000,9 @@ body { transition: background 0.1s; } -.pk-group-header:hover { background: #f0f4ff; } +.pk-group-header:hover { + background: #f0f4ff; +} .pk-group-name { font-size: 12px; @@ -1091,4 +1105,4 @@ body { .reprompt-actions .btn-primary, .reprompt-actions .btn-ghost { margin-top: 0; -} +} \ No newline at end of file diff --git a/extension/popup/popup.js b/extension/popup/popup.js index 23a2c48..ed9411c 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -69,10 +69,10 @@ function escHtml(str) { // Auto-clear clipboard 30 s after a sensitive copy. let _clipTimer = null; function _copyWithAutoClear(text) { - navigator.clipboard.writeText(text).catch(() => {}); + navigator.clipboard.writeText(text).catch(() => { }); if (_clipTimer) clearTimeout(_clipTimer); _clipTimer = setTimeout(() => { - navigator.clipboard.writeText("").catch(() => {}); + navigator.clipboard.writeText("").catch(() => { }); _clipTimer = null; }, 30_000); } @@ -186,7 +186,7 @@ function siteLabel(item) { if (item.plain?.url) { try { return new URL(item.plain.url).hostname.replace(/^www\./, ""); - } catch {} + } catch { } } return item.name; } @@ -441,7 +441,7 @@ async function completeLogin(data, masterPassword) { refresh_token: data.refresh_token, enc_key_salt: data.enc_key_salt, }) - .catch(() => {}); + .catch(() => { }); showView("vault"); await checkPendingSave(); @@ -502,9 +502,9 @@ async function signOut() { Authorization: `Bearer ${access_token}`, }, body: JSON.stringify({ refresh_token }), - }).catch(() => {}); + }).catch(() => { }); } - } catch {} + } catch { } await chrome.storage.session.clear(); await chrome.storage.local.remove(["refresh_token", "enc_key_salt"]); // vault_items_cs is now in session storage — cleared by the session.clear() call above. @@ -616,7 +616,7 @@ async function fetchAndDecryptVault() { type: "VAULT_UPDATED", vault_items: itemsForContentScript, }) - .catch(() => {}); + .catch(() => { }); // Run health checks in the background — sync metrics first, then HIBP. // Results are sent to the background SW via HEALTH_UPDATE so the toolbar @@ -645,7 +645,7 @@ async function _runPopupHealthCheck() { if (!pwItems.length) { chrome.runtime .sendMessage({ type: "HEALTH_UPDATE", breached: 0, weak: 0, reused: 0 }) - .catch(() => {}); + .catch(() => { }); return; } @@ -676,7 +676,7 @@ async function _runPopupHealthCheck() { weak: weak.length, reused: reused.length, }) - .catch(() => {}); + .catch(() => { }); // HIBP — k-anonymity, parallel. const hibpResults = await Promise.all( @@ -695,7 +695,7 @@ async function _runPopupHealthCheck() { weak: weak.length, reused: reused.length, }) - .catch(() => {}); + .catch(() => { }); } catch (err) { console.error("[PassKeeper] Popup health check failed:", err); } @@ -1053,7 +1053,7 @@ function renderList() { password: item.plain.password || "", autologin: !!item.plain.autologin, }) - .catch(() => {}); + .catch(() => { }); } window.close(); }), @@ -1093,44 +1093,64 @@ function renderList() { const menuItems = [ item.plain?.url ? { - label: "Open URL", - icon: '', - action: async () => { - chrome.tabs.create({ url: item.plain.url }); - flyout.remove(); - }, - } + label: "Open URL", + icon: '', + action: async () => { + chrome.tabs.create({ url: item.plain.url }); + flyout.remove(); + }, + } : null, item.plain?.username ? { - label: "Copy username", - icon: '', - action: async () => { - if (item.plain?.reprompt) { - const ok = await _repromptMasterPassword(); - if (!ok) return; - } - _copyWithAutoClear(item.plain.username); - flyout.remove(); - }, - } + label: "Copy username", + icon: '', + action: async () => { + if (item.plain?.reprompt) { + const ok = await _repromptMasterPassword(); + if (!ok) return; + } + _copyWithAutoClear(item.plain.username); + flyout.remove(); + }, + } : null, item.plain?.password ? { - label: "Copy password", - icon: '', - action: async () => { - if (item.plain?.reprompt) { - const ok = await _repromptMasterPassword(); - if (!ok) return; - } - _copyWithAutoClear(item.plain.password); - flyout.remove(); - }, - } + label: "Copy password", + icon: '', + action: async () => { + if (item.plain?.reprompt) { + const ok = await _repromptMasterPassword(); + if (!ok) return; + } + _copyWithAutoClear(item.plain.password); + flyout.remove(); + }, + } : null, ].filter(Boolean); + // Add a non-action info row showing when the password was last changed. + // Only shown for password items that have password_changed_at set. + if (item.item_type === "password" && item.plain?.password_changed_at) { + const daysAgo = Math.floor( + (Date.now() - new Date(item.plain.password_changed_at).getTime()) / + 86400000, + ); + const ageText = + daysAgo === 0 + ? "Changed today" + : daysAgo === 1 + ? "Changed yesterday" + : `Changed ${daysAgo} days ago`; + const infoRow = document.createElement("div"); + infoRow.className = "pk-flyout-info"; + infoRow.textContent = ageText; + // Append after action rows are added. + flyout._ageInfoRow = infoRow; + } + menuItems.forEach((mi) => { const row = document.createElement("button"); row.className = "pk-flyout-item"; @@ -1142,6 +1162,11 @@ function renderList() { flyout.appendChild(row); }); + // Append age info row if present. + if (flyout._ageInfoRow) { + flyout.appendChild(flyout._ageInfoRow); + } + // Position flyout using fixed coords so it escapes vault-list overflow clipping. const appEl = document.getElementById("app"); appEl.appendChild(flyout); @@ -1246,7 +1271,7 @@ async function checkPendingSave() { function _clearPendingSave() { $("save-prompt-overlay").classList.add("hidden"); chrome.storage.local.remove("pending_save"); - chrome.runtime.sendMessage({ type: "CLEAR_SAVE_BADGE" }).catch(() => {}); + chrome.runtime.sendMessage({ type: "CLEAR_SAVE_BADGE" }).catch(() => { }); } $("btn-save-yes").onclick = async () => { @@ -1484,7 +1509,7 @@ async function initAddView() { : new URL(tab.url).hostname.replace(/^www\./, ""); } } - } catch (e) {} + } catch (e) { } // Load folders into the add-folder select. const addFolderSel = $("add-folder"); @@ -1500,7 +1525,7 @@ async function initAddView() { addFolderSel.appendChild(opt); }); } - } catch (e) {} + } catch (e) { } // Wire one-time event listeners. if (!_addViewInitialised) { @@ -1635,7 +1660,7 @@ async function initAccountView() { // Tell background to re-apply the new interval. chrome.runtime .sendMessage({ type: "SET_IDLE_TIMEOUT", seconds: val }) - .catch(() => {}); + .catch(() => { }); // Brief "Saved" confirmation. const saved = $("acct-idle-saved"); saved.classList.remove("hidden"); @@ -1656,7 +1681,7 @@ async function init() { currentWindow: true, }); _currentUrl = tab?.url || ""; - } catch {} + } catch { } // Set vault link $("btn-open-vault").href = VAULT_URL; @@ -1672,7 +1697,7 @@ async function init() { showView("generator"); initGenerator(); // Still load vault data in background so the Vault tab is ready. - fetchAndDecryptVault().then(() => {}); + fetchAndDecryptVault().then(() => { }); // Wire event listeners below, then return early from vault-specific setup. } else { showView("vault"); @@ -1752,7 +1777,7 @@ async function init() { // browser is open. We ping it every 20 s; the ping itself is a no-op but // prevents the SW from being killed between popup openings. const _keepalive = setInterval(() => { - chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => {}); + chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => { }); }, 20_000); // Search