05/18 Enhanced codes and functionalities 5
This commit is contained in:
+132
-4
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user