better-auth
Version:
The most comprehensive authentication framework for TypeScript.
270 lines (269 loc) • 9.99 kB
JavaScript
import { isAPIError } from "../../utils/is-api-error.mjs";
import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
import { buildSyntheticUserOutput, parseUserInput, parseUserOutput } from "../../db/schema.mjs";
import { setSessionCookie } from "../../cookies/index.mjs";
import { safeCloneRequest } from "../../utils/request.mjs";
import { createEmailVerificationToken } from "./email-verification.mjs";
import { runWithTransaction } from "@better-auth/core/context";
import { isDevelopment } from "@better-auth/core/env";
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { generateId } from "@better-auth/core/utils/id";
import { createAuthEndpoint } from "@better-auth/core/api";
import * as z from "zod";
//#region src/api/routes/sign-up.ts
const signUpEmailBodySchema = z.object({
name: z.string(),
email: z.email(),
password: z.string().nonempty(),
image: z.string().optional(),
callbackURL: z.string().optional(),
rememberMe: z.boolean().optional()
}).and(z.record(z.string(), z.any()));
const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
method: "POST",
operationId: "signUpWithEmailAndPassword",
use: [formCsrfMiddleware],
body: signUpEmailBodySchema,
cloneRequest: true,
metadata: {
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
$Infer: {
body: {},
returned: {}
},
openapi: {
operationId: "signUpWithEmailAndPassword",
description: "Sign up a user using email and password",
requestBody: { content: { "application/json": { schema: {
type: "object",
properties: {
name: {
type: "string",
description: "The name of the user"
},
email: {
type: "string",
description: "The email of the user"
},
password: {
type: "string",
description: "The password of the user"
},
image: {
type: "string",
description: "The profile image URL of the user"
},
callbackURL: {
type: "string",
description: "The URL to use for email verification callback"
},
rememberMe: {
type: "boolean",
description: "If this is false, the session will not be remembered. Default is `true`."
}
},
required: [
"name",
"email",
"password"
]
} } } },
responses: {
"200": {
description: "Successfully created user",
content: { "application/json": { schema: {
type: "object",
properties: {
token: {
type: "string",
nullable: true,
description: "Authentication token for the session"
},
user: {
type: "object",
properties: {
id: {
type: "string",
description: "The unique identifier of the user"
},
email: {
type: "string",
format: "email",
description: "The email address of the user"
},
name: {
type: "string",
description: "The name of the user"
},
image: {
type: "string",
format: "uri",
nullable: true,
description: "The profile image URL of the user"
},
emailVerified: {
type: "boolean",
description: "Whether the email has been verified"
},
createdAt: {
type: "string",
format: "date-time",
description: "When the user was created"
},
updatedAt: {
type: "string",
format: "date-time",
description: "When the user was last updated"
}
},
required: [
"id",
"email",
"name",
"emailVerified",
"createdAt",
"updatedAt"
]
}
},
required: ["user"]
} } }
},
"422": {
description: "Unprocessable Entity. User already exists or failed to create user.",
content: { "application/json": { schema: {
type: "object",
properties: { message: { type: "string" } }
} } }
}
}
}
}
}, async (ctx) => {
return runWithTransaction(ctx.context.adapter, async () => {
if (!ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp) throw APIError.from("BAD_REQUEST", {
message: "Email and password sign up is not enabled",
code: "EMAIL_PASSWORD_SIGN_UP_DISABLED"
});
const body = ctx.body;
const { name, email, password, image, callbackURL: _callbackURL, rememberMe, ...rest } = body;
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL);
if (!password || typeof password !== "string") throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (password.length < minPasswordLength) {
ctx.context.logger.warn("Password is too short");
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_SHORT);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (password.length > maxPasswordLength) {
ctx.context.logger.warn("Password is too long");
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PASSWORD_TOO_LONG);
}
const shouldReturnGenericDuplicateResponse = ctx.context.options.emailAndPassword.requireEmailVerification || ctx.context.options.emailAndPassword.autoSignIn === false;
const shouldSkipAutoSignIn = ctx.context.options.emailAndPassword.autoSignIn === false || shouldReturnGenericDuplicateResponse;
const additionalUserFields = parseUserInput(ctx.context.options, rest, "create");
const normalizedEmail = email.toLowerCase();
const dbUser = await ctx.context.internalAdapter.findUserByEmail(normalizedEmail);
if (dbUser?.user) {
ctx.context.logger.info(`Sign-up attempt for existing email: ${email}`);
if (shouldReturnGenericDuplicateResponse) {
/**
* Hash the password to reduce timing differences
* between existing and non-existing emails.
*/
await ctx.context.password.hash(password);
if (ctx.context.options.emailAndPassword?.onExistingUserSignUp) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailAndPassword.onExistingUserSignUp({ user: dbUser.user }, safeCloneRequest(ctx.request)));
const now = /* @__PURE__ */ new Date();
const generatedId = ctx.context.generateId({ model: "user" }) || generateId();
const coreFields = {
name,
email: normalizedEmail,
emailVerified: false,
image: image ?? null,
createdAt: now,
updatedAt: now
};
const customSyntheticUser = ctx.context.options.emailAndPassword?.customSyntheticUser;
let syntheticUser;
if (customSyntheticUser) {
const additionalFieldKeys = Object.keys(ctx.context.options.user?.additionalFields ?? {});
const additionalFields = {};
for (const key of additionalFieldKeys) if (key in additionalUserFields) additionalFields[key] = additionalUserFields[key];
const customResult = customSyntheticUser({
coreFields,
additionalFields,
id: generatedId
});
syntheticUser = buildSyntheticUserOutput(ctx.context.options, customResult);
} else syntheticUser = buildSyntheticUserOutput(ctx.context.options, {
...coreFields,
...additionalUserFields,
id: generatedId
});
return ctx.json({
token: null,
user: parseUserOutput(ctx.context.options, syntheticUser)
});
}
throw APIError.from("UNPROCESSABLE_ENTITY", 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;
try {
createdUser = await ctx.context.internalAdapter.createUser({
email: normalizedEmail,
name,
image,
...additionalUserFields,
emailVerified: false
});
if (!createdUser) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_CREATE_USER);
} catch (e) {
if (isDevelopment()) ctx.context.logger.error("Failed to create user", e);
if (isAPIError(e)) throw e;
ctx.context.logger?.error("Failed to create user", e);
throw APIError.from("UNPROCESSABLE_ENTITY", BASE_ERROR_CODES.FAILED_TO_CREATE_USER);
}
if (!createdUser) throw APIError.from("UNPROCESSABLE_ENTITY", BASE_ERROR_CODES.FAILED_TO_CREATE_USER);
await ctx.context.internalAdapter.linkAccount({
userId: createdUser.id,
providerId: "credential",
accountId: createdUser.id,
password: hash
});
if (ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification) {
const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn);
const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
user: createdUser,
url,
token
}, safeCloneRequest(ctx.request)));
}
if (shouldSkipAutoSignIn) return ctx.json({
token: null,
user: parseUserOutput(ctx.context.options, createdUser)
});
const session = await ctx.context.internalAdapter.createSession(createdUser.id, rememberMe === false);
if (!session) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
await setSessionCookie(ctx, {
session,
user: createdUser
}, rememberMe === false);
return ctx.json({
token: session.token,
user: parseUserOutput(ctx.context.options, createdUser)
});
});
});
//#endregion
export { signUpEmail };