Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/users/components/LoginForm.tsx
T

159 lines
7.4 KiB
TypeScript

import { Form, Link, useNavigate, useNavigation, useSearchParams } from "react-router";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import LoadingButton from "~/components/ui/buttons/LoadingButton";
import ExclamationTriangleIcon from "~/components/ui/icons/ExclamationTriangleIcon";
import InputText, { RefInputText } from "~/components/ui/input/InputText";
import { useRootData } from "~/utils/data/useRootData";
export type LoginActionData = {
error?: string;
fieldErrors?: {
email: string | undefined;
password: string | undefined;
};
fields?: {
email: string;
password: string;
};
/**
* D1 — gesetzt, wenn das Passwort stimmt, der Nutzer aber 2FA aktiviert hat. Das Session-Cookie
* traegt dann nur den Pending-Zustand; der Client wird zur Verify-Route (`mfaVerifyPath`) geleitet.
*/
requiresMfa?: boolean;
mfaVerifyPath?: string;
/** D-D0a — Magic-Link-Login: gesetzt, wenn der Anmeldelink ausgegeben wurde (zeigt den "Link gesendet"-Screen). */
magicLinkSent?: boolean;
magicLinkEmail?: string;
};
export default function LoginForm({ actionData, redirectTo }: { actionData: LoginActionData | null | undefined; redirectTo?: string }) {
const { t } = useTranslation();
const { appConfiguration, csrf } = useRootData();
const navigation = useNavigation();
const [searchParams] = useSearchParams();
const emailErrorId = actionData?.fieldErrors?.email ? "email-error" : undefined;
const passwordErrorId = actionData?.fieldErrors?.password ? "password-error" : undefined;
const navigate = useNavigate();
const mfaVerifyPath = actionData?.mfaVerifyPath ?? "/login-mfa";
const emailInput = useRef<RefInputText>(null);
useEffect(() => {
if (appConfiguration.auth.authMethods.emailPassword.enabled) {
setTimeout(() => {
emailInput.current?.input.current?.focus();
}, 300);
}
}, [appConfiguration.auth.authMethods.emailPassword.enabled]);
// D1 — Passwort ok, aber 2FA noetig: das Pending-Cookie ist bereits gesetzt, jetzt zur Verify-Route.
useEffect(() => {
if (actionData?.requiresMfa) {
navigate(mfaVerifyPath);
}
}, [actionData?.requiresMfa, mfaVerifyPath, navigate]);
if (actionData?.requiresMfa) {
return (
<div className="space-y-4 text-center" data-ui-id="ui.auth.login.mfaRedirect">
<p className="text-sm">{t("account.loginMfa.redirecting")}</p>
<Link to={mfaVerifyPath} className="text-primary font-medium hover:underline" data-ui-id="ui.auth.login.mfaContinue">
{t("account.loginMfa.continue")}
</Link>
</div>
);
}
return (
<div className="space-y-5">
<div className="flex flex-col items-center">
<h1 className="text-left text-2xl font-extrabold">{t("account.login.headline")}</h1>
{appConfiguration.subscription.allowSignUpBeforeSubscribe && (
<p className="mt-1 text-left">
<Link to="/register" className="text-primary font-medium hover:underline">
{t("account.login.orRegister")}
</Link>
</p>
)}
</div>
<div className="border-border mx-auto flex flex-col items-center space-y-6 rounded-lg border p-6">
{appConfiguration.auth.authMethods.emailPassword.enabled && (
<Form method="post" noValidate className="w-full space-y-3" data-ui-id="ui.auth.login.form">
<input type="hidden" name="action" value="login" hidden readOnly />
<input type="hidden" name="csrf" value={csrf} hidden readOnly />
<input type="hidden" name="redirectTo" value={redirectTo ?? searchParams.get("redirect") ?? searchParams.get("redirectTo") ?? undefined} />
<div>
{/* <label className="flex justify-between space-x-2 text-xs font-medium text-foreground/80 dark:text-muted-foreground" htmlFor="email">
{t("account.shared.email")}
</label> */}
<InputText
ref={emailInput}
title={t("account.shared.email")}
autoFocus
id="email"
name="email"
dataUiId="ui.auth.login.email"
type="email"
required
ariaDescribedBy={emailErrorId}
ariaInvalid={emailErrorId ? true : undefined}
// className="focus:border-theme-500 focus:ring-ring relative mt-1 block w-full appearance-none rounded-md border border-border px-3 py-2 text-sm text-foreground placeholder-gray-500 focus:z-10 focus:outline-hidden sm:text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-slate-200"
placeholder={t("account.shared.emailPlaceholder")}
disabled={navigation.state !== "idle"}
defaultValue={actionData?.fields?.email}
/>
{actionData?.fieldErrors?.email ? (
<p className="text-destructive-foreground py-2 text-xs" role="alert" id="email-error">
{actionData.fieldErrors.email}
</p>
) : null}
</div>
<div>
{/* <label className="flex justify-between space-x-2 text-xs font-medium text-foreground/80 dark:text-muted-foreground" htmlFor="password">
{t("account.shared.password")}
</label> */}
<InputText
id="password"
title={t("account.shared.password")}
name="password"
dataUiId="ui.auth.login.password"
type="password"
required
ariaDescribedBy={passwordErrorId}
ariaInvalid={passwordErrorId ? true : undefined}
// className="focus:border-theme-500 focus:ring-ring relative mt-1 block w-full appearance-none rounded-md border border-border px-3 py-2 text-sm text-foreground placeholder-gray-500 focus:z-10 focus:outline-hidden sm:text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-slate-200"
placeholder="************"
disabled={navigation.state !== "idle"}
defaultValue={actionData?.fields?.password}
/>
{actionData?.fieldErrors?.password ? (
<p className="text-destructive-foreground py-2 text-xs" role="alert" id="password-error">
{actionData.fieldErrors.password}
</p>
) : null}
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Link to="/forgot-password" className="text-foreground inline-block align-baseline text-xs font-medium hover:underline" data-ui-id="ui.auth.login.forgot">
{t("account.login.forgot")}
</Link>
<LoadingButton disabled={navigation.state !== "idle"} className="block w-full sm:w-auto" type="submit" actionName="login" dataUiId="ui.auth.login.submit">
{t("account.login.button")}
</LoadingButton>
</div>
<div id="form-error-message">
{actionData?.error && navigation.state === "idle" ? (
<div className="flex items-center justify-center space-x-2 text-sm text-red-600 dark:text-red-300" role="alert">
<ExclamationTriangleIcon className="h-4 w-4" />
<div>{actionData.error}</div>
</div>
) : null}
</div>
</Form>
)}
</div>
</div>
);
}