diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index 5e6f6b40..768f106a 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -1,5 +1,10 @@ import { betterAuth } from "better-auth"; -import { organization, passkey, twoFactor } from "better-auth/plugins"; +import { + organization, + passkey, + phoneNumber, + twoFactor, +} from "better-auth/plugins"; import { reactInvitationEmail } from "./email/invitation"; import { LibsqlDialect } from "@libsql/kysely-libsql"; import { reactResetPasswordEmail } from "./email/rest-password"; @@ -24,6 +29,7 @@ export const auth = betterAuth({ }, sendEmailVerificationOnSignUp: true, async sendVerificationEmail(email, url) { + console.log("Sending verification email to", email); const res = await resend.emails.send({ from, to: to || email, @@ -34,6 +40,14 @@ export const auth = betterAuth({ }, }, plugins: [ + phoneNumber({ + otp: { + sendOTP(phoneNumber, code) { + console.log(`Sending OTP to ${phoneNumber}: ${code}`); + }, + sendOTPonSignUp: true, + }, + }), organization({ async sendInvitationEmail(data) { const res = await resend.emails.send({ diff --git a/demo/nextjs/package.json b/demo/nextjs/package.json index 1ac0da9c..bc72b0f3 100644 --- a/demo/nextjs/package.json +++ b/demo/nextjs/package.json @@ -52,6 +52,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "1.0.0", + "consola": "^3.2.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.1", "framer-motion": "^11.5.4", diff --git a/packages/better-auth/src/api/routes/callback.ts b/packages/better-auth/src/api/routes/callback.ts index 0fbe9840..21c2a311 100644 --- a/packages/better-auth/src/api/routes/callback.ts +++ b/packages/better-auth/src/api/routes/callback.ts @@ -79,6 +79,7 @@ export const callbackOAuth = createAuthEndpoint( const { callbackURL, currentURL, dontRememberMe } = parsedState.data; if (!user || data.success === false) { + logger.error("Unable to get user info", data.error); throw c.redirect( `${c.context.baseURL}/error?error=oauth_validation_failed`, ); diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts index 351bd231..baceef24 100644 --- a/packages/better-auth/src/api/routes/sign-up.ts +++ b/packages/better-auth/src/api/routes/sign-up.ts @@ -23,53 +23,93 @@ export const signUpEmail = createAuthEndpoint( }, async (ctx) => { if (!ctx.context.options.emailAndPassword?.enabled) { - return ctx.json(null, { - status: 400, - body: { - message: "Email and password is not enabled", + return ctx.json( + { + user: null, + session: null, + error: { + message: "Email and password is not enabled", + }, }, - }); + { + status: 400, + body: { + message: "Email and password is not enabled", + }, + }, + ); } const { name, email, password, image } = ctx.body; const isValidEmail = z.string().email().safeParse(email); if (!isValidEmail.success) { - return ctx.json(null, { - status: 400, - body: { - message: "Invalid email address", + return ctx.json( + { + user: null, + session: null, + error: { + message: "Invalid email address", + }, }, - }); + { + status: 400, + body: { + message: "Invalid email address", + }, + }, + ); } const minPasswordLength = ctx.context.password.config.minPasswordLength; if (password.length < minPasswordLength) { ctx.context.logger.error("Password is too short"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too short" }, - }); + return ctx.json( + { + user: null, + session: null, + error: { + message: "Password is too short", + }, + }, + { + status: 400, + body: { message: "Password is too short" }, + }, + ); } const maxPasswordLength = ctx.context.password.config.maxPasswordLength; if (password.length > maxPasswordLength) { ctx.context.logger.error("Password is too long"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too long" }, - }); - } - - const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); - /** - * hash first to avoid timing attacks - */ - const hash = await ctx.context.password.hash(password); - if (dbUser?.user) { - return ctx.json(null, { - status: 400, - body: { - message: "User already exists", + return ctx.json( + { + user: null, + session: null, + error: { + message: "Password is too long", + }, }, - }); + { + status: 400, + body: { message: "Password is too long" }, + }, + ); + } + const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); + if (dbUser?.user) { + return ctx.json( + { + user: null, + session: null, + error: { + message: "User already exists", + }, + }, + { + status: 400, + body: { + message: "User already exists", + }, + }, + ); } const createdUser = await ctx.context.internalAdapter.createUser({ id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")), @@ -81,16 +121,26 @@ export const signUpEmail = createAuthEndpoint( updatedAt: new Date(), }); if (!createdUser) { - return ctx.json(null, { - status: 400, - body: { - message: "Could not create user", + return ctx.json( + { + user: null, + session: null, + error: { + message: "Could not create user", + }, }, - }); + { + status: 400, + body: { + message: "Could not create user", + }, + }, + ); } /** * Link the account to the user */ + const hash = await ctx.context.password.hash(password); await ctx.context.internalAdapter.linkAccount({ id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")), userId: createdUser.id, @@ -103,12 +153,21 @@ export const signUpEmail = createAuthEndpoint( ctx.request, ); if (!session) { - return ctx.json(null, { - status: 400, - body: { - message: "Could not create session", + return ctx.json( + { + user: null, + session: null, + error: { + message: "Could not create session", + }, }, - }); + { + status: 400, + body: { + message: "Could not create session", + }, + }, + ); } await setSessionCookie(ctx, session.id); if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) { @@ -131,6 +190,7 @@ export const signUpEmail = createAuthEndpoint( { user: createdUser, session, + error: null, }, { body: ctx.body.callbackURL diff --git a/packages/better-auth/src/plugins/phone-number/index.ts b/packages/better-auth/src/plugins/phone-number/index.ts index 2f09aa7a..d8a4c448 100644 --- a/packages/better-auth/src/plugins/phone-number/index.ts +++ b/packages/better-auth/src/plugins/phone-number/index.ts @@ -10,13 +10,7 @@ import { getDate } from "../../utils/date"; import { logger } from "../../utils/logger"; import { setSessionCookie } from "../../utils/cookies"; -interface OTP { - code: string; - phoneNumber: string; - createdAt: Date; -} - -interface UserWithPhoneNumber extends User { +export interface UserWithPhoneNumber extends User { phoneNumber: string; phoneNumberVerified: boolean; } @@ -46,6 +40,12 @@ export const phoneNumber = (options?: { expiresIn?: number; }; enableAutoSignIn?: boolean; + /** + * Function to validate phone number + * + * by default any string is accepted + */ + phoneNumberValidator?: (phoneNumber: string) => boolean; }) => { const opts = { phoneNumber: "phoneNumber", @@ -171,19 +171,77 @@ export const phoneNumber = (options?: { }), }, async (ctx) => { + if ( + options?.phoneNumberValidator && + !options.phoneNumberValidator(ctx.body.phoneNumber) + ) { + return ctx.json( + { + user: null, + session: null, + error: { + message: "Invalid phone number", + }, + }, + { + status: 400, + body: { + message: "Invalid phone number", + status: 400, + }, + }, + ); + } + + const existing = await ctx.context.adapter.findOne({ + model: ctx.context.tables.user.tableName, + where: [ + { + field: opts.phoneNumber, + value: ctx.body.phoneNumber, + }, + ], + }); + if (existing) { + return ctx.json( + { + user: null, + session: null, + error: { + message: "Phone number already exists", + }, + }, + { + status: 400, + body: { + message: "Phone number already exists", + status: 400, + }, + }, + ); + } const res = await signUpEmail({ ...ctx, //@ts-expect-error + options: { + ...ctx.context.options, + }, _flag: undefined, }); - if (!res) { - return ctx.json(null, { - status: 400, - body: { - message: "Sign up failed", - status: 400, + if (res.error) { + return ctx.json( + { + user: null, + session: null, }, - }); + { + status: 400, + body: { + message: res.error.message, + status: 400, + }, + }, + ); } if (options?.otp?.sendOTPonSignUp) { if (!options.otp.sendOTP) { @@ -200,6 +258,7 @@ export const phoneNumber = (options?: { }); await options.otp.sendOTP(ctx.body.phoneNumber, code); } + const updated = await ctx.context.internalAdapter.updateUserByEmail( res.user.email, { diff --git a/packages/better-auth/src/social-providers/github.ts b/packages/better-auth/src/social-providers/github.ts index ade7679c..fe842fd9 100644 --- a/packages/better-auth/src/social-providers/github.ts +++ b/packages/better-auth/src/social-providers/github.ts @@ -117,8 +117,6 @@ export const github = (options: GithubOptions) => { email: profile.email, image: profile.avatar_url, emailVerified, - createdAt: new Date(), - updatedAt: new Date(), }, data: profile, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20802e44..4951b76f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: cmdk: specifier: 1.0.0 version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@19.0.0-rc-7771d3a7-20240827)(react@19.0.0-rc-7771d3a7-20240827) + consola: + specifier: ^3.2.3 + version: 3.2.3 date-fns: specifier: ^3.6.0 version: 3.6.0