- Extracted `PasskeyTable` and `WebAuthnScript` into `ui/components/auth/`. - Refactored `PasskeysPage.tsx` and `RegisterPage.tsx` to use the new components instead of inline scripts and HTML. - Deleted the duplicate `ui/public/ui/utils/bip39_wordlist.ts` and `ui/public/ui/utils/bip39.ts`. - Updated all import references to use `ui/utils/bip39_wordlist.ts` and `/public/utils/bip39.ts`. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
189 lines
6.3 KiB
TypeScript
189 lines
6.3 KiB
TypeScript
import { AuthenticatedLayout } from "./AuthenticatedLayout.tsx";
|
|
import { PasskeyTable } from "./auth/PasskeyTable.tsx";
|
|
import { WebAuthnScript } from "./auth/WebAuthnScript.tsx";
|
|
|
|
export const PasskeysPage = ({
|
|
passkeys,
|
|
isAdmin = false,
|
|
}: {
|
|
passkeys: any[];
|
|
isAdmin?: boolean;
|
|
}) => {
|
|
return (
|
|
<AuthenticatedLayout
|
|
title="Passkeys"
|
|
currentPath="/dashboard/passkeys"
|
|
isAdmin={isAdmin}
|
|
>
|
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
|
<div>
|
|
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
|
|
Registered Passkeys
|
|
</h1>
|
|
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
|
|
Manage FIDO2 WebAuthn credentials and biometric authenticators.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
id="addPasskeyBtn"
|
|
class="btn-primary"
|
|
style="min-height: 44px; box-shadow: var(--shadow-sm);"
|
|
>
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
</svg>
|
|
<span>Enroll New Passkey</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Add Passkey Drawer / Container */}
|
|
<div
|
|
id="addPasskeyContainer"
|
|
class="card"
|
|
style="display: none; border-color: var(--primary); background: var(--primary-light); margin-bottom: 1.5rem;"
|
|
>
|
|
<h3 style="margin: 0 0 0.5rem 0; color: var(--primary);">
|
|
Enroll New Passkey
|
|
</h3>
|
|
<p style="font-size: 0.9rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
|
|
Insert a hardware security key (YubiKey) or follow your device's
|
|
biometric prompt.
|
|
</p>
|
|
|
|
<div
|
|
id="addPasskeyStatus"
|
|
style="margin-bottom: 1rem; font-size: 0.9rem; font-weight: 600;"
|
|
>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.75rem;">
|
|
<button
|
|
type="button"
|
|
id="confirmAddPasskeyBtn"
|
|
class="btn-primary"
|
|
>
|
|
Start Device Prompt
|
|
</button>
|
|
<button
|
|
type="button"
|
|
id="cancelAddPasskeyBtn"
|
|
class="btn-outline"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<PasskeyTable passkeys={passkeys} />
|
|
|
|
{/* Emergency Recovery Backup Section */}
|
|
<div class="card" style="border-left: 4px solid var(--warning);">
|
|
<div style="display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 1rem; margin-bottom: 0.75rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.65rem;">
|
|
<div style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; background: var(--warning-bg); color: var(--warning-text); border-radius: var(--radius-md);">
|
|
<svg
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 style="margin: 0; font-size: 1.1rem; color: var(--text-primary);">
|
|
Zero-Trust Recovery Backup
|
|
</h3>
|
|
<p style="margin: 0; font-size: 0.85rem; color: var(--text-secondary);">
|
|
2-of-3 Shamir's Secret Sharing Matrix & Cold 12-Word Voucher
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<span class="badge badge-warning">Active Protection</span>
|
|
</div>
|
|
|
|
<p style="color: var(--text-secondary); font-size: 0.875rem; line-height: 1.5; margin: 0 0 1.25rem 0;">
|
|
If you lose your hardware keys or mobile device, your 12-word recovery
|
|
voucher combined with your secret PIN allows you to re-enroll a fresh
|
|
passkey without admin assistance.
|
|
</p>
|
|
|
|
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
|
<button
|
|
type="button"
|
|
id="regenVoucherBtn"
|
|
class="btn-outline"
|
|
style="font-size: 0.85rem;"
|
|
>
|
|
📋 Generate Fresh 12-Word Recovery Voucher
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
id="voucherModal"
|
|
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-muted); border-radius: var(--radius-md); border: 1px solid var(--border-subtle);"
|
|
>
|
|
<div style="font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem;">
|
|
New 12-Word Recovery Voucher:
|
|
</div>
|
|
<div
|
|
id="generatedWordGrid"
|
|
style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 1rem;"
|
|
>
|
|
{/* Populated dynamically */}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
id="copyVoucherBtn"
|
|
class="btn-primary"
|
|
style="width: 100%;"
|
|
>
|
|
Copy All 12 Words
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
{`
|
|
@media (min-width: 768px) {
|
|
.desktop-only { display: block !important; }
|
|
.mobile-only { display: none !important; }
|
|
}
|
|
@media (max-width: 767px) {
|
|
.desktop-only { display: none !important; }
|
|
.mobile-only { display: flex !important; }
|
|
}
|
|
.word-cell {
|
|
background: var(--surface-card);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: var(--radius-sm);
|
|
padding: 0.35rem 0.65rem;
|
|
font-size: 0.85rem;
|
|
font-family: monospace;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
`}
|
|
</style>
|
|
|
|
<WebAuthnScript isPasskeys />
|
|
</AuthenticatedLayout>
|
|
);
|
|
};
|