117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { Building2, LogIn, UserPlus } from "lucide-react";
|
||
import { PasswordInput } from "@/components/password-input";
|
||
import { completeAuthenticatedEntry } from "@/lib/auth-entry-routing";
|
||
import { getIdentityCapabilities, type IdentityCapabilities } from "@/lib/identity-capabilities";
|
||
|
||
export default function LoginPage() {
|
||
const [saving, setSaving] = useState(false);
|
||
const [message, setMessage] = useState<string | null>(null);
|
||
const [capabilities, setCapabilities] = useState<IdentityCapabilities | null>(null);
|
||
const [draft, setDraft] = useState({
|
||
email: "",
|
||
password: "",
|
||
});
|
||
|
||
useEffect(() => {
|
||
getIdentityCapabilities().then(setCapabilities).catch(() => setCapabilities(null));
|
||
}, []);
|
||
|
||
async function submit(event: React.FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
setSaving(true);
|
||
setMessage(null);
|
||
try {
|
||
const response = await fetch("/api/auth/login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
email: draft.email,
|
||
password: draft.password,
|
||
}),
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error(await readError(response));
|
||
}
|
||
completeAuthenticatedEntry((pathname) => window.location.replace(pathname));
|
||
} catch (error) {
|
||
setMessage(error instanceof Error ? error.message : "登录失败。");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="page-layout auth-page login-auth-page">
|
||
<div className="page-heading auth-page-heading">
|
||
<div>
|
||
<p className="eyebrow">Workspace access</p>
|
||
<h1>登录工作空间</h1>
|
||
<p className="muted">使用邮箱登录,进入你的个人或组织工作空间。</p>
|
||
</div>
|
||
{capabilities?.registration_enabled && (
|
||
<Link className="button button-soft" href="/register">
|
||
<UserPlus size={16} />
|
||
注册
|
||
</Link>
|
||
)}
|
||
</div>
|
||
|
||
<div className="login-auth-body">
|
||
<div className="login-auth-stack">
|
||
{message && <div className="empty-state identity-message">{message}</div>}
|
||
|
||
<section className="panel auth-panel">
|
||
<div className="panel-header">
|
||
<div>
|
||
<p className="eyebrow">Account</p>
|
||
<h2 className="panel-title">账号登录</h2>
|
||
</div>
|
||
<Building2 size={20} />
|
||
</div>
|
||
<form className="panel-body identity-form" onSubmit={submit}>
|
||
<input
|
||
value={draft.email}
|
||
onChange={(event) => setDraft((value) => ({ ...value, email: event.target.value }))}
|
||
placeholder="用户邮箱"
|
||
type="email"
|
||
required
|
||
/>
|
||
<PasswordInput
|
||
autoComplete="current-password"
|
||
value={draft.password}
|
||
onChange={(event) => setDraft((value) => ({ ...value, password: event.target.value }))}
|
||
placeholder="登录密码"
|
||
required
|
||
/>
|
||
{capabilities?.password_reset_enabled && (
|
||
<div className="auth-inline-actions">
|
||
<Link href="/password-reset">忘记密码?</Link>
|
||
</div>
|
||
)}
|
||
<button className="button button-primary" disabled={saving} type="submit">
|
||
<LogIn size={16} />
|
||
{saving ? "正在登录..." : "进入系统"}
|
||
</button>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
async function readError(response: Response) {
|
||
if (response.status === 401) {
|
||
return "邮箱或密码错误,请检查后重试。";
|
||
}
|
||
if (response.status === 429) {
|
||
return "登录尝试过于频繁,请稍后再试。";
|
||
}
|
||
const payload = await response.json().catch(() => null) as { detail?: string; message?: string } | null;
|
||
return payload?.detail ?? payload?.message ?? "请求失败。";
|
||
}
|