fix: 添加邮箱验证重发功能
- 登录时检测 403 错误(邮箱未验证) - 显示重发验证邮件按钮 - 修复邮件发送失败时静默忽略的问题 - 添加 8 种语言的验证相关翻译
This commit is contained in:
@@ -175,7 +175,12 @@
|
|||||||
"requestNewToken": "Neuen Reset-Link anfordern",
|
"requestNewToken": "Neuen Reset-Link anfordern",
|
||||||
"resetPasswordSuccess": "Passwort erfolgreich zurückgesetzt",
|
"resetPasswordSuccess": "Passwort erfolgreich zurückgesetzt",
|
||||||
"resetPasswordSuccessTitle": "Passwort-Zurücksetzung abgeschlossen",
|
"resetPasswordSuccessTitle": "Passwort-Zurücksetzung abgeschlossen",
|
||||||
"resetPasswordSuccessHint": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden."
|
"resetPasswordSuccessHint": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden.",
|
||||||
|
"emailNotVerified": "Bitte verifizieren Sie Ihre E-Mail-Adresse",
|
||||||
|
"emailNotVerifiedHint": "Ihre E-Mail-Adresse wurde nicht verifiziert. Bitte überprüfen Sie Ihren Posteingang oder fordern Sie eine neue Verifizierungs-E-Mail an.",
|
||||||
|
"resendVerification": "Verifizierungs-E-Mail erneut senden",
|
||||||
|
"resendSuccess": "Verifizierungs-E-Mail gesendet! Bitte überprüfen Sie Ihren Posteingang.",
|
||||||
|
"resendFailed": "Verifizierungs-E-Mail konnte nicht gesendet werden"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -166,7 +166,12 @@
|
|||||||
"requestNewToken": "Request New Reset Link",
|
"requestNewToken": "Request New Reset Link",
|
||||||
"resetPasswordSuccess": "Password reset successfully",
|
"resetPasswordSuccess": "Password reset successfully",
|
||||||
"resetPasswordSuccessTitle": "Password Reset Complete",
|
"resetPasswordSuccessTitle": "Password Reset Complete",
|
||||||
"resetPasswordSuccessHint": "Your password has been reset successfully. You can now log in with your new password."
|
"resetPasswordSuccessHint": "Your password has been reset successfully. You can now log in with your new password.",
|
||||||
|
"emailNotVerified": "Please verify your email address",
|
||||||
|
"emailNotVerifiedHint": "Your email has not been verified. Please check your inbox or request a new verification email.",
|
||||||
|
"resendVerification": "Resend Verification Email",
|
||||||
|
"resendSuccess": "Verification email sent! Please check your inbox.",
|
||||||
|
"resendFailed": "Failed to send verification email"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -175,7 +175,12 @@
|
|||||||
"requestNewToken": "Demander un nouveau lien de réinitialisation",
|
"requestNewToken": "Demander un nouveau lien de réinitialisation",
|
||||||
"resetPasswordSuccess": "Mot de passe réinitialisé avec succès",
|
"resetPasswordSuccess": "Mot de passe réinitialisé avec succès",
|
||||||
"resetPasswordSuccessTitle": "Réinitialisation du mot de passe terminée",
|
"resetPasswordSuccessTitle": "Réinitialisation du mot de passe terminée",
|
||||||
"resetPasswordSuccessHint": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
"resetPasswordSuccessHint": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.",
|
||||||
|
"emailNotVerified": "Veuillez vérifier votre adresse e-mail",
|
||||||
|
"emailNotVerifiedHint": "Votre adresse e-mail n'a pas été vérifiée. Veuillez vérifier votre boîte de réception ou demander un nouvel e-mail de vérification.",
|
||||||
|
"resendVerification": "Renvoyer l'e-mail de vérification",
|
||||||
|
"resendSuccess": "E-mail de vérification envoyé ! Veuillez vérifier votre boîte de réception.",
|
||||||
|
"resendFailed": "Échec de l'envoi de l'e-mail de vérification"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -175,7 +175,12 @@
|
|||||||
"requestNewToken": "Richiedi Nuovo Link di Reset",
|
"requestNewToken": "Richiedi Nuovo Link di Reset",
|
||||||
"resetPasswordSuccess": "Password reimpostata con successo",
|
"resetPasswordSuccess": "Password reimpostata con successo",
|
||||||
"resetPasswordSuccessTitle": "Reimpostazione Password Completata",
|
"resetPasswordSuccessTitle": "Reimpostazione Password Completata",
|
||||||
"resetPasswordSuccessHint": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password."
|
"resetPasswordSuccessHint": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password.",
|
||||||
|
"emailNotVerified": "Verifica il tuo indirizzo email",
|
||||||
|
"emailNotVerifiedHint": "Il tuo indirizzo email non è stato verificato. Controlla la tua casella di posta o richiedi una nuova email di verifica.",
|
||||||
|
"resendVerification": "Invia di nuovo email di verifica",
|
||||||
|
"resendSuccess": "Email di verifica inviata! Controlla la tua casella di posta.",
|
||||||
|
"resendFailed": "Impossibile inviare l'email di verifica"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -166,7 +166,12 @@
|
|||||||
"requestNewToken": "新しいリセットリンクをリクエスト",
|
"requestNewToken": "新しいリセットリンクをリクエスト",
|
||||||
"resetPasswordSuccess": "パスワードのリセットに成功しました",
|
"resetPasswordSuccess": "パスワードのリセットに成功しました",
|
||||||
"resetPasswordSuccessTitle": "パスワードリセット完了",
|
"resetPasswordSuccessTitle": "パスワードリセット完了",
|
||||||
"resetPasswordSuccessHint": "パスワードが正常にリセットされました。新しいパスワードでログインできます。"
|
"resetPasswordSuccessHint": "パスワードが正常にリセットされました。新しいパスワードでログインできます。",
|
||||||
|
"emailNotVerified": "メールアドレスを確認してください",
|
||||||
|
"emailNotVerifiedHint": "メールアドレスが確認されていません。受信トレイをご確認いただくか、新しい確認メールをリクエストしてください。",
|
||||||
|
"resendVerification": "確認メールを再送信",
|
||||||
|
"resendSuccess": "確認メールを送信しました!受信トレイをご確認ください。",
|
||||||
|
"resendFailed": "確認メールの送信に失敗しました"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -175,7 +175,12 @@
|
|||||||
"requestNewToken": "새 재설정 링크 요청",
|
"requestNewToken": "새 재설정 링크 요청",
|
||||||
"resetPasswordSuccess": "비밀번호 재설정 성공",
|
"resetPasswordSuccess": "비밀번호 재설정 성공",
|
||||||
"resetPasswordSuccessTitle": "비밀번호 재설정 완료",
|
"resetPasswordSuccessTitle": "비밀번호 재설정 완료",
|
||||||
"resetPasswordSuccessHint": "비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인할 수 있습니다."
|
"resetPasswordSuccessHint": "비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인할 수 있습니다.",
|
||||||
|
"emailNotVerified": "이메일 주소를 인증해 주세요",
|
||||||
|
"emailNotVerifiedHint": "이메일이 인증되지 않았습니다. 받은 편지함을 확인하거나 새 인증 이메일을 요청해 주세요.",
|
||||||
|
"resendVerification": "인증 이메일 다시 보내기",
|
||||||
|
"resendSuccess": "인증 이메일이 발송되었습니다! 받은 편지함을 확인해 주세요.",
|
||||||
|
"resendFailed": "인증 이메일 발송에 실패했습니다"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -175,7 +175,12 @@
|
|||||||
"requestNewToken": "يېڭى ئەسلىگە قايتۇرۇش ئۇلانمىسى سوراش",
|
"requestNewToken": "يېڭى ئەسلىگە قايتۇرۇش ئۇلانمىسى سوراش",
|
||||||
"resetPasswordSuccess": "پارول مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى",
|
"resetPasswordSuccess": "پارول مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى",
|
||||||
"resetPasswordSuccessTitle": "پارول ئەسلىگە قايتۇرۇش تاماملاندى",
|
"resetPasswordSuccessTitle": "پارول ئەسلىگە قايتۇرۇش تاماملاندى",
|
||||||
"resetPasswordSuccessHint": "پارولىڭىز مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى. يېڭى پارول بىلەن كىرسىڭىز بولىدۇ."
|
"resetPasswordSuccessHint": "پارولىڭىز مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى. يېڭى پارول بىلەن كىرسىڭىز بولىدۇ.",
|
||||||
|
"emailNotVerified": "ئېلخەت ئادرېسىڭىزنى دەلىللەڭ",
|
||||||
|
"emailNotVerifiedHint": "ئېلخەت ئادرېسىڭىز دەلىللەنمىگەن. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ ياكى يېڭى دەلىللەش ئېلخېتى سوراڭ.",
|
||||||
|
"resendVerification": "دەلىللەش ئېلخېتىنى قايتا ئەۋەتىش",
|
||||||
|
"resendSuccess": "دەلىللەش ئېلخېتى ئەۋەتىلدى! ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.",
|
||||||
|
"resendFailed": "دەلىللەش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -166,7 +166,12 @@
|
|||||||
"requestNewToken": "重新申请重置链接",
|
"requestNewToken": "重新申请重置链接",
|
||||||
"resetPasswordSuccess": "密码重置成功",
|
"resetPasswordSuccess": "密码重置成功",
|
||||||
"resetPasswordSuccessTitle": "密码重置完成",
|
"resetPasswordSuccessTitle": "密码重置完成",
|
||||||
"resetPasswordSuccessHint": "您的密码已成功重置,现在可以使用新密码登录了。"
|
"resetPasswordSuccessHint": "您的密码已成功重置,现在可以使用新密码登录了。",
|
||||||
|
"emailNotVerified": "请验证您的邮箱地址",
|
||||||
|
"emailNotVerifiedHint": "您的邮箱尚未验证。请检查收件箱或重新发送验证邮件。",
|
||||||
|
"resendVerification": "重新发送验证邮件",
|
||||||
|
"resendSuccess": "验证邮件已发送!请检查您的收件箱。",
|
||||||
|
"resendFailed": "发送验证邮件失败"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"deck_selector": {
|
"deck_selector": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toast } from "sonner";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Card, CardBody } from "@/design-system/base/card";
|
import { Card, CardBody } from "@/design-system/base/card";
|
||||||
import { Input } from "@/design-system/base/input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { PrimaryButton } from "@/design-system/base/button";
|
import { PrimaryButton, LinkButton } from "@/design-system/base/button";
|
||||||
import { VStack } from "@/design-system/layout/stack";
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
@@ -16,6 +16,9 @@ export default function LoginPage() {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [resendLoading, setResendLoading] = useState(false);
|
||||||
|
const [showResendOption, setShowResendOption] = useState(false);
|
||||||
|
const [unverifiedEmail, setUnverifiedEmail] = useState("");
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirect");
|
const redirectTo = searchParams.get("redirect");
|
||||||
@@ -25,10 +28,31 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPending && session?.user?.username && !redirectTo) {
|
if (!isPending && session?.user?.username && !redirectTo) {
|
||||||
router.push("/folders");
|
router.push("/decks");
|
||||||
}
|
}
|
||||||
}, [session, isPending, router, redirectTo]);
|
}, [session, isPending, router, redirectTo]);
|
||||||
|
|
||||||
|
const handleResendVerification = async () => {
|
||||||
|
if (!unverifiedEmail) return;
|
||||||
|
|
||||||
|
setResendLoading(true);
|
||||||
|
try {
|
||||||
|
const { error } = await authClient.sendVerificationEmail({
|
||||||
|
email: unverifiedEmail,
|
||||||
|
callbackURL: "/login",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(t("resendFailed"));
|
||||||
|
} else {
|
||||||
|
toast.success(t("resendSuccess"));
|
||||||
|
setShowResendOption(false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setResendLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
toast.error(t("enterCredentials"));
|
toast.error(t("enterCredentials"));
|
||||||
@@ -36,6 +60,7 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setShowResendOption(false);
|
||||||
try {
|
try {
|
||||||
if (username.includes("@")) {
|
if (username.includes("@")) {
|
||||||
const { error } = await authClient.signIn.email({
|
const { error } = await authClient.signIn.email({
|
||||||
@@ -43,7 +68,13 @@ export default function LoginPage() {
|
|||||||
password: password,
|
password: password,
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message ?? t("loginFailed"));
|
if (error.status === 403) {
|
||||||
|
setUnverifiedEmail(username);
|
||||||
|
setShowResendOption(true);
|
||||||
|
toast.error(t("emailNotVerified"));
|
||||||
|
} else {
|
||||||
|
toast.error(error.message ?? t("loginFailed"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -52,11 +83,15 @@ export default function LoginPage() {
|
|||||||
password: password,
|
password: password,
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message ?? t("loginFailed"));
|
if (error.status === 403) {
|
||||||
|
toast.error(t("emailNotVerified"));
|
||||||
|
} else {
|
||||||
|
toast.error(error.message ?? t("loginFailed"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
router.push(redirectTo ?? "/folders");
|
router.push(redirectTo ?? "/decks");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -91,6 +126,21 @@ export default function LoginPage() {
|
|||||||
{t("forgotPassword")}
|
{t("forgotPassword")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{showResendOption && (
|
||||||
|
<div className="w-full p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg text-sm">
|
||||||
|
<p className="text-yellow-800 dark:text-yellow-200 mb-2">
|
||||||
|
{t("emailNotVerifiedHint")}
|
||||||
|
</p>
|
||||||
|
<LinkButton
|
||||||
|
onClick={handleResendVerification}
|
||||||
|
loading={resendLoading}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t("resendVerification")}
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
10
src/auth.ts
10
src/auth.ts
@@ -18,21 +18,27 @@ export const auth = betterAuth({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
requireEmailVerification: true,
|
requireEmailVerification: true,
|
||||||
sendResetPassword: async ({ user, url }) => {
|
sendResetPassword: async ({ user, url }) => {
|
||||||
void sendEmail({
|
const result = await sendEmail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: "重置您的密码 - Learn Languages",
|
subject: "重置您的密码 - Learn Languages",
|
||||||
html: generateResetPasswordEmailHtml(url, user.name || "用户"),
|
html: generateResetPasswordEmailHtml(url, user.name || "用户"),
|
||||||
});
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("[email] Failed to send reset password email:", result.error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
sendOnSignUp: true,
|
sendOnSignUp: true,
|
||||||
sendVerificationEmail: async ({ user, url }) => {
|
sendVerificationEmail: async ({ user, url }) => {
|
||||||
void sendEmail({
|
const result = await sendEmail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: "验证您的邮箱 - Learn Languages",
|
subject: "验证您的邮箱 - Learn Languages",
|
||||||
html: generateVerificationEmailHtml(url, user.name || "用户"),
|
html: generateVerificationEmailHtml(url, user.name || "用户"),
|
||||||
});
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("[email] Failed to send verification email:", result.error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
socialProviders: {
|
socialProviders: {
|
||||||
|
|||||||
Reference in New Issue
Block a user