156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
import * as React from "react";
|
||
import Link from "next/link";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { CaptchaField } from "@/components/auth/captcha-field";
|
||
|
||
export function LoginForm(props: {
|
||
clientId: string;
|
||
tenantId: string;
|
||
callback: string;
|
||
initialEmail: string;
|
||
prefillPassword?: string;
|
||
disabled?: boolean;
|
||
externalError?: string | null;
|
||
onClearExternalError?: () => void;
|
||
captcha: string;
|
||
onCaptchaChange: (next: string) => void;
|
||
captchaKey: string;
|
||
onRefreshCaptcha: () => void;
|
||
}) {
|
||
const [email, setEmail] = React.useState(props.initialEmail);
|
||
const [password, setPassword] = React.useState("");
|
||
const [rememberMe, setRememberMe] = React.useState(
|
||
Boolean(props.initialEmail),
|
||
);
|
||
const [submitting, setSubmitting] = React.useState(false);
|
||
const [error, setError] = React.useState<string | null>(null);
|
||
|
||
React.useEffect(() => {
|
||
setEmail(props.initialEmail);
|
||
setRememberMe(Boolean(props.initialEmail));
|
||
}, [props.initialEmail]);
|
||
|
||
React.useEffect(() => {
|
||
if (props.prefillPassword) {
|
||
setPassword(props.prefillPassword);
|
||
}
|
||
}, [props.prefillPassword]);
|
||
|
||
const missingParams = !props.clientId || !props.tenantId || !props.callback;
|
||
|
||
async function onSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
setError(null);
|
||
props.onClearExternalError?.();
|
||
setSubmitting(true);
|
||
try {
|
||
const res = await fetch("/api/auth/login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
clientId: props.clientId,
|
||
tenantId: props.tenantId,
|
||
callback: props.callback,
|
||
email,
|
||
password,
|
||
captcha: props.captcha,
|
||
rememberMe,
|
||
}),
|
||
});
|
||
const json = (await res.json()) as {
|
||
redirectTo?: string;
|
||
message?: string;
|
||
};
|
||
if (!res.ok || !json.redirectTo) {
|
||
throw new Error(json.message || "登录失败");
|
||
}
|
||
window.location.href = json.redirectTo;
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "登录失败");
|
||
props.onCaptchaChange("");
|
||
props.onRefreshCaptcha();
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
}
|
||
|
||
const disabled = Boolean(props.disabled) || submitting;
|
||
|
||
return (
|
||
<form onSubmit={onSubmit} className="space-y-4">
|
||
{missingParams ? (
|
||
<div className="text-sm text-destructive">
|
||
缺少 clientId、tenantId 或 callback 参数,无法继续登录
|
||
</div>
|
||
) : null}
|
||
{props.externalError ? (
|
||
<div className="text-sm text-destructive">{props.externalError}</div>
|
||
) : null}
|
||
{error ? <div className="text-sm text-destructive">{error}</div> : null}
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="email">用户名</Label>
|
||
<Input
|
||
id="email"
|
||
autoComplete="username"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="user@example.com"
|
||
disabled={disabled}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="password">密码</Label>
|
||
<Input
|
||
id="password"
|
||
type="password"
|
||
autoComplete="current-password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
disabled={disabled}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<CaptchaField
|
||
captcha={props.captcha}
|
||
onCaptchaChange={props.onCaptchaChange}
|
||
captchaKey={props.captchaKey}
|
||
onRefresh={props.onRefreshCaptcha}
|
||
disabled={disabled}
|
||
/>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<Checkbox
|
||
checked={rememberMe}
|
||
onCheckedChange={(v) => setRememberMe(Boolean(v))}
|
||
disabled={disabled}
|
||
/>
|
||
记住我
|
||
</label>
|
||
<Link
|
||
className="text-sm underline underline-offset-4"
|
||
href={`/forgot-password?tenantId=${encodeURIComponent(props.tenantId)}`}
|
||
>
|
||
忘记密码
|
||
</Link>
|
||
</div>
|
||
|
||
<Button
|
||
type="submit"
|
||
className="w-full"
|
||
disabled={disabled || missingParams}
|
||
>
|
||
{submitting ? "登录中..." : "登录"}
|
||
</Button>
|
||
</form>
|
||
);
|
||
}
|