mirror of
https://github.com/better-auth/better-auth.git
synced 2026-06-09 08:13:56 -05:00
refactor: move user input parser to api endpoints
This commit is contained in:
committed by
Alex Yang
parent
cdea367b40
commit
593a40f48d
@@ -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 &&
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user