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:
SpecialX
2026-06-23 17:38:56 +08:00
parent 1a9377222c
commit 4f0ef217a0
56 changed files with 1251 additions and 850 deletions

View File

@@ -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>