Skip to content

Commit 46e9f7f

Browse files
committed
feat: allow reset password
1 parent 969e260 commit 46e9f7f

File tree

12 files changed

+1058
-57
lines changed

12 files changed

+1058
-57
lines changed

PRPs/reset-password-implementation.md

Lines changed: 443 additions & 0 deletions
Large diffs are not rendered by default.

public/locales/en/auth.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"accountCreatedAndJoined": "Your account has been created and joined {{tenantSlug}}.",
4848
"checkEmailForConfirmation": "Please check your email and click the confirmation link.",
4949
"createAccountFailed": "Failed to create account",
50-
"passwordMinLength": "Password must be at least 6 characters",
50+
"passwordMinLength": "Password must be at least 8 characters",
5151
"emailAlreadyRegistered": "This email is already registered",
5252
"creatingAccount": "Creating account...",
5353
"createAccount": "Create Account",
@@ -87,5 +87,19 @@
8787
"signingInWithGoogle": "Signing in with Google...",
8888
"completingAuthentication": "Completing authentication...",
8989
"authenticationFailed": "Authentication Failed",
90-
"tryAgain": "Try Again"
90+
"tryAgain": "Try Again",
91+
"currentPasswordRequired": "Current password is required",
92+
"passwordMismatch": "Passwords do not match",
93+
"rateLimitExceeded": "Too many password reset requests. Please wait before trying again (2 emails per hour limit).",
94+
"checkEmailForResetWithDetails": "Please check your email inbox and click the link to reset your password. If you don't see the email, check your spam folder.",
95+
"setNewPassword": "Set New Password",
96+
"setNewPasswordDesc": "Enter your new password below.",
97+
"newPassword": "New Password",
98+
"enterNewPassword": "Enter new password",
99+
"confirmNewPassword": "Confirm New Password",
100+
"updatingPassword": "Updating Password...",
101+
"updatePassword": "Update Password",
102+
"resetPasswordSuccess": "Password Reset Successful",
103+
"passwordResetSuccessfully": "Your password has been reset successfully.",
104+
"resetPasswordErrorDesc": "An error occurred while resetting your password."
91105
}

public/locales/en/profile.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,30 @@
1414
"profileUpdated": "Profile Updated",
1515
"profileUpdatedSuccess": "Your profile has been updated successfully.",
1616
"profileUpdateError": "Error updating profile",
17-
"profileUpdateErrorDesc": "An unknown error occurred"
17+
"profileUpdateErrorDesc": "An unknown error occurred",
18+
"security": "Security",
19+
"passwordSetupDescription": "Set up a password for your account to enable email/password login in addition to Google Sign-in.",
20+
"passwordChangeDescription": "Change your account password for security purposes.",
21+
"currentPassword": "Current Password",
22+
"enterCurrentPassword": "Enter your current password",
23+
"newPassword": "New Password",
24+
"enterNewPassword": "Enter new password",
25+
"confirmNewPassword": "Confirm New Password",
26+
"changePassword": "Change Password",
27+
"changingPassword": "Changing Password...",
28+
"passwordChangeSuccess": "Password Changed Successfully",
29+
"passwordChangedSuccessfully": "Your password has been changed successfully.",
30+
"passwordChangeError": "Password Change Error",
31+
"passwordChangeErrorDesc": "An error occurred while changing your password.",
32+
"password": "Password",
33+
"enterPassword": "Enter password",
34+
"confirmPassword": "Confirm Password",
35+
"setupPassword": "Set Up Password",
36+
"settingUpPassword": "Setting Up Password...",
37+
"passwordSetupSuccess": "Password Set Up Successfully",
38+
"passwordSetupSuccessfully": "Your password has been set up successfully.",
39+
"passwordSetupError": "Password Setup Error",
40+
"passwordSetupErrorDesc": "An error occurred while setting up your password.",
41+
"passwordSetupExplanation": "Since you signed in with Google, you can optionally set up a password to enable email/password login as well.",
42+
"userNotFound": "User not found"
1843
}

public/locales/zh-TW/auth.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"accountCreatedAndJoined": "您的帳號已建立並加入 {{tenantSlug}}。",
4848
"checkEmailForConfirmation": "請前往電子郵件收信並點擊確認連結。",
4949
"createAccountFailed": "建立帳號失敗",
50-
"passwordMinLength": "密碼需至少 6 個字元",
50+
"passwordMinLength": "密碼需至少 8 個字元",
5151
"emailAlreadyRegistered": "此電子郵件已經註冊",
5252
"creatingAccount": "建立帳號中...",
5353
"createAccount": "建立帳號",
@@ -86,5 +86,19 @@
8686
"signingInWithGoogle": "正在使用 Google 登入...",
8787
"completingAuthentication": "正在完成身份驗證...",
8888
"authenticationFailed": "身份驗證失敗",
89-
"tryAgain": "重試"
89+
"tryAgain": "重試",
90+
"currentPasswordRequired": "需要輸入目前密碼",
91+
"passwordMismatch": "密碼不相符",
92+
"rateLimitExceeded": "密碼重設請求過於頻繁。請稍候再試(每小時限制 2 封電子郵件)。",
93+
"checkEmailForResetWithDetails": "請檢查您的電子郵件收件匣並點擊連結重設密碼。如果沒看到郵件,請檢查垃圾郵件資料夾。",
94+
"setNewPassword": "設定新密碼",
95+
"setNewPasswordDesc": "請在下方輸入您的新密碼。",
96+
"newPassword": "新密碼",
97+
"enterNewPassword": "輸入新密碼",
98+
"confirmNewPassword": "確認新密碼",
99+
"updatingPassword": "更新密碼中...",
100+
"updatePassword": "更新密碼",
101+
"resetPasswordSuccess": "密碼重設成功",
102+
"passwordResetSuccessfully": "您的密碼已成功重設。",
103+
"resetPasswordErrorDesc": "重設密碼時發生錯誤。"
90104
}

public/locales/zh-TW/profile.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,30 @@
1414
"profileUpdated": "個人資料已更新",
1515
"profileUpdatedSuccess": "你的個人資料已成功更新。",
1616
"profileUpdateError": "更新個人資料時出錯",
17-
"profileUpdateErrorDesc": "發生未知錯誤"
17+
"profileUpdateErrorDesc": "發生未知錯誤",
18+
"security": "安全設定",
19+
"passwordSetupDescription": "為你的帳號設定密碼,以啟用電子郵件/密碼登入功能,作為 Google 登入的補充。",
20+
"passwordChangeDescription": "為了安全考量,變更你的帳號密碼。",
21+
"currentPassword": "目前密碼",
22+
"enterCurrentPassword": "輸入你目前的密碼",
23+
"newPassword": "新密碼",
24+
"enterNewPassword": "輸入新密碼",
25+
"confirmNewPassword": "確認新密碼",
26+
"changePassword": "變更密碼",
27+
"changingPassword": "密碼變更中...",
28+
"passwordChangeSuccess": "密碼變更成功",
29+
"passwordChangedSuccessfully": "你的密碼已成功變更。",
30+
"passwordChangeError": "密碼變更錯誤",
31+
"passwordChangeErrorDesc": "變更密碼時發生錯誤。",
32+
"password": "密碼",
33+
"enterPassword": "輸入密碼",
34+
"confirmPassword": "確認密碼",
35+
"setupPassword": "設定密碼",
36+
"settingUpPassword": "密碼設定中...",
37+
"passwordSetupSuccess": "密碼設定成功",
38+
"passwordSetupSuccessfully": "你的密碼已成功設定。",
39+
"passwordSetupError": "密碼設定錯誤",
40+
"passwordSetupErrorDesc": "設定密碼時發生錯誤。",
41+
"passwordSetupExplanation": "由於你使用 Google 登入,你可以選擇性地設定密碼來啟用電子郵件/密碼登入功能。",
42+
"userNotFound": "找不到使用者"
1843
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useState } from "react";
2+
import { useForm } from "react-hook-form";
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { z } from "zod";
5+
import { useToast } from "@/components/ui/use-toast";
6+
import { Button } from "@/components/ui/button";
7+
import { Input } from "@/components/ui/input";
8+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9+
import {
10+
Form,
11+
FormControl,
12+
FormField,
13+
FormItem,
14+
FormLabel,
15+
FormMessage,
16+
} from "@/components/ui/form";
17+
import { updateUserPassword } from "@/lib/auth-service";
18+
import { useTranslation } from "react-i18next";
19+
20+
const createResetPasswordSchema = (t: (key: string) => string) =>
21+
z
22+
.object({
23+
newPassword: z.string().min(8, t("auth:passwordMinLength")),
24+
confirmPassword: z.string(),
25+
})
26+
.refine((data) => data.newPassword === data.confirmPassword, {
27+
message: t("auth:passwordMismatch"),
28+
path: ["confirmPassword"],
29+
});
30+
31+
type ResetPasswordFormValues = z.infer<ReturnType<typeof createResetPasswordSchema>>;
32+
33+
interface ResetPasswordFormProps {
34+
onSuccess: () => void;
35+
}
36+
37+
export function ResetPasswordForm({ onSuccess }: ResetPasswordFormProps) {
38+
const { t } = useTranslation();
39+
const { toast } = useToast();
40+
const [isSubmitting, setIsSubmitting] = useState(false);
41+
42+
const resetPasswordSchema = createResetPasswordSchema(t);
43+
44+
const form = useForm<ResetPasswordFormValues>({
45+
resolver: zodResolver(resetPasswordSchema),
46+
defaultValues: {
47+
newPassword: "",
48+
confirmPassword: "",
49+
},
50+
});
51+
52+
const onSubmit = async (values: ResetPasswordFormValues) => {
53+
setIsSubmitting(true);
54+
try {
55+
// Update to new password (user is already authenticated via reset token)
56+
await updateUserPassword(values.newPassword);
57+
58+
toast({
59+
title: t("auth:resetPasswordSuccess"),
60+
description: t("auth:passwordResetSuccessfully"),
61+
});
62+
63+
// Clear the form and call success callback
64+
form.reset();
65+
onSuccess();
66+
} catch (error) {
67+
toast({
68+
title: t("auth:resetPasswordError"),
69+
description: error instanceof Error ? error.message : t("auth:resetPasswordErrorDesc"),
70+
variant: "destructive",
71+
});
72+
} finally {
73+
setIsSubmitting(false);
74+
}
75+
};
76+
77+
return (
78+
<Card className="w-full max-w-md mx-auto">
79+
<CardHeader>
80+
<CardTitle>{t("auth:setNewPassword")}</CardTitle>
81+
<CardDescription>{t("auth:setNewPasswordDesc")}</CardDescription>
82+
</CardHeader>
83+
<CardContent>
84+
<Form {...form}>
85+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
86+
<FormField
87+
control={form.control}
88+
name="newPassword"
89+
render={({ field }) => (
90+
<FormItem>
91+
<FormLabel>{t("auth:newPassword")}</FormLabel>
92+
<FormControl>
93+
<Input
94+
type="password"
95+
placeholder={t("auth:enterNewPassword")}
96+
{...field}
97+
disabled={isSubmitting}
98+
/>
99+
</FormControl>
100+
<FormMessage />
101+
</FormItem>
102+
)}
103+
/>
104+
105+
<FormField
106+
control={form.control}
107+
name="confirmPassword"
108+
render={({ field }) => (
109+
<FormItem>
110+
<FormLabel>{t("auth:confirmNewPassword")}</FormLabel>
111+
<FormControl>
112+
<Input
113+
type="password"
114+
placeholder={t("auth:confirmNewPassword")}
115+
{...field}
116+
disabled={isSubmitting}
117+
/>
118+
</FormControl>
119+
<FormMessage />
120+
</FormItem>
121+
)}
122+
/>
123+
124+
<Button type="submit" className="w-full" disabled={isSubmitting}>
125+
{isSubmitting ? t("auth:updatingPassword") : t("auth:updatePassword")}
126+
</Button>
127+
</form>
128+
</Form>
129+
</CardContent>
130+
</Card>
131+
);
132+
}

src/components/Auth/SignInForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ export function SignInForm({
140140
errorMessage = t("auth:userNotFound");
141141
} else if (error.message?.includes("invalid email")) {
142142
errorMessage = t("auth:emailFormatIncorrect");
143+
} else if (
144+
error.message?.toLowerCase().includes("rate limit") ||
145+
error.message?.toLowerCase().includes("too many")
146+
) {
147+
errorMessage = t("auth:rateLimitExceeded");
143148
} else if (error.message) {
144149
errorMessage = error.message;
145150
}
@@ -148,7 +153,7 @@ export function SignInForm({
148153

149154
toast({
150155
title: t("auth:resetPasswordEmailSent"),
151-
description: t("auth:checkEmailForReset"),
156+
description: t("auth:checkEmailForResetWithDetails"),
152157
});
153158

154159
setResetPasswordMode(false);

0 commit comments

Comments
 (0)