diff --git a/dev/bun/_auth.ts b/dev/bun/_auth.ts index 4486251e..fa396b16 100644 --- a/dev/bun/_auth.ts +++ b/dev/bun/_auth.ts @@ -1,5 +1,6 @@ import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; +import { APIError } from "better-auth/api"; import { twoFactor } from "better-auth/plugins"; export const auth = betterAuth({ @@ -11,3 +12,10 @@ export const auth = betterAuth({ ), plugins: [twoFactor()], }); + +try { + await auth.api.signOut(); +} catch (e) { + if (e instanceof APIError) { + } +} diff --git a/docs/content/docs/concepts/api.mdx b/docs/content/docs/concepts/api.mdx index f71a1a72..3eec6f35 100644 --- a/docs/content/docs/concepts/api.mdx +++ b/docs/content/docs/concepts/api.mdx @@ -42,3 +42,24 @@ await auth.api.getSession({ Unlike the client, the server needs the values to be passed as an object with the key `body` for the body, `headers` for the headers, and `query` for the query. + +## Error Handling + +When you call an API endpoint in the server, it will throw an error if the request fails. You can catch the error and handle it as you see fit. The error instance is an instance of `APIError`. + +```ts title="server.ts" +import { APIError } from "better-auth/api"; + +try { + await auth.api.signInEmail({ + body: { + email: "", + password: "" + } + }) +} catch (error) { + if (error instanceof APIError) { + console.log(error.message, error.status) + } +} +``` diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts index ef0adc9b..6d6a9804 100644 --- a/packages/better-auth/src/api/index.ts +++ b/packages/better-auth/src/api/index.ts @@ -254,3 +254,4 @@ export const router = ( export * from "./routes"; export * from "./middlewares"; export * from "./call"; +export { APIError } from "better-call"; diff --git a/packages/better-auth/src/api/routes/forget-password.ts b/packages/better-auth/src/api/routes/forget-password.ts index 61591807..8952ba5f 100644 --- a/packages/better-auth/src/api/routes/forget-password.ts +++ b/packages/better-auth/src/api/routes/forget-password.ts @@ -3,6 +3,7 @@ import { createJWT, parseJWT, type JWT } from "oslo/jwt"; import { validateJWT } from "oslo/jwt"; import { z } from "zod"; import { createAuthEndpoint } from "../call"; +import { APIError } from "better-call"; export const forgetPassword = createAuthEndpoint( "/forget-password", @@ -27,17 +28,14 @@ export const forgetPassword = createAuthEndpoint( ctx.context.logger.error( "Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!", ); - return ctx.json(null, { - status: 400, - statusText: "RESET_PASSWORD_EMAIL_NOT_SENT", - body: { - message: "Reset password isn't enabled", - }, + throw new APIError("BAD_REQUEST", { + message: "Reset password isn't enabled", }); } const { email } = ctx.body; const user = await ctx.context.internalAdapter.findUserByEmail(email); if (!user) { + ctx.context.logger.error("Reset Password: User not found", { email }); //only on the server status is false for the client it's always true //to avoid leaking information return ctx.json( @@ -129,19 +127,9 @@ export const resetPassword = createAuthEndpoint( async (ctx) => { const token = ctx.query?.currentURL.split("?token=")[1]; if (!token) { - return ctx.json( - { - error: "Invalid token", - data: null, - }, - { - status: 400, - statusText: "INVALID_TOKEN", - body: { - message: "Invalid token", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Token not found", + }); } const { newPassword } = ctx.body; try { @@ -175,19 +163,9 @@ export const resetPassword = createAuthEndpoint( newPassword.length > (ctx.context.options.emailAndPassword?.maxPasswordLength || 32) ) { - return ctx.json( - { - data: null, - error: "password is too short or too long", - }, - { - status: 400, - statusText: "INVALID_PASSWORD_LENGTH", - body: { - message: "password is too short or too long", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Password is too short or too long", + }); } const hashedPassword = await ctx.context.password.hash(newPassword); const updatedUser = await ctx.context.internalAdapter.updatePassword( @@ -195,12 +173,8 @@ export const resetPassword = createAuthEndpoint( hashedPassword, ); if (!updatedUser) { - return ctx.json(null, { - status: 400, - statusText: "USER_NOT_FOUND", - body: { - message: "User doesn't have a credential account", - }, + throw new APIError("BAD_REQUEST", { + message: "Failed to update password", }); } return ctx.json( @@ -221,20 +195,10 @@ export const resetPassword = createAuthEndpoint( }, ); } catch (e) { - console.log(e); - return ctx.json( - { - error: "Invalid token", - data: null, - }, - { - status: 400, - statusText: "INVALID_TOKEN", - body: { - message: "Invalid token", - }, - }, - ); + ctx.context.logger.error("Failed to reset password", e); + throw new APIError("BAD_REQUEST", { + message: "Failed to reset password", + }); } }, ); diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts index 7b185790..f5dbb058 100644 --- a/packages/better-auth/src/api/routes/session.ts +++ b/packages/better-auth/src/api/routes/session.ts @@ -2,7 +2,7 @@ import { APIError, type Context } from "better-call"; import { createAuthEndpoint, createAuthMiddleware } from "../call"; import { getDate } from "../../utils/date"; import { deleteSessionCookie, setSessionCookie } from "../../utils/cookies"; -import type { Session, User } from "../../db/schema"; +import type { Session } from "../../db/schema"; import { z } from "zod"; import { getIp } from "../../utils/get-request-ip"; import type { @@ -205,16 +205,18 @@ export const revokeSession = createAuthEndpoint( const id = ctx.body.id; const findSession = await ctx.context.internalAdapter.findSession(id); if (!findSession) { - return ctx.json(null, { status: 400 }); + throw new APIError("BAD_REQUEST", { + message: "Session not found", + }); } if (findSession.session.userId !== ctx.context.session.user.id) { - return ctx.json(null, { status: 403 }); + throw new APIError("UNAUTHORIZED"); } try { await ctx.context.internalAdapter.deleteSession(id); } catch (error) { ctx.context.logger.error(error); - return ctx.json(null, { status: 500 }); + throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true, @@ -238,7 +240,7 @@ export const revokeSessions = createAuthEndpoint( ); } catch (error) { ctx.context.logger.error(error); - return ctx.json(null, { status: 500 }); + throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true, diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts index baceef24..0cb1b59d 100644 --- a/packages/better-auth/src/api/routes/sign-up.ts +++ b/packages/better-auth/src/api/routes/sign-up.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { createAuthEndpoint } from "../call"; import { createEmailVerificationToken } from "./verify-email"; import { setSessionCookie } from "../../utils/cookies"; +import { APIError } from "better-call"; export const signUpEmail = createAuthEndpoint( "/sign-up/email", @@ -23,93 +24,37 @@ export const signUpEmail = createAuthEndpoint( }, async (ctx) => { if (!ctx.context.options.emailAndPassword?.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", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Email and password sign up is not enabled", + }); } const { name, email, password, image } = ctx.body; const isValidEmail = z.string().email().safeParse(email); if (!isValidEmail.success) { - return ctx.json( - { - user: null, - session: null, - error: { - message: "Invalid email address", - }, - }, - { - status: 400, - body: { - message: "Invalid email address", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Invalid email", + }); } const minPasswordLength = ctx.context.password.config.minPasswordLength; if (password.length < minPasswordLength) { ctx.context.logger.error("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" }, - }, - ); + throw new APIError("BAD_REQUEST", { + 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( - { - user: null, - session: null, - error: { - message: "Password is too long", - }, - }, - { - status: 400, - body: { message: "Password is too long" }, - }, - ); + throw new APIError("BAD_REQUEST", { + 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", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "User already exists", + }); } const createdUser = await ctx.context.internalAdapter.createUser({ id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")), @@ -121,21 +66,9 @@ export const signUpEmail = createAuthEndpoint( updatedAt: new Date(), }); if (!createdUser) { - return ctx.json( - { - user: null, - session: null, - error: { - message: "Could not create user", - }, - }, - { - status: 400, - body: { - message: "Could not create user", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Couldn't create user", + }); } /** * Link the account to the user @@ -153,21 +86,9 @@ export const signUpEmail = createAuthEndpoint( ctx.request, ); if (!session) { - return ctx.json( - { - user: null, - session: null, - error: { - message: "Could not create session", - }, - }, - { - status: 400, - body: { - message: "Could not create session", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Couldn't create session", + }); } await setSessionCookie(ctx, session.id); if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) { diff --git a/packages/better-auth/src/api/routes/update-user.ts b/packages/better-auth/src/api/routes/update-user.ts index e02d8c29..565ea66d 100644 --- a/packages/better-auth/src/api/routes/update-user.ts +++ b/packages/better-auth/src/api/routes/update-user.ts @@ -3,6 +3,7 @@ import { createAuthEndpoint } from "../call"; import { alphabet, generateRandomString } from "../../crypto/random"; import { setSessionCookie } from "../../utils/cookies"; import { sessionMiddleware } from "./session"; +import { APIError } from "better-call"; export const updateUser = createAuthEndpoint( "/user/update", @@ -58,9 +59,8 @@ export const changePassword = createAuthEndpoint( const minPasswordLength = ctx.context.password.config.minPasswordLength; if (newPassword.length < minPasswordLength) { ctx.context.logger.error("Password is too short"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too short" }, + throw new APIError("BAD_REQUEST", { + message: "Password is too short", }); } @@ -68,9 +68,8 @@ export const changePassword = createAuthEndpoint( if (newPassword.length > maxPasswordLength) { ctx.context.logger.error("Password is too long"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too long" }, + throw new APIError("BAD_REQUEST", { + message: "Password too long", }); } @@ -81,9 +80,8 @@ export const changePassword = createAuthEndpoint( (account) => account.providerId === "credential" && account.password, ); if (!account || !account.password) { - return ctx.json(null, { - status: 400, - body: { message: "User does not have a password" }, + throw new APIError("BAD_REQUEST", { + message: "User does not have a password", }); } const passwordHash = await ctx.context.password.hash(newPassword); @@ -92,9 +90,8 @@ export const changePassword = createAuthEndpoint( currentPassword, ); if (!verify) { - return ctx.json(null, { - status: 400, - body: { message: "Invalid password" }, + throw new APIError("BAD_REQUEST", { + message: "Incorrect password", }); } await ctx.context.internalAdapter.updateAccount(account.id, { @@ -107,9 +104,8 @@ export const changePassword = createAuthEndpoint( ctx.headers, ); if (!newSession) { - return ctx.json(null, { - status: 500, - body: { message: "Failed to create session" }, + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "Unable to create session", }); } // set the new session cookie @@ -138,9 +134,8 @@ export const setPassword = createAuthEndpoint( const minPasswordLength = ctx.context.password.config.minPasswordLength; if (newPassword.length < minPasswordLength) { ctx.context.logger.error("Password is too short"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too short" }, + throw new APIError("BAD_REQUEST", { + message: "Password is too short", }); } @@ -148,9 +143,8 @@ export const setPassword = createAuthEndpoint( if (newPassword.length > maxPasswordLength) { ctx.context.logger.error("Password is too long"); - return ctx.json(null, { - status: 400, - body: { message: "Password is too long" }, + throw new APIError("BAD_REQUEST", { + message: "Password too long", }); } @@ -171,9 +165,8 @@ export const setPassword = createAuthEndpoint( }); return ctx.json(session.user); } - return ctx.json(null, { - status: 400, - body: { message: "User already has a password" }, + throw new APIError("BAD_REQUEST", { + message: "user already has a password", }); }, ); @@ -197,9 +190,8 @@ export const deleteUser = createAuthEndpoint( (account) => account.providerId === "credential" && account.password, ); if (!account || !account.password) { - return ctx.json(null, { - status: 400, - body: { message: "User does not have a password" }, + throw new APIError("BAD_REQUEST", { + message: "User does not have a password", }); } const verify = await ctx.context.password.verify( @@ -207,9 +199,8 @@ export const deleteUser = createAuthEndpoint( password, ); if (!verify) { - return ctx.json(null, { - status: 400, - body: { message: "Invalid password" }, + throw new APIError("BAD_REQUEST", { + message: "Incorrect password", }); } await ctx.context.internalAdapter.deleteUser(session.user.id); diff --git a/packages/better-auth/src/api/routes/verify-email.ts b/packages/better-auth/src/api/routes/verify-email.ts index 78b396a0..c6063150 100644 --- a/packages/better-auth/src/api/routes/verify-email.ts +++ b/packages/better-auth/src/api/routes/verify-email.ts @@ -2,6 +2,7 @@ import { TimeSpan } from "oslo"; import { createJWT, validateJWT, type JWT } from "oslo/jwt"; import { z } from "zod"; import { createAuthEndpoint } from "../call"; +import { APIError } from "better-call"; export async function createEmailVerificationToken( secret: string, @@ -43,12 +44,8 @@ export const sendVerificationEmail = createAuthEndpoint( ctx.context.logger.error( "Verification email isn't enabled. Pass `sendVerificationEmail` in `emailAndPassword` options to enable it.", ); - return ctx.json(null, { - status: 400, - statusText: "VERIFICATION_EMAIL_NOT_SENT", - body: { - message: "Verification email isn't enabled", - }, + throw new APIError("BAD_REQUEST", { + message: "Verification email isn't enabled", }); } const { email } = ctx.body; diff --git a/packages/better-auth/src/plugins/magic-link/index.ts b/packages/better-auth/src/plugins/magic-link/index.ts index 90e9457c..afb42ac0 100644 --- a/packages/better-auth/src/plugins/magic-link/index.ts +++ b/packages/better-auth/src/plugins/magic-link/index.ts @@ -95,12 +95,8 @@ export const magicLink = (options: MagicLinkOptions) => { if (callbackURL) { throw ctx.redirect(`${callbackURL}?error=INVALID_TOKEN`); } - return ctx.json(null, { - status: 400, - statusText: "INVALID_TOKEN", - body: { - message: "Invalid token", - }, + throw new APIError("BAD_REQUEST", { + message: "Invalid token", }); } const schema = z.object({ @@ -114,12 +110,8 @@ export const magicLink = (options: MagicLinkOptions) => { if (callbackURL) { throw ctx.redirect(`${callbackURL}?error=USER_NOT_FOUND`); } - return ctx.json(null, { - status: 400, - statusText: "USER_NOT_FOUND", - body: { - message: "User not found", - }, + throw new APIError("BAD_REQUEST", { + message: "User not found", }); } const session = await ctx.context.internalAdapter.createSession( @@ -130,12 +122,8 @@ export const magicLink = (options: MagicLinkOptions) => { if (callbackURL) { throw ctx.redirect(`${callbackURL}?error=SESSION_NOT_CREATED`); } - return ctx.json(null, { - status: 400, - statusText: "SESSION NOT CREATED", - body: { - message: "Failed to create session", - }, + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "Unable to create session", }); } await setSessionCookie(ctx, session.id); diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index 432ecf02..e4b5727d 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -128,7 +128,7 @@ describe("organization", async (it) => { headers, }, }); - expect(wrongPerson.error?.status).toBe(400); + expect(wrongPerson.error?.status).toBe(403); const invitation = await client.organization.acceptInvitation({ invitationId: invite.data.id, diff --git a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts index 260b2823..1caac683 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts @@ -6,6 +6,7 @@ import { getOrgAdapter } from "../adapter"; import { orgMiddleware, orgSessionMiddleware } from "../call"; import { role } from "../schema"; import { logger } from "../../../utils/logger"; +import { APIError } from "better-call"; export const createInvitation = createAuthEndpoint( "/organization/invite-member", @@ -24,11 +25,8 @@ export const createInvitation = createAuthEndpoint( logger.warn( "Invitation email is not enabled. Pass `sendInvitationEmail` to the plugin options to enable it.", ); - return ctx.json(null, { - status: 400, - body: { - message: "invitation is not enabled", - }, + throw new APIError("BAD_REQUEST", { + message: "Invitation email is not enabled", }); } @@ -36,11 +34,8 @@ export const createInvitation = createAuthEndpoint( const orgId = ctx.body.organizationId || session.session.activeOrganizationId; if (!orgId) { - return ctx.json(null, { - status: 400, - body: { - message: "Organization id not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization not found", }); } const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions); @@ -49,31 +44,22 @@ export const createInvitation = createAuthEndpoint( organizationId: orgId, }); if (!member) { - return ctx.json(null, { - status: 400, - body: { - message: "User is not a member of this organization!", - }, + throw new APIError("BAD_REQUEST", { + message: "Member not found!", }); } const role = ctx.context.roles[member.role]; if (!role) { - return ctx.json(null, { - status: 400, - body: { - message: "Role not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Role not found!", }); } const canInvite = role.authorize({ invitation: ["create"], }); if (canInvite.error) { - return ctx.json(null, { - body: { - message: "You are not allowed to invite users to this organization", - }, - status: 403, + throw new APIError("FORBIDDEN", { + message: "You are not allowed to invite members", }); } const alreadyMember = await adapter.findMemberByEmail({ @@ -81,11 +67,8 @@ export const createInvitation = createAuthEndpoint( organizationId: orgId, }); if (alreadyMember) { - return ctx.json(null, { - status: 400, - body: { - message: "User is already a member of this organization", - }, + throw new APIError("BAD_REQUEST", { + message: "User is already a member of this organization", }); } const alreadyInvited = await adapter.findPendingInvitation({ @@ -93,11 +76,8 @@ export const createInvitation = createAuthEndpoint( organizationId: orgId, }); if (alreadyInvited.length && !ctx.body.resend) { - return ctx.json(null, { - status: 400, - body: { - message: "User is already invited to this organization", - }, + throw new APIError("BAD_REQUEST", { + message: "User is already invited to this organization", }); } const invitation = await adapter.createInvitation({ @@ -112,11 +92,8 @@ export const createInvitation = createAuthEndpoint( const organization = await adapter.findOrganizationById(orgId); if (!organization) { - return ctx.json(null, { - status: 400, - body: { - message: "Organization not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization not found", }); } @@ -155,19 +132,13 @@ export const acceptInvitation = createAuthEndpoint( invitation.expiresAt < new Date() || invitation.status !== "pending" ) { - return ctx.json(null, { - status: 400, - body: { - message: "Invitation not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Invitation not found!", }); } if (invitation.email !== session.user.email) { - return ctx.json(null, { - status: 400, - body: { - message: "You are not the recipient of the invitation", - }, + throw new APIError("FORBIDDEN", { + message: "You are not the recipient of the invitation", }); } const acceptedI = await adapter.updateInvitation({ @@ -218,19 +189,13 @@ export const rejectInvitation = createAuthEndpoint( invitation.expiresAt < new Date() || invitation.status !== "pending" ) { - return ctx.json(null, { - status: 400, - body: { - message: "Invitation not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Invitation not found!", }); } if (invitation.email !== session.user.email) { - return ctx.json(null, { - status: 400, - body: { - message: "You are not the recipient of the invitation", - }, + throw new APIError("FORBIDDEN", { + message: "You are not the recipient of the invitation", }); } const rejectedI = await adapter.updateInvitation({ @@ -258,11 +223,8 @@ export const cancelInvitation = createAuthEndpoint( const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions); const invitation = await adapter.findInvitationById(ctx.body.invitationId); if (!invitation) { - return ctx.json(null, { - status: 400, - body: { - message: "Invitation not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Invitation not found!", }); } const member = await adapter.findMemberByOrgId({ @@ -270,22 +232,16 @@ export const cancelInvitation = createAuthEndpoint( organizationId: invitation.organizationId, }); if (!member) { - return ctx.json(null, { - status: 400, - body: { - message: "User is not a member of this organization", - }, + throw new APIError("BAD_REQUEST", { + message: "Member not found!", }); } const canCancel = ctx.context.roles[member.role].authorize({ invitation: ["cancel"], }); if (canCancel.error) { - return ctx.json(null, { - status: 403, - body: { - message: "You are not allowed to cancel this invitation", - }, + throw new APIError("FORBIDDEN", { + message: "You are not allowed to cancel this invitation", }); } const canceledI = await adapter.updateInvitation({ @@ -309,11 +265,8 @@ export const getInvitation = createAuthEndpoint( async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) { - return ctx.json(null, { - status: 400, - body: { - message: "User not logged in", - }, + throw new APIError("UNAUTHORIZED", { + message: "Not authenticated", }); } const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions); @@ -323,30 +276,21 @@ export const getInvitation = createAuthEndpoint( invitation.status !== "pending" || invitation.expiresAt < new Date() ) { - return ctx.json(null, { - status: 400, - body: { - message: "Invitation not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Invitation not found!", }); } if (invitation.email !== session.user.email) { - return ctx.json(null, { - status: 400, - body: { - message: "You are not the recipient of the invitation", - }, + throw new APIError("FORBIDDEN", { + message: "You are not the recipient of the invitation", }); } const organization = await adapter.findOrganizationById( invitation.organizationId, ); if (!organization) { - return ctx.json(null, { - status: 400, - body: { - message: "Organization not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization not found", }); } const member = await adapter.findMemberByOrgId({ @@ -354,11 +298,8 @@ export const getInvitation = createAuthEndpoint( organizationId: invitation.organizationId, }); if (!member) { - return ctx.json(null, { - status: 400, - body: { - message: "Inviter is no longer a member of this organization", - }, + throw new APIError("BAD_REQUEST", { + message: "Inviter is no longer a member of the organization", }); } return ctx.json({ diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.ts index 09e161eb..7e5094d7 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-members.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-members.ts @@ -3,6 +3,7 @@ import { createAuthEndpoint } from "../../../api/call"; import { getOrgAdapter } from "../adapter"; import { orgMiddleware, orgSessionMiddleware } from "../call"; import type { Member } from "../schema"; +import { APIError } from "better-call"; export const removeMember = createAuthEndpoint( "/organization/remove-member", @@ -35,20 +36,14 @@ export const removeMember = createAuthEndpoint( organizationId: orgId, }); if (!member) { - return ctx.json(null, { - status: 400, - body: { - message: "Member not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Member not found!", }); } const role = ctx.context.roles[member.role]; if (!role) { - return ctx.json(null, { - status: 400, - body: { - message: "Role not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Role not found!", }); } const isLeaving = @@ -58,11 +53,8 @@ export const removeMember = createAuthEndpoint( isLeaving && member.role === (ctx.context.orgOptions?.creatorRole || "owner"); if (isOwnerLeaving) { - return ctx.json(null, { - status: 400, - body: { - message: "You cannot leave the organization as the owner", - }, + throw new APIError("BAD_REQUEST", { + message: "You cannot leave the organization as the owner", }); } @@ -72,11 +64,8 @@ export const removeMember = createAuthEndpoint( member: ["delete"], }).success; if (!canDeleteMember) { - return ctx.json(null, { - body: { - message: "You are not allowed to delete this member", - }, - status: 403, + throw new APIError("UNAUTHORIZED", { + message: "You are not allowed to delete this member", }); } let existing: Member | null = null; @@ -89,11 +78,8 @@ export const removeMember = createAuthEndpoint( existing = await adapter.findMemberById(ctx.body.memberIdOrEmail); } if (existing?.organizationId !== orgId) { - return ctx.json(null, { - status: 400, - body: { - message: "Member not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Member not found!", }); } await adapter.deleteMember(existing.id); diff --git a/packages/better-auth/src/plugins/organization/routes/crud-org.ts b/packages/better-auth/src/plugins/organization/routes/crud-org.ts index eca3bcfe..6acf0cd4 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-org.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-org.ts @@ -3,6 +3,7 @@ import { createAuthEndpoint } from "../../../api/call"; import { generateId } from "../../../utils/id"; import { getOrgAdapter } from "../adapter"; import { orgMiddleware, orgSessionMiddleware } from "../call"; +import { APIError } from "better-call"; export const createOrganization = createAuthEndpoint( "/organization/create", @@ -33,11 +34,8 @@ export const createOrganization = createAuthEndpoint( : options.allowUserToCreateOrganization; if (!canCreateOrg) { - return ctx.json(null, { - status: 403, - body: { - message: "You are not allowed to create organizations", - }, + throw new APIError("FORBIDDEN", { + message: "You are not allowed to create an organization", }); } const adapter = getOrgAdapter(ctx.context.adapter, options); @@ -51,11 +49,8 @@ export const createOrganization = createAuthEndpoint( : false; if (hasReachedOrgLimit) { - return ctx.json(null, { - status: 403, - body: { - message: "You have reached the maximum number of organizations", - }, + throw new APIError("FORBIDDEN", { + message: "You have reached the organization limit", }); } @@ -63,11 +58,8 @@ export const createOrganization = createAuthEndpoint( ctx.body.slug, ); if (existingOrganization) { - return ctx.json(null, { - status: 400, - body: { - message: "Organization with this slug already exists", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization with this slug already exists", }); } const organization = await adapter.createOrganization({ @@ -104,8 +96,8 @@ export const updateOrganization = createAuthEndpoint( async (ctx) => { const session = await ctx.context.getSession(ctx); if (!session) { - return ctx.json(null, { - status: 401, + throw new APIError("UNAUTHORIZED", { + message: "User not found", }); } const orgId = ctx.body.orgId || session.session.activeOrganizationId; @@ -207,11 +199,8 @@ export const deleteOrganization = createAuthEndpoint( organization: ["delete"], }); if (canDeleteOrg.error) { - return ctx.json(null, { - body: { - message: "You are not allowed to delete this organization", - }, - status: 403, + throw new APIError("FORBIDDEN", { + message: "You are not allowed to delete this organization", }); } if (orgId === session.session.activeOrganizationId) { @@ -239,11 +228,8 @@ export const getFullOrganization = createAuthEndpoint( const session = ctx.context.session; const orgId = ctx.query.orgId || session.session.activeOrganizationId; if (!orgId) { - return ctx.json(null, { - status: 400, - body: { - message: "Organization id not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization not found", }); } const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions); @@ -252,11 +238,8 @@ export const getFullOrganization = createAuthEndpoint( ctx.context.db || undefined, ); if (!organization) { - return ctx.json(null, { - status: 404, - body: { - message: "Organization not found!", - }, + throw new APIError("BAD_REQUEST", { + message: "Organization not found", }); } return ctx.json(organization); @@ -297,11 +280,8 @@ export const setActiveOrganization = createAuthEndpoint( }); if (!isMember) { await adapter.setActiveOrganization(session.session.id, null); - return ctx.json(null, { - status: 400, - body: { - message: "You are not a member of this organization", - }, + throw new APIError("FORBIDDEN", { + message: "You are not a member of this organization", }); } await adapter.setActiveOrganization(session.session.id, orgId); diff --git a/packages/better-auth/src/plugins/passkey/index.ts b/packages/better-auth/src/plugins/passkey/index.ts index 4431f663..1c77da81 100644 --- a/packages/better-auth/src/plugins/passkey/index.ts +++ b/packages/better-auth/src/plugins/passkey/index.ts @@ -264,12 +264,8 @@ export const passkey = (options?: PasskeyOptions) => { ctx.context.secret, ); if (!challengeId) { - return ctx.json(null, { - status: 400, - statusText: "No challenge found", - body: { - message: "No challenge found", - }, + throw new APIError("BAD_REQUEST", { + message: "Challenge not found", }); } @@ -335,11 +331,8 @@ export const passkey = (options?: PasskeyOptions) => { }); } catch (e) { console.log(e); - return ctx.json(null, { - status: 400, - body: { - message: "Registration failed", - }, + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "Failed to verify registration", }); } }, @@ -355,8 +348,8 @@ export const passkey = (options?: PasskeyOptions) => { async (ctx) => { const origin = options?.origin || ctx.headers?.get("origin") || ""; if (!origin) { - return ctx.json(null, { - status: 400, + throw new APIError("BAD_REQUEST", { + message: "origin missing", }); } const resp = ctx.body.response; @@ -365,8 +358,8 @@ export const passkey = (options?: PasskeyOptions) => { ctx.context.secret, ); if (!challengeId) { - return ctx.json(null, { - status: 400, + throw new APIError("BAD_REQUEST", { + message: "Challenge not found", }); } @@ -375,8 +368,8 @@ export const passkey = (options?: PasskeyOptions) => { challengeId, ); if (!data) { - return ctx.json(null, { - status: 400, + throw new APIError("BAD_REQUEST", { + message: "Challenge not found", }); } const { expectedChallenge, callbackURL } = JSON.parse( @@ -392,11 +385,8 @@ export const passkey = (options?: PasskeyOptions) => { ], }); if (!passkey) { - return ctx.json(null, { - status: 401, - body: { - message: "Passkey not found", - }, + throw new APIError("UNAUTHORIZED", { + message: "Passkey not found", }); } try { @@ -418,11 +408,8 @@ export const passkey = (options?: PasskeyOptions) => { }); const { verified } = verification; if (!verified) - return ctx.json(null, { - status: 401, - body: { - message: "verification failed", - }, + throw new APIError("UNAUTHORIZED", { + message: "Authentication failed", }); await ctx.context.adapter.update({ @@ -442,11 +429,8 @@ export const passkey = (options?: PasskeyOptions) => { ctx.request, ); if (!s) { - return ctx.json(null, { - status: 500, - body: { - message: "Failed to create session", - }, + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "Unable to create session", }); } await setSessionCookie(ctx, s.id); @@ -467,11 +451,8 @@ export const passkey = (options?: PasskeyOptions) => { ); } catch (e) { ctx.context.logger.error(e); - return ctx.json(null, { - status: 400, - body: { - message: "Authentication failed", - }, + throw new APIError("BAD_REQUEST", { + message: "Failed to verify authentication", }); } }, diff --git a/packages/better-auth/src/plugins/phone-number/index.ts b/packages/better-auth/src/plugins/phone-number/index.ts index d8a4c448..53fbacdd 100644 --- a/packages/better-auth/src/plugins/phone-number/index.ts +++ b/packages/better-auth/src/plugins/phone-number/index.ts @@ -220,71 +220,66 @@ export const phoneNumber = (options?: { }, ); } - const res = await signUpEmail({ - ...ctx, - //@ts-expect-error - options: { - ...ctx.context.options, - }, - _flag: undefined, - }); - if (res.error) { - return ctx.json( - { - user: null, - session: null, + try { + const res = await signUpEmail({ + ...ctx, + //@ts-expect-error + options: { + ...ctx.context.options, }, - { - status: 400, - body: { - message: res.error.message, - status: 400, - }, - }, - ); - } - if (options?.otp?.sendOTPonSignUp) { - if (!options.otp.sendOTP) { - logger.warn("sendOTP not implemented"); - throw new APIError("NOT_IMPLEMENTED", { - message: "sendOTP not implemented", - }); - } - const code = generateOTP(options?.otp?.otpLength || 6); - await ctx.context.internalAdapter.createVerificationValue({ - value: code, - identifier: ctx.body.phoneNumber, - expiresAt: getDate(opts.otp.expiresIn, "sec"), + _flag: undefined, }); - await options.otp.sendOTP(ctx.body.phoneNumber, code); - } - const updated = await ctx.context.internalAdapter.updateUserByEmail( - res.user.email, - { - [opts.phoneNumber]: ctx.body.phoneNumber, - }, - ); + if (options?.otp?.sendOTPonSignUp) { + if (!options.otp.sendOTP) { + logger.warn("sendOTP not implemented"); + throw new APIError("NOT_IMPLEMENTED", { + message: "sendOTP not implemented", + }); + } + const code = generateOTP(options?.otp?.otpLength || 6); + await ctx.context.internalAdapter.createVerificationValue({ + value: code, + identifier: ctx.body.phoneNumber, + expiresAt: getDate(opts.otp.expiresIn, "sec"), + }); + await options.otp.sendOTP(ctx.body.phoneNumber, code); + } - if (ctx.body.callbackURL) { - return ctx.json( + const updated = await ctx.context.internalAdapter.updateUserByEmail( + res.user.email, { - user: updated, - session: res.session, - }, - { - body: { - url: ctx.body.callbackURL, - redirect: true, - ...res, - }, + [opts.phoneNumber]: ctx.body.phoneNumber, }, ); + + if (ctx.body.callbackURL) { + return ctx.json( + { + user: updated, + session: res.session, + }, + { + body: { + url: ctx.body.callbackURL, + redirect: true, + ...res, + }, + }, + ); + } + return ctx.json({ + user: updated, + session: res.session, + }); + } catch (e) { + if (e instanceof APIError) { + throw e; + } + throw new APIError("INTERNAL_SERVER_ERROR", { + message: "Failed to create user", + }); } - return ctx.json({ - user: updated, - session: res.session, - }); }, ), sendVerificationCode: createAuthEndpoint( @@ -338,31 +333,18 @@ export const phoneNumber = (options?: { if (!otp || otp.expiresAt < new Date()) { if (otp && otp.expiresAt < new Date()) { await ctx.context.internalAdapter.deleteVerificationValue(otp.id); + throw new APIError("BAD_REQUEST", { + message: "OTP expired", + }); } - return ctx.json( - { - status: false, - }, - { - body: { - message: "Invalid code", - }, - status: 400, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "OTP not found", + }); } if (otp.value !== ctx.body.code) { - return ctx.json( - { - status: false, - }, - { - body: { - message: "Invalid code", - }, - status: 400, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Invalid OTP", + }); } await ctx.context.internalAdapter.deleteVerificationValue(otp.id); const user = await ctx.context.adapter.findOne({ diff --git a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts index e0eecd89..2ea0d158 100644 --- a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts +++ b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts @@ -5,6 +5,7 @@ import { sessionMiddleware } from "../../../api"; import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto"; import { verifyTwoFactorMiddleware } from "../verify-middleware"; import type { TwoFactorProvider, UserWithTwoFactor } from "../types"; +import { APIError } from "better-call"; export interface BackupCodeOptions { /** @@ -98,12 +99,9 @@ export const backupCode2fa = (options?: BackupCodeOptions) => { ctx.context.secret, ); if (!validate) { - return ctx.json( - { status: false }, - { - status: 401, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Invalid backup code", + }); } return ctx.json({ status: true }); }, diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index d934e7a1..c148970b 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -11,6 +11,7 @@ import type { TwoFactorOptions, UserWithTwoFactor } from "./types"; import type { Session } from "../../db/schema"; import { TWO_FACTOR_COOKIE_NAME, TRUST_DEVICE_COOKIE_NAME } from "./constant"; import { validatePassword } from "../../utils/password"; +import { APIError } from "better-call"; export const twoFactor = (options?: TwoFactorOptions) => { const totp = totp2fa({ @@ -42,15 +43,9 @@ export const twoFactor = (options?: TwoFactorOptions) => { userId: user.id, }); if (!isPasswordValid) { - return ctx.json( - { status: false }, - { - status: 400, - body: { - message: "Invalid password", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Invalid password", + }); } const secret = generateRandomString(16, alphabet("a-z", "0-9", "-")); const encryptedSecret = symmetricEncrypt({ @@ -95,15 +90,9 @@ export const twoFactor = (options?: TwoFactorOptions) => { userId: user.id, }); if (!isPasswordValid) { - return ctx.json( - { status: false }, - { - status: 400, - body: { - message: "Invalid password", - }, - }, - ); + throw new APIError("BAD_REQUEST", { + message: "Invalid password", + }); } await ctx.context.adapter.update({ model: "user", diff --git a/packages/better-auth/src/plugins/two-factor/totp/index.ts b/packages/better-auth/src/plugins/two-factor/totp/index.ts index 3cd19be5..87321e30 100644 --- a/packages/better-auth/src/plugins/two-factor/totp/index.ts +++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts @@ -112,7 +112,7 @@ export const totp2fa = (options: TOTPOptions) => { } const totp = new TOTPController(opts); const secret = Buffer.from( - await symmetricDecrypt({ + symmetricDecrypt({ key: ctx.context.secret, data: ctx.context.session.user.twoFactorSecret, }), diff --git a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts index b4e7f21f..edbcabe3 100644 --- a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts +++ b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts @@ -111,15 +111,9 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware( return ctx.json({ status: true }); }, invalid: async () => { - return ctx.json( - { status: false }, - { - status: 401, - body: { - message: "Invalid code", - }, - }, - ); + throw new APIError("UNAUTHORIZED", { + message: "invalid two factor authentication", + }); }, session: { id: session.id, diff --git a/studio/next-env.d.ts b/studio/next-env.d.ts deleted file mode 100644 index 40c3d680..00000000 --- a/studio/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.