use crate::plugins::{email_verification::EmailVerificationPlugin, helpers::apply_default_role};
use async_trait::async_trait;
use better_auth_core::utils::password as password_utils;
use better_auth_core::{
AuthContext, AuthError, AuthRequest, AuthResult, AuthSchema, AuthSession, AuthUser,
CreateAccount, CreateSession, CreateUser, FieldMap, RequestMeta, SchemaField, wire::UserView,
};
use serde_json::Value;
use super::{EmailPasswordConfig, SignUpRequest, SignUpResponse};
#[async_trait]
pub trait OnExistingUserSignUp: Send + Sync {
async fn on_existing_user_sign_up(
&self,
user: &UserView,
request: Option<&AuthRequest>,
) -> AuthResult<()>;
}
pub struct SyntheticUserInput {
pub core_fields: FieldMap,
pub additional_fields: FieldMap,
pub id: String,
}
pub type CustomSyntheticUser = dyn Fn(SyntheticUserInput) -> AuthResult<FieldMap> + Send + Sync;
pub(super) fn synthetic_response<S: AuthSchema>(
body: &SignUpRequest,
create: &CreateUser,
config: &EmailPasswordConfig,
ctx: &AuthContext<S>,
) -> AuthResult<SignUpResponse> {
let now = better_auth_core::FieldDate::from(chrono::Utc::now());
let mut core = FieldMap::from_iter([
("name".into(), body.name.clone().into()),
("email".into(), body.email.to_lowercase().into()),
("emailVerified".into(), false.into()),
("image".into(), body.image.clone().into_field()),
("createdAt".into(), now.clone().into()),
("updatedAt".into(), now.into()),
]);
let id = ctx
.config
.advanced
.generate_id("user", None)?
.filter(|id| !id.is_empty())
.unwrap_or_else(|| better_auth_core::id::random_id(None));
let data = if let Some(factory) = &config.custom_synthetic_user {
factory(SyntheticUserInput {
core_fields: core,
additional_fields: ctx.select_application_user_fields(&create.additional_fields),
id,
})?
} else {
core.extend(create.additional_fields.clone());
let _ = core.insert("id".into(), id.into());
core
};
let user = UserView::synthetic_output(data, ctx.adapter_user_fields(), &ctx.config.user)?;
let mut user = better_auth_core::StructuredCloneContext::new().clone_map(&user)?;
ctx.config.user.filter_returned_fields(&mut user);
Ok(SignUpResponse {
token: better_auth_core::FieldValue::Null,
user,
})
}
pub(super) async fn sign_up_core<S: AuthSchema>(
body: &SignUpRequest,
endpoint_body: Value,
config: &EmailPasswordConfig,
req: &AuthRequest,
ctx: &AuthContext<S>,
) -> AuthResult<SignUpResponse> {
if !config.enable_signup {
return Err(AuthError::forbidden("User registration is not enabled"));
}
password_utils::validate_password(
&body.password,
ctx.password_policy.min_length,
ctx.password_policy.max_length,
ctx,
)?;
let mut create_user = CreateUser::new()
.with_email(body.email.to_lowercase())
.with_name(&body.name);
create_user.assign_user_fields(ctx.parse_user_input(&body.additional_fields, true)?)?;
create_user.image = body
.image
.clone()
.map(|value| Some(value).into())
.unwrap_or_default();
let protect_enumeration = config.require_email_verification || !config.auto_sign_in();
if let Some(user) = ctx
.database
.get_user_by_email(&body.email.to_lowercase())
.await?
{
if !protect_enumeration {
return Err(AuthError::UnprocessableEntity(
"User already exists. Use another email.".into(),
));
}
let body = body.clone();
let config = config.clone();
let request = req.clone();
let auth = ctx.clone();
return better_auth_core::store::transaction(ctx.database.as_ref(), move |tx| {
Box::pin(async move {
let _ = password_utils::hash_password(
auth.password_policy.hasher.as_ref(),
&body.password,
)
.await?;
let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
Some(&request),
better_auth_core::FieldValue::from_json(endpoint_body)?,
&auth,
);
endpoint.transaction = Some(tx);
let task = super::callbacks::delivery(&config, &user, &endpoint)?;
better_auth_core::background::run_or_await(
task,
auth.config.advanced.background_tasks.as_ref(),
auth.logger(),
)
.await?;
synthetic_response(&body, &create_user, &config, &auth)
})
})
.await;
}
let password_hash =
password_utils::hash_password(ctx.password_policy.hasher.as_ref(), &body.password).await?;
let synthetic_create = create_user.clone();
let auto_sign_in = config.auto_sign_in() && !config.require_email_verification;
let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
let expires_in = if body.remember_me == Some(false) {
chrono::Duration::days(1)
} else {
ctx.config.session.expires_in()
};
let ip_address = meta.ip_address.clone();
let user_agent = meta.user_agent.clone();
let database = ctx.database.clone();
let user_config = ctx.config.user.clone();
let adapter_user_config = ctx.adapter_user_fields().clone();
let user_metadata = ctx.metadata.clone();
let supports_native_json = database.supports_native_json();
let signup_callback_url = body.callback_url.clone();
let require_email_verification = config.require_email_verification;
let dont_remember = body.remember_me == Some(false);
let admission_context = ctx.clone();
let admission_request = req.clone();
let signup_body = body.clone();
let signup_config = config.clone();
better_auth_core::store::transaction(database.as_ref(), move |tx| {
Box::pin(async move {
let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
Some(&admission_request),
better_auth_core::FieldValue::from_json(endpoint_body)?,
&admission_context,
);
endpoint.transaction = Some(tx);
let creation = async {
let mut fields = FieldMap::from([
("email".into(), create_user.email.into_field()),
("name".into(), create_user.name.into_field_value()),
("image".into(), create_user.image.into_field_value()),
]);
fields.extend(create_user.additional_fields);
let _ = fields.insert("emailVerified".into(), false.into());
let mut create_user = CreateUser {
additional_fields: fields,
..Default::default()
}
.into_user_fields()?;
if let Some(rejection) = crate::plugins::user_admission::validate_create(
&create_user,
crate::plugins::user_admission::UserValidationSource::new(
"email-password",
crate::plugins::user_admission::UserValidationAction::CreateUser,
),
&endpoint,
)
.await?
{
return Err(rejection.into_auth_error());
}
apply_default_role(&admission_context, &mut create_user);
tx.create_user_fields_optional(create_user)
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"FAILED_TO_CREATE_USER",
"Failed to create user",
)
})
}
.await;
let user = match creation {
Ok(user) => user,
Err(error) => {
if error.is_api_error()? {
if error
.status_code()?
.field_value()
.strict_equals(&403.0.into())
&& protect_enumeration
{
return synthetic_response(
&signup_body,
&synthetic_create,
&signup_config,
&admission_context,
);
}
return Err(error);
}
admission_context.logger().error(
"Failed to create user",
&[better_auth_core::observability::LogArgument::Error(&error)],
)?;
return Err(AuthError::from_code(
"UNPROCESSABLE_ENTITY",
"FAILED_TO_CREATE_USER",
"Failed to create user",
));
}
};
let _ = tx
.create_account_optional(CreateAccount {
user_id: user.id().into_owned(),
account_id: user.id().into_owned(),
provider_id: ("credential".to_string()).into(),
access_token: Default::default(),
refresh_token: Default::default(),
id_token: Default::default(),
access_token_expires_at: Default::default(),
refresh_token_expires_at: Default::default(),
scope: Default::default(),
password: (Some(password_hash.clone()))
.map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
.unwrap_or_default(),
..Default::default()
})
.await?;
if let Some(verification) = EmailVerificationPlugin::from_context(&admission_context) {
verification
.send_verification_on_sign_up(
&user,
require_email_verification,
signup_callback_url.as_deref(),
&endpoint,
)
.await?;
}
if auto_sign_in {
let session = tx
.create_session_optional(CreateSession {
inherited_fields: Default::default(),
additional_fields: Default::default(),
user_id: user.id().into_owned(),
expires_at: (chrono::Utc::now() + expires_in).into(),
ip_address,
user_agent,
impersonated_by: None,
active_organization_id: None,
})
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"FAILED_TO_CREATE_SESSION",
"Failed to create session",
)
})?;
let token = session.token().field_value();
let manager = admission_context.session_manager();
let data = manager.internal_data(&user, &session).await?;
manager
.set_session_cookie_in_transaction(
&admission_request,
data,
Some(dont_remember),
tx,
)
.await?;
Ok(SignUpResponse {
token,
user: UserView::with_field_policies(
&user,
&adapter_user_config,
&user_config,
&user_metadata,
supports_native_json,
)
.await?
.into(),
})
} else {
Ok(SignUpResponse {
token: better_auth_core::FieldValue::Null,
user: UserView::with_field_policies(
&user,
&adapter_user_config,
&user_config,
&user_metadata,
supports_native_json,
)
.await?
.into(),
})
}
})
})
.await
}