05/18 Enhanced codes and functionalities 7
This commit is contained in:
@@ -2463,3 +2463,90 @@ html.sidebar-open {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* ── Vault health notifications ─────────────────────────────────────────────── */
|
||||
|
||||
/* Sidebar badge — shows issue count on Security link */
|
||||
.sidebar-badge {
|
||||
margin-left: auto;
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 2px 5px;
|
||||
border-radius: 10px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-badge.badge-warn {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
/* Health banner — dismissible strip above the vault list */
|
||||
.health-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.health-banner.banner-danger {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.health-banner.banner-warn {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.health-banner.banner-ok {
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.health-banner-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.health-banner-text {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.health-banner-link {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.health-banner-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.health-banner-dismiss:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
+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);
|
||||
@@ -119,6 +119,7 @@
|
||||
>
|
||||
<span class="sidebar-icon">🛡️</span>
|
||||
<span class="sidebar-label">Security</span>
|
||||
<span id="security-badge" class="sidebar-badge hidden"></span>
|
||||
</li>
|
||||
<li
|
||||
class="sidebar-item"
|
||||
@@ -208,6 +209,9 @@
|
||||
|
||||
<!-- ── Main ───────────────────────────────────────────────────── -->
|
||||
<main class="vault-main">
|
||||
<!-- Health notification banner — shown when background checks find issues -->
|
||||
<div id="health-banner" class="health-banner hidden" role="alert"></div>
|
||||
|
||||
<!-- Vault view -->
|
||||
<div id="view-vault">
|
||||
<header class="vault-header">
|
||||
@@ -1238,4 +1242,4 @@
|
||||
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/sharing.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/vault.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user