fix: rename forgetPassword APIs to requestPasswordReset (#2947)

* fix: rename  to

* Update demo/nextjs/app/(auth)/forget-password/page.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Bereket Engida
2025-06-07 16:28:47 -07:00
committed by GitHub
parent db2c0d0c8a
commit eaf80cf945
17 changed files with 240 additions and 35 deletions

View File

@@ -29,7 +29,7 @@ export default function Component() {
setError(""); setError("");
try { try {
const res = await client.forgetPassword({ await client.requestPasswordReset({
email, email,
redirectTo: "/reset-password", redirectTo: "/reset-password",
}); });

View File

@@ -22,7 +22,7 @@ export default function CodeTabs() {
? `emailAndPassword: { ? `emailAndPassword: {
enabled: true, enabled: true,
${ ${
options.forgetPassword options.requestPasswordReset
? `async sendResetPassword(data, request) { ? `async sendResetPassword(data, request) {
// Send an email to the user with a link to reset their password // Send an email to the user with a link to reset their password
},` },`

View File

@@ -372,11 +372,11 @@ export function Builder() {
</div> </div>
<Switch <Switch
id="email-provider-forget-password" id="email-provider-forget-password"
checked={options.forgetPassword} checked={options.requestPasswordReset}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setOptions((prev) => ({ setOptions((prev) => ({
...prev, ...prev,
forgetPassword: checked, requestPasswordReset: checked,
})); }));
}} }}
/> />

View File

@@ -46,7 +46,7 @@ export default function SignIn() {
<div className="grid gap-2"> <div className="grid gap-2">
<div className="flex items-center"> <div className="flex items-center">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
{options.forgetPassword && ( {options.requestPasswordReset && (
<Link <Link
href="#" href="#"
className="ml-auto inline-block text-sm underline" className="ml-auto inline-block text-sm underline"
@@ -208,7 +208,7 @@ export default function SignIn() {
<div className="flex items-center"> <div className="flex items-center">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
${ ${
options.forgetPassword options.requestPasswordReset
? `<Link ? `<Link
href="#" href="#"
className="ml-auto inline-block text-sm underline" className="ml-auto inline-block text-sm underline"

View File

@@ -8,5 +8,5 @@ export const optionsAtom = atom({
signUp: true, signUp: true,
label: true, label: true,
rememberMe: true, rememberMe: true,
forgetPassword: true, requestPasswordReset: true,
}); });

View File

@@ -167,7 +167,7 @@ await authClient.sendVerificationEmail({
}); });
``` ```
### Forget Password ### Request Password Reset
To allow users to reset a password first you need to provide `sendResetPassword` function to the email and password authenticator. The `sendResetPassword` function takes a data object with the following properties: To allow users to reset a password first you need to provide `sendResetPassword` function to the email and password authenticator. The `sendResetPassword` function takes a data object with the following properties:
@@ -195,7 +195,7 @@ export const auth = betterAuth({
}); });
``` ```
Once you configured your server you can call `forgetPassword` function to send reset password link to user. If the user exists, it will trigger the `sendResetPassword` function you provided in the auth config. Once you configured your server you can call `requestPasswordReset` function to send reset password link to user. If the user exists, it will trigger the `sendResetPassword` function you provided in the auth config.
It takes an object with the following properties: It takes an object with the following properties:
@@ -203,7 +203,7 @@ It takes an object with the following properties:
- `redirectTo`: The URL to redirect to after the user clicks on the link in the email. If the token is valid, the user will be redirected to this URL with the token in the query string. If the token is invalid, the user will be redirected to this URL with an error message in the query string `?error=invalid_token`. - `redirectTo`: The URL to redirect to after the user clicks on the link in the email. If the token is valid, the user will be redirected to this URL with the token in the query string. If the token is invalid, the user will be redirected to this URL with an error message in the query string `?error=invalid_token`.
```ts title="auth-client.ts" ```ts title="auth-client.ts"
const { data, error } = await authClient.forgetPassword({ const { data, error } = await authClient.requestPasswordReset({
email: "test@example.com", email: "test@example.com",
redirectTo: "/reset-password", redirectTo: "/reset-password",
}); });

View File

@@ -159,12 +159,12 @@ const isVerified = await authClient.phoneNumber.verify({
}) })
``` ```
### Forget Password ### Request Password Reset
To initiate a forget password flow using `phoneNumber`, you can start by calling `forgetPassword` on the client to send an OTP code to the user's phone number. To initiate a request password reset flow using `phoneNumber`, you can start by calling `requestPasswordReset` on the client to send an OTP code to the user's phone number.
```ts title="auth-client.ts" ```ts title="auth-client.ts"
await authClient.phoneNumber.forgetPassword({ await authClient.phoneNumber.requestPasswordReset({
phoneNumber: "+1234567890" phoneNumber: "+1234567890"
}) })
``` ```
@@ -199,6 +199,7 @@ export const auth = betterAuth({
] ]
}) })
``` ```
- `sendPasswordResetOTP`: A function that sends the OTP code to the user's phone number for password reset. It takes the phone number and the OTP code as arguments.
- `phoneNumberValidator`: A custom function to validate the phone number. It takes the phone number as an argument and returns a boolean indicating whether the phone number is valid. - `phoneNumberValidator`: A custom function to validate the phone number. It takes the phone number as an argument and returns a boolean indicating whether the phone number is valid.
- `signUpOnVerification`: An object with the following properties: - `signUpOnVerification`: An object with the following properties:
- `getTempEmail`: A function that generates a temporary email for the user. It takes the phone number as an argument and returns the temporary email. - `getTempEmail`: A function that generates a temporary email for the user. It takes the phone number as an argument and returns the temporary email.

View File

@@ -14,7 +14,7 @@ import { View } from "react-native";
import Icons from "@expo/vector-icons/AntDesign"; import Icons from "@expo/vector-icons/AntDesign";
import { router } from "expo-router"; import { router } from "expo-router";
export default function ForgetPassword() { export default function RequestPasswordReset() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
return ( return (
<Card className="w-10/12 "> <Card className="w-10/12 ">
@@ -36,7 +36,7 @@ export default function ForgetPassword() {
<View className="w-full gap-2"> <View className="w-full gap-2">
<Button <Button
onPress={() => { onPress={() => {
authClient.forgetPassword({ authClient.requestPasswordReset({
email, email,
redirectTo: "/reset-password", redirectTo: "/reset-password",
}); });

View File

@@ -7,6 +7,6 @@ export const {
signOut, signOut,
signUp, signUp,
useSession, useSession,
forgetPassword, requestPasswordReset,
resetPassword, resetPassword,
} = authClient; } = authClient;

View File

@@ -1,20 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { forgetPassword } from "~/lib/auth-client.js"; import { requestPasswordReset } from "~/lib/auth-client.js";
const email = ref(""); const email = ref("");
const handleForgetPassword = async () => { const handleRequestPasswordReset = async () => {
if (!email.value) { if (!email.value) {
alert("Please enter your email address"); alert("Please enter your email address");
return; return;
} }
await forgetPassword( await requestPasswordReset(
{ {
email: email.value, email: email.value,
redirectTo: "/reset-password", redirectTo: "/reset-password",
}, },
{ {
// onSuccess find the url with token in server console. For detail check forgetPassword section: https://www.better-auth.com/docs/authentication/email-password // onSuccess find the url with token in server console. For detail check requestPasswordReset section: https://www.better-auth.com/docs/authentication/email-password
onSuccess() { onSuccess() {
alert("Password reset link sent to your email"); alert("Password reset link sent to your email");
window.location.href = "/sign-in"; window.location.href = "/sign-in";
@@ -43,7 +43,7 @@ const handleForgetPassword = async () => {
<Label for="email">Email</Label> <Label for="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required v-model="email" /> <Input id="email" type="email" placeholder="m@example.com" required v-model="email" />
</div> </div>
<Button type="button" class="w-full" @click="handleForgetPassword"> <Button type="button" class="w-full" @click="handleRequestPasswordReset">
Reset Password Reset Password
</Button> </Button>
</div> </div>

View File

@@ -30,6 +30,8 @@ import {
refreshToken, refreshToken,
getAccessToken, getAccessToken,
accountInfo, accountInfo,
requestPasswordReset,
requestPasswordResetCallback,
} from "./routes"; } from "./routes";
import { ok } from "./routes/ok"; import { ok } from "./routes/ok";
import { signUpEmail } from "./routes/sign-up"; import { signUpEmail } from "./routes/sign-up";
@@ -105,6 +107,8 @@ export function getEndpoints<
updateUser: updateUser<Option>(), updateUser: updateUser<Option>(),
deleteUser, deleteUser,
forgetPasswordCallback, forgetPasswordCallback,
requestPasswordReset,
requestPasswordResetCallback,
listSessions: listSessions<Option>(), listSessions: listSessions<Option>(),
revokeSession, revokeSession,
revokeSessions, revokeSessions,

View File

@@ -217,7 +217,7 @@ describe("Origin Check", async (it) => {
customFetchImpl, customFetchImpl,
}, },
}); });
const res = await client.forgetPassword({ const res = await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "http://malicious.com", redirectTo: "http://malicious.com",
}); });
@@ -235,7 +235,7 @@ describe("Origin Check", async (it) => {
}, },
}, },
}); });
const res = await client.forgetPassword({ const res = await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "http://localhost:5000/reset-password", redirectTo: "http://localhost:5000/reset-password",
}); });

View File

@@ -2,7 +2,7 @@ export * from "./sign-in";
export * from "./callback"; export * from "./callback";
export * from "./session"; export * from "./session";
export * from "./sign-out"; export * from "./sign-out";
export * from "./forget-password"; export * from "./reset-password";
export * from "./email-verification"; export * from "./email-verification";
export * from "./update-user"; export * from "./update-user";
export * from "./error"; export * from "./error";

View File

@@ -5,7 +5,7 @@ describe("forget password", async (it) => {
const mockSendEmail = vi.fn(); const mockSendEmail = vi.fn();
let token = ""; let token = "";
const { client, testUser } = await getTestInstance( const { client, testUser, auth } = await getTestInstance(
{ {
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
@@ -20,7 +20,7 @@ describe("forget password", async (it) => {
}, },
); );
it("should send a reset password email when enabled", async () => { it("should send a reset password email when enabled", async () => {
await client.forgetPassword({ await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "http://localhost:3000", redirectTo: "http://localhost:3000",
}); });
@@ -99,7 +99,7 @@ describe("forget password", async (it) => {
}, },
}); });
const { headers } = await signInWithTestUser(); const { headers } = await signInWithTestUser();
await client.forgetPassword({ await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "/sign-in", redirectTo: "/sign-in",
fetchOptions: { fetchOptions: {
@@ -126,7 +126,7 @@ describe("forget password", async (it) => {
token, token,
}); });
expect(res.data?.status).toBe(true); expect(res.data?.status).toBe(true);
await client.forgetPassword({ await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "/sign-in", redirectTo: "/sign-in",
fetchOptions: { fetchOptions: {
@@ -158,7 +158,7 @@ describe("forget password", async (it) => {
const queryParams = "foo=bar&baz=qux"; const queryParams = "foo=bar&baz=qux";
const redirectTo = `http://localhost:3000?${queryParams}`; const redirectTo = `http://localhost:3000?${queryParams}`;
const res = await client.forgetPassword({ const res = await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo, redirectTo,
}); });
@@ -192,7 +192,7 @@ describe("revoke sessions on password reset", async (it) => {
it("should revoke other sessions when revokeSessionsOnPasswordReset is enabled", async () => { it("should revoke other sessions when revokeSessionsOnPasswordReset is enabled", async () => {
const { headers } = await signInWithTestUser(); const { headers } = await signInWithTestUser();
await client.forgetPassword({ await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "http://localhost:3000", redirectTo: "http://localhost:3000",
}); });
@@ -234,7 +234,7 @@ describe("revoke sessions on password reset", async (it) => {
const { headers } = await signInWithTestUser(); const { headers } = await signInWithTestUser();
await client.forgetPassword({ await client.requestPasswordReset({
email: testUser.email, email: testUser.email,
redirectTo: "http://localhost:3000", redirectTo: "http://localhost:3000",
}); });

View File

@@ -31,6 +31,116 @@ function redirectCallback(
return url.href; return url.href;
} }
export const requestPasswordReset = createAuthEndpoint(
"/request-password-reset",
{
method: "POST",
body: z.object({
/**
* The email address of the user to send a password reset email to.
*/
email: z
.string({
description:
"The email address of the user to send a password reset email to",
})
.email(),
/**
* The URL to redirect the user to reset their password.
* If the token isn't valid or expired, it'll be redirected with a query parameter `?
* error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?
* token=VALID_TOKEN
*/
redirectTo: z
.string({
description:
"The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN",
})
.optional(),
}),
metadata: {
openapi: {
description: "Send a password reset email to the user",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
},
message: {
type: "string",
},
},
},
},
},
},
},
},
},
},
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
ctx.context.logger.error(
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPassword function in your auth config!",
);
throw new APIError("BAD_REQUEST", {
message: "Reset password isn't enabled",
});
}
const { email, redirectTo } = ctx.body;
const user = await ctx.context.internalAdapter.findUserByEmail(email, {
includeAccounts: true,
});
if (!user) {
ctx.context.logger.error("Reset Password: User not found", { email });
return ctx.json({
status: true,
message:
"If this email exists in our system, check your email for the reset link",
});
}
const defaultExpiresIn = 60 * 60 * 1;
const expiresAt = getDate(
ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn ||
defaultExpiresIn,
"sec",
);
const verificationToken = generateId(24);
await ctx.context.internalAdapter.createVerificationValue(
{
value: user.user.id,
identifier: `reset-password:${verificationToken}`,
expiresAt,
},
ctx,
);
const callbackURL = redirectTo ? encodeURIComponent(redirectTo) : "";
const url = `${ctx.context.baseURL}/reset-password/${verificationToken}?callbackURL=${callbackURL}`;
await ctx.context.options.emailAndPassword.sendResetPassword(
{
user: user.user,
url,
token: verificationToken,
},
ctx.request,
);
return ctx.json({
status: true,
});
},
);
/**
* @deprecated Use requestPasswordReset instead. This endpoint will be removed in the next major
* version.
*/
export const forgetPassword = createAuthEndpoint( export const forgetPassword = createAuthEndpoint(
"/forget-password", "/forget-password",
{ {
@@ -137,7 +247,7 @@ export const forgetPassword = createAuthEndpoint(
}, },
); );
export const forgetPasswordCallback = createAuthEndpoint( export const requestPasswordResetCallback = createAuthEndpoint(
"/reset-password/:token", "/reset-password/:token",
{ {
method: "GET", method: "GET",
@@ -192,6 +302,11 @@ export const forgetPasswordCallback = createAuthEndpoint(
}, },
); );
/**
* @deprecated Use requestPasswordResetCallback instead
*/
export const forgetPasswordCallback = requestPasswordResetCallback;
export const resetPassword = createAuthEndpoint( export const resetPassword = createAuthEndpoint(
"/reset-password", "/reset-password",
{ {

View File

@@ -48,6 +48,18 @@ export interface PhoneNumberOptions {
* @param request - the request object * @param request - the request object
* @returns * @returns
*/ */
sendPasswordResetOTP?: (
data: { phoneNumber: string; code: string },
request?: Request,
) => Promise<void> | void;
/**
* a callback to send otp on user requesting to reset their password
*
* @param data - contains phone number and code
* @param request - the request object
* @returns
* @deprecated Use sendPasswordResetOTP instead. This function will be removed in the next major version.
*/
sendForgetPasswordOTP?: ( sendForgetPasswordOTP?: (
data: { phoneNumber: string; code: string }, data: { phoneNumber: string; code: string },
request?: Request, request?: Request,
@@ -693,6 +705,9 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
}); });
}, },
), ),
/**
* @deprecated Use requestPasswordResetPhoneNumber instead. This endpoint will be removed in the next major version.
*/
forgetPasswordPhoneNumber: createAuthEndpoint( forgetPasswordPhoneNumber: createAuthEndpoint(
"/phone-number/forget-password", "/phone-number/forget-password",
{ {
@@ -746,7 +761,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
await ctx.context.internalAdapter.createVerificationValue( await ctx.context.internalAdapter.createVerificationValue(
{ {
value: `${code}:0`, value: `${code}:0`,
identifier: `${ctx.body.phoneNumber}-forget-password`, identifier: `${ctx.body.phoneNumber}-request-password-reset`,
expiresAt: getDate(opts.expiresIn, "sec"), expiresAt: getDate(opts.expiresIn, "sec"),
}, },
ctx, ctx,
@@ -763,6 +778,76 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
}); });
}, },
), ),
requestPasswordResetPhoneNumber: createAuthEndpoint(
"/phone-number/request-password-reset",
{
method: "POST",
body: z.object({
phoneNumber: z.string(),
}),
metadata: {
openapi: {
description: "Request OTP for password reset via phone number",
responses: {
"200": {
description: "OTP sent successfully for password reset",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description:
"Indicates if the OTP was sent successfully",
enum: [true],
},
},
required: ["status"],
},
},
},
},
},
},
},
},
async (ctx) => {
const user = await ctx.context.adapter.findOne<UserWithPhoneNumber>({
model: "user",
where: [
{
value: ctx.body.phoneNumber,
field: opts.phoneNumber,
},
],
});
if (!user) {
throw new APIError("BAD_REQUEST", {
message: "phone number isn't registered",
});
}
const code = generateOTP(opts.otpLength);
await ctx.context.internalAdapter.createVerificationValue(
{
value: `${code}:0`,
identifier: `${ctx.body.phoneNumber}-request-password-reset`,
expiresAt: getDate(opts.expiresIn, "sec"),
},
ctx,
);
await options?.sendPasswordResetOTP?.(
{
phoneNumber: ctx.body.phoneNumber,
code,
},
ctx.request,
);
return ctx.json({
status: true,
});
},
),
resetPasswordPhoneNumber: createAuthEndpoint( resetPasswordPhoneNumber: createAuthEndpoint(
"/phone-number/reset-password", "/phone-number/reset-password",
{ {
@@ -802,7 +887,7 @@ export const phoneNumber = (options?: PhoneNumberOptions) => {
async (ctx) => { async (ctx) => {
const verification = const verification =
await ctx.context.internalAdapter.findVerificationValue( await ctx.context.internalAdapter.findVerificationValue(
`${ctx.body.phoneNumber}-forget-password`, `${ctx.body.phoneNumber}-request-password-reset`,
); );
if (!verification) { if (!verification) {
throw new APIError("BAD_REQUEST", { throw new APIError("BAD_REQUEST", {

View File

@@ -333,7 +333,7 @@ describe("reset password flow attempts", async (it) => {
code: otp, code: otp,
}); });
await client.phoneNumber.forgetPassword({ await client.phoneNumber.requestPasswordReset({
phoneNumber: testPhoneNumber, phoneNumber: testPhoneNumber,
}); });