diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py index 0bd82e5..69423b9 100644 --- a/dashboard/backend/routers/auth.py +++ b/dashboard/backend/routers/auth.py @@ -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)): diff --git a/dashboard/frontend/src/routes/Settings.svelte b/dashboard/frontend/src/routes/Settings.svelte index 8af41db..3b090c4 100644 --- a/dashboard/frontend/src/routes/Settings.svelte +++ b/dashboard/frontend/src/routes/Settings.svelte @@ -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 @@
{passkeyCount} passkey{passkeyCount !== 1 ? 's' : ''} registered
+ {#if passkeys.length > 0} +No passkeys registered
+ {/if}