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