189 lines
7.2 KiB
TypeScript
189 lines
7.2 KiB
TypeScript
import { useEffect } from "react";
|
|
import { Form, Link, useNavigate, useNavigation, useSearchParams } from "react-router";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FeatherArrowLeft, FeatherMail, FeatherRotateCw, FeatherWand2 } from "@subframe/core";
|
|
import { Button } from "~/ui/components/Button";
|
|
import { LinkButton } from "~/ui/components/LinkButton";
|
|
import { TextInput } from "~/ui/components/TextInput";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import { AuthShell, AuthStatus, AuthDivider } from "~/components/auth/AuthShell";
|
|
import type { LoginActionData } from "~/modules/users/components/LoginForm";
|
|
|
|
// Login-Screen nach Davids Subframe-Design ("Login"): Logo + Card (Anmelden/Willkommen zurueck) +
|
|
// E-Mail/Passwort + "Passwort vergessen?" + Anmelden + "oder" + Magic-Link + Footer "Registrieren".
|
|
// Voll an die bestehende Login-Action verdrahtet (action=login, csrf, redirectTo, Feldfehler, 2FA-Redirect).
|
|
// Reines ~/ui (TextInput/Button/LinkButton) + AuthShell. Wird vom Login-Route fuer den NICHT-Admin-Pfad
|
|
// gerendert; der Admin-/SSO-Pfad bleibt in login.tsx unveraendert.
|
|
|
|
export default function LoginScreen({
|
|
actionData,
|
|
redirectTo,
|
|
}: {
|
|
actionData: LoginActionData | null | undefined;
|
|
redirectTo?: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { csrf } = useRootData();
|
|
const navigation = useNavigation();
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const isSubmitting = navigation.state !== "idle";
|
|
const mfaVerifyPath = actionData?.mfaVerifyPath ?? "/login-mfa";
|
|
const emailError = actionData?.fieldErrors?.email;
|
|
const passwordError = actionData?.fieldErrors?.password;
|
|
|
|
// D1 — Passwort ok, aber 2FA noetig: Pending-Cookie ist gesetzt, jetzt zur Verify-Route.
|
|
useEffect(() => {
|
|
if (actionData?.requiresMfa) {
|
|
navigate(mfaVerifyPath);
|
|
}
|
|
}, [actionData?.requiresMfa, mfaVerifyPath, navigate]);
|
|
|
|
if (actionData?.requiresMfa) {
|
|
return (
|
|
<AuthShell title={t("account.loginMfa.title")} dataUiId="ui.auth.login.page">
|
|
<div className="flex flex-col items-center gap-3 text-center" data-ui-id="ui.auth.login.mfaRedirect">
|
|
<p className="text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary">
|
|
{t("account.loginMfa.redirecting")}
|
|
</p>
|
|
<Link to={mfaVerifyPath} className="no-underline" data-ui-id="ui.auth.login.mfaContinue">
|
|
<LinkButton hideIcon>{t("account.loginMfa.continue")}</LinkButton>
|
|
</Link>
|
|
</div>
|
|
</AuthShell>
|
|
);
|
|
}
|
|
|
|
// D-D0a — Magic-Link ausgegeben -> Davids "Link gesendet"-Screen (AuthStatus).
|
|
if (actionData?.magicLinkSent) {
|
|
return (
|
|
<AuthStatus
|
|
icon={<FeatherMail />}
|
|
tone="positive"
|
|
title={t("account.login.magicLinkSentTitle")}
|
|
message={t("account.login.magicLinkSentBody", { email: actionData.magicLinkEmail ?? "" })}
|
|
hint={t("account.login.magicLinkSentHint")}
|
|
dataUiId="ui.auth.login.magicLinkSent"
|
|
>
|
|
<Form method="post" className="contents">
|
|
<input type="hidden" name="intent" value="magicLink" readOnly />
|
|
<input type="hidden" name="csrf" value={csrf} readOnly />
|
|
<input type="hidden" name="email" value={actionData.magicLinkEmail ?? ""} readOnly />
|
|
<LinkButton type="submit" size="small" icon={<FeatherRotateCw />} disabled={isSubmitting} data-ui-id="ui.auth.login.magicLinkResend">
|
|
{t("account.login.magicLinkResend")}
|
|
</LinkButton>
|
|
</Form>
|
|
<LinkButton size="small" icon={<FeatherArrowLeft />} onClick={() => navigate("/login")} data-ui-id="ui.auth.login.backToLogin">
|
|
{t("account.login.backToLogin")}
|
|
</LinkButton>
|
|
</AuthStatus>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthShell
|
|
title={t("account.login.title")}
|
|
subtitle={t("account.login.headline")}
|
|
titleAlign="center"
|
|
dataUiId="ui.auth.login.page"
|
|
footer={
|
|
<>
|
|
<span className="text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary">
|
|
{t("account.login.noAccount")}
|
|
</span>
|
|
<Link to="/register" className="no-underline" data-ui-id="ui.auth.login.register">
|
|
<LinkButton size="small" hideIcon>
|
|
{t("account.login.register")}
|
|
</LinkButton>
|
|
</Link>
|
|
</>
|
|
}
|
|
>
|
|
<Form id="loginForm" method="post" noValidate className="flex w-full flex-col gap-4" data-ui-id="ui.auth.login.form">
|
|
<input type="hidden" name="action" value="login" readOnly />
|
|
<input type="hidden" name="csrf" value={csrf} readOnly />
|
|
<input
|
|
type="hidden"
|
|
name="redirectTo"
|
|
value={redirectTo ?? searchParams.get("redirect") ?? searchParams.get("redirectTo") ?? ""}
|
|
readOnly
|
|
/>
|
|
|
|
<TextInput
|
|
className="h-auto w-full flex-none"
|
|
showLabel
|
|
label={t("account.shared.email")}
|
|
helpText={emailError ?? ""}
|
|
error={!!emailError}
|
|
type="email"
|
|
>
|
|
<TextInput.Input
|
|
name="email"
|
|
type="email"
|
|
required
|
|
autoFocus
|
|
autoComplete="email"
|
|
placeholder={t("account.shared.emailPlaceholder")}
|
|
defaultValue={actionData?.fields?.email}
|
|
disabled={isSubmitting}
|
|
data-ui-id="ui.auth.login.email"
|
|
/>
|
|
</TextInput>
|
|
|
|
<TextInput
|
|
className="h-auto w-full flex-none"
|
|
showLabel
|
|
label={t("account.shared.password")}
|
|
helpText={passwordError ?? ""}
|
|
error={!!passwordError}
|
|
type="password"
|
|
>
|
|
<TextInput.Input
|
|
name="password"
|
|
type="password"
|
|
required
|
|
autoComplete="current-password"
|
|
placeholder=""
|
|
defaultValue={actionData?.fields?.password}
|
|
disabled={isSubmitting}
|
|
data-ui-id="ui.auth.login.password"
|
|
/>
|
|
</TextInput>
|
|
|
|
<Link to="/forgot-password" className="no-underline" data-ui-id="ui.auth.login.forgot">
|
|
<LinkButton size="small" hideIcon>
|
|
{t("account.login.forgot")}
|
|
</LinkButton>
|
|
</Link>
|
|
|
|
<Button type="submit" loading={isSubmitting} disabled={isSubmitting} data-ui-id="ui.auth.login.submit">
|
|
{t("account.login.button")}
|
|
</Button>
|
|
|
|
{actionData?.error && navigation.state === "idle" ? (
|
|
<p className="text-center text-body-small-regular-0 font-body-small-regular-0 text-text-feedback-negative" role="alert">
|
|
{actionData.error}
|
|
</p>
|
|
) : null}
|
|
</Form>
|
|
|
|
<AuthDivider label={t("account.login.divider")} />
|
|
|
|
{/* Magic-Link-Login: submitet die Login-Form (via form="loginForm") mit intent=magicLink ->
|
|
login-Action gibt den Login-Magic-Link aus -> "Link gesendet"-Screen. */}
|
|
<Button
|
|
type="submit"
|
|
form="loginForm"
|
|
name="intent"
|
|
value="magicLink"
|
|
variant="secondary"
|
|
icon={<FeatherWand2 />}
|
|
loading={isSubmitting}
|
|
data-ui-id="ui.auth.login.magicLink"
|
|
>
|
|
{t("account.login.magicLink")}
|
|
</Button>
|
|
</AuthShell>
|
|
);
|
|
}
|