feat: add reset password functionality for users with social login

- Introduced `ResetPasswordCard` component to guide users who signed up with social providers in setting up a password through the forgot password flow.
- Updated `ConditionalUpdatePasswordCard` to conditionally render `ResetPasswordCard` for users without a credential provider but with an email.
- Enhanced localization files to include new messages for the password setup process in both English and Chinese.
- Implemented email pre-filling in the forgot password form based on URL parameters for improved user experience.
This commit is contained in:
javayhu 2025-03-16 23:45:29 +08:00
parent 4f8b6d56c9
commit 1fd3ef7eeb
6 changed files with 118 additions and 9 deletions

View File

@ -346,6 +346,12 @@
"success": "Password updated successfully",
"fail": "Failed to update password"
},
"setupPassword": {
"title": "Set Up Password",
"description": "Set up a password to enable email login.",
"info": "Setting up a password will allow you to sign in using your email and password in addition to your social login methods. You will receive an email with instructions to set your password.",
"button": "Set Up Password"
},
"deleteAccount": {
"title": "Delete Account",
"description": "Permanently remove your account and all of its contents",

View File

@ -341,6 +341,12 @@
"success": "密码更新成功",
"fail": "更新密码失败"
},
"setupPassword": {
"title": "设置密码",
"description": "设置密码以启用邮箱登录",
"info": "设置密码将允许您除了社交登录方式外,还可以使用邮箱和密码登录,您将收到一封包含设置密码链接的电子邮件",
"button": "设置密码"
},
"deleteAccount": {
"title": "删除账号",
"description": "永久删除您的账号和所有内容",

View File

@ -15,7 +15,7 @@ import {
import { Input } from '@/components/ui/input';
import { ForgotPasswordSchema } from '@/lib/schemas';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState, useTransition } from 'react';
import { useState, useTransition, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import type * as z from 'zod';
import { Icons } from '@/components/icons/icons';
@ -23,12 +23,14 @@ import { authClient } from '@/lib/auth-client';
import { Routes } from '@/routes';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { useSearchParams } from 'next/navigation';
export const ForgotPasswordForm = ({ className }: { className?: string }) => {
const [error, setError] = useState<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, setIsPending] = useState(false);
const t = useTranslations('AuthPage.forgotPassword');
const searchParams = useSearchParams();
const form = useForm<z.infer<typeof ForgotPasswordSchema>>({
resolver: zodResolver(ForgotPasswordSchema),
@ -37,6 +39,14 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
},
});
// Pre-fill the email field if it's provided in the URL
useEffect(() => {
const emailFromUrl = searchParams.get('email');
if (emailFromUrl) {
form.setValue('email', emailFromUrl);
}
}, [searchParams, form]);
const onSubmit = async (values: z.infer<typeof ForgotPasswordSchema>) => {
console.log('forgotPassword, values:', values);
const { data, error } = await authClient.forgetPassword(

View File

@ -1,12 +1,15 @@
'use client';
import { UpdatePasswordCard } from '@/components/settings/account/update-password-card';
import { ResetPasswordCard } from '@/components/settings/account/reset-password-card';
import { authClient } from '@/lib/auth-client';
import { useEffect, useState } from 'react';
/**
* Conditionally renders the UpdatePasswordCard component
* only if the user has a credential provider (email/password login)
* Conditionally renders either:
* - UpdatePasswordCard: if the user has a credential provider (email/password login)
* - ResetPasswordCard: if the user only has social login providers and has an email
* - Nothing: if the user has no credential provider and no email
*/
export function ConditionalUpdatePasswordCard() {
const { data: session } = authClient.useSession();
@ -27,11 +30,11 @@ export function ConditionalUpdatePasswordCard() {
// Check if the response is successful and contains accounts data
if ('data' in accounts && Array.isArray(accounts.data)) {
// Check if any account has a credential provider (provider === 'credentials')
const hasCredentials = accounts.data.some(
// Check if any account has a credential provider (provider === 'credential')
const hasCredential = accounts.data.some(
(account) => account.provider === 'credential'
);
setHasCredentialProvider(hasCredentials);
setHasCredentialProvider(hasCredential);
}
} catch (error) {
console.error('Error checking credential provider:', error);
@ -43,10 +46,22 @@ export function ConditionalUpdatePasswordCard() {
checkCredentialProvider();
}, [session]);
// Don't render anything while loading or if user doesn't have credential provider
if (isLoading || !hasCredentialProvider) {
// Don't render anything while loading
if (isLoading) {
return null;
}
return <UpdatePasswordCard />;
// If user has credential provider, show UpdatePasswordCard
if (hasCredentialProvider) {
return <UpdatePasswordCard />;
}
// If user doesn't have credential provider but has an email, show ResetPasswordCard
// The forgot password flow requires an email address
if (session?.user?.email) {
return <ResetPasswordCard />;
}
// If user has no credential provider and no email, don't show anything
return null;
}

View File

@ -0,0 +1,70 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from '@/components/ui/card';
import { useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { useTranslations } from 'next-intl';
/**
* Reset Password Card
*
* This component guides users who signed up with social providers
* to set up a password through the forgot password flow.
*
* How it works:
* 1. When a user signs in with a social provider, they don't have a password set up
* 2. This card provides a way for them to set up a password using the forgot password flow
* 3. The user clicks the button and is redirected to the forgot password page
* 4. They enter their email (which is already associated with their account)
* 5. They receive a password reset email
* 6. After setting a password, they can now login with either:
* - Their social provider (as before)
* - Their email and the new password
*
* This effectively adds a credential provider to their account, enabling email/password login.
*/
export function ResetPasswordCard() {
const router = useLocaleRouter();
const t = useTranslations('Dashboard.sidebar.settings.items.account');
const { data: session } = authClient.useSession();
const handleSetupPassword = () => {
// Pre-fill the email if available to make it easier for the user
if (session?.user?.email) {
router.push(`/auth/forgot-password?email=${encodeURIComponent(session.user.email)}`);
} else {
router.push('/auth/forgot-password');
}
};
return (
<Card className="max-w-md md:max-w-lg overflow-hidden">
<CardHeader>
<CardTitle className="text-lg font-bold">
{t('setupPassword.title')}
</CardTitle>
<CardDescription>
{t('setupPassword.description')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
{t('setupPassword.info')}
</p>
</CardContent>
<CardFooter className="px-6 py-4 flex justify-end items-center bg-muted rounded-none">
<Button onClick={handleSetupPassword}>
{t('setupPassword.button')}
</Button>
</CardFooter>
</Card>
);
}

View File

@ -46,6 +46,7 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/authentication/email-password#forget-password
async sendResetPassword({ user, url }, request) {
const locale = getLocaleFromRequest(request);
// TODO: add locale to url
await send({
to: user.email,
template: 'forgotPassword',
@ -63,6 +64,7 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/authentication/email-password#require-email-verification
sendVerificationEmail: async ({ user, url, token }, request) => {
const locale = getLocaleFromRequest(request);
// TODO: add locale to url
await send({
to: user.email,
template: 'verifyEmail',