UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

270 lines (269 loc) • 9.99 kB
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 };