05/18 Enhanced codes and functionalities 5

This commit is contained in:
2026-05-18 18:27:24 -04:00
parent 09d3bbdc15
commit 46765b9448
11 changed files with 1042 additions and 4 deletions
+132 -4
View File
@@ -1753,7 +1753,9 @@ const Vault = (() => {
granteePubKey,
);
// Re-encrypt all decrypted vault items
// Re-encrypt all decrypted vault items.
// Item name is encrypted with the same ECDH shared key so the server
// never sees plaintext names inside enc_vault (zero-knowledge).
const encItems = await Promise.all(
_items
.filter((i) => i.plain)
@@ -1762,12 +1764,17 @@ const Vault = (() => {
sharedKey,
i.plain,
);
const { enc_name, iv_name } = await SharingCrypto.encryptName(
sharedKey,
i.name,
);
return {
id: i.id,
name: i.name,
item_type: i.item_type,
enc_data,
iv,
enc_name,
iv_name,
};
}),
);
@@ -1889,9 +1896,20 @@ const Vault = (() => {
i.enc_data,
i.iv,
);
return { ...i, plain };
// Decrypt the item display name if available (new format).
// Fall back to item_type label for legacy enc_vault snapshots.
let displayName = i.item_type || "password";
if (i.enc_name && i.iv_name) {
const decName = await SharingCrypto.decryptName(
sharedKey,
i.enc_name,
i.iv_name,
);
if (decName) displayName = decName;
}
return { ...i, name: displayName, plain };
} catch {
return { ...i, plain: null };
return { ...i, name: i.item_type || "password", plain: null };
}
}),
);
@@ -2211,6 +2229,7 @@ const Vault = (() => {
loadSharingKeysStatus(),
loadRecoveryStatus(),
loadAuditLog(0, true),
loadPasskeys(),
]);
}
@@ -2229,6 +2248,115 @@ const Vault = (() => {
let _auditOffset = 0;
let _auditTotal = 0;
// ── Passkey (WebAuthn) management ────────────────────────────────────────────
async function loadPasskeys() {
const listEl = document.getElementById("passkeys-list");
const errEl = document.getElementById("passkeys-error");
if (!listEl) return;
errEl?.classList.add("hidden");
// Hide the section if WebAuthn is not supported in this browser.
if (!window.PublicKeyCredential) {
document.getElementById("passkeys-section")?.classList.add("hidden");
return;
}
try {
const res = await apiFetch("/api/webauthn/credentials");
const creds = await res.json();
if (!Array.isArray(creds) || !creds.length) {
listEl.innerHTML = '<p class="settings-desc">No passkeys registered yet.</p>';
} else {
listEl.innerHTML = creds
.map((c) => {
const created = c.created_at
? new Date(c.created_at).toLocaleDateString()
: "";
const lastUsed = c.last_used_at
? `Last used ${new Date(c.last_used_at).toLocaleDateString()}`
: "Never used";
return `<div class="passkey-item" data-cred-id="${c.id}">
<div class="passkey-info">
<span class="passkey-name">${escHtml(c.name)}</span>
<span class="passkey-meta">${lastUsed} · Added ${escHtml(created)}</span>
</div>
<div class="passkey-actions">
<button class="btn-text btn-sm btn-rename-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Rename</button>
<button class="btn-danger-text btn-sm btn-delete-passkey" data-id="${c.id}" data-name="${escHtml(c.name)}">Remove</button>
</div>
</div>`;
})
.join("");
// Wire rename buttons.
listEl.querySelectorAll(".btn-rename-passkey").forEach((btn) => {
btn.addEventListener("click", async () => {
const newName = prompt("New passkey name:", btn.dataset.name);
if (!newName?.trim()) return;
try {
await apiFetch(`/api/webauthn/credentials/${btn.dataset.id}`, {
method: "PATCH",
body: JSON.stringify({ name: newName.trim() }),
});
await loadPasskeys();
} catch (e) {
showToast("Failed to rename passkey", "error");
}
});
});
// Wire delete buttons.
listEl.querySelectorAll(".btn-delete-passkey").forEach((btn) => {
btn.addEventListener("click", async () => {
if (!confirm(`Remove passkey "${btn.dataset.name}"?`)) return;
try {
await apiFetch(`/api/webauthn/credentials/${btn.dataset.id}`, {
method: "DELETE",
});
showToast("Passkey removed");
await loadPasskeys();
} catch (e) {
showToast("Failed to remove passkey", "error");
}
});
});
}
} catch (e) {
listEl.innerHTML = '<p class="settings-desc">Could not load passkeys.</p>';
}
// Wire register button (only bind once).
const registerBtn = document.getElementById("btn-register-passkey");
if (registerBtn && !registerBtn.dataset.bound) {
registerBtn.dataset.bound = "1";
registerBtn.addEventListener("click", async () => {
const nameInput = document.getElementById("passkey-name-input");
const name = (nameInput?.value || "").trim() || "Passkey";
const errEl = document.getElementById("passkeys-error");
errEl?.classList.add("hidden");
registerBtn.disabled = true;
registerBtn.textContent = "Waiting…";
const result = await PasskeyAuth.registerPasskey(name);
registerBtn.disabled = false;
registerBtn.textContent = "+ Add passkey";
if (result.error) {
if (errEl) {
errEl.textContent = result.error;
errEl.classList.remove("hidden");
}
return;
}
if (nameInput) nameInput.value = "";
showToast(`Passkey "${escHtml(result.credential.name)}" registered`);
await loadPasskeys();
});
}
}
async function loadAuditLog(offset = 0, reset = false) {
try {
const res = await apiFetch(