05/19 Enhance codes
This commit is contained in:
+61
-48
@@ -471,8 +471,8 @@ const Vault = (() => {
|
|||||||
_sortOrder === "folder"
|
_sortOrder === "folder"
|
||||||
? Object.keys(groups).sort()
|
? Object.keys(groups).sort()
|
||||||
: ["(No folder)", ..._folders.map((f) => f.name)].filter(
|
: ["(No folder)", ..._folders.map((f) => f.name)].filter(
|
||||||
(k) => groups[k],
|
(k) => groups[k],
|
||||||
);
|
);
|
||||||
Object.keys(groups).forEach((k) => {
|
Object.keys(groups).forEach((k) => {
|
||||||
if (!keys.includes(k)) keys.push(k);
|
if (!keys.includes(k)) keys.push(k);
|
||||||
});
|
});
|
||||||
@@ -540,7 +540,7 @@ const Vault = (() => {
|
|||||||
case "card":
|
case "card":
|
||||||
subText = item.plain.card_number
|
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;
|
break;
|
||||||
case "bank":
|
case "bank":
|
||||||
@@ -920,9 +920,9 @@ const Vault = (() => {
|
|||||||
0,
|
0,
|
||||||
Math.round(
|
Math.round(
|
||||||
100 -
|
100 -
|
||||||
(weak.length / total) * 40 -
|
(weak.length / total) * 40 -
|
||||||
(reused.length / total) * 30 -
|
(reused.length / total) * 30 -
|
||||||
(old.length / total) * 15,
|
(old.length / total) * 15,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const cls =
|
const cls =
|
||||||
@@ -943,24 +943,22 @@ const Vault = (() => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
sectionsEl.innerHTML = "";
|
sectionsEl.innerHTML = "";
|
||||||
const makeSection = (title, icon, items, desc) => {
|
const makeSection = (title, icon, items, desc, renderItem) => {
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
const sec = document.createElement("div");
|
const sec = document.createElement("div");
|
||||||
sec.className = "sec-section";
|
sec.className = "sec-section";
|
||||||
|
const defaultRender = (i) => `<li class="sec-item">
|
||||||
|
<span class="sec-item-name">${escHtml(i.name)}</span>
|
||||||
|
<span class="sec-item-sub">${escHtml(i.plain?.username || "")}</span>
|
||||||
|
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
||||||
|
</li>`;
|
||||||
|
const renderFn = renderItem || defaultRender;
|
||||||
sec.innerHTML = `
|
sec.innerHTML = `
|
||||||
<div class="sec-section-header"><span class="sec-section-icon">${icon}</span>
|
<div class="sec-section-header"><span class="sec-section-icon">${icon}</span>
|
||||||
<div><div class="sec-section-title">${title} (${items.length})</div><div class="sec-section-desc">${desc}</div></div>
|
<div><div class="sec-section-title">${title} (${items.length})</div><div class="sec-section-desc">${desc}</div></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sec-item-list">
|
<ul class="sec-item-list">
|
||||||
${items
|
${items.map(renderFn).join("")}
|
||||||
.map(
|
|
||||||
(i) => `<li class="sec-item">
|
|
||||||
<span class="sec-item-name">${escHtml(i.name)}</span>
|
|
||||||
<span class="sec-item-sub">${escHtml(i.plain?.username || "")}</span>
|
|
||||||
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
|
||||||
</li>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</ul>`;
|
</ul>`;
|
||||||
sec.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
sec.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
@@ -995,6 +993,20 @@ const Vault = (() => {
|
|||||||
"🕐",
|
"🕐",
|
||||||
old,
|
old,
|
||||||
"Weak or reused passwords not changed in over 180 days.",
|
"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 `<li class="sec-item">
|
||||||
|
<span class="sec-item-name">${escHtml(i.name)}</span>
|
||||||
|
<span class="sec-item-sub">${escHtml(ageLabel)}</span>
|
||||||
|
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Edit</button>
|
||||||
|
</li>`;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Missing 2FA warning ──────────────────────────────────────────────────
|
// ── Missing 2FA warning ──────────────────────────────────────────────────
|
||||||
@@ -1060,16 +1072,16 @@ const Vault = (() => {
|
|||||||
</div>
|
</div>
|
||||||
<ul class="sec-item-list">
|
<ul class="sec-item-list">
|
||||||
${breached
|
${breached
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
const count =
|
const count =
|
||||||
hibpResults.find((r) => r.item.id === i.id)?.count || 0;
|
hibpResults.find((r) => r.item.id === i.id)?.count || 0;
|
||||||
return `<li class="sec-item">
|
return `<li class="sec-item">
|
||||||
<span class="sec-item-name">${escHtml(i.name)}</span>
|
<span class="sec-item-name">${escHtml(i.name)}</span>
|
||||||
<span class="sec-item-sub">${escHtml(i.plain?.username || "")} — seen ${count.toLocaleString()} time${count !== 1 ? "s" : ""} in breaches</span>
|
<span class="sec-item-sub">${escHtml(i.plain?.username || "")} — seen ${count.toLocaleString()} time${count !== 1 ? "s" : ""} in breaches</span>
|
||||||
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Change</button>
|
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Change</button>
|
||||||
</li>`;
|
</li>`;
|
||||||
})
|
})
|
||||||
.join("")}
|
.join("")}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
hibpSection.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
hibpSection.querySelectorAll("[data-sec-edit]").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
@@ -1423,6 +1435,9 @@ const Vault = (() => {
|
|||||||
payload = _importRows;
|
payload = _importRows;
|
||||||
} else {
|
} else {
|
||||||
// Plaintext CSV rows — encrypt each one now.
|
// 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(
|
payload = await Promise.all(
|
||||||
_importRows.map(async (row) => {
|
_importRows.map(async (row) => {
|
||||||
const plain = {
|
const plain = {
|
||||||
@@ -1430,6 +1445,7 @@ const Vault = (() => {
|
|||||||
username: row.username || "",
|
username: row.username || "",
|
||||||
password: row.password,
|
password: row.password,
|
||||||
notes: row.notes || "",
|
notes: row.notes || "",
|
||||||
|
password_changed_at: importedAt,
|
||||||
};
|
};
|
||||||
const { enc_data, iv } = await Crypto.encryptItem(
|
const { enc_data, iv } = await Crypto.encryptItem(
|
||||||
vaultKey,
|
vaultKey,
|
||||||
@@ -1607,8 +1623,7 @@ const Vault = (() => {
|
|||||||
<span class="share-name">${escHtml(s.item_name)}</span>
|
<span class="share-name">${escHtml(s.item_name)}</span>
|
||||||
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
|
<span class="share-meta">From ${escHtml(s.owner_email)}</span>
|
||||||
</div>
|
</div>
|
||||||
${
|
${!s.accepted
|
||||||
!s.accepted
|
|
||||||
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
|
||||||
: `<button class="btn-secondary btn-sm"
|
: `<button class="btn-secondary btn-sm"
|
||||||
data-view-share="${s.id}"
|
data-view-share="${s.id}"
|
||||||
@@ -1619,7 +1634,7 @@ const Vault = (() => {
|
|||||||
data-iv-name="${escHtml(s.iv_name || "")}"
|
data-iv-name="${escHtml(s.iv_name || "")}"
|
||||||
data-name="${escHtml(s.item_name)}"
|
data-name="${escHtml(s.item_name)}"
|
||||||
data-type="${escHtml(s.item_type || "")}">View</button>`
|
data-type="${escHtml(s.item_type || "")}">View</button>`
|
||||||
}
|
}
|
||||||
</li>`,
|
</li>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
@@ -2155,10 +2170,9 @@ const Vault = (() => {
|
|||||||
<span class="em-vault-chevron">▸</span>
|
<span class="em-vault-chevron">▸</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
|
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
|
||||||
${
|
${hasFields
|
||||||
hasFields
|
? '<div class="detail-body em-detail-body"></div>'
|
||||||
? '<div class="detail-body em-detail-body"></div>'
|
: '<p class="vault-empty">Could not decrypt this item.</p>'
|
||||||
: '<p class="vault-empty">Could not decrypt this item.</p>'
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</li>`;
|
</li>`;
|
||||||
@@ -2487,8 +2501,8 @@ const Vault = (() => {
|
|||||||
const typeLabel = transports.includes("internal")
|
const typeLabel = transports.includes("internal")
|
||||||
? "📱 Device"
|
? "📱 Device"
|
||||||
: transports.some((t) => ["usb", "nfc", "ble", "smart-card"].includes(t))
|
: transports.some((t) => ["usb", "nfc", "ble", "smart-card"].includes(t))
|
||||||
? "🔑 Security key"
|
? "🔑 Security key"
|
||||||
: "🔑 Passkey";
|
: "🔑 Passkey";
|
||||||
return `<div class="passkey-item" data-cred-id="${c.id}">
|
return `<div class="passkey-item" data-cred-id="${c.id}">
|
||||||
<div class="passkey-info">
|
<div class="passkey-info">
|
||||||
<span class="passkey-name">${escHtml(c.name)}</span>
|
<span class="passkey-name">${escHtml(c.name)}</span>
|
||||||
@@ -2751,16 +2765,15 @@ const Vault = (() => {
|
|||||||
<h3 style="margin:0 0 8px;font-size:16px;color:#111827;">
|
<h3 style="margin:0 0 8px;font-size:16px;color:#111827;">
|
||||||
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
|
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
|
||||||
</h3>
|
</h3>
|
||||||
${
|
${isFirstTime
|
||||||
isFirstTime
|
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
||||||
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
|
||||||
These codes let you sign in if you lose access to your authenticator app.
|
These codes let you sign in if you lose access to your authenticator app.
|
||||||
<strong>Save them now — they will not be shown again.</strong>
|
<strong>Save them now — they will not be shown again.</strong>
|
||||||
</p>`
|
</p>`
|
||||||
: `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
: `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
|
||||||
Your previous codes have been invalidated. Save these new codes securely.
|
Your previous codes have been invalidated. Save these new codes securely.
|
||||||
</p>`
|
</p>`
|
||||||
}
|
}
|
||||||
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:12px;display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">
|
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:12px;display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">
|
||||||
${codesHtml}
|
${codesHtml}
|
||||||
</div>
|
</div>
|
||||||
@@ -3269,14 +3282,14 @@ const Vault = (() => {
|
|||||||
const pool = !_activeFilter
|
const pool = !_activeFilter
|
||||||
? _items
|
? _items
|
||||||
: _activeFilter.type === "itemType"
|
: _activeFilter.type === "itemType"
|
||||||
? _items.filter((i) => i.item_type === _activeFilter.value)
|
? _items.filter((i) => i.item_type === _activeFilter.value)
|
||||||
: _activeFilter.type === "folder"
|
: _activeFilter.type === "folder"
|
||||||
? _items.filter((i) => i.folder_id === _activeFilter.value)
|
? _items.filter((i) => i.folder_id === _activeFilter.value)
|
||||||
: _activeFilter.type === "tag"
|
: _activeFilter.type === "tag"
|
||||||
? _items.filter((i) =>
|
? _items.filter((i) =>
|
||||||
(i.plain?.tags || []).includes(_activeFilter.value),
|
(i.plain?.tags || []).includes(_activeFilter.value),
|
||||||
)
|
)
|
||||||
: _items;
|
: _items;
|
||||||
renderItemList(
|
renderItemList(
|
||||||
pool.filter(
|
pool.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -3427,7 +3440,7 @@ const Vault = (() => {
|
|||||||
if (
|
if (
|
||||||
(mode === "add" &&
|
(mode === "add" &&
|
||||||
(document.getElementById("field-type").value || "password") ===
|
(document.getElementById("field-type").value || "password") ===
|
||||||
"password") ||
|
"password") ||
|
||||||
(mode === "edit" && (item?.item_type || "password") === "password")
|
(mode === "edit" && (item?.item_type || "password") === "password")
|
||||||
) {
|
) {
|
||||||
initPasswordFieldEnhancements();
|
initPasswordFieldEnhancements();
|
||||||
@@ -3684,7 +3697,7 @@ const Vault = (() => {
|
|||||||
"X-CSRFToken": csrfToken(),
|
"X-CSRFToken": csrfToken(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
VaultSession.clear();
|
VaultSession.clear();
|
||||||
SharingSession.clear();
|
SharingSession.clear();
|
||||||
sessionStorage.removeItem("access_token");
|
sessionStorage.removeItem("access_token");
|
||||||
@@ -3789,11 +3802,11 @@ const Vault = (() => {
|
|||||||
// Auto-clear clipboard after 30 seconds — industry-standard hygiene.
|
// Auto-clear clipboard after 30 seconds — industry-standard hygiene.
|
||||||
if (_clipboardClearTimer) clearTimeout(_clipboardClearTimer);
|
if (_clipboardClearTimer) clearTimeout(_clipboardClearTimer);
|
||||||
_clipboardClearTimer = setTimeout(() => {
|
_clipboardClearTimer = setTimeout(() => {
|
||||||
navigator.clipboard.writeText("").catch(() => {});
|
navigator.clipboard.writeText("").catch(() => { });
|
||||||
_clipboardClearTimer = null;
|
_clipboardClearTimer = null;
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
function escHtml(str) {
|
function escHtml(str) {
|
||||||
|
|||||||
@@ -932,6 +932,7 @@ body {
|
|||||||
background: #e0f2fe;
|
background: #e0f2fe;
|
||||||
color: #0369a1;
|
color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
|
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
.pk-flyout {
|
.pk-flyout {
|
||||||
@@ -940,7 +941,7 @@ body {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
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;
|
min-width: 150px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -972,9 +973,20 @@ body {
|
|||||||
color: #6b7280;
|
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 ────────────────────────────────────────────── */
|
/* ── Collapsible folder groups ────────────────────────────────────────────── */
|
||||||
|
|
||||||
.pk-group { margin-bottom: 2px; }
|
.pk-group {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-group-header {
|
.pk-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -988,7 +1000,9 @@ body {
|
|||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-group-header:hover { background: #f0f4ff; }
|
.pk-group-header:hover {
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-group-name {
|
.pk-group-name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
+72
-47
@@ -69,10 +69,10 @@ function escHtml(str) {
|
|||||||
// Auto-clear clipboard 30 s after a sensitive copy.
|
// Auto-clear clipboard 30 s after a sensitive copy.
|
||||||
let _clipTimer = null;
|
let _clipTimer = null;
|
||||||
function _copyWithAutoClear(text) {
|
function _copyWithAutoClear(text) {
|
||||||
navigator.clipboard.writeText(text).catch(() => {});
|
navigator.clipboard.writeText(text).catch(() => { });
|
||||||
if (_clipTimer) clearTimeout(_clipTimer);
|
if (_clipTimer) clearTimeout(_clipTimer);
|
||||||
_clipTimer = setTimeout(() => {
|
_clipTimer = setTimeout(() => {
|
||||||
navigator.clipboard.writeText("").catch(() => {});
|
navigator.clipboard.writeText("").catch(() => { });
|
||||||
_clipTimer = null;
|
_clipTimer = null;
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ function siteLabel(item) {
|
|||||||
if (item.plain?.url) {
|
if (item.plain?.url) {
|
||||||
try {
|
try {
|
||||||
return new URL(item.plain.url).hostname.replace(/^www\./, "");
|
return new URL(item.plain.url).hostname.replace(/^www\./, "");
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
return item.name;
|
return item.name;
|
||||||
}
|
}
|
||||||
@@ -441,7 +441,7 @@ async function completeLogin(data, masterPassword) {
|
|||||||
refresh_token: data.refresh_token,
|
refresh_token: data.refresh_token,
|
||||||
enc_key_salt: data.enc_key_salt,
|
enc_key_salt: data.enc_key_salt,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
|
|
||||||
showView("vault");
|
showView("vault");
|
||||||
await checkPendingSave();
|
await checkPendingSave();
|
||||||
@@ -502,9 +502,9 @@ async function signOut() {
|
|||||||
Authorization: `Bearer ${access_token}`,
|
Authorization: `Bearer ${access_token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ refresh_token }),
|
body: JSON.stringify({ refresh_token }),
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.local.remove(["refresh_token", "enc_key_salt"]);
|
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.
|
// 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",
|
type: "VAULT_UPDATED",
|
||||||
vault_items: itemsForContentScript,
|
vault_items: itemsForContentScript,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
|
|
||||||
// Run health checks in the background — sync metrics first, then HIBP.
|
// Run health checks in the background — sync metrics first, then HIBP.
|
||||||
// Results are sent to the background SW via HEALTH_UPDATE so the toolbar
|
// Results are sent to the background SW via HEALTH_UPDATE so the toolbar
|
||||||
@@ -645,7 +645,7 @@ async function _runPopupHealthCheck() {
|
|||||||
if (!pwItems.length) {
|
if (!pwItems.length) {
|
||||||
chrome.runtime
|
chrome.runtime
|
||||||
.sendMessage({ type: "HEALTH_UPDATE", breached: 0, weak: 0, reused: 0 })
|
.sendMessage({ type: "HEALTH_UPDATE", breached: 0, weak: 0, reused: 0 })
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +676,7 @@ async function _runPopupHealthCheck() {
|
|||||||
weak: weak.length,
|
weak: weak.length,
|
||||||
reused: reused.length,
|
reused: reused.length,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
|
|
||||||
// HIBP — k-anonymity, parallel.
|
// HIBP — k-anonymity, parallel.
|
||||||
const hibpResults = await Promise.all(
|
const hibpResults = await Promise.all(
|
||||||
@@ -695,7 +695,7 @@ async function _runPopupHealthCheck() {
|
|||||||
weak: weak.length,
|
weak: weak.length,
|
||||||
reused: reused.length,
|
reused: reused.length,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[PassKeeper] Popup health check failed:", err);
|
console.error("[PassKeeper] Popup health check failed:", err);
|
||||||
}
|
}
|
||||||
@@ -1053,7 +1053,7 @@ function renderList() {
|
|||||||
password: item.plain.password || "",
|
password: item.plain.password || "",
|
||||||
autologin: !!item.plain.autologin,
|
autologin: !!item.plain.autologin,
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
window.close();
|
window.close();
|
||||||
}),
|
}),
|
||||||
@@ -1093,44 +1093,64 @@ function renderList() {
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
item.plain?.url
|
item.plain?.url
|
||||||
? {
|
? {
|
||||||
label: "Open URL",
|
label: "Open URL",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
chrome.tabs.create({ url: item.plain.url });
|
chrome.tabs.create({ url: item.plain.url });
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
item.plain?.username
|
item.plain?.username
|
||||||
? {
|
? {
|
||||||
label: "Copy username",
|
label: "Copy username",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (item.plain?.reprompt) {
|
if (item.plain?.reprompt) {
|
||||||
const ok = await _repromptMasterPassword();
|
const ok = await _repromptMasterPassword();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
_copyWithAutoClear(item.plain.username);
|
_copyWithAutoClear(item.plain.username);
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
item.plain?.password
|
item.plain?.password
|
||||||
? {
|
? {
|
||||||
label: "Copy password",
|
label: "Copy password",
|
||||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (item.plain?.reprompt) {
|
if (item.plain?.reprompt) {
|
||||||
const ok = await _repromptMasterPassword();
|
const ok = await _repromptMasterPassword();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
_copyWithAutoClear(item.plain.password);
|
_copyWithAutoClear(item.plain.password);
|
||||||
flyout.remove();
|
flyout.remove();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean);
|
].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) => {
|
menuItems.forEach((mi) => {
|
||||||
const row = document.createElement("button");
|
const row = document.createElement("button");
|
||||||
row.className = "pk-flyout-item";
|
row.className = "pk-flyout-item";
|
||||||
@@ -1142,6 +1162,11 @@ function renderList() {
|
|||||||
flyout.appendChild(row);
|
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.
|
// Position flyout using fixed coords so it escapes vault-list overflow clipping.
|
||||||
const appEl = document.getElementById("app");
|
const appEl = document.getElementById("app");
|
||||||
appEl.appendChild(flyout);
|
appEl.appendChild(flyout);
|
||||||
@@ -1246,7 +1271,7 @@ async function checkPendingSave() {
|
|||||||
function _clearPendingSave() {
|
function _clearPendingSave() {
|
||||||
$("save-prompt-overlay").classList.add("hidden");
|
$("save-prompt-overlay").classList.add("hidden");
|
||||||
chrome.storage.local.remove("pending_save");
|
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 () => {
|
$("btn-save-yes").onclick = async () => {
|
||||||
@@ -1484,7 +1509,7 @@ async function initAddView() {
|
|||||||
: new URL(tab.url).hostname.replace(/^www\./, "");
|
: new URL(tab.url).hostname.replace(/^www\./, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
// Load folders into the add-folder select.
|
// Load folders into the add-folder select.
|
||||||
const addFolderSel = $("add-folder");
|
const addFolderSel = $("add-folder");
|
||||||
@@ -1500,7 +1525,7 @@ async function initAddView() {
|
|||||||
addFolderSel.appendChild(opt);
|
addFolderSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
// Wire one-time event listeners.
|
// Wire one-time event listeners.
|
||||||
if (!_addViewInitialised) {
|
if (!_addViewInitialised) {
|
||||||
@@ -1635,7 +1660,7 @@ async function initAccountView() {
|
|||||||
// Tell background to re-apply the new interval.
|
// Tell background to re-apply the new interval.
|
||||||
chrome.runtime
|
chrome.runtime
|
||||||
.sendMessage({ type: "SET_IDLE_TIMEOUT", seconds: val })
|
.sendMessage({ type: "SET_IDLE_TIMEOUT", seconds: val })
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
// Brief "Saved" confirmation.
|
// Brief "Saved" confirmation.
|
||||||
const saved = $("acct-idle-saved");
|
const saved = $("acct-idle-saved");
|
||||||
saved.classList.remove("hidden");
|
saved.classList.remove("hidden");
|
||||||
@@ -1656,7 +1681,7 @@ async function init() {
|
|||||||
currentWindow: true,
|
currentWindow: true,
|
||||||
});
|
});
|
||||||
_currentUrl = tab?.url || "";
|
_currentUrl = tab?.url || "";
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// Set vault link
|
// Set vault link
|
||||||
$("btn-open-vault").href = VAULT_URL;
|
$("btn-open-vault").href = VAULT_URL;
|
||||||
@@ -1672,7 +1697,7 @@ async function init() {
|
|||||||
showView("generator");
|
showView("generator");
|
||||||
initGenerator();
|
initGenerator();
|
||||||
// Still load vault data in background so the Vault tab is ready.
|
// 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.
|
// Wire event listeners below, then return early from vault-specific setup.
|
||||||
} else {
|
} else {
|
||||||
showView("vault");
|
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
|
// 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.
|
// prevents the SW from being killed between popup openings.
|
||||||
const _keepalive = setInterval(() => {
|
const _keepalive = setInterval(() => {
|
||||||
chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => {});
|
chrome.runtime.sendMessage({ type: "KEEPALIVE" }).catch(() => { });
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|||||||
Reference in New Issue
Block a user