05/18 Enhanced codes and functionalities 7
This commit is contained in:
+158
-10
@@ -234,6 +234,13 @@ const Vault = (() => {
|
||||
renderFolderList();
|
||||
renderTagList();
|
||||
applyCurrentFilter();
|
||||
// Reset banner dismissed flag so fresh health results are visible.
|
||||
const banner = document.getElementById("health-banner");
|
||||
if (banner) banner.dataset.dismissed = "0";
|
||||
// Run health checks in the background — updates badge + banner without
|
||||
// blocking the vault render. Results are cached so opening the Security
|
||||
// tab doesn't re-run HIBP checks.
|
||||
runBackgroundHealthCheck();
|
||||
} catch (err) {
|
||||
showToast("Failed to load vault: " + err.message, "error");
|
||||
} finally {
|
||||
@@ -678,6 +685,141 @@ const Vault = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background vault health checks ──────────────────────────────────────────
|
||||
//
|
||||
// Runs after every vault load. Computes weak/reused counts synchronously,
|
||||
// then fires HIBP checks in parallel. Updates the Security sidebar badge
|
||||
// and a dismissible top banner without requiring the user to open the
|
||||
// Security tab. Results are cached so re-opening the tab skips re-checking.
|
||||
|
||||
let _healthCache = null; // { weak, reused, breached } — populated after first run
|
||||
let _hibpRunning = false; // prevents concurrent HIBP runs
|
||||
|
||||
async function runBackgroundHealthCheck() {
|
||||
if (_hibpRunning) return;
|
||||
_hibpRunning = true;
|
||||
_healthCache = null;
|
||||
|
||||
try {
|
||||
const pwItems = _items.filter(
|
||||
(i) => i.item_type === "password" && i.plain?.password,
|
||||
);
|
||||
if (!pwItems.length) {
|
||||
_updateHealthUI({ weak: 0, reused: 0, breached: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchronous metrics — instant.
|
||||
const weak = pwItems.filter((i) => {
|
||||
const p = i.plain.password;
|
||||
if (p.length < 10) return true;
|
||||
return (
|
||||
[/[A-Z]/, /[a-z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(p))
|
||||
.length < 2
|
||||
);
|
||||
});
|
||||
|
||||
const passCounts = {};
|
||||
pwItems.forEach((i) => {
|
||||
const p = i.plain.password;
|
||||
(passCounts[p] = passCounts[p] || []).push(i);
|
||||
});
|
||||
const reused = Object.values(passCounts)
|
||||
.filter((a) => a.length > 1)
|
||||
.flat();
|
||||
|
||||
// Update badge immediately with sync results — HIBP will update again.
|
||||
_updateHealthUI({ weak: weak.length, reused: reused.length, breached: null });
|
||||
|
||||
// HIBP — k-anonymity, runs in parallel.
|
||||
const hibpResults = await Promise.all(
|
||||
pwItems.map(async (item) => ({
|
||||
item,
|
||||
count: await checkHibp(item.plain.password),
|
||||
})),
|
||||
);
|
||||
const breachedItems = hibpResults.filter((r) => r.count > 0).map((r) => r.item);
|
||||
|
||||
_healthCache = {
|
||||
weak: weak.length,
|
||||
reused: reused.length,
|
||||
breached: breachedItems.length,
|
||||
breachedItems,
|
||||
hibpResults,
|
||||
};
|
||||
|
||||
_updateHealthUI({
|
||||
weak: weak.length,
|
||||
reused: reused.length,
|
||||
breached: breachedItems.length,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[PassKeeper] Background health check failed:", err);
|
||||
} finally {
|
||||
_hibpRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
function _updateHealthUI({ weak, reused, breached }) {
|
||||
const badge = document.getElementById("security-badge");
|
||||
const banner = document.getElementById("health-banner");
|
||||
if (!badge || !banner) return;
|
||||
|
||||
const knownBreached = breached !== null;
|
||||
const issueCount =
|
||||
(weak || 0) + (reused || 0) + (knownBreached ? (breached || 0) : 0);
|
||||
const hasBreaches = knownBreached && breached > 0;
|
||||
const hasSyncIssues = (weak || 0) + (reused || 0) > 0;
|
||||
|
||||
// ── Sidebar badge ──────────────────────────────────────────────────────
|
||||
if (issueCount > 0) {
|
||||
badge.textContent = issueCount > 99 ? "99+" : String(issueCount);
|
||||
badge.classList.remove("hidden", "badge-warn");
|
||||
if (hasBreaches) {
|
||||
badge.classList.remove("badge-warn"); // red (default)
|
||||
} else {
|
||||
badge.classList.add("badge-warn"); // amber
|
||||
}
|
||||
} else {
|
||||
badge.classList.add("hidden");
|
||||
}
|
||||
|
||||
// ── Banner ─────────────────────────────────────────────────────────────
|
||||
// Don't re-render if user already dismissed it this session.
|
||||
if (banner.dataset.dismissed === "1") return;
|
||||
if (issueCount === 0) {
|
||||
banner.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (hasBreaches)
|
||||
parts.push(`<strong>${breached}</strong> breached password${breached !== 1 ? "s" : ""}`);
|
||||
if ((weak || 0) > 0)
|
||||
parts.push(`<strong>${weak}</strong> weak password${weak !== 1 ? "s" : ""}`);
|
||||
if ((reused || 0) > 0)
|
||||
parts.push(`<strong>${reused}</strong> reused password${reused !== 1 ? "s" : ""}`);
|
||||
|
||||
const severity = hasBreaches ? "danger" : "warn";
|
||||
const icon = hasBreaches ? "🚨" : "⚠️";
|
||||
|
||||
banner.className = `health-banner banner-${severity}`;
|
||||
banner.innerHTML = `
|
||||
<span class="health-banner-icon">${icon}</span>
|
||||
<span class="health-banner-text">${parts.join(" · ")}</span>
|
||||
<button class="health-banner-link" id="btn-health-banner-view">View report</button>
|
||||
<button class="health-banner-dismiss" id="btn-health-banner-dismiss" title="Dismiss">×</button>`;
|
||||
banner.classList.remove("hidden");
|
||||
|
||||
document.getElementById("btn-health-banner-view")?.addEventListener("click", () => {
|
||||
switchView("security");
|
||||
});
|
||||
document.getElementById("btn-health-banner-dismiss")?.addEventListener("click", () => {
|
||||
banner.classList.add("hidden");
|
||||
banner.dataset.dismissed = "1";
|
||||
});
|
||||
}
|
||||
|
||||
async function renderSecurityDashboard() {
|
||||
const summaryEl = document.getElementById("security-summary");
|
||||
const sectionsEl = document.getElementById("security-sections");
|
||||
@@ -828,8 +970,8 @@ const Vault = (() => {
|
||||
);
|
||||
|
||||
// ── HaveIBeenPwned breach check ──────────────────────────────────────────
|
||||
// Run after the synchronous sections are rendered so the UI is immediately
|
||||
// useful. The HIBP API is queried in parallel for all passwords.
|
||||
// Use cached results from the background check when available — avoids
|
||||
// re-querying HIBP every time the user opens the Security tab.
|
||||
const hibpSection = document.createElement("div");
|
||||
hibpSection.className = "sec-section";
|
||||
hibpSection.innerHTML = `
|
||||
@@ -842,13 +984,19 @@ const Vault = (() => {
|
||||
</div>`;
|
||||
sectionsEl.appendChild(hibpSection);
|
||||
|
||||
// Run all HIBP checks in parallel — k-anonymity: only 5-char SHA-1 prefix sent.
|
||||
const hibpResults = await Promise.all(
|
||||
pwItems.map(async (item) => ({
|
||||
item,
|
||||
count: await checkHibp(item.plain.password),
|
||||
})),
|
||||
);
|
||||
// Use cached results if available, otherwise run fresh checks.
|
||||
let hibpResults;
|
||||
if (_healthCache?.hibpResults) {
|
||||
hibpResults = _healthCache.hibpResults;
|
||||
} else {
|
||||
// Run all HIBP checks in parallel — k-anonymity: only 5-char SHA-1 prefix sent.
|
||||
hibpResults = await Promise.all(
|
||||
pwItems.map(async (item) => ({
|
||||
item,
|
||||
count: await checkHibp(item.plain.password),
|
||||
})),
|
||||
);
|
||||
}
|
||||
const breached = hibpResults.filter((r) => r.count > 0).map((r) => r.item);
|
||||
|
||||
if (!breached.length) {
|
||||
@@ -4173,4 +4321,4 @@ const Vault = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||
Reference in New Issue
Block a user