05/19 Enhance codes 4

This commit is contained in:
2026-05-19 10:35:34 -04:00
parent 3cdcbbe8f7
commit abea19d9dd
4 changed files with 460 additions and 53 deletions
+73 -52
View File
@@ -480,8 +480,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);
});
@@ -549,7 +549,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":
@@ -891,6 +891,11 @@ const Vault = (() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`${selected.length} item${selected.length !== 1 ? "s" : ""} exported`);
// Log the export server-side for the audit trail.
apiFetch("/api/vault/audit-export", {
method: "POST",
body: JSON.stringify({ item_ids: selected.map((i) => i.id) }),
}).catch(() => {}); // best-effort — don't block on this
_exitSelectMode();
}
@@ -1125,9 +1130,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 =
@@ -1277,16 +1282,16 @@ const Vault = (() => {
</div>
<ul class="sec-item-list">
${breached
.map((i) => {
const count =
hibpResults.find((r) => r.item.id === i.id)?.count || 0;
return `<li class="sec-item">
.map((i) => {
const count =
hibpResults.find((r) => r.item.id === i.id)?.count || 0;
return `<li class="sec-item">
<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>
<button class="btn-secondary btn-sm" data-sec-edit="${i.id}">Change</button>
</li>`;
})
.join("")}
})
.join("")}
</ul>`;
hibpSection.querySelectorAll("[data-sec-edit]").forEach((btn) => {
btn.addEventListener("click", () => {
@@ -1848,9 +1853,10 @@ const Vault = (() => {
<span class="share-name">${escHtml(s.item_name)}</span>
<span class="share-meta">From ${escHtml(s.owner_email)}${expiryLabel}</span>
</div>
${!s.accepted
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
: `<button class="btn-secondary btn-sm"
${
!s.accepted
? `<button class="btn-primary btn-sm" data-accept="${s.id}">Accept</button>`
: `<button class="btn-secondary btn-sm"
data-view-share="${s.id}"
data-owner-key="${escHtml(s.owner_public_key || "")}"
data-enc="${escHtml(s.enc_data)}"
@@ -1859,7 +1865,7 @@ const Vault = (() => {
data-iv-name="${escHtml(s.iv_name || "")}"
data-name="${escHtml(s.item_name)}"
data-type="${escHtml(s.item_type || "")}">View</button>`
}
}
</li>`;
},
)
@@ -2401,9 +2407,10 @@ const Vault = (() => {
<span class="em-vault-chevron">▸</span>
</button>
<div class="em-vault-item-body hidden" id="em-vault-body-${idx}">
${hasFields
? '<div class="detail-body em-detail-body"></div>'
: '<p class="vault-empty">Could not decrypt this item.</p>'
${
hasFields
? '<div class="detail-body em-detail-body"></div>'
: '<p class="vault-empty">Could not decrypt this item.</p>'
}
</div>
</li>`;
@@ -2732,8 +2739,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 `<div class="passkey-item" data-cred-id="${c.id}">
<div class="passkey-info">
<span class="passkey-name">${escHtml(c.name)}</span>
@@ -2996,15 +3003,16 @@ const Vault = (() => {
<h3 style="margin:0 0 8px;font-size:16px;color:#111827;">
${isFirstTime ? "🔐 MFA enabled — save your backup codes" : "🔐 New backup codes"}
</h3>
${isFirstTime
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
${
isFirstTime
? `<p style="font-size:13px;color:#374151;margin-bottom:12px;">
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>
</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.
</p>`
}
}
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:12px;display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;">
${codesHtml}
</div>
@@ -3513,14 +3521,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) => {
const p = item.plain || {};
@@ -3682,36 +3690,49 @@ const Vault = (() => {
const history = item.plain?.password_history || [];
if (history.length) {
historyPanel.classList.remove("hidden");
// Store plaintext passwords in a WeakMap keyed by button element so
// they never appear in the DOM (no data-pw attributes to scrape).
const _historyPasswords = new WeakMap();
historyPanel.innerHTML =
`<div class="pw-history-title">🕐 Previous passwords (${history.length})</div>` +
history.map((h) => {
history.map((h, idx) => {
const date = h.changed_at
? new Date(h.changed_at).toLocaleDateString()
: "Unknown date";
return `<div class="pw-history-row">
return `<div class="pw-history-row" data-hist-idx="${idx}">
<span class="pw-history-masked">••••••••</span>
<span class="pw-history-date">${escHtml(date)}</span>
<button class="btn-text btn-sm pw-history-reveal" data-pw="${escHtml(h.password || "")}" title="Reveal">👁</button>
<button class="btn-text btn-sm pw-history-restore" data-pw="${escHtml(h.password || "")}" title="Restore this password">↩ Restore</button>
<button class="btn-text btn-sm pw-history-reveal" title="Reveal">👁</button>
<button class="btn-text btn-sm pw-history-restore" title="Restore this password">↩ Restore</button>
</div>`;
}).join("");
historyPanel.querySelectorAll(".pw-history-reveal").forEach((btn) => {
btn.addEventListener("click", () => {
const masked = btn.closest(".pw-history-row").querySelector(".pw-history-masked");
// Register passwords in the WeakMap after DOM elements exist.
historyPanel.querySelectorAll(".pw-history-row").forEach((row) => {
const idx = parseInt(row.dataset.histIdx);
const pw = history[idx]?.password || "";
const revealBtn = row.querySelector(".pw-history-reveal");
const restoreBtn = row.querySelector(".pw-history-restore");
_historyPasswords.set(revealBtn, pw);
_historyPasswords.set(restoreBtn, pw);
revealBtn.addEventListener("click", () => {
const masked = row.querySelector(".pw-history-masked");
if (masked.textContent === "••••••••") {
masked.textContent = btn.dataset.pw;
btn.textContent = "🙈";
masked.textContent = _historyPasswords.get(revealBtn);
revealBtn.textContent = "🙈";
} else {
masked.textContent = "••••••••";
btn.textContent = "👁";
revealBtn.textContent = "👁";
}
});
});
historyPanel.querySelectorAll(".pw-history-restore").forEach((btn) => {
btn.addEventListener("click", () => {
restoreBtn.addEventListener("click", () => {
const pwField = document.getElementById("field-password");
if (pwField) {
pwField.value = btn.dataset.pw;
pwField.value = _historyPasswords.get(restoreBtn);
pwField.dispatchEvent(new Event("input"));
showToast("Password restored — click Save to apply");
}
@@ -3729,7 +3750,7 @@ const Vault = (() => {
if (
(mode === "add" &&
(document.getElementById("field-type").value || "password") ===
"password") ||
"password") ||
(mode === "edit" && (item?.item_type || "password") === "password")
) {
initPasswordFieldEnhancements();
@@ -3994,7 +4015,7 @@ const Vault = (() => {
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify({ refresh_token: refreshToken }),
}).catch(() => { });
}).catch(() => {});
VaultSession.clear();
SharingSession.clear();
sessionStorage.removeItem("access_token");
@@ -4099,11 +4120,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) {
@@ -4723,4 +4744,4 @@ const Vault = (() => {
}
})();
document.addEventListener("DOMContentLoaded", Vault.init);
document.addEventListener("DOMContentLoaded", Vault.init);