05/18 Enhanced codes and functionalities 7

This commit is contained in:
2026-05-18 19:16:45 -04:00
parent c9808edde2
commit b9e8f5c357
5 changed files with 354 additions and 14 deletions
+87
View File
@@ -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
View File
@@ -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);
+5 -1
View File
@@ -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 %}