';
}
}
function showRenameForm(passkeyId: number) {
const passkeyItem = document.querySelector(
`[data-passkey-id="${passkeyId}"]`,
);
if (!passkeyItem) return;
const infoDiv = passkeyItem.querySelector(".passkey-info");
const nameDiv = infoDiv?.querySelector(".passkey-name");
if (!nameDiv) return;
const currentName = nameDiv.textContent || "";
// Replace the info div with a rename form
if (infoDiv) {
infoDiv.innerHTML = `
`;
const input = infoDiv.querySelector(".rename-input") as HTMLInputElement;
input.focus();
input.select();
// Save button
infoDiv
.querySelector(".save-rename-btn")
?.addEventListener("click", async () => {
await renamePasskeyHandler(passkeyId, input.value);
});
// Cancel button
infoDiv
.querySelector(".cancel-rename-btn")
?.addEventListener("click", () => {
loadPasskeys();
});
// Enter to save
input.addEventListener("keypress", async (e) => {
if (e.key === "Enter") {
await renamePasskeyHandler(passkeyId, input.value);
}
});
// Escape to cancel
input.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
loadPasskeys();
}
});
}
}
async function renamePasskeyHandler(passkeyId: number, newName: string) {
if (!newName.trim()) {
showToast("Passkey name cannot be empty", "error");
return;
}
try {
const response = await fetch(`/api/passkeys/${passkeyId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: newName }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to rename passkey");
}
showToast("Passkey renamed successfully!", "success");
loadPasskeys();
} catch (error) {
showToast((error as Error).message || "Failed to rename passkey", "error");
}
}
async function deletePasskeyHandler(passkeyId: number) {
if (
!confirm(
"Are you sure you want to delete this passkey? You will no longer be able to use it to sign in.",
)
) {
return;
}
try {
const response = await fetch(`/api/passkeys/${passkeyId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to delete passkey");
}
showToast("Passkey deleted successfully!", "success");
loadPasskeys();
} catch (error) {
showToast((error as Error).message || "Failed to delete passkey", "error");
}
}
// Add passkey button handler
addPasskeyBtn.addEventListener("click", async () => {
addPasskeyBtn.disabled = true;
addPasskeyBtn.textContent = "preparing...";
try {
// Get registration options
const optionsRes = await fetch("/api/passkeys/add/options", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!optionsRes.ok) {
const error = await optionsRes.json();
throw new Error(error.error || "Failed to get passkey options");
}
const options = await optionsRes.json();
addPasskeyBtn.textContent = "create your passkey...";
// Start registration
const regResponse = await startRegistration(options);
addPasskeyBtn.textContent = "verifying...";
// Ask for a name
const name = prompt(
"Give this passkey a name (e.g., 'iPhone', 'Work Laptop'):",
);
// Verify registration
const verifyRes = await fetch("/api/passkeys/add/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
response: regResponse,
challenge: options.challenge,
name: name || undefined,
}),
});
if (!verifyRes.ok) {
const error = await verifyRes.json();
throw new Error(error.error || "Failed to add passkey");
}
showToast("Passkey added successfully!", "success");
loadPasskeys();
} catch (error) {
showToast((error as Error).message || "Failed to add passkey", "error");
} finally {
addPasskeyBtn.disabled = false;
addPasskeyBtn.textContent = "add new passkey";
}
});
checkAuth();