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

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>
);
}