Files
zetaloop 2ab075d173 fix(api): propagate requestId for register and reset-password
Backend requires X-Request-Id header from the verification code send
response. Wire requestId through email/auth-extra API returns, register
and forgot-password pages, and auth API request headers.
2026-04-24 05:06:03 +08:00

170 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Button } from "@/components/ui/button"
import { IconInput } from "@/components/ui/icon-input"
import { Label } from "@/components/ui/label"
import { resetPassword, sendForgotPasswordCode } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { KeyRound, Lock, Mail } from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
const forgotSchema = z
.object({
email: z.string().email("请输入正确的邮箱地址"),
vcode: z.string().min(6, "验证码至少6位"),
newPassword: z.string().min(8, "密码至少8位"),
confirmPassword: z.string(),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "两次输入的密码不一致",
path: ["confirmPassword"],
})
export default function ForgotPasswordPage() {
const router = useRouter()
const [countdown, setCountdown] = useState(0)
const [requestId, setRequestId] = useState("")
const {
register,
handleSubmit,
getValues,
trigger,
formState: { errors, isSubmitting },
} = useForm({
resolver: standardSchemaResolver(forgotSchema),
})
useEffect(() => {
if (countdown <= 0) return
const timer = setInterval(() => setCountdown((c) => c - 1), 1000)
return () => clearInterval(timer)
}, [countdown])
const handleSendCode = async () => {
const isValid = await trigger("email")
if (!isValid) return
try {
const email = getValues("email")
const rid = await sendForgotPasswordCode(email)
setRequestId(rid)
setCountdown(60)
notifySuccess("验证码已发送到你的邮箱")
} catch (err) {
notifyInfo(toApiError(err).msg)
}
}
const onSubmit = async (data: z.infer<typeof forgotSchema>) => {
try {
await resetPassword({
email: data.email,
vcode: data.vcode,
newPassword: data.newPassword,
requestId,
})
notifySuccess("密码已重置")
router.push("/login")
} catch (err) {
notifyInfo(toApiError(err).msg)
}
}
return (
<>
<div className="mb-8">
<h2 className="text-2xl font-bold"></h2>
<p className="mt-2 text-sm text-muted-foreground"></p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="email"></Label>
<IconInput
id="email"
icon={<Mail />}
type="email"
placeholder="输入注册邮箱"
{...register("email")}
/>
{errors.email && (
<p className="text-xs text-destructive">{errors.email.message as string}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="vcode"></Label>
<div className="flex gap-2">
<div className="flex-1">
<IconInput
id="vcode"
icon={<KeyRound />}
placeholder="输入6位验证码"
{...register("vcode")}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={countdown > 0}
className="w-[110px]"
>
{countdown > 0 ? `${countdown}s` : "获取验证码"}
</Button>
</div>
{errors.vcode && (
<p className="text-xs text-destructive">{errors.vcode.message as string}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="newPassword"></Label>
<IconInput
id="newPassword"
icon={<Lock />}
type="password"
placeholder="输入新密码"
{...register("newPassword")}
/>
{errors.newPassword && (
<p className="text-xs text-destructive">{errors.newPassword.message as string}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="confirmPassword"></Label>
<IconInput
id="confirmPassword"
icon={<Lock />}
type="password"
placeholder="再次输入新密码"
{...register("confirmPassword")}
/>
{errors.confirmPassword && (
<p className="text-xs text-destructive">{errors.confirmPassword.message as string}</p>
)}
</div>
<Button type="submit" className="mt-2 w-full" size="lg" disabled={isSubmitting}>
{isSubmitting ? "提交中..." : "重置密码"}
</Button>
</form>
<p className="mt-8 text-center text-sm text-muted-foreground">
{" "}
<Link href="/login" className="font-medium text-primary hover:underline">
</Link>
</p>
</>
)
}