Show passkey names and dates, add individual delete
Deploy Dashboard / deploy (push) Successful in 31s

This commit is contained in:
Gan, Jimmy
2026-02-22 12:20:05 +08:00
parent 888d3e5c49
commit ffac6f47bf
2 changed files with 51 additions and 6 deletions
+14 -1
View File
@@ -209,6 +209,8 @@ async def passkey_register_verify(request: Request, current_user: str = Depends(
"credential_id": bytes_to_base64url(verification.credential_id),
"public_key": bytes_to_base64url(verification.credential_public_key),
"sign_count": verification.sign_count,
"name": body.get("name", "Passkey"),
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
})
return {"ok": True}
@@ -260,7 +262,18 @@ async def passkey_login_verify(request: Request):
@router.get("/passkey/list")
async def passkey_list(current_user: str = Depends(auth.get_current_user)):
return {"count": len(auth.load_passkey_credentials())}
creds = auth.load_passkey_credentials()
return {"passkeys": [{"id": c["credential_id"], "name": c.get("name", "Passkey"), "created_at": c.get("created_at", "")} for c in creds]}
@router.post("/passkey/delete")
async def passkey_delete(request: Request, current_user: str = Depends(auth.get_current_user)):
body = await request.json()
cred_id = body.get("id")
data = auth._load_auth_data()
creds = data.get("passkey_credentials", [])
data["passkey_credentials"] = [c for c in creds if c["credential_id"] != cred_id]
auth._save_auth_data(data)
return {"ok": True}
@router.post("/passkey/clear")
async def passkey_clear(current_user: str = Depends(auth.get_current_user)):
+37 -5
View File
@@ -9,7 +9,7 @@
let error = $state("");
let saving = $state(false);
let passkeyCount = $state(0);
let passkeys = $state([]);
let passkeyMsg = $state("");
let passkeyErr = $state("");
let passkeyLoading = $state(false);
@@ -19,7 +19,7 @@
async function loadPasskeys() {
try {
const res = await get("/auth/passkey/list");
passkeyCount = res.count;
passkeys = res.passkeys;
} catch {}
}
@@ -50,10 +50,12 @@
opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
}
const cred = await navigator.credentials.create({ publicKey: opts });
const name = prompt("Name this passkey (e.g. MacBook, iPhone):", "Passkey") || "Passkey";
const body = {
id: cred.id,
rawId: bufferToBase64url(cred.rawId),
type: cred.type,
name,
response: {
attestationObject: bufferToBase64url(cred.response.attestationObject),
clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
@@ -75,13 +77,27 @@
try {
await post("/auth/passkey/clear");
passkeyMsg = "All passkeys removed";
passkeyCount = 0;
passkeys = [];
} catch (e) {
passkeyErr = e.body?.detail || "Failed to remove passkeys";
}
passkeyLoading = false;
}
async function deletePasskey(id, name) {
if (!confirm(`Remove passkey "${name}"?`)) return;
passkeyLoading = true;
passkeyMsg = ""; passkeyErr = "";
try {
await post("/auth/passkey/delete", { id });
passkeyMsg = `Passkey "${name}" removed`;
await loadPasskeys();
} catch (e) {
passkeyErr = e.body?.detail || "Failed to remove passkey";
}
passkeyLoading = false;
}
async function changePassword() {
error = "";
message = "";
@@ -144,13 +160,29 @@
<div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{passkeyErr}</div>
{/if}
<p class="text-sm text-surface-500 mb-4">{passkeyCount} passkey{passkeyCount !== 1 ? 's' : ''} registered</p>
{#if passkeys.length > 0}
<div class="space-y-2 mb-4">
{#each passkeys as pk}
<div class="flex items-center justify-between px-3 py-2 bg-surface-50 rounded-lg">
<div>
<span class="text-sm font-medium text-surface-700">{pk.name}</span>
{#if pk.created_at}
<span class="text-xs text-surface-400 ml-2">{new Date(pk.created_at).toLocaleDateString()}</span>
{/if}
</div>
<button onclick={() => deletePasskey(pk.id, pk.name)} disabled={passkeyLoading} class="text-xs text-rose-500 hover:text-rose-700 disabled:opacity-50">Remove</button>
</div>
{/each}
</div>
{:else}
<p class="text-sm text-surface-400 mb-4">No passkeys registered</p>
{/if}
<div class="flex gap-2">
<button onclick={registerPasskey} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors disabled:opacity-50">
Register new passkey
</button>
{#if passkeyCount > 0}
{#if passkeys.length > 1}
<button onclick={clearPasskeys} disabled={passkeyLoading} class="px-4 py-2 text-sm font-medium text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-colors disabled:opacity-50">
Remove all
</button>