63 lines
3.1 KiB
TypeScript
63 lines
3.1 KiB
TypeScript
import { Form, Link, useActionData, useLoaderData, useNavigation } from "react-router";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ADMIN_MFA_ENROLLMENT_PATH } from "~/utils/auth/mfa/paths";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import type { ActionData, LoaderData } from "~/routeModules/admin/mfa-challenge.impl";
|
|
|
|
export default function AdminMfaChallengeRoute() {
|
|
const { t } = useTranslation();
|
|
const data = useLoaderData<LoaderData>();
|
|
const actionData = useActionData<ActionData>();
|
|
const navigation = useNavigation();
|
|
const rootData = useRootData();
|
|
const isSubmitting = navigation.state === "submitting";
|
|
|
|
return (
|
|
<main className="mx-auto flex min-h-screen max-w-md flex-col justify-center px-6 py-12">
|
|
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
<h1 className="text-xl font-semibold text-gray-900">{t("admin.mfaChallenge.title")}</h1>
|
|
<p className="mt-2 text-sm text-gray-600">{t("admin.mfaChallenge.description")}</p>
|
|
{actionData?.error ? <p className="mt-4 rounded-md bg-red-50 p-3 text-sm text-red-700">{actionData.error}</p> : null}
|
|
<Form method="post" className="mt-6 space-y-4">
|
|
{rootData.csrf && <input type="hidden" name="csrf" value={rootData.csrf} />}
|
|
<input type="hidden" name="redirectTo" value={actionData?.redirectTo ?? data.redirectTo} />
|
|
<div>
|
|
<label htmlFor="code" className="block text-sm font-medium text-gray-700">
|
|
{t("admin.mfaChallenge.fields.authenticatorCode")}
|
|
</label>
|
|
<input
|
|
id="code"
|
|
name="code"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
pattern="[0-9]{6}"
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-gray-900 shadow-sm focus:border-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-900"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="recoveryCode" className="block text-sm font-medium text-gray-700">
|
|
{t("admin.mfaChallenge.fields.recoveryCode")}
|
|
</label>
|
|
<input
|
|
id="recoveryCode"
|
|
name="recoveryCode"
|
|
autoComplete="off"
|
|
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 uppercase text-gray-900 shadow-sm focus:border-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-900"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting || !data.totpEnabled}
|
|
className="w-full rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
{isSubmitting ? t("admin.mfaChallenge.actions.verifying") : t("admin.mfaChallenge.actions.verifyAndContinue")}
|
|
</button>
|
|
</Form>
|
|
<Link to={data.enrollmentPath || ADMIN_MFA_ENROLLMENT_PATH} className="mt-4 inline-block text-sm text-gray-600 hover:text-gray-900">
|
|
{t("admin.mfaChallenge.actions.manageSettings")}
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|