1use super::{SqlxStore, lock_exclusive, lock_shared};
2use crate::error::record_not_updated;
3use crate::model::{self, SqlxModel};
4use crate::pool::{Exec, SqlxTransaction};
5use crate::schema::{AuthSchema, SqlxUserModel};
6use crate::sql::Sql;
7use crate::value::{ColumnKind, SqlValue};
8use alibi_core::AuthUser;
9use alibi_core::error::{AuthError, AuthResult};
10use alibi_core::store::adapter::cancelled_by_hook;
11use alibi_core::store::{NumericTextInput, UserStore};
12use alibi_core::types::{CreateUser, ListUsersParams, UpdateUser};
13use async_trait::async_trait;
14use chrono::Utc;
15
16#[derive(Clone, Copy)]
18pub(super) enum Lock {
19 None,
20 Shared,
21 Exclusive,
22}
23
24pub(super) async fn find_user_by_id<M: SqlxUserModel>(
25 exec: Exec<'_>,
26 id: &str,
27 lock: Lock,
28) -> AuthResult<Option<M>> {
29 let id = M::parse_id(id)?;
30 let mut sql = model::select_model::<M>(exec);
31 sql.push(" WHERE ");
32 sql.compare_model::<M>(M::TABLE, M::id_column(), " = ", id);
33 sql.push(" LIMIT 1");
34 match lock {
35 Lock::None => {}
36 Lock::Shared => lock_shared(&mut sql),
37 Lock::Exclusive => lock_exclusive(&mut sql),
38 }
39 exec.fetch_optional(sql).await
40}
41
42pub(super) async fn provider_verification_output<M: SqlxUserModel>(
43 exec: Exec<'_>,
44 id: &str,
45) -> AuthResult<Option<serde_json::Value>> {
46 if exec.engine() != crate::pool::Engine::Sqlite {
47 return Ok(None);
48 }
49 let Some(column) = M::PROVIDER_VERIFICATION_COLUMN else {
50 return Ok(None);
51 };
52 let mut sql = Sql::new(exec.engine());
53 sql.push("SELECT CASE typeof(");
54 sql.ident(column);
55 sql.push(") WHEN 'text' THEN json_quote(");
56 sql.ident(column);
57 sql.push(") WHEN 'null' THEN 'null' ELSE CASE WHEN ");
58 sql.ident(column);
59 sql.push(" = 1 THEN 'true' ELSE 'false' END END FROM ");
60 sql.ident(M::TABLE);
61 sql.push(" WHERE ");
62 sql.compare(M::TABLE, M::id_column(), " = ", M::parse_id(id)?);
63 sql.push(" LIMIT 1");
64 exec.fetch_scalar::<String>(sql)
65 .await?
66 .map(|value| {
67 alibi_core::utils::json::from_slice(value.as_bytes())
68 .map_err(|error| AuthError::internal(error.to_string()))
69 })
70 .transpose()
71}
72
73fn stage_provider_verification<M: SqlxUserModel>(
74 active: &mut crate::model::ActiveRow,
75 value: Option<serde_json::Value>,
76 engine: crate::pool::Engine,
77) -> AuthResult<()> {
78 let Some(value) = value else {
79 return Ok(());
80 };
81 if value.is_boolean() {
83 return Ok(());
84 }
85 let column = M::PROVIDER_VERIFICATION_COLUMN.ok_or_else(|| {
86 AuthError::internal("The user model does not expose its provider verification column")
87 })?;
88 let value = match value {
89 serde_json::Value::Null => SqlValue::Bool(None),
90 serde_json::Value::String(value) => SqlValue::Text(Some(value)),
91 serde_json::Value::Number(value) => {
92 if engine == crate::pool::Engine::Postgres {
93 SqlValue::Text(Some(
94 alibi_core::utils::json::number_to_string(&value)
95 .map_err(|error| AuthError::internal(error.to_string()))?,
96 ))
97 } else if let Some(value) = value.as_i64() {
98 SqlValue::BigInt(Some(value))
99 } else {
100 SqlValue::Double(value.as_f64())
101 }
102 }
103 _ => {
104 return Err(AuthError::internal(
105 "Unsupported provider verification SQL parameter",
106 ));
107 }
108 };
109 active.set(column, value);
110 Ok(())
111}
112
113async fn stage_provider_text<M: SqlxUserModel>(
114 exec: Exec<'_>,
115 active: &mut crate::model::ActiveRow,
116 name: Option<serde_json::Value>,
117 image: Option<serde_json::Value>,
118) -> AuthResult<()> {
119 for (field, raw) in [("name", name), ("image", image)] {
120 let Some(raw) = raw else {
121 continue;
122 };
123 if raw.is_array() || raw.is_object() {
124 return Err(AuthError::internal(
125 "Unsupported provider text SQL parameter",
126 ));
127 }
128 let column = if field == "name" {
129 Some(M::name_column())
130 } else {
131 M::list_users_column("image")
132 }
133 .ok_or_else(|| {
134 AuthError::internal("The user model does not expose its provider text column")
135 })?;
136 let value =
137 crate::additional_fields::raw_value(&alibi_core::utils::json::JsValue::from(raw))?;
138 let value =
139 crate::additional_fields::prepare_value(exec, M::column_kind(column), value).await?;
140 active.set(column, value);
141 }
142 Ok(())
143}
144
145impl<S> SqlxStore<S>
146where
147 S: AuthSchema,
148 S::User: SqlxUserModel,
149{
150 async fn create_user_with_connection(
151 &self,
152 exec: Exec<'_>,
153 tx: Option<&SqlxTransaction>,
154 mut create_user: CreateUser,
155 defaults: alibi_core::store::UserCreationDefaults,
156 ) -> AuthResult<S::User> {
157 let hook_context = self.hook_context(tx);
158 for hook in self.hooks() {
159 if hook
160 .before_create_user(&mut create_user, &hook_context)
161 .await?
162 .is_cancelled()
163 {
164 return Err(AuthError::UserCreationCancelled);
165 }
166 }
167 let mut create_user = defaults.apply(create_user)?;
168 if let Some(username) = create_user.username.as_mut() {
169 *username = username.to_lowercase();
170 }
171 let now = Utc::now();
172 let user_id = create_user
173 .id
174 .as_deref()
175 .map(S::User::parse_id)
176 .transpose()?;
177 let mut fields = std::mem::take(&mut create_user.additional_fields);
178 fields.apply_adapter_transforms_async().await?;
179 let provider_name = create_user.provider_name.take();
180 let provider_image = create_user.provider_image.take();
181 let provider_verification = create_user.provider_email_verified.take();
182 let mut active = S::User::new_active(user_id, create_user, now);
183 stage_provider_verification::<S::User>(&mut active, provider_verification, exec.engine())?;
184 stage_provider_text::<S::User>(exec, &mut active, provider_name, provider_image).await?;
185 let backend = exec.engine();
186 for (column, value) in S::User::additional_field_bindings(&fields, backend)? {
187 let value = crate::additional_fields::prepare_value(
188 exec,
189 <S::User as SqlxModel>::column_kind(column),
190 value,
191 )
192 .await?;
193 S::User::set_additional_field(&mut active, column, value, backend)?;
194 }
195 S::User::prepare_json_metadata(&mut active, backend)?;
196
197 let user = model::insert::<S::User>(exec, &active).await?;
198 if tx.is_none() {
199 for hook in self.hooks() {
200 hook.after_create_user(&user, &hook_context).await?;
201 }
202 }
203 Ok(user)
204 }
205
206 pub(crate) async fn create_user_in_tx(
207 &self,
208 tx: &SqlxTransaction,
209 mut create_user: CreateUser,
210 ) -> AuthResult<S::User> {
211 create_user.email = create_user.email.map(|email| normalize_user_email(&email));
212 self.create_user_with_connection(
213 Exec::Tx(tx),
214 Some(tx),
215 create_user,
216 alibi_core::store::UserCreationDefaults::default(),
217 )
218 .await
219 }
220
221 pub(crate) async fn create_user_prepared_in_tx(
222 &self,
223 tx: &SqlxTransaction,
224 prepared: alibi_core::user_validation::PreparedUserCreation,
225 ) -> AuthResult<S::User> {
226 let (data, defaults) = prepared.into_parts();
227 self.create_user_with_connection(Exec::Tx(tx), Some(tx), data, defaults)
228 .await
229 }
230
231 fn user_lookup(&self, column: &str, value: impl Into<SqlValue>) -> Sql {
232 let mut sql = model::select_model::<S::User>(self.exec());
233 sql.push(" WHERE ");
234 sql.compare_model::<S::User>(<S::User as SqlxModel>::TABLE, column, " = ", value);
235 sql.push(" LIMIT 1");
236 sql
237 }
238}
239
240#[async_trait]
241impl<S> UserStore<S> for SqlxStore<S>
242where
243 S: AuthSchema + Send + Sync,
244 S::User: SqlxUserModel,
245{
246 async fn provider_verification_output(
247 &self,
248 id: &str,
249 ) -> AuthResult<Option<serde_json::Value>> {
250 provider_verification_output::<S::User>(self.exec(), id).await
251 }
252
253 async fn create_user(&self, mut create_user: CreateUser) -> AuthResult<S::User> {
254 create_user.email = create_user.email.map(|email| normalize_user_email(&email));
255 self.create_user_with_connection(
256 self.exec(),
257 None,
258 create_user,
259 alibi_core::store::UserCreationDefaults::default(),
260 )
261 .await
262 }
263
264 async fn create_user_prepared(
265 &self,
266 prepared: alibi_core::user_validation::PreparedUserCreation,
267 ) -> AuthResult<S::User> {
268 let (data, defaults) = prepared.into_parts();
269 self.create_user_with_connection(self.exec(), None, data, defaults)
270 .await
271 }
272
273 async fn coerce_user_text_number(&self, input: NumericTextInput) -> AuthResult<String> {
274 let value: SqlValue = match input {
275 NumericTextInput::Integer(value) => value.into(),
276 NumericTextInput::Real(value) if !value.is_nan() => value.into(),
277 NumericTextInput::Real(_) => {
278 return Err(AuthError::bad_request("Numeric text input must not be NaN"));
279 }
280 };
281 let backend = self.exec().engine();
282 let mut sql = Sql::with(backend, "SELECT CAST(");
283 sql.bind(value);
284 sql.push(" AS TEXT) AS value");
285 self.exec()
287 .fetch_scalar::<String>(sql)
288 .await?
289 .ok_or_else(|| AuthError::internal("Numeric text coercion returned no value"))
290 }
291
292 async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<S::User>> {
293 find_user_by_id::<S::User>(self.exec(), id, Lock::None).await
294 }
295
296 async fn list_users_by_ids(&self, ids: &[String]) -> AuthResult<Vec<S::User>> {
297 if ids.is_empty() {
298 return Ok(Vec::new());
299 }
300
301 let user_ids = ids
302 .iter()
303 .map(|id| S::User::parse_id(id))
304 .collect::<AuthResult<Vec<_>>>()?;
305 let mut sql = model::select_model::<S::User>(self.exec());
306 sql.push(" WHERE ");
307 sql.column(<S::User as SqlxModel>::TABLE, S::User::id_column());
308 sql.push(" IN ");
309 sql.bind_list(user_ids);
310 self.exec().fetch_all(sql).await
311 }
312
313 async fn list_users_by_ids_page(&self, ids: &[String], limit: f64) -> AuthResult<Vec<S::User>> {
314 if ids.is_empty() {
315 return Ok(Vec::new());
316 }
317 let user_ids = ids
318 .iter()
319 .map(|id| S::User::parse_id(id))
320 .collect::<AuthResult<Vec<_>>>()?;
321 let mut sql = model::select_model::<S::User>(self.exec());
322 sql.push(" WHERE ");
323 sql.column(<S::User as SqlxModel>::TABLE, S::User::id_column());
324 sql.push(" IN ");
325 sql.bind_list(user_ids);
326 super::bind_page(&mut sql, Some(limit), None);
327 self.exec().fetch_all(sql).await
328 }
329
330 async fn get_user_by_email(&self, email: &str) -> AuthResult<Option<S::User>> {
331 let email = normalize_user_email(email);
332 let sql = self.user_lookup(S::User::email_column(), email);
333 self.exec().fetch_optional(sql).await
334 }
335
336 async fn get_user_by_username(&self, username: &str) -> AuthResult<Option<S::User>> {
337 let Some(column) = S::User::username_column() else {
338 return Ok(None);
339 };
340 let sql = self.user_lookup(column, username);
341 self.exec().fetch_optional(sql).await
342 }
343
344 async fn get_user_by_phone_number(&self, phone_number: &str) -> AuthResult<Option<S::User>> {
345 let column = S::User::phone_number_column()
346 .ok_or_else(|| AuthError::internal("the user schema has no phone-number field"))?;
347 let sql = self.user_lookup(column, phone_number);
348 self.exec().fetch_optional(sql).await
349 }
350
351 async fn update_user(&self, id: &str, mut update: UpdateUser) -> AuthResult<S::User> {
352 update.email = update.email.map(|email| normalize_user_email(&email));
353 drop(S::User::parse_id(id)?);
354 let hook_context = self.hook_context(None);
355 for hook in self.hooks() {
356 if hook
357 .before_update_user(id, &mut update, &hook_context)
358 .await
359 .map_err(alibi_core::store::adapter::callback_error)?
360 .is_cancelled()
361 {
362 return Err(cancelled_by_hook("user update"));
363 }
364 }
365 if let Some(username) = update.username.as_mut() {
366 *username = username.to_lowercase();
367 }
368 let Some(model) = find_user_by_id::<S::User>(self.exec(), id, Lock::None).await? else {
369 return Err(AuthError::UserNotFound);
370 };
371
372 let mut active = model.into_active();
373 let mut fields = std::mem::take(&mut update.additional_fields);
374 fields.apply_adapter_transforms_async().await?;
375 let provider_name = update.provider_name.take();
376 let provider_image = update.provider_image.take();
377 let provider_verification = update.provider_email_verified.take();
378 S::User::apply_update(&mut active, update, Utc::now());
379 stage_provider_verification::<S::User>(
380 &mut active,
381 provider_verification,
382 self.exec().engine(),
383 )?;
384 stage_provider_text::<S::User>(self.exec(), &mut active, provider_name, provider_image)
385 .await?;
386 let backend = self.exec().engine();
387 for (column, value) in S::User::additional_field_bindings(&fields, backend)? {
388 let value = crate::additional_fields::prepare_value(
389 self.exec(),
390 <S::User as SqlxModel>::column_kind(column),
391 value,
392 )
393 .await?;
394 S::User::set_additional_field(&mut active, column, value, backend)?;
395 }
396 S::User::prepare_json_metadata(&mut active, backend)?;
397
398 let user = model::update::<S::User>(self.exec(), &active)
399 .await?
400 .ok_or_else(record_not_updated)?;
401 for hook in self.hooks() {
402 hook.after_update_user(&user, &hook_context)
403 .await
404 .map_err(alibi_core::store::adapter::callback_error)?;
405 }
406 Ok(user)
407 }
408
409 async fn delete_user(&self, id: &str) -> AuthResult<()> {
410 drop(S::User::parse_id(id)?);
411 let Some(user) = self.get_user_by_id(id).await? else {
412 return Err(AuthError::UserNotFound);
413 };
414 let hook_context = self.hook_context(None);
415 for hook in self.hooks() {
416 if hook
417 .before_delete_user(&user, &hook_context)
418 .await?
419 .is_cancelled()
420 {
421 return Err(cancelled_by_hook("user deletion"));
422 }
423 }
424 let owner = user.id().into_owned();
428 let id = id.to_owned();
429 self.in_transaction(true, async move |tx| {
430 let exec = Exec::Tx(tx);
431 drop(find_user_by_id::<S::User>(exec, &id, Lock::Exclusive).await?);
432 super::teams::remove_owned_team_members(tx, &owner, None).await?;
433 super::wallets::remove_owned_wallets(tx, &owner).await?;
434 if super::migrator::has_table(exec, "api_keys").await? {
438 let mut keys = Sql::with(exec.engine(), "DELETE FROM ");
439 keys.ident("api_keys");
440 keys.push(" WHERE ");
441 keys.compare("api_keys", "reference_id", " = ", owner.clone());
442 _ = exec.execute(keys).await?;
443 }
444 let mut users = Sql::with(exec.engine(), "DELETE FROM ");
445 users.ident(<S::User as SqlxModel>::TABLE);
446 users.push(" WHERE ");
447 users.compare_model::<S::User>(
448 <S::User as SqlxModel>::TABLE,
449 S::User::id_column(),
450 " = ",
451 S::User::parse_id(&id)?,
452 );
453 _ = exec.execute(users).await?;
454 Ok(())
455 })
456 .await?;
457 for hook in self.hooks() {
458 hook.after_delete_user(&user, &hook_context).await?;
459 }
460 Ok(())
461 }
462
463 async fn list_users(&self, mut params: ListUsersParams) -> AuthResult<(Vec<S::User>, usize)> {
464 use alibi_core::UserFilterValue;
465 let table = <S::User as SqlxModel>::TABLE;
466 let mut sql = model::select_model::<S::User>(self.exec());
467 if let Some(value) = ¶ms.filter_value {
468 let operator = params.filter_operator.as_deref().unwrap_or("eq");
469 if matches!(value, UserFilterValue::Multiple(_))
470 || matches!(operator, "in" | "not_in")
471 || !matches!(
472 params.filter_field.as_deref().unwrap_or("email"),
473 "email"
474 | "name"
475 | "username"
476 | "role"
477 | "banned"
478 | "createdAt"
479 | "updatedAt"
480 | "banExpires"
481 )
482 {
483 let field = params
484 .filter_field
485 .as_deref()
486 .filter(|field| !field.is_empty())
487 .unwrap_or("email");
488 let column = S::User::list_users_column(field).ok_or_else(|| {
489 AuthError::bad_request("User filter field has no configured column")
490 })?;
491 let operands = match value {
492 UserFilterValue::Multiple(values) => values.as_slice(),
493 UserFilterValue::Scalar(value) if operator != "in" => {
494 std::slice::from_ref(value)
495 }
496 UserFilterValue::Scalar(_) => {
497 return Err(AuthError::bad_request("Value must be an array"));
498 }
499 };
500 let numeric_cast = if self.exec().engine() == crate::pool::Engine::Postgres {
505 match <S::User as SqlxModel>::column_kind(column) {
506 ColumnKind::Int => Some("int4"),
507 ColumnKind::BigInt => Some("int8"),
508 ColumnKind::Float => Some("float4"),
509 ColumnKind::Double => Some("float8"),
510 _ => None,
511 }
512 } else {
513 None
514 };
515 let bindings: Vec<SqlValue> = if numeric_cast.is_some() {
516 numeric_filter_text(operands)
517 .into_iter()
518 .map(Into::into)
519 .collect()
520 } else if matches!(value, UserFilterValue::Scalar(_))
521 && <S::User as SqlxModel>::column_kind(column) == ColumnKind::Boolean
522 {
523 operands
524 .iter()
525 .map(|value_2| (value_2 == "true").into())
526 .collect()
527 } else {
528 operands
529 .iter()
530 .cloned()
531 .map(|value| S::User::column_value(column, value.into()))
532 .collect()
533 };
534 sql.push(" WHERE ");
535 match operator {
536 "in" | "not_in" if bindings.is_empty() && numeric_cast.is_none() => {
537 sql.push(if operator == "in" { "1 = 2" } else { "1 = 1" });
538 }
539 "in" => {
540 sql.column(table, column);
541 sql.push(" IN ");
542 bind_filter_values(&mut sql, bindings, numeric_cast);
543 }
544 "not_in" => {
545 sql.column(table, column);
546 sql.push(" NOT IN ");
547 bind_filter_values(&mut sql, bindings, numeric_cast);
548 }
549 "contains" => {
553 sql.compare_model::<S::User>(
554 table,
555 column,
556 " LIKE ",
557 format!("%{}%", operands.join(",")),
558 );
559 }
560 "starts_with" => {
561 sql.compare_model::<S::User>(
562 table,
563 column,
564 " LIKE ",
565 format!("{}%", operands.join(",")),
566 );
567 }
568 "ends_with" => {
569 sql.compare_model::<S::User>(
570 table,
571 column,
572 " LIKE ",
573 format!("%{}", operands.join(",")),
574 );
575 }
576 "eq" | "ne" | "lt" | "lte" | "gt" | "gte" => {
577 let comparison = match operator {
578 "eq" => " = ",
579 "ne" => " <> ",
580 "lt" => " < ",
581 "lte" => " <= ",
582 "gt" => " > ",
583 _ => " >= ",
584 };
585 sql.ident(column);
586 sql.push(comparison);
587 bind_filter_values(&mut sql, bindings, numeric_cast);
588 }
589 _ => return Err(AuthError::bad_request("Unsupported user filter operator")),
590 }
591 params.filter_value = None;
594 }
595 }
596 let physical_sort = params.sort_by.as_deref().filter(|field| {
599 !matches!(
600 *field,
601 "email"
602 | "name"
603 | "username"
604 | "role"
605 | "banned"
606 | "createdAt"
607 | "updatedAt"
608 | "banExpires"
609 )
610 });
611 let presorted = if let Some(field) = physical_sort {
612 let column = S::User::list_users_column(field).ok_or_else(|| {
613 AuthError::bad_request("User sort field has no configured column")
614 })?;
615 sql.push(" ORDER BY ");
616 sql.column(table, column);
617 sql.push(if params.sort_direction.as_deref() == Some("desc") {
618 " DESC"
619 } else {
620 " ASC"
621 });
622 true
623 } else {
624 false
625 };
626 let models = self.exec().fetch_all(sql).await?;
627
628 Ok(if presorted {
629 alibi_core::user_query::apply_list_users_presorted(models, ¶ms)
630 } else {
631 alibi_core::user_query::apply_list_users(models, ¶ms)
632 })
633 }
634}
635
636fn normalize_user_email(email: &str) -> String {
637 email.to_lowercase()
638}
639
640fn numeric_filter_text(values: &[String]) -> Vec<String> {
644 let numbers = values
645 .iter()
646 .map(|value| {
647 (!alibi_core::utils::javascript::trim(value).is_empty())
648 .then(|| alibi_core::utils::javascript::string_to_number(value))
649 .flatten()
650 .filter(|number| !number.is_nan())
651 })
652 .collect::<Option<Vec<_>>>();
653 numbers.map_or_else(
654 || values.to_vec(),
655 |numbers| {
656 numbers
657 .into_iter()
658 .map(|number| ryu_js::Buffer::new().format(number).to_owned())
659 .collect()
660 },
661 )
662}
663
664fn bind_filter_values(sql: &mut Sql, values: Vec<SqlValue>, cast: Option<&str>) {
665 sql.push("(");
666 for (index, value) in values.into_iter().enumerate() {
667 if index > 0 {
668 sql.push(", ");
669 }
670 sql.bind(value);
671 if let Some(cast) = cast {
672 sql.push("::");
673 sql.push(cast);
674 }
675 }
676 sql.push(")");
677}