From e91be5a3692f8f30804536e0828db69bda2fda03 Mon Sep 17 00:00:00 2001 From: Jason Gerbes Date: Wed, 11 Dec 2024 23:43:34 +1300 Subject: [PATCH] fix: email OTP error codes and docs (#845) --- .../content/docs/guides/your-first-plugin.mdx | 8 ++- docs/content/docs/plugins/email-otp.mdx | 37 ++++++---- .../src/plugins/email-otp/index.ts | 67 ++++++++++--------- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/docs/content/docs/guides/your-first-plugin.mdx b/docs/content/docs/guides/your-first-plugin.mdx index a7aeef9e..0804bd70 100644 --- a/docs/content/docs/guides/your-first-plugin.mdx +++ b/docs/content/docs/guides/your-first-plugin.mdx @@ -127,12 +127,16 @@ import { createAuthMiddleware } from "better-auth/plugins"; //... handler: createAuthMiddleware(async (ctx) => { const { birthday } = ctx.body; - if(!birthday instanceof Date) throw APIError("BAD_REQUEST", { message: "Birthday must be of type Date." }); + if(!birthday instanceof Date) { + throw new APIError("BAD_REQUEST", { message: "Birthday must be of type Date." }); + } const today = new Date(); const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5)); - if(birthday <= fiveYearsAgo) throw APIError("BAD_REQUEST", { message: "User must be above 5 years old." }); + if(birthday <= fiveYearsAgo) { + throw new APIError("BAD_REQUEST", { message: "User must be above 5 years old." }); + } return { context: ctx }; }), diff --git a/docs/content/docs/plugins/email-otp.mdx b/docs/content/docs/plugins/email-otp.mdx index 33475f92..b618d892 100644 --- a/docs/content/docs/plugins/email-otp.mdx +++ b/docs/content/docs/plugins/email-otp.mdx @@ -3,7 +3,7 @@ title: Email OTP description: Email OTP plugin for Better Auth. --- -The Email OTP plugin allows user to sign-in and verify their email using a one-time password (OTP) sent to their email address. +The Email OTP plugin allows user to sign-in, verify their email, or reset their password using a one-time password (OTP) sent to their email address. ## Installation @@ -50,12 +50,12 @@ The Email OTP plugin allows user to sign-in and verify their email using a one-t ### Send OTP -Before signing in or verifying email, you need to send an OTP to the user's email address. +First, send an OTP to the user's email address. ```ts title="example.ts" await authClient.emailOtp.sendVerificationOtp({ email: "user-email@email.com", - type: "sign-in" // or "email-verification" + type: "sign-in" // or "email-verification", "forget-password" }) ``` @@ -70,8 +70,7 @@ const user = await authClient.signIn.emailOtp({ }) ``` -If the user is not registered, it'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the options. - +If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the options. ### Verify Email @@ -84,12 +83,24 @@ const user = await authClient.emailOtp.verifyEmail({ }) ``` +### Reset Password + +To reset the user's password, use the `resetPassword()` method. + +```ts title="example.ts" +await authClient.emailOtp.resetPassword({ + email: "user-email@email.com", + otp: "123456", + password: "password" +}) +``` + ## Options - `sendVerificationOTP`: A function that sends the OTP to the user's email address. The function receives an object with the following properties: - `email`: The user's email address. - `otp`: The OTP to send. - - `type`: The type of OTP to send. Can be either "sign-in" or "email-verification". + - `type`: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password". ### Example @@ -104,10 +115,12 @@ export const auth = betterAuth({ otp, type }) { - if(type === "sign-in") { + if (type === "sign-in") { // Send the OTP for sign-in - } else { + } else if (type === "email-verification") { // Send the OTP for email verification + } else { + // Send the OTP for password reset } }, }) @@ -115,8 +128,8 @@ export const auth = betterAuth({ }) ``` -- `otpLength`: The length of the OTP. Defaults to 6. -- `otpExpiry`: The expiry time of the OTP in seconds. Defaults to 300 seconds. +- `otpLength`: The length of the OTP. Defaults to `6`. +- `otpExpiry`: The expiry time of the OTP in seconds. Defaults to `300` seconds. ```ts title="auth.ts" import { betterAuth } from "better-auth" @@ -131,6 +144,6 @@ export const auth = betterAuth({ }) ``` -- `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to false. +- `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to `false`. -- `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to false. \ No newline at end of file +- `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to `false`. \ No newline at end of file diff --git a/packages/better-auth/src/plugins/email-otp/index.ts b/packages/better-auth/src/plugins/email-otp/index.ts index 3908cc54..1bc5d4a3 100644 --- a/packages/better-auth/src/plugins/email-otp/index.ts +++ b/packages/better-auth/src/plugins/email-otp/index.ts @@ -1,10 +1,9 @@ -import { INVALID, z } from "zod"; +import { z } from "zod"; import { APIError, createAuthEndpoint } from "../../api"; import type { BetterAuthPlugin, User } from "../../types"; import { alphabet, generateRandomString } from "../../crypto"; import { getDate } from "../../utils/date"; import { setSessionCookie } from "../../cookies"; -import { getEndpointResponse } from "../../utils/plugin-helper"; interface EmailOTPOptions { /** @@ -273,30 +272,30 @@ export const emailOTP = (options: EmailOTPOptions) => { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(email)) { throw new APIError("BAD_REQUEST", { - message: "Invalid email", + message: ERROR_CODES.INVALID_EMAIL, }); } const verificationValue = await ctx.context.internalAdapter.findVerificationValue( `email-verification-otp-${email}`, ); - if (!verificationValue || verificationValue.expiresAt < new Date()) { - if (verificationValue) { - await ctx.context.internalAdapter.deleteVerificationValue( - verificationValue.id, - ); - throw new APIError("BAD_REQUEST", { - message: "OTP expired", - }); - } + if (!verificationValue) { throw new APIError("BAD_REQUEST", { - message: "Invalid OTP", + message: ERROR_CODES.INVALID_OTP, + }); + } + if (verificationValue.expiresAt < new Date()) { + await ctx.context.internalAdapter.deleteVerificationValue( + verificationValue.id, + ); + throw new APIError("BAD_REQUEST", { + message: ERROR_CODES.OTP_EXPIRED, }); } const otp = ctx.body.otp; if (verificationValue.value !== otp) { throw new APIError("BAD_REQUEST", { - message: "Invalid OTP", + message: ERROR_CODES.INVALID_OTP, }); } await ctx.context.internalAdapter.deleteVerificationValue( @@ -305,7 +304,7 @@ export const emailOTP = (options: EmailOTPOptions) => { const user = await ctx.context.internalAdapter.findUserByEmail(email); if (!user) { throw new APIError("BAD_REQUEST", { - message: "User not found", + message: ERROR_CODES.USER_NOT_FOUND, }); } const updatedUser = await ctx.context.internalAdapter.updateUser( @@ -370,20 +369,23 @@ export const emailOTP = (options: EmailOTPOptions) => { await ctx.context.internalAdapter.findVerificationValue( `sign-in-otp-${email}`, ); - if (!verificationValue || verificationValue.expiresAt < new Date()) { - if (verificationValue) { - await ctx.context.internalAdapter.deleteVerificationValue( - verificationValue.id, - ); - } + if (!verificationValue) { throw new APIError("BAD_REQUEST", { - message: "Invalid OTP", + message: ERROR_CODES.INVALID_OTP, + }); + } + if (verificationValue.expiresAt < new Date()) { + await ctx.context.internalAdapter.deleteVerificationValue( + verificationValue.id, + ); + throw new APIError("BAD_REQUEST", { + message: ERROR_CODES.OTP_EXPIRED, }); } const otp = ctx.body.otp; if (verificationValue.value !== otp) { throw new APIError("BAD_REQUEST", { - message: "Invalid OTP", + message: ERROR_CODES.INVALID_OTP, }); } await ctx.context.internalAdapter.deleteVerificationValue( @@ -393,7 +395,7 @@ export const emailOTP = (options: EmailOTPOptions) => { if (!user) { if (opts.disableSignUp) { throw new APIError("BAD_REQUEST", { - message: "User not found", + message: ERROR_CODES.USER_NOT_FOUND, }); } const newUser = await ctx.context.internalAdapter.createUser({ @@ -472,7 +474,7 @@ export const emailOTP = (options: EmailOTPOptions) => { const user = await ctx.context.internalAdapter.findUserByEmail(email); if (!user) { throw new APIError("BAD_REQUEST", { - message: "User not found", + message: ERROR_CODES.USER_NOT_FOUND, }); } const otp = generateRandomString(opts.otpLength, alphabet("0-9")); @@ -544,12 +546,15 @@ export const emailOTP = (options: EmailOTPOptions) => { await ctx.context.internalAdapter.findVerificationValue( `forget-password-otp-${email}`, ); - if (!verificationValue || verificationValue.expiresAt < new Date()) { - if (verificationValue) { - await ctx.context.internalAdapter.deleteVerificationValue( - verificationValue.id, - ); - } + if (!verificationValue) { + throw new APIError("BAD_REQUEST", { + message: ERROR_CODES.INVALID_OTP, + }); + } + if (verificationValue.expiresAt < new Date()) { + await ctx.context.internalAdapter.deleteVerificationValue( + verificationValue.id, + ); throw new APIError("BAD_REQUEST", { message: ERROR_CODES.OTP_EXPIRED, });