Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 443 additions & 0 deletions PRPs/reset-password-implementation.md

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions public/locales/en/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"accountCreatedAndJoined": "Your account has been created and joined {{tenantSlug}}.",
"checkEmailForConfirmation": "Please check your email and click the confirmation link.",
"createAccountFailed": "Failed to create account",
"passwordMinLength": "Password must be at least 6 characters",
"passwordMinLength": "Password must be at least 8 characters",
"emailAlreadyRegistered": "This email is already registered",
"creatingAccount": "Creating account...",
"createAccount": "Create Account",
Expand Down Expand Up @@ -87,5 +87,19 @@
"signingInWithGoogle": "Signing in with Google...",
"completingAuthentication": "Completing authentication...",
"authenticationFailed": "Authentication Failed",
"tryAgain": "Try Again"
"tryAgain": "Try Again",
"currentPasswordRequired": "Current password is required",
"passwordMismatch": "Passwords do not match",
"rateLimitExceeded": "Too many password reset requests. Please wait before trying again (2 emails per hour limit).",
"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.",
"setNewPassword": "Set New Password",
"setNewPasswordDesc": "Enter your new password below.",
"newPassword": "New Password",
"enterNewPassword": "Enter new password",
"confirmNewPassword": "Confirm New Password",
"updatingPassword": "Updating Password...",
"updatePassword": "Update Password",
"resetPasswordSuccess": "Password Reset Successful",
"passwordResetSuccessfully": "Your password has been reset successfully.",
"resetPasswordErrorDesc": "An error occurred while resetting your password."
}
27 changes: 26 additions & 1 deletion public/locales/en/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,30 @@
"profileUpdated": "Profile Updated",
"profileUpdatedSuccess": "Your profile has been updated successfully.",
"profileUpdateError": "Error updating profile",
"profileUpdateErrorDesc": "An unknown error occurred"
"profileUpdateErrorDesc": "An unknown error occurred",
"security": "Security",
"passwordSetupDescription": "Set up a password for your account to enable email/password login in addition to Google Sign-in.",
"passwordChangeDescription": "Change your account password for security purposes.",
"currentPassword": "Current Password",
"enterCurrentPassword": "Enter your current password",
"newPassword": "New Password",
"enterNewPassword": "Enter new password",
"confirmNewPassword": "Confirm New Password",
"changePassword": "Change Password",
"changingPassword": "Changing Password...",
"passwordChangeSuccess": "Password Changed Successfully",
"passwordChangedSuccessfully": "Your password has been changed successfully.",
"passwordChangeError": "Password Change Error",
"passwordChangeErrorDesc": "An error occurred while changing your password.",
"password": "Password",
"enterPassword": "Enter password",
"confirmPassword": "Confirm Password",
"setupPassword": "Set Up Password",
"settingUpPassword": "Setting Up Password...",
"passwordSetupSuccess": "Password Set Up Successfully",
"passwordSetupSuccessfully": "Your password has been set up successfully.",
"passwordSetupError": "Password Setup Error",
"passwordSetupErrorDesc": "An error occurred while setting up your password.",
"passwordSetupExplanation": "Since you signed in with Google, you can optionally set up a password to enable email/password login as well.",
"userNotFound": "User not found"
}
18 changes: 16 additions & 2 deletions public/locales/zh-TW/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"accountCreatedAndJoined": "您的帳號已建立並加入 {{tenantSlug}}。",
"checkEmailForConfirmation": "請前往電子郵件收信並點擊確認連結。",
"createAccountFailed": "建立帳號失敗",
"passwordMinLength": "密碼需至少 6 個字元",
"passwordMinLength": "密碼需至少 8 個字元",
"emailAlreadyRegistered": "此電子郵件已經註冊",
"creatingAccount": "建立帳號中...",
"createAccount": "建立帳號",
Expand Down Expand Up @@ -86,5 +86,19 @@
"signingInWithGoogle": "正在使用 Google 登入...",
"completingAuthentication": "正在完成身份驗證...",
"authenticationFailed": "身份驗證失敗",
"tryAgain": "重試"
"tryAgain": "重試",
"currentPasswordRequired": "需要輸入目前密碼",
"passwordMismatch": "密碼不相符",
"rateLimitExceeded": "密碼重設請求過於頻繁。請稍候再試(每小時限制 2 封電子郵件)。",
"checkEmailForResetWithDetails": "請檢查您的電子郵件收件匣並點擊連結重設密碼。如果沒看到郵件,請檢查垃圾郵件資料夾。",
"setNewPassword": "設定新密碼",
"setNewPasswordDesc": "請在下方輸入您的新密碼。",
"newPassword": "新密碼",
"enterNewPassword": "輸入新密碼",
"confirmNewPassword": "確認新密碼",
"updatingPassword": "更新密碼中...",
"updatePassword": "更新密碼",
"resetPasswordSuccess": "密碼重設成功",
"passwordResetSuccessfully": "您的密碼已成功重設。",
"resetPasswordErrorDesc": "重設密碼時發生錯誤。"
}
27 changes: 26 additions & 1 deletion public/locales/zh-TW/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,30 @@
"profileUpdated": "個人資料已更新",
"profileUpdatedSuccess": "你的個人資料已成功更新。",
"profileUpdateError": "更新個人資料時出錯",
"profileUpdateErrorDesc": "發生未知錯誤"
"profileUpdateErrorDesc": "發生未知錯誤",
"security": "安全設定",
"passwordSetupDescription": "為你的帳號設定密碼,以啟用電子郵件/密碼登入功能,作為 Google 登入的補充。",
"passwordChangeDescription": "為了安全考量,變更你的帳號密碼。",
"currentPassword": "目前密碼",
"enterCurrentPassword": "輸入你目前的密碼",
"newPassword": "新密碼",
"enterNewPassword": "輸入新密碼",
"confirmNewPassword": "確認新密碼",
"changePassword": "變更密碼",
"changingPassword": "密碼變更中...",
"passwordChangeSuccess": "密碼變更成功",
"passwordChangedSuccessfully": "你的密碼已成功變更。",
"passwordChangeError": "密碼變更錯誤",
"passwordChangeErrorDesc": "變更密碼時發生錯誤。",
"password": "密碼",
"enterPassword": "輸入密碼",
"confirmPassword": "確認密碼",
"setupPassword": "設定密碼",
"settingUpPassword": "密碼設定中...",
"passwordSetupSuccess": "密碼設定成功",
"passwordSetupSuccessfully": "你的密碼已成功設定。",
"passwordSetupError": "密碼設定錯誤",
"passwordSetupErrorDesc": "設定密碼時發生錯誤。",
"passwordSetupExplanation": "由於你使用 Google 登入,你可以選擇性地設定密碼來啟用電子郵件/密碼登入功能。",
"userNotFound": "找不到使用者"
}
132 changes: 132 additions & 0 deletions src/components/Auth/ResetPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useToast } from "@/components/ui/use-toast";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { updateUserPassword } from "@/lib/auth-service";
import { useTranslation } from "react-i18next";

const createResetPasswordSchema = (t: (key: string) => string) =>
z
.object({
newPassword: z.string().min(8, t("auth:passwordMinLength")),
confirmPassword: z.string(),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: t("auth:passwordMismatch"),
path: ["confirmPassword"],
});

type ResetPasswordFormValues = z.infer<ReturnType<typeof createResetPasswordSchema>>;

interface ResetPasswordFormProps {
onSuccess: () => void;
}

export function ResetPasswordForm({ onSuccess }: ResetPasswordFormProps) {
const { t } = useTranslation();
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);

const resetPasswordSchema = createResetPasswordSchema(t);

const form = useForm<ResetPasswordFormValues>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: {
newPassword: "",
confirmPassword: "",
},
});

const onSubmit = async (values: ResetPasswordFormValues) => {
setIsSubmitting(true);
try {
// Update to new password (user is already authenticated via reset token)
await updateUserPassword(values.newPassword);

toast({
title: t("auth:resetPasswordSuccess"),
description: t("auth:passwordResetSuccessfully"),
});

// Clear the form and call success callback
form.reset();
onSuccess();
} catch (error) {
toast({
title: t("auth:resetPasswordError"),
description: error instanceof Error ? error.message : t("auth:resetPasswordErrorDesc"),
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};

return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>{t("auth:setNewPassword")}</CardTitle>
<CardDescription>{t("auth:setNewPasswordDesc")}</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("auth:newPassword")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t("auth:enterNewPassword")}
{...field}
disabled={isSubmitting}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("auth:confirmNewPassword")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t("auth:confirmNewPassword")}
{...field}
disabled={isSubmitting}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? t("auth:updatingPassword") : t("auth:updatePassword")}
</Button>
</form>
</Form>
</CardContent>
</Card>
);
}
7 changes: 6 additions & 1 deletion src/components/Auth/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export function SignInForm({
errorMessage = t("auth:userNotFound");
} else if (error.message?.includes("invalid email")) {
errorMessage = t("auth:emailFormatIncorrect");
} else if (
error.message?.toLowerCase().includes("rate limit") ||
error.message?.toLowerCase().includes("too many")
) {
errorMessage = t("auth:rateLimitExceeded");
} else if (error.message) {
errorMessage = error.message;
}
Expand All @@ -148,7 +153,7 @@ export function SignInForm({

toast({
title: t("auth:resetPasswordEmailSent"),
description: t("auth:checkEmailForReset"),
description: t("auth:checkEmailForResetWithDetails"),
});

setResetPasswordMode(false);
Expand Down
Loading