better_auth_api/plugins/email_password/
signup.rs1use crate::plugins::{
2 email_verification::EmailVerificationPlugin,
3 helpers::{apply_default_role, apply_user_create_fields},
4};
5use async_trait::async_trait;
6use better_auth_core::utils::password as password_utils;
7use better_auth_core::{
8 AuthContext, AuthError, AuthRequest, AuthResult, AuthSchema, AuthSession, AuthUser,
9 CreateAccount, CreateSession, CreateUser, FieldMap, RequestMeta, SchemaField, wire::UserView,
10};
11use serde_json::Value;
12
13use super::{EmailPasswordConfig, SignUpRequest, SignUpResponse};
14
15#[async_trait]
17pub trait OnExistingUserSignUp: Send + Sync {
18 async fn on_existing_user_sign_up(
20 &self,
21 user: &UserView,
22 request: Option<&AuthRequest>,
23 ) -> AuthResult<()>;
24}
25
26pub struct SyntheticUserInput {
28 pub core_fields: FieldMap,
30 pub additional_fields: FieldMap,
32 pub id: String,
34}
35
36pub type CustomSyntheticUser = dyn Fn(SyntheticUserInput) -> AuthResult<FieldMap> + Send + Sync;
38
39type SignUpResult = SignUpResponse;
40
41pub(super) fn synthetic_response<S: AuthSchema>(
42 body: &SignUpRequest,
43 create: &CreateUser,
44 config: &EmailPasswordConfig,
45 ctx: &AuthContext<S>,
46) -> AuthResult<SignUpResult> {
47 let now = better_auth_core::FieldDate::from(chrono::Utc::now());
48 let mut core = FieldMap::from_iter([
49 ("name".into(), body.name.clone().into()),
50 ("email".into(), body.email.to_lowercase().into()),
51 ("emailVerified".into(), false.into()),
52 ("image".into(), body.image.clone().into_field()),
53 ("createdAt".into(), now.clone().into()),
54 ("updatedAt".into(), now.into()),
55 ]);
56 let id = ctx
57 .config
58 .advanced
59 .generate_id("user", None)?
60 .filter(|id| !id.is_empty())
61 .unwrap_or_else(|| better_auth_core::id::random_id(None));
62 let data = if let Some(factory) = &config.custom_synthetic_user {
63 factory(SyntheticUserInput {
64 core_fields: core,
65 additional_fields: create.additional_fields.clone(),
66 id,
67 })?
68 } else {
69 core.extend(create.additional_fields.clone());
70 for (name, value) in [
71 ("username", create.username.as_ref()),
72 ("displayUsername", create.display_username.as_ref()),
73 ] {
74 if let Some(value) = value {
75 let _ = core.insert(name.into(), value.clone().into_field());
76 }
77 }
78 if let Some(phone) = &create.phone_number {
79 let _ = core.insert("phoneNumber".into(), phone.clone().into());
80 }
81 let _ = core.insert("id".into(), id.into());
82 core
83 };
84 Ok(SignUpResponse {
85 token: None,
86 user: UserView::synthetic_output(data, &ctx.config.user, &ctx.metadata)?,
87 })
88}
89
90pub(super) async fn sign_up_core<S: AuthSchema>(
94 body: &SignUpRequest,
95 endpoint_body: Value,
96 config: &EmailPasswordConfig,
97 req: &AuthRequest,
98 ctx: &AuthContext<S>,
99) -> AuthResult<SignUpResult> {
100 if !config.enable_signup {
101 return Err(AuthError::forbidden("User registration is not enabled"));
102 }
103
104 password_utils::validate_password(
105 &body.password,
106 ctx.password_policy.min_length,
107 ctx.password_policy.max_length,
108 ctx,
109 )?;
110
111 let mut create_user = CreateUser::new()
112 .with_email(body.email.to_lowercase())
113 .with_name(&body.name);
114 apply_user_create_fields(ctx, &body.additional_fields, &mut create_user)?;
115 create_user.image = body
116 .image
117 .clone()
118 .map(|value| Some(value).into())
119 .unwrap_or_default();
120 let protect_enumeration = config.require_email_verification || !config.auto_sign_in();
121 if let Some(user) = ctx
122 .database
123 .get_user_by_email(&body.email.to_lowercase())
124 .await?
125 {
126 if !protect_enumeration {
127 return Err(AuthError::UnprocessableEntity(
128 "User already exists. Use another email.".into(),
129 ));
130 }
131 let body = body.clone();
132 let config = config.clone();
133 let request = req.clone();
134 let auth = ctx.clone();
135 return better_auth_core::store::transaction(ctx.database.as_ref(), move |tx| {
136 Box::pin(async move {
137 let _ = password_utils::hash_password(
138 auth.password_policy.hasher.as_ref(),
139 &body.password,
140 )
141 .await?;
142 let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
143 Some(&request),
144 better_auth_core::FieldValue::from_json(endpoint_body)?,
145 &auth,
146 );
147 endpoint.transaction = Some(tx);
148 let task = super::callbacks::delivery(&config, &user, &endpoint)?;
149 better_auth_core::background::run_or_await(
150 task,
151 auth.config.advanced.background_tasks.as_ref(),
152 &auth.config.logger,
153 )
154 .await;
155 synthetic_response(&body, &create_user, &config, &auth)
156 })
157 })
158 .await;
159 }
160 let password_hash =
161 password_utils::hash_password(ctx.password_policy.hasher.as_ref(), &body.password).await?;
162 let synthetic_create = create_user.clone();
163 apply_default_role(ctx, &mut create_user);
164 let auto_sign_in = config.auto_sign_in() && !config.require_email_verification;
165 let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
166 let expires_in = if body.remember_me == Some(false) {
167 chrono::Duration::days(1)
168 } else {
169 ctx.config.session.expires_in()
170 };
171 let ip_address = meta.ip_address.clone();
172 let user_agent = meta.user_agent.clone();
173 let database = ctx.database.clone();
174 let transaction_database = database.clone();
175 let user_config = ctx.config.user.clone();
176 let adapter_user_config = ctx.adapter_user_fields().clone();
177 let user_metadata = ctx.metadata.clone();
178 let supports_native_json = database.supports_native_json();
179
180 let signup_callback_url = body.callback_url.clone();
181 let require_email_verification = config.require_email_verification;
182 let dont_remember = body.remember_me == Some(false);
183 let admission_context = ctx.clone();
184 let admission_request = req.clone();
185 let mut admission_input = synthetic_create.clone();
186 admission_input.email_verified = Some(false);
187 let result: Option<SignUpResult> =
188 better_auth_core::store::transaction(database.as_ref(), move |tx| {
189 let _database = transaction_database.clone();
190 Box::pin(async move {
191 let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
192 Some(&admission_request),
193 better_auth_core::FieldValue::from_json(endpoint_body)?,
194 &admission_context,
195 );
196 endpoint.transaction = Some(tx);
197 let admitted = crate::plugins::user_admission::validate_create(
198 &admission_input,
199 crate::plugins::user_admission::UserValidationSource::new(
200 "email-password",
201 crate::plugins::user_admission::UserValidationAction::CreateUser,
202 ),
203 &endpoint,
204 )
205 .await;
206 if let Err(error) = admitted {
207 if protect_enumeration {
208 return Ok(None);
209 }
210 return Err(error.into_auth_error());
211 }
212 let user = match tx.create_user(create_user).await {
213 Ok(user) => user,
214 Err(error) if protect_enumeration && error.status_code() == 403 => {
215 return Ok(None);
216 }
217 Err(AuthError::Database(_)) => {
218 return Err(AuthError::UnprocessableEntity(
219 "Failed to create user".to_string(),
220 ));
221 }
222 Err(error) => return Err(error),
223 };
224
225 let _ = tx
226 .create_account(CreateAccount {
227 user_id: user.id().into_owned(),
228 account_id: user.id().into_owned(),
229 provider_id: ("credential".to_string()).into(),
230 access_token: Default::default(),
231 refresh_token: Default::default(),
232 id_token: Default::default(),
233 access_token_expires_at: Default::default(),
234 refresh_token_expires_at: Default::default(),
235 scope: Default::default(),
236 password: (Some(password_hash.clone()))
237 .map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
238 .unwrap_or_default(),
239 ..Default::default()
240 })
241 .await?;
242
243 if let Some(verification) =
244 EmailVerificationPlugin::from_context(&admission_context)
245 {
246 verification
247 .send_verification_on_sign_up(
248 &user,
249 require_email_verification,
250 signup_callback_url.as_deref(),
251 &endpoint,
252 )
253 .await?;
254 }
255
256 if auto_sign_in {
257 let session = tx
258 .create_session(CreateSession {
259 additional_fields: Default::default(),
260 user_id: user.id().into_owned(),
261 expires_at: (chrono::Utc::now() + expires_in).into(),
262 ip_address,
263 user_agent,
264 impersonated_by: None,
265 active_organization_id: None,
266 })
267 .await?;
268 let token = session.token().to_string();
269
270 let manager = admission_context.session_manager();
271 let data = manager.internal_data(&user, &session).await?;
272 manager
273 .set_session_cookie_in_transaction(
274 &admission_request,
275 data,
276 Some(dont_remember),
277 tx,
278 )
279 .await?;
280 Ok(Some(SignUpResponse {
281 token: Some(token),
282 user: UserView::with_field_policies(
283 &user,
284 &adapter_user_config,
285 &user_config,
286 &user_metadata,
287 supports_native_json,
288 )
289 .await?
290 .into(),
291 }))
292 } else {
293 Ok(Some(SignUpResponse {
294 token: None,
295 user: UserView::with_field_policies(
296 &user,
297 &adapter_user_config,
298 &user_config,
299 &user_metadata,
300 supports_native_json,
301 )
302 .await?
303 .into(),
304 }))
305 }
306 })
307 })
308 .await?;
309 let Some(response) = result else {
310 return synthetic_response(body, &synthetic_create, config, ctx);
311 };
312 Ok(response)
313}