05/19 Enhance codes 3

This commit is contained in:
2026-05-19 10:07:33 -04:00
parent 976845f96e
commit eaeb5bd64f
+52 -42
View File
@@ -480,8 +480,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);
}); });
@@ -549,7 +549,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":
@@ -634,6 +634,8 @@ const Vault = (() => {
// Wire the bulk-select checkbox. // Wire the bulk-select checkbox.
const checkbox = li.querySelector(".item-checkbox"); const checkbox = li.querySelector(".item-checkbox");
if (checkbox) { if (checkbox) {
// Stop the click from bubbling to the li (which would open the modal).
checkbox.addEventListener("click", (e) => e.stopPropagation());
checkbox.addEventListener("change", () => { checkbox.addEventListener("change", () => {
if (checkbox.checked) { if (checkbox.checked) {
_selectedIds.add(item.id); _selectedIds.add(item.id);
@@ -714,7 +716,18 @@ const Vault = (() => {
confirmDeleteItem(item); confirmDeleteItem(item);
}, },
); );
li.addEventListener("click", () => openModal("edit", item)); li.addEventListener("click", (e) => {
if (_selectMode) {
// Toggle the checkbox when the row itself is clicked (not the checkbox).
const cb = li.querySelector(".item-checkbox");
if (cb && e.target !== cb) {
cb.checked = !cb.checked;
cb.dispatchEvent(new Event("change"));
}
return;
}
openModal("edit", item);
});
return li; return li;
} }
@@ -1112,9 +1125,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 =
@@ -1264,16 +1277,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", () => {
@@ -1835,10 +1848,9 @@ 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)}${expiryLabel}</span> <span class="share-meta">From ${escHtml(s.owner_email)}${expiryLabel}</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}"
data-owner-key="${escHtml(s.owner_public_key || "")}" data-owner-key="${escHtml(s.owner_public_key || "")}"
data-enc="${escHtml(s.enc_data)}" data-enc="${escHtml(s.enc_data)}"
@@ -1847,7 +1859,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>`;
}, },
) )
@@ -2389,10 +2401,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>`;
@@ -2721,8 +2732,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>
@@ -2985,16 +2996,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>
@@ -3503,14 +3513,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((item) => { pool.filter((item) => {
const p = item.plain || {}; const p = item.plain || {};
@@ -3719,7 +3729,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();
@@ -3984,7 +3994,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");
@@ -4089,11 +4099,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) {
@@ -4713,4 +4723,4 @@ const Vault = (() => {
} }
})(); })();
document.addEventListener("DOMContentLoaded", Vault.init); document.addEventListener("DOMContentLoaded", Vault.init);