refactor: move user input parser to api endpoints

This commit is contained in:
Bereket Engida
2025-10-02 08:57:40 -07:00
committed by Alex Yang
parent cdea367b40
commit 593a40f48d
4 changed files with 165 additions and 119 deletions

View File

@@ -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<string, any> = {};
const fields = schema[defaultModelName]!.fields;
switch (defaultModelName) {
case "user": {
data = parseUserInput(options, data, action);
}
}
const newMappedKeys = config.mapKeysTransformInput ?? {};
if (
!config.disableIdGeneration &&

View File

@@ -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();
});
});

View File

@@ -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 = <O extends BetterAuthOptions>() =>
createAuthEndpoint(
@@ -169,15 +170,8 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
} & {
[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 = <O extends BetterAuthOptions>() =>
});
}
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<BetterAuthOptions>["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<BetterAuthOptions>["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 ||

View File

@@ -141,7 +141,7 @@ export function parseInputData<T extends Record<string, any>>(
const fields = schema.fields;
const parsedData: Record<string, any> = Object.assign(
Object.create(null),
data,
null,
);
for (const key in fields) {
if (key in data) {
@@ -150,6 +150,11 @@ export function parseInputData<T extends Record<string, any>>(
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) {