diff --git a/messages/de-DE.json b/messages/de-DE.json index 1011057..83110de 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -150,7 +150,23 @@ "loginFailed": "Anmeldung fehlgeschlagen", "signUpFailed": "Registrierung fehlgeschlagen", "fillAllFields": "Bitte füllen Sie alle Felder aus", - "enterCredentials": "Bitte geben Sie Benutzername und Passwort ein" + "enterCredentials": "Bitte geben Sie Benutzername und Passwort ein", + "forgotPassword": "Passwort vergessen", + "forgotPasswordHint": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.", + "sendResetEmail": "Reset-E-Mail senden", + "resetPasswordFailed": "Reset-E-Mail konnte nicht gesendet werden", + "resetPasswordEmailSent": "Reset-E-Mail erfolgreich gesendet", + "resetPasswordEmailSentHint": "Wir haben einen Link zum Zurücksetzen Ihres Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang.", + "checkYourEmail": "Überprüfen Sie Ihre E-Mail", + "backToLogin": "Zurück zur Anmeldung", + "resetPassword": "Passwort zurücksetzen", + "newPassword": "Neues Passwort", + "invalidToken": "Ungültiger oder abgelaufener Link", + "invalidTokenHint": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen an.", + "requestNewToken": "Neuen Reset-Link anfordern", + "resetPasswordSuccess": "Passwort erfolgreich zurückgesetzt", + "resetPasswordSuccessTitle": "Passwort-Zurücksetzung abgeschlossen", + "resetPasswordSuccessHint": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden." }, "memorize": { "folder_selector": { diff --git a/messages/en-US.json b/messages/en-US.json index c3de865..06158a5 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -150,7 +150,23 @@ "loginFailed": "Login failed", "signUpFailed": "Sign up failed", "fillAllFields": "Please fill in all fields", - "enterCredentials": "Please enter username and password" + "enterCredentials": "Please enter username and password", + "forgotPassword": "Forgot Password", + "forgotPasswordHint": "Enter your email address and we'll send you a link to reset your password.", + "sendResetEmail": "Send Reset Email", + "resetPasswordFailed": "Failed to send reset email", + "resetPasswordEmailSent": "Reset email sent successfully", + "resetPasswordEmailSentHint": "We've sent a password reset link to your email address. Please check your inbox.", + "checkYourEmail": "Check Your Email", + "backToLogin": "Back to Login", + "resetPassword": "Reset Password", + "newPassword": "New Password", + "invalidToken": "Invalid or Expired Link", + "invalidTokenHint": "This password reset link is invalid or has expired. Please request a new one.", + "requestNewToken": "Request New Reset Link", + "resetPasswordSuccess": "Password reset successfully", + "resetPasswordSuccessTitle": "Password Reset Complete", + "resetPasswordSuccessHint": "Your password has been reset successfully. You can now log in with your new password." }, "memorize": { "folder_selector": { diff --git a/messages/fr-FR.json b/messages/fr-FR.json index cc62794..d2943d4 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -150,7 +150,23 @@ "loginFailed": "Échec de la connexion", "signUpFailed": "Échec de l'inscription", "fillAllFields": "Veuillez remplir tous les champs", - "enterCredentials": "Veuillez entrer le nom d'utilisateur et le mot de passe" + "enterCredentials": "Veuillez entrer le nom d'utilisateur et le mot de passe", + "forgotPassword": "Mot de passe oublié", + "forgotPasswordHint": "Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.", + "sendResetEmail": "Envoyer l'e-mail de réinitialisation", + "resetPasswordFailed": "Échec de l'envoi de l'e-mail de réinitialisation", + "resetPasswordEmailSent": "E-mail de réinitialisation envoyé avec succès", + "resetPasswordEmailSentHint": "Nous avons envoyé un lien de réinitialisation de mot de passe à votre adresse e-mail. Veuillez vérifier votre boîte de réception.", + "checkYourEmail": "Vérifiez votre e-mail", + "backToLogin": "Retour à la connexion", + "resetPassword": "Réinitialiser le mot de passe", + "newPassword": "Nouveau mot de passe", + "invalidToken": "Lien invalide ou expiré", + "invalidTokenHint": "Ce lien de réinitialisation de mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.", + "requestNewToken": "Demander un nouveau lien de réinitialisation", + "resetPasswordSuccess": "Mot de passe réinitialisé avec succès", + "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." }, "memorize": { "folder_selector": { diff --git a/messages/it-IT.json b/messages/it-IT.json index 4c898ee..0edc8b8 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -150,7 +150,23 @@ "loginFailed": "Accesso fallito", "signUpFailed": "Registrazione fallita", "fillAllFields": "Per favore compila tutti i campi", - "enterCredentials": "Per favore inserisci nome utente e password" + "enterCredentials": "Per favore inserisci nome utente e password", + "forgotPassword": "Password Dimenticata", + "forgotPasswordHint": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password.", + "sendResetEmail": "Invia Email di Reset", + "resetPasswordFailed": "Impossibile inviare email di reset", + "resetPasswordEmailSent": "Email di reset inviata con successo", + "resetPasswordEmailSentHint": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo email. Controlla la tua casella di posta.", + "checkYourEmail": "Controlla la tua Email", + "backToLogin": "Torna al Login", + "resetPassword": "Reimposta Password", + "newPassword": "Nuova Password", + "invalidToken": "Link Non Valido o Scaduto", + "invalidTokenHint": "Questo link per reimpostare la password non è valido o è scaduto. Richiedine uno nuovo.", + "requestNewToken": "Richiedi Nuovo Link di Reset", + "resetPasswordSuccess": "Password reimpostata con successo", + "resetPasswordSuccessTitle": "Reimpostazione Password Completata", + "resetPasswordSuccessHint": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password." }, "memorize": { "folder_selector": { diff --git a/messages/ja-JP.json b/messages/ja-JP.json index 9807fa0..73732e1 100644 --- a/messages/ja-JP.json +++ b/messages/ja-JP.json @@ -150,7 +150,23 @@ "loginFailed": "ログインに失敗しました", "signUpFailed": "新規登録に失敗しました", "fillAllFields": "すべてのフィールドに入力してください", - "enterCredentials": "ユーザー名とパスワードを入力してください" + "enterCredentials": "ユーザー名とパスワードを入力してください", + "forgotPassword": "パスワードをお忘れですか", + "forgotPasswordHint": "メールアドレスを入力してください。パスワードリセット用のリンクをお送りします。", + "sendResetEmail": "リセットメールを送信", + "resetPasswordFailed": "リセットメールの送信に失敗しました", + "resetPasswordEmailSent": "リセットメールを送信しました", + "resetPasswordEmailSentHint": "パスワードリセット用のリンクをメールでお送りしました。受信トレイをご確認ください。", + "checkYourEmail": "メールをご確認ください", + "backToLogin": "ログインに戻る", + "resetPassword": "パスワードをリセット", + "newPassword": "新しいパスワード", + "invalidToken": "無効または期限切れのリンク", + "invalidTokenHint": "このパスワードリセットリンクは無効または期限切れです。新しいものをリクエストしてください。", + "requestNewToken": "新しいリセットリンクをリクエスト", + "resetPasswordSuccess": "パスワードのリセットに成功しました", + "resetPasswordSuccessTitle": "パスワードリセット完了", + "resetPasswordSuccessHint": "パスワードが正常にリセットされました。新しいパスワードでログインできます。" }, "memorize": { "folder_selector": { diff --git a/messages/ko-KR.json b/messages/ko-KR.json index d0a3441..c2297bd 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -150,7 +150,23 @@ "loginFailed": "로그인 실패", "signUpFailed": "회원가입 실패", "fillAllFields": "모든 필드를 입력하세요", - "enterCredentials": "사용자명과 비밀번호를 입력하세요" + "enterCredentials": "사용자명과 비밀번호를 입력하세요", + "forgotPassword": "비밀번호 찾기", + "forgotPasswordHint": "이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드립니다.", + "sendResetEmail": "재설정 이메일 보내기", + "resetPasswordFailed": "재설정 이메일 전송 실패", + "resetPasswordEmailSent": "재설정 이메일이 전송되었습니다", + "resetPasswordEmailSentHint": "비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함을 확인해주세요.", + "checkYourEmail": "이메일을 확인하세요", + "backToLogin": "로그인으로 돌아가기", + "resetPassword": "비밀번호 재설정", + "newPassword": "새 비밀번호", + "invalidToken": "유효하지 않거나 만료된 링크", + "invalidTokenHint": "이 비밀번호 재설정 링크는 유효하지 않거나 만료되었습니다. 새로 요청해 주세요.", + "requestNewToken": "새 재설정 링크 요청", + "resetPasswordSuccess": "비밀번호 재설정 성공", + "resetPasswordSuccessTitle": "비밀번호 재설정 완료", + "resetPasswordSuccessHint": "비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인할 수 있습니다." }, "memorize": { "folder_selector": { diff --git a/messages/ug-CN.json b/messages/ug-CN.json index 9b92728..c22a7c0 100644 --- a/messages/ug-CN.json +++ b/messages/ug-CN.json @@ -150,7 +150,23 @@ "loginFailed": "كىرىش مەغلۇپ بولدى", "signUpFailed": "تىزىملىتىش مەغلۇپ بولدى", "fillAllFields": "ھەممە بۆلەكلەرنى تولدۇرۇڭ", - "enterCredentials": "ئىشلەتكۈچى ئاتى ۋە پارول كىرگۈزۈڭ" + "enterCredentials": "ئىشلەتكۈچى ئاتى ۋە پارول كىرگۈزۈڭ", + "forgotPassword": "پارولنى ئۇنتۇپ قالدىڭىزمۇ", + "forgotPasswordHint": "ئېلخەت ئادرېسىڭىزنى كىرگۈزۈڭ، بىز سىزگە پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسى ئەۋەتىمىز.", + "sendResetEmail": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش", + "resetPasswordFailed": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى", + "resetPasswordEmailSent": "ئەسلىگە قايتۇرۇش ئېلخېتى مۇۋەپپەقىيەتلىك ئەۋەتىلدى", + "resetPasswordEmailSentHint": "پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسىنى ئېلخەت ئادرېسىڭىزغا ئەۋەتتۇق. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.", + "checkYourEmail": "ئېلخېتىڭىزنى تەكشۈرۈڭ", + "backToLogin": "كىرىشكە قايتىش", + "resetPassword": "پارولنى ئەسلىگە قايتۇرۇش", + "newPassword": "يېڭى پارول", + "invalidToken": "ئۇلانما ئىناۋەتسىز ياكى ۋاقتى ئۆتكەن", + "invalidTokenHint": "بۇ پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسى ئىناۋەتسىز ياكى ۋاقتى ئۆتكەن. يېڭىدىن سوراڭ.", + "requestNewToken": "يېڭى ئەسلىگە قايتۇرۇش ئۇلانمىسى سوراش", + "resetPasswordSuccess": "پارول مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى", + "resetPasswordSuccessTitle": "پارول ئەسلىگە قايتۇرۇش تاماملاندى", + "resetPasswordSuccessHint": "پارولىڭىز مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى. يېڭى پارول بىلەن كىرسىڭىز بولىدۇ." }, "memorize": { "folder_selector": { diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 4cf5255..91027cb 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -150,7 +150,23 @@ "loginFailed": "登录失败", "signUpFailed": "注册失败", "fillAllFields": "请填写所有字段", - "enterCredentials": "请输入用户名和密码" + "enterCredentials": "请输入用户名和密码", + "forgotPassword": "忘记密码", + "forgotPasswordHint": "输入您的邮箱地址,我们将向您发送重置密码的链接。", + "sendResetEmail": "发送重置邮件", + "resetPasswordFailed": "发送重置邮件失败", + "resetPasswordEmailSent": "重置邮件已发送", + "resetPasswordEmailSentHint": "我们已向您的邮箱发送了密码重置链接,请查收。", + "checkYourEmail": "请查收邮件", + "backToLogin": "返回登录", + "resetPassword": "重置密码", + "newPassword": "新密码", + "invalidToken": "链接无效或已过期", + "invalidTokenHint": "此密码重置链接无效或已过期,请重新申请。", + "requestNewToken": "重新申请重置链接", + "resetPasswordSuccess": "密码重置成功", + "resetPasswordSuccessTitle": "密码重置完成", + "resetPasswordSuccessHint": "您的密码已成功重置,现在可以使用新密码登录了。" }, "memorize": { "folder_selector": { diff --git a/package.json b/package.json index e14525d..a3fd4d9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "lucide-react": "^0.562.0", "next": "16.1.1", "next-intl": "^4.7.0", + "nodemailer": "^8.0.2", "pg": "^8.16.3", "react": "19.2.3", "react-dom": "19.2.3", @@ -36,6 +37,7 @@ "@eslint/eslintrc": "^3.3.3", "@tailwindcss/postcss": "^4.1.18", "@types/node": "^25.0.3", + "@types/nodemailer": "^7.0.11", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "^8.51.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4822dd..fcc98c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ importers: next-intl: specifier: ^4.7.0 version: 4.7.0(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + nodemailer: + specifier: ^8.0.2 + version: 8.0.2 pg: specifier: ^8.16.3 version: 8.16.3 @@ -82,6 +85,9 @@ importers: '@types/node': specifier: ^25.0.3 version: 25.0.3 + '@types/nodemailer': + specifier: ^7.0.11 + version: 7.0.11 '@types/react': specifier: 19.2.7 version: 19.2.7 @@ -1049,6 +1055,9 @@ packages: '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/nodemailer@7.0.11': + resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} + '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} @@ -2661,6 +2670,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nodemailer@8.0.2: + resolution: {integrity: sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==} + engines: {node: '>=6.0.0'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -4393,6 +4406,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/nodemailer@7.0.11': + dependencies: + '@types/node': 25.0.3 + '@types/pg@8.15.6': dependencies: '@types/node': 25.0.3 @@ -6068,6 +6085,8 @@ snapshots: node-releases@2.0.27: {} + nodemailer@8.0.2: {} + normalize-path@3.0.0: {} nypm@0.6.2: diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..2a6397e --- /dev/null +++ b/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { authClient } from "@/lib/auth-client"; +import Link from "next/link"; +import { toast } from "sonner"; +import { useTranslations } from "next-intl"; +import { Card, CardBody } from "@/design-system/base/card"; +import { Input } from "@/design-system/base/input"; +import { PrimaryButton } from "@/design-system/base/button"; +import { VStack } from "@/design-system/layout/stack"; + +export default function ForgotPasswordPage() { + const t = useTranslations("auth"); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [sent, setSent] = useState(false); + + const handleResetRequest = async () => { + if (!email) { + toast.error(t("emailRequired")); + return; + } + + setLoading(true); + const { error } = await authClient.requestPasswordReset({ + email, + redirectTo: "/reset-password", + }); + + if (error) { + toast.error(error.message ?? t("resetPasswordFailed")); + } else { + setSent(true); + toast.success(t("resetPasswordEmailSent")); + } + setLoading(false); + }; + + if (sent) { + return ( +
+ {t("resetPasswordEmailSentHint")} +
+ + {t("backToLogin")} + ++ {t("forgotPasswordHint")} +
++ {t("resetPasswordSuccessHint")} +
+ + {t("backToLogin")} + ++ {t("invalidTokenHint")} +
+ + {t("requestNewToken")} + +您好,${userName}!
+感谢您注册。请点击下方按钮验证您的邮箱地址:
++ 验证邮箱 +
+或者复制以下链接到浏览器:
+${url}
+此链接将在 24 小时后过期。
+ +您好,${userName}!
+我们收到了重置您账户密码的请求。请点击下方按钮设置新密码:
++ 重置密码 +
+或者复制以下链接到浏览器:
+${url}
+此链接将在 1 小时后过期。
+ +