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),
|
"credential_id": bytes_to_base64url(verification.credential_id),
|
||||||
"public_key": bytes_to_base64url(verification.credential_public_key),
|
"public_key": bytes_to_base64url(verification.credential_public_key),
|
||||||
"sign_count": verification.sign_count,
|
"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}
|
return {"ok": True}
|
||||||
|
|
||||||
@@ -260,7 +262,18 @@ async def passkey_login_verify(request: Request):
|
|||||||
|
|
||||||
@router.get("/passkey/list")
|
@router.get("/passkey/list")
|
||||||
async def passkey_list(current_user: str = Depends(auth.get_current_user)):
|
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")
|
@router.post("/passkey/clear")
|
||||||
async def passkey_clear(current_user: str = Depends(auth.get_current_user)):
|
async def passkey_clear(current_user: str = Depends(auth.get_current_user)):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
let error = $state("");
|
let error = $state("");
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
|
||||||
let passkeyCount = $state(0);
|
let passkeys = $state([]);
|
||||||
let passkeyMsg = $state("");
|
let passkeyMsg = $state("");
|
||||||
let passkeyErr = $state("");
|
let passkeyErr = $state("");
|
||||||
let passkeyLoading = $state(false);
|
let passkeyLoading = $state(false);
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
async function loadPasskeys() {
|
async function loadPasskeys() {
|
||||||
try {
|
try {
|
||||||
const res = await get("/auth/passkey/list");
|
const res = await get("/auth/passkey/list");
|
||||||
passkeyCount = res.count;
|
passkeys = res.passkeys;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +50,12 @@
|
|||||||
opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
|
opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: base64urlToBuffer(c.id) }));
|
||||||
}
|
}
|
||||||
const cred = await navigator.credentials.create({ publicKey: opts });
|
const cred = await navigator.credentials.create({ publicKey: opts });
|
||||||
|
const name = prompt("Name this passkey (e.g. MacBook, iPhone):", "Passkey") || "Passkey";
|
||||||
const body = {
|
const body = {
|
||||||
id: cred.id,
|
id: cred.id,
|
||||||
rawId: bufferToBase64url(cred.rawId),
|
rawId: bufferToBase64url(cred.rawId),
|
||||||
type: cred.type,
|
type: cred.type,
|
||||||
|
name,
|
||||||
response: {
|
response: {
|
||||||
attestationObject: bufferToBase64url(cred.response.attestationObject),
|
attestationObject: bufferToBase64url(cred.response.attestationObject),
|
||||||
clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
|
clientDataJSON: bufferToBase64url(cred.response.clientDataJSON),
|
||||||
@@ -75,13 +77,27 @@
|
|||||||
try {
|
try {
|
||||||
await post("/auth/passkey/clear");
|
await post("/auth/passkey/clear");
|
||||||
passkeyMsg = "All passkeys removed";
|
passkeyMsg = "All passkeys removed";
|
||||||
passkeyCount = 0;
|
passkeys = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
passkeyErr = e.body?.detail || "Failed to remove passkeys";
|
passkeyErr = e.body?.detail || "Failed to remove passkeys";
|
||||||
}
|
}
|
||||||
passkeyLoading = false;
|
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() {
|
async function changePassword() {
|
||||||
error = "";
|
error = "";
|
||||||
message = "";
|
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>
|
<div class="text-sm text-rose-600 bg-rose-50 px-3 py-2 rounded-lg mb-4">{passkeyErr}</div>
|
||||||
{/if}
|
{/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">
|
<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">
|
<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
|
Register new passkey
|
||||||
</button>
|
</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">
|
<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
|
Remove all
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user