1use super::endpoint_context::EndpointContext;
4use async_trait::async_trait;
5use better_auth_core::{AuthError, AuthResponse, AuthResult, AuthSchema, CreateUser};
6use better_auth_core::{FieldMap, FieldValue};
7use serde::Serialize;
8use serde_json::Value;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
13#[serde(rename_all = "kebab-case")]
14pub enum UserValidationAction {
15 CreateUser,
16 LinkAccount,
17 SignIn,
18}
19
20#[derive(Debug, Clone, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct UserValidationProvider {
24 pub provider_id: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub profile: Option<Value>,
28}
29
30#[derive(Debug, Clone, Serialize)]
32pub struct UserValidationSource {
33 pub action: UserValidationAction,
34 pub method: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub oauth: Option<UserValidationProvider>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub sso: Option<UserValidationProvider>,
39}
40impl UserValidationSource {
41 pub fn new(method: impl Into<String>, action: UserValidationAction) -> Self {
42 Self {
43 action,
44 method: method.into(),
45 oauth: None,
46 sso: None,
47 }
48 }
49 pub(crate) fn oauth(
50 provider: &str,
51 profile: Option<&Value>,
52 action: UserValidationAction,
53 ) -> Self {
54 Self {
55 oauth: Some(UserValidationProvider {
56 provider_id: provider.to_owned(),
57 profile: profile.cloned(),
58 }),
59 ..Self::new("oauth", action)
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize)]
66pub struct UserValidationData {
67 #[serde(with = "better_auth_core::field_value::serde::map")]
69 pub user: FieldMap,
70 pub source: UserValidationSource,
71}
72
73#[derive(Debug, Clone)]
75pub struct UserValidationRejection {
76 pub error: String,
77 pub error_description: Option<String>,
78}
79impl UserValidationRejection {
80 pub fn new(error: impl Into<String>) -> Self {
81 Self {
82 error: error.into(),
83 error_description: None,
84 }
85 }
86 pub fn with_description(mut self, description: impl Into<String>) -> Self {
87 self.error_description = Some(description.into());
88 self
89 }
90 pub(crate) fn message(&self) -> &str {
91 self.error_description
92 .as_deref()
93 .filter(|value| !value.is_empty())
94 .unwrap_or(&self.error)
95 }
96 pub(crate) fn into_auth_error(self) -> AuthError {
97 match AuthResponse::json(
98 403,
99 &serde_json::json!({"code": self.error, "message": self.message()}),
100 ) {
101 Ok(response) => response.into(),
102 Err(error) => error.into(),
103 }
104 }
105}
106
107#[async_trait]
111pub trait ValidateUserInfo<S: AuthSchema>: Send + Sync {
112 async fn validate(
113 &self,
114 data: &UserValidationData,
115 context: &EndpointContext<'_, S>,
116 ) -> AuthResult<Option<UserValidationRejection>>;
117}
118
119pub(crate) async fn validate<S: AuthSchema>(
120 data: UserValidationData,
121 endpoint: &EndpointContext<'_, S>,
122) -> Result<(), UserValidationRejection> {
123 let Some(callback) = endpoint
124 .auth
125 .extensions
126 .get::<Arc<dyn ValidateUserInfo<S>>>()
127 else {
128 return Ok(());
129 };
130 let invalid_source = if data.source.method.is_empty() {
131 Some("User validation source is required")
132 } else if data.source.method == "oauth"
133 && data
134 .source
135 .oauth
136 .as_ref()
137 .is_none_or(|provider| provider.provider_id.is_empty())
138 {
139 Some("OAuth user validation source requires oauth.providerId")
140 } else if matches!(data.source.method.as_str(), "sso-oidc" | "sso-saml")
141 && data
142 .source
143 .sso
144 .as_ref()
145 .is_none_or(|provider| provider.provider_id.is_empty())
146 {
147 Some("SSO user validation source requires sso.providerId")
148 } else {
149 None
150 };
151 if let Some(message) = invalid_source {
152 return Err(
153 UserValidationRejection::new("validation_source_missing").with_description(message)
154 );
155 }
156 match callback.validate(&data, endpoint).await {
157 Ok(Some(rejection)) if !rejection.error.is_empty() => Err(rejection),
158 Ok(_) => Ok(()),
159 Err(error) => {
160 better_auth_core::observability::logger::current().error(
162 "User validation callback failed",
163 &[better_auth_core::observability::LogArgument::Error(&error)],
164 );
165 Err(UserValidationRejection::new("validation_failed")
166 .with_description("User validation failed"))
167 }
168 }
169}
170
171pub(crate) async fn validate_create<S: AuthSchema>(
172 input: &CreateUser,
173 source: UserValidationSource,
174 endpoint: &EndpointContext<'_, S>,
175) -> Result<(), UserValidationRejection> {
176 if endpoint
177 .auth
178 .extensions
179 .get::<Arc<dyn ValidateUserInfo<S>>>()
180 .is_none()
181 {
182 return Ok(());
183 }
184 let mut user = input.additional_fields.clone();
185 for (name, field) in [("name", &input.name), ("image", &input.image)] {
186 let value = field.field_value();
187 if value.is_undefined() {
188 continue;
189 }
190 let _ = user.insert(name.into(), value);
191 }
192 for (name, value) in [
193 ("id", input.id.as_ref()),
194 ("email", input.email.as_ref()),
195 ("phoneNumber", input.phone_number.as_ref()),
196 ("role", input.role.as_ref()),
197 ] {
198 if let Some(value) = value {
199 let _ = user.insert(
200 name.into(),
201 FieldValue::String(if name == "email" {
202 value.to_lowercase()
203 } else {
204 value.clone()
205 }),
206 );
207 }
208 }
209 for (name, value) in [
210 ("username", &input.username),
211 ("displayUsername", &input.display_username),
212 ] {
213 if let Some(value) = value {
214 let _ = user.insert(
215 name.into(),
216 value
217 .clone()
218 .map(FieldValue::String)
219 .unwrap_or(FieldValue::Null),
220 );
221 }
222 }
223 for (name, value) in [
224 ("banned", input.banned),
225 ("emailVerified", input.email_verified),
226 ("isAnonymous", input.is_anonymous),
227 ("phoneNumberVerified", input.phone_number_verified),
228 ] {
229 if let Some(value) = value {
230 let _ = user.insert(name.into(), FieldValue::Bool(value));
231 }
232 }
233 if let Some(value) = &input.ban_reason {
234 let _ = user.insert("banReason".into(), value.clone().into());
235 }
236 if let Some(value) = &input.ban_expires {
237 let _ = user.insert("banExpires".into(), FieldValue::Date(value.clone()));
238 }
239 if let Some(value) = &input.metadata {
240 let _ = user.insert("metadata".into(), value.clone());
241 }
242 let now = chrono::Utc::now();
243 let _ = user.entry("createdAt".into()).or_insert(FieldValue::Date(
244 input.created_at.clone().unwrap_or_else(|| now.into()),
245 ));
246 let _ = user.entry("updatedAt".into()).or_insert(FieldValue::Date(
247 input.updated_at.clone().unwrap_or_else(|| now.into()),
248 ));
249 validate(UserValidationData { user, source }, endpoint).await
250}
251
252pub(crate) async fn create_user<S: AuthSchema>(
253 mut input: CreateUser,
254 method: &str,
255 endpoint: &EndpointContext<'_, S>,
256) -> AuthResult<better_auth_core::wire::UserView> {
257 validate_create(
258 &input,
259 UserValidationSource::new(method, UserValidationAction::CreateUser),
260 endpoint,
261 )
262 .await
263 .map_err(UserValidationRejection::into_auth_error)?;
264 super::helpers::apply_default_role(endpoint.auth, &mut input);
265 endpoint.auth.database.create_user(input).await
266}