import * as z from 'zod/v4'; import { APIError, createAuthEndpoint, createEmailVerificationToken } from 'better-auth/api'; import type { Account, AdditionalUserFieldsInput, BetterAuthOptions, BetterAuthPlugin, User } from 'better-auth/types'; import { parseUserInput } from 'better-auth/db'; import { setSessionCookie } from 'better-auth/cookies'; const customSignUp = () => createAuthEndpoint( '/user/create', { method: 'POST', body: z.record(z.string(), z.any()), metadata: { $Infer: { body: {} as { name: string; email: string; password: string; image?: string; callbackURL?: string; rememberMe?: boolean; } & AdditionalUserFieldsInput }, openapi: { 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'] // token is optional } } } } } } } }, async (ctx) => { if ( !ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp ) { throw new APIError('BAD_REQUEST', { message: 'Email and password sign up is not enabled' }); } const body = ctx.body as unknown as User & { password: string; callbackURL?: string; rememberMe?: boolean; } & { [key: string]: unknown; }; const { name, email, password, image, callbackURL, rememberMe, ...additionalFields } = body; const isValidEmail = z.email().safeParse(email); if (!isValidEmail.success) { throw new APIError('BAD_REQUEST', { message: 'Invalid email' }); } const minPasswordLength = ctx.context.password.config.minPasswordLength; if (password.length < minPasswordLength) { ctx.context.logger.error('Password is too short'); throw new APIError('BAD_REQUEST', { message: 'Password is too short' }); } 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: 'Password is too long' }); } const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); let user: User | undefined = dbUser?.user; const accounts: Account[] | undefined = dbUser?.accounts; // Check if user exists and has an account if (user && accounts && accounts.length > 0) { throw new APIError('BAD_REQUEST', { message: 'Account already exists' }); } const additionalData = parseUserInput( ctx.context.options, additionalFields as Record ); /** * 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); // Create user if it doesn't exist user = user ?? (await ctx.context.internalAdapter.createUser( { email: email.toLowerCase(), name, image, ...additionalData, emailVerified: false }, ctx )); if (!user) { throw new APIError('BAD_REQUEST', { message: 'Failed to create user' }); } await ctx.context.internalAdapter.linkAccount( { userId: user.id, providerId: 'credential', accountId: user.id, password: hash }, ctx ); if ( ctx.context.options.emailVerification?.sendOnSignUp || ctx.context.options.emailAndPassword.requireEmailVerification ) { const token = await createEmailVerificationToken( ctx.context.secret, user.email, undefined, ctx.context.options.emailVerification?.expiresIn ); const url = `${ ctx.context.baseURL }/verify-email?token=${token}&callbackURL=${callbackURL || '/'}`; await ctx.context.options.emailVerification?.sendVerificationEmail?.( { user: user, url, token }, ctx.request ); } if ( ctx.context.options.emailAndPassword.autoSignIn === false || ctx.context.options.emailAndPassword.requireEmailVerification ) { return ctx.json({ token: null, user: { id: user.id, email: user.email, name: user.name, image: user.image, emailVerified: user.emailVerified, createdAt: user.createdAt, updatedAt: user.updatedAt } }); } const session = await ctx.context.internalAdapter.createSession( user.id, ctx, rememberMe === false ); if (!session) { throw new APIError('BAD_REQUEST', { message: 'Failed to create session' }); } await setSessionCookie( ctx, { session, user: user }, rememberMe === false ); return ctx.json({ token: session.token, user: { id: user.id, email: user.email, name: user.name, image: user.image, emailVerified: user.emailVerified, createdAt: user.createdAt, updatedAt: user.updatedAt } }); } ); export const userPlugin = () => ({ id: 'user', endpoints: { createUser: customSignUp() } }) satisfies BetterAuthPlugin;