Show passkey names and dates, add individual delete
Deploy Dashboard / deploy (push) Successful in 31s
Deploy Dashboard / deploy (push) Successful in 31s
This commit is contained in:
@@ -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)):
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user