diff --git a/packages/better-auth/src/adapters/adapter-factory/index.ts b/packages/better-auth/src/adapters/adapter-factory/index.ts index cd54f2bcb1..90f518f589 100644 --- a/packages/better-auth/src/adapters/adapter-factory/index.ts +++ b/packages/better-auth/src/adapters/adapter-factory/index.ts @@ -16,7 +16,6 @@ import type { } from "./types"; import { colors } from "../../utils/colors"; import type { DBFieldAttribute } from "@better-auth/core/db"; -import { parseUserInput } from "../../db"; export * from "./types"; let debugLogs: { instance: string; args: any[] }[] = []; @@ -314,11 +313,7 @@ export const createAdapterFactory = ) => { const transformedData: Record = {}; const fields = schema[defaultModelName]!.fields; - switch (defaultModelName) { - case "user": { - data = parseUserInput(options, data, action); - } - } + const newMappedKeys = config.mapKeysTransformInput ?? {}; if ( !config.disableIdGeneration && diff --git a/packages/better-auth/src/api/routes/sign-up.test.ts b/packages/better-auth/src/api/routes/sign-up.test.ts index 3b670b2871..1e7eec64ac 100644 --- a/packages/better-auth/src/api/routes/sign-up.test.ts +++ b/packages/better-auth/src/api/routes/sign-up.test.ts @@ -24,6 +24,12 @@ describe("sign-up with custom fields", async (it) => { isAdmin: { type: "boolean", defaultValue: true, + input: false, + }, + role: { + input: false, + type: "string", + required: false, }, }, }, @@ -95,4 +101,51 @@ describe("sign-up with custom fields", async (it) => { ipAddress: "127.0.0.1", }); }); + + it("should rollback when session creation fails", async ({ skip }) => { + const ctx = await auth.$context; + if (!ctx.adapter.options?.adapterConfig.transaction) { + skip(); + } + const originalCreateSession = ctx.internalAdapter.createSession; + ctx.internalAdapter.createSession = vi + .fn() + .mockRejectedValue(new Error("Session creation failed")); + + await expect( + auth.api.signUpEmail({ + body: { + email: "rollback@test.com", + password: "password", + name: "Rollback Test", + }, + }), + ).rejects.toThrow(); + + const users = await db.findMany({ model: "user" }); + const rollbackUser = users.find( + (u: any) => u.email === "rollback@test.com", + ); + expect(rollbackUser).toBeUndefined(); + + ctx.internalAdapter.createSession = originalCreateSession; + }); + + it("should not allow user to set the field that is set to input: false", async () => { + const res = await auth.api.signUpEmail({ + body: { + email: "input-false@test.com", + password: "password", + name: "Input False Test", + //@ts-expect-error + role: "admin", + }, + }); + const session = await auth.api.getSession({ + headers: new Headers({ + authorization: `Bearer ${res.token}`, + }), + }); + expect(session?.user.role).toBeNull(); + }); }); diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts index f4b073f8c1..057cf52399 100644 --- a/packages/better-auth/src/api/routes/sign-up.ts +++ b/packages/better-auth/src/api/routes/sign-up.ts @@ -10,6 +10,7 @@ import type { } from "../../types"; import { BASE_ERROR_CODES } from "../../error/codes"; import { isDevelopment } from "../../utils/env"; +import { parseUserInput } from "../../db"; export const signUpEmail = () => createAuthEndpoint( @@ -169,15 +170,8 @@ export const signUpEmail = () => } & { [key: string]: any; }; - const { - name, - email, - password, - image, - callbackURL, - rememberMe, - ...rest - } = body; + const { name, email, password, image, callbackURL, rememberMe, ...rest } = + body; const isValidEmail = z.email().safeParse(email); if (!isValidEmail.success) { @@ -194,114 +188,113 @@ export const signUpEmail = () => }); } - const maxPasswordLength = ctx.context.password.config.maxPasswordLength; - if (password.length > maxPasswordLength) { - ctx.context.logger.error("Password is too long"); - throw new APIError("BAD_REQUEST", { - message: BASE_ERROR_CODES.PASSWORD_TOO_LONG, - }); - } - const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); - if (dbUser?.user) { - ctx.context.logger.info( - `Sign-up attempt for existing email: ${email}`, - ); - throw new APIError("UNPROCESSABLE_ENTITY", { - message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL, - }); - } - /** - * Hash the password - * - * This is done prior to creating the user - * to ensure that any plugin that - * may break the hashing should break - * before the user is created. - */ - const hash = await ctx.context.password.hash(password); - let createdUser: User; - try { - createdUser = await ctx.context.internalAdapter.createUser( - { - ...rest, - email: email.toLowerCase(), - name, - image, - emailVerified: false, - }, - ctx, - ); - if (!createdUser) { - throw new APIError("BAD_REQUEST", { - message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, - }); - } - } catch (e) { - if (isDevelopment) { - ctx.context.logger.error("Failed to create user", e); - } - if (e instanceof APIError) { - throw e; - } - ctx.context.logger?.error("Failed to create user", e); - throw new APIError("UNPROCESSABLE_ENTITY", { - message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, - details: e, - }); - } - if (!createdUser) { - throw new APIError("UNPROCESSABLE_ENTITY", { - message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, - }); - } - await ctx.context.internalAdapter.linkAccount( + const maxPasswordLength = ctx.context.password.config.maxPasswordLength; + if (password.length > maxPasswordLength) { + ctx.context.logger.error("Password is too long"); + throw new APIError("BAD_REQUEST", { + message: BASE_ERROR_CODES.PASSWORD_TOO_LONG, + }); + } + const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); + if (dbUser?.user) { + ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`); + throw new APIError("UNPROCESSABLE_ENTITY", { + message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL, + }); + } + /** + * Hash the password + * + * This is done prior to creating the user + * to ensure that any plugin that + * may break the hashing should break + * before the user is created. + */ + const hash = await ctx.context.password.hash(password); + let createdUser: User; + try { + const data = parseUserInput(ctx.context.options, rest, "create"); + createdUser = await ctx.context.internalAdapter.createUser( { - userId: createdUser.id, - providerId: "credential", - accountId: createdUser.id, - password: hash, + email: email.toLowerCase(), + name, + image, + ...data, + emailVerified: false, }, ctx, ); - if ( - ctx.context.options.emailVerification?.sendOnSignUp || - ctx.context.options.emailAndPassword.requireEmailVerification - ) { - const token = await createEmailVerificationToken( - ctx.context.secret, - createdUser.email, - undefined, - ctx.context.options.emailVerification?.expiresIn, - ); - const url = `${ - ctx.context.baseURL - }/verify-email?token=${token}&callbackURL=${body.callbackURL || "/"}`; - - const args: Parameters< - Required< - Required["emailVerification"] - >["sendVerificationEmail"] - > = ctx.request - ? [ - { - user: createdUser, - url, - token, - }, - ctx.request, - ] - : [ - { - user: createdUser, - url, - token, - }, - ]; - - await ctx.context.options.emailVerification?.sendVerificationEmail?.( - ...args, - ); + if (!createdUser) { + throw new APIError("BAD_REQUEST", { + message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, + }); } + } catch (e) { + if (isDevelopment) { + ctx.context.logger.error("Failed to create user", e); + } + if (e instanceof APIError) { + throw e; + } + ctx.context.logger?.error("Failed to create user", e); + throw new APIError("UNPROCESSABLE_ENTITY", { + message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, + details: e, + }); + } + if (!createdUser) { + throw new APIError("UNPROCESSABLE_ENTITY", { + message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, + }); + } + await ctx.context.internalAdapter.linkAccount( + { + userId: createdUser.id, + providerId: "credential", + accountId: createdUser.id, + password: hash, + }, + ctx, + ); + if ( + ctx.context.options.emailVerification?.sendOnSignUp || + ctx.context.options.emailAndPassword.requireEmailVerification + ) { + const token = await createEmailVerificationToken( + ctx.context.secret, + createdUser.email, + undefined, + ctx.context.options.emailVerification?.expiresIn, + ); + const url = `${ + ctx.context.baseURL + }/verify-email?token=${token}&callbackURL=${body.callbackURL || "/"}`; + + const args: Parameters< + Required< + Required["emailVerification"] + >["sendVerificationEmail"] + > = ctx.request + ? [ + { + user: createdUser, + url, + token, + }, + ctx.request, + ] + : [ + { + user: createdUser, + url, + token, + }, + ]; + + await ctx.context.options.emailVerification?.sendVerificationEmail?.( + ...args, + ); + } if ( ctx.context.options.emailAndPassword.autoSignIn === false || diff --git a/packages/better-auth/src/db/schema.ts b/packages/better-auth/src/db/schema.ts index 1a087bced3..d22dceb282 100644 --- a/packages/better-auth/src/db/schema.ts +++ b/packages/better-auth/src/db/schema.ts @@ -141,7 +141,7 @@ export function parseInputData>( const fields = schema.fields; const parsedData: Record = Object.assign( Object.create(null), - data, + null, ); for (const key in fields) { if (key in data) { @@ -150,6 +150,11 @@ export function parseInputData>( parsedData[key] = fields[key]!.defaultValue; continue; } + if (parsedData[key]) { + throw new APIError("BAD_REQUEST", { + message: `${key} is not allowed to be set`, + }); + } continue; } if (fields[key]!.validator?.input && data[key] !== undefined) {