/** * Security Settings page - Passkey management * * Only available when using passkey auth. When external auth (e.g., Cloudflare Access) * is configured, this page shows an informational message instead. */ import { Button } from "@cloudflare/kumo"; import { useLingui } from "@lingui/react/macro"; import { Shield, Plus, CheckCircle, WarningCircle, Info } from "@phosphor-icons/react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import * as React from "react"; import { fetchPasskeys, renamePasskey, deletePasskey, fetchManifest } from "../../lib/api"; import { PasskeyRegistration } from "../auth/PasskeyRegistration"; import { BackToSettingsLink } from "./BackToSettingsLink.js"; import { PasskeyList } from "./PasskeyList"; export function SecuritySettings() { const { t } = useLingui(); const queryClient = useQueryClient(); const [isAdding, setIsAdding] = React.useState(false); const [saveStatus, setSaveStatus] = React.useState<{ type: "success" | "error"; message: string; } | null>(null); // Fetch manifest for auth mode const { data: manifest, isLoading: manifestLoading } = useQuery({ queryKey: ["manifest"], queryFn: fetchManifest, }); const isExternalAuth = manifest?.authMode && manifest.authMode !== "passkey"; // Fetch passkeys (only when using passkey auth) const { data: passkeys, isLoading, error, } = useQuery({ queryKey: ["passkeys"], queryFn: fetchPasskeys, enabled: !isExternalAuth && !manifestLoading, }); // Clear status message after 3 seconds React.useEffect(() => { if (saveStatus) { const timer = setTimeout(setSaveStatus, 3000, null); return () => clearTimeout(timer); } }, [saveStatus]); // Rename mutation const renameMutation = useMutation({ mutationFn: ({ id, name }: { id: string; name: string }) => renamePasskey(id, name), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["passkeys"] }); setSaveStatus({ type: "success", message: t`Passkey renamed` }); }, onError: (mutationError) => { setSaveStatus({ type: "error", message: mutationError instanceof Error ? mutationError.message : t`Failed to rename passkey`, }); }, }); // Delete mutation const deleteMutation = useMutation({ mutationFn: (id: string) => deletePasskey(id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["passkeys"] }); setSaveStatus({ type: "success", message: t`Passkey removed` }); }, onError: (mutationError) => { setSaveStatus({ type: "error", message: mutationError instanceof Error ? mutationError.message : t`Failed to remove passkey`, }); }, }); const handleRename = async (id: string, name: string) => { await renameMutation.mutateAsync({ id, name }); }; const handleDelete = async (id: string) => { await deleteMutation.mutateAsync(id); }; const handleAddSuccess = () => { void queryClient.invalidateQueries({ queryKey: ["passkeys"] }); setIsAdding(false); setSaveStatus({ type: "success", message: t`Passkey added successfully` }); }; const settingsHeader = (

{t`Security Settings`}

); if (manifestLoading || isLoading) { return (
{settingsHeader}

{t`Loading...`}

); } // Show message when external auth is configured if (isExternalAuth) { return (
{settingsHeader}

{t`Authentication is managed by an external provider (${manifest?.authMode}). Passkey settings are not available when using external authentication.`}

); } if (error) { return (
{settingsHeader}

{error instanceof Error ? error.message : t`Failed to load passkeys`}

); } return (
{settingsHeader} {/* Status message */} {saveStatus && (
{saveStatus.type === "success" ? ( ) : ( )} {saveStatus.message}
)} {/* Passkeys Section */}

{t`Passkeys`}

{t`Passkeys are a secure, passwordless way to sign in to your account. You can register multiple passkeys for different devices.`}

{/* Passkey list */} {passkeys && passkeys.length > 0 ? ( ) : (
{t`No passkeys registered yet.`}
)} {/* Add passkey section */}
{isAdding ? (

{t`Add a new passkey`}

setSaveStatus({ type: "error", message: registrationError.message, }) } showNameInput buttonText={t`Register Passkey`} />
) : ( )}
); } export default SecuritySettings;