refactor(modules): update existing module implementations across attendance, audit, auth, classes, course-plans, exams, files, homework, layout, proctoring, questions, scheduling, textbooks, users
- Update attendance components and data-access for record management - Update audit log views, filters, and data-access - Update auth login and register forms - Update classes actions, components, and data-access (admin, schedule, stats) - Update course-plans actions, form, list, progress, and schema - Update exams actions, AI pipeline, preview components, and hooks - Update files components (icon, list, preview, upload) and data-access - Update homework assignment form, review view, auto-save hook, and stats-service - Update layout sidebar, header, and navigation config - Update proctoring actions, anti-cheat monitor, and data-access - Update questions actions, components (dialog, actions, columns, filters), and data-access - Update scheduling actions, auto-scheduler, components, and schema - Update textbooks constants and text-selection hook - Update users class-registration, import-dialog, data-access, and user-service
This commit is contained in:
@@ -8,18 +8,23 @@ import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { Label } from "@/shared/components/ui/label"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { Loader2, Github } from "lucide-react"
|
||||
import { Loader2, Github, ShieldCheck } from "lucide-react"
|
||||
import { preflightTwoFactorAction } from "@/modules/settings/actions-security"
|
||||
|
||||
type LoginFormProps = React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export function LoginForm({ className, ...props }: LoginFormProps) {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false)
|
||||
const [requiresTwoFactor, setRequiresTwoFactor] = React.useState<boolean>(false)
|
||||
const [totpCode, setTotpCode] = React.useState<string>("")
|
||||
const [error, setError] = React.useState<string>("")
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
async function onSubmit(event: React.SyntheticEvent) {
|
||||
event.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError("")
|
||||
|
||||
const form = event.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
@@ -27,10 +32,25 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
|
||||
const password = String(formData.get("password") ?? "")
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard"
|
||||
|
||||
// 首次提交:检查是否需要 2FA
|
||||
if (!requiresTwoFactor) {
|
||||
try {
|
||||
const preflight = await preflightTwoFactorAction(email)
|
||||
if (preflight.required) {
|
||||
setRequiresTwoFactor(true)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 预检失败时静默降级为普通登录
|
||||
}
|
||||
}
|
||||
|
||||
const result = await signIn("credentials", {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
totpCode: requiresTwoFactor ? totpCode : undefined,
|
||||
callbackUrl,
|
||||
})
|
||||
|
||||
@@ -39,6 +59,13 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
|
||||
if (!result?.error) {
|
||||
router.push(result?.url ?? callbackUrl)
|
||||
router.refresh()
|
||||
} else {
|
||||
// 2FA 验证码错误时保留 2FA 输入框,允许用户重新输入
|
||||
if (requiresTwoFactor) {
|
||||
setError("Invalid 2FA code. Please try again.")
|
||||
} else {
|
||||
setError("Invalid email or password.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,47 +76,91 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
|
||||
Welcome back
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email to sign in to your account
|
||||
{requiresTwoFactor
|
||||
? "Enter the 6-digit code from your authenticator app"
|
||||
: "Enter your email to sign in to your account"}
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm font-medium text-muted-foreground hover:underline"
|
||||
{!requiresTwoFactor ? (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm font-medium text-muted-foreground hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="totpCode" className="flex items-center gap-1.5">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
2FA Code
|
||||
</Label>
|
||||
<Input
|
||||
id="totpCode"
|
||||
name="totpCode"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
placeholder="123456"
|
||||
maxLength={8}
|
||||
value={totpCode}
|
||||
onChange={(e) => setTotpCode(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter your 6-digit authenticator code or an 8-character backup code.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRequiresTwoFactor(false)
|
||||
setTotpCode("")
|
||||
setError("")
|
||||
}}
|
||||
className="text-xs text-muted-foreground hover:underline justify-self-start"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
← Back to login
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{error ? (
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
) : null}
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Sign In with Email
|
||||
{requiresTwoFactor ? "Verify & Sign In" : "Sign In with Email"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user