import { useState, useEffect, useCallback } from "react"; import { data, useFetcher } from "react-router"; import { useTranslation } from "react-i18next"; import type { Route } from "./+types/settings.security"; import { ClientDate } from "~/components/ClientDate"; import { loadSecuritySettings } from "./settings.security.server"; export function meta() { return [{ title: "Security — Settings — trails.cool" }]; } export async function loader({ request }: Route.LoaderArgs) { return data(await loadSecuritySettings(request)); } function transportLabel(transports: string[] | null, t: (key: string) => string): string { if (!transports || transports.length === 0) return t("settings.security.passkey"); if (transports.includes("internal")) return t("settings.security.thisDevice"); if (transports.includes("usb")) return t("settings.security.securityKey"); if (transports.includes("ble")) return t("settings.security.bluetooth"); if (transports.includes("hybrid")) return t("settings.security.phoneOrTablet"); return t("settings.security.passkey"); } export default function SecuritySettings({ loaderData }: Route.ComponentProps) { const { userId, passkeys } = loaderData; const { t } = useTranslation(["journal", "common"]); const deleteFetcher = useFetcher(); const [supportsPasskey, setSupportsPasskey] = useState(null); const [addingPasskey, setAddingPasskey] = useState(false); const [passkeyError, setPasskeyError] = useState(null); useEffect(() => { import("@simplewebauthn/browser").then(({ browserSupportsWebAuthn }) => { setSupportsPasskey(browserSupportsWebAuthn()); }); }, []); const handleAddPasskey = useCallback(async () => { setAddingPasskey(true); setPasskeyError(null); try { const startResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "add-passkey", userId }), }); const startData = await startResp.json(); if (startData.error) { setPasskeyError(startData.error); return; } const { startRegistration } = await import("@simplewebauthn/browser"); const webAuthnResp = await startRegistration(startData.options); const finishResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "finish-add-passkey", userId, response: webAuthnResp, challenge: startData.options.challenge, }), }); const finishData = await finishResp.json(); if (finishData.error) { setPasskeyError(finishData.error); } else { window.location.reload(); } } catch (err) { setPasskeyError((err as Error).message); } finally { setAddingPasskey(false); } }, [userId]); return (

{t("settings.security.title")}

{t("settings.security.description")}

{passkeys.map((passkey) => (

{transportLabel(passkey.transports, t)}

{t("settings.security.addedOn", { date: "" })}

{ const isLast = passkeys.length === 1; const msg = isLast ? t("settings.security.deleteLastWarning") : t("settings.security.deleteConfirm"); if (!confirm(msg)) e.preventDefault(); }} >
))} {passkeys.length === 0 && (

{t("settings.security.noPasskeys")}

)}
{passkeyError && (

{passkeyError}

)} {supportsPasskey === true && ( )} {supportsPasskey === false && (

{t("auth.passkeyNotSupported")}

)}
); }