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 sql.push(" WHERE ");
313 match operator {
314 "in" | "not_in" if bindings.is_empty() && numeric_cast.is_none() => {
315 sql.push(if operator == "in" { "1 = 2" } else { "1 = 1" });
316 }
317 "in" => {
318 sql.column(table, column);
319 sql.push(" IN ");
320 bind_filter_values(sql, bindings, numeric_cast);
321 }
322 "not_in" => {
323 sql.column(table, column);
324 sql.push(" NOT IN ");
325 bind_filter_values(sql, bindings, numeric_cast);
326 }
327 "contains" => {
331 sql.compare_model::<S::User>(
332 table,
333 column,
334 " LIKE ",
335 format!("%{}%", operands.join(",")),
336 );
337 }
338 "starts_with" => {
339 sql.compare_model::<S::User>(
340 table,
341 column,
342 " LIKE ",
343 format!("{}%", operands.join(",")),
344 );
345 }
346 "ends_with" => {
347 sql.compare_model::<S::User>(
348 table,
349 column,
350 " LIKE ",
351 format!("%{}", operands.join(",")),
352 );
353 }
354 "eq" | "ne" | "lt" | "lte" | "gt" | "gte" => {
355 let comparison = match operator {
356 "eq" => " = ",
357 "ne" => " <> ",
358 "lt" => " < ",
359 "lte" => " <= ",
360 "gt" => " > ",
361 _ => " >= ",
362 };
363 sql.ident(column);
364 sql.push(comparison);
365 bind_filter_values(sql, bindings, numeric_cast);
366 }
367 _ => return Err(AuthError::bad_request("Unsupported user filter operator")),
368 }
369 Ok(())
370 }
371
372 fn user_lookup(&self, column: &str, value: impl Into<SqlValue>) -> Sql {
373 let mut sql = model::select_model::<S::User>(self.exec());
374 sql.push(" WHERE ");
375 sql.compare_model::<S::User>(<S::User as SqlxModel>::TABLE, column, " = ", value);
376 sql.push(" LIMIT 1");
377 sql
378 }
379}
380
381#[async_trait]
382impl<S> UserStore<S> for SqlxStore<S>
383where
384 S: AuthSchema + Send + Sync,
385 S::User: SqlxUserModel,
386{
387 async fn provider_verification_output(
388 &self,
389 id: &str,
390 ) -> AuthResult<Option<serde_json::Value>> {
391 provider_verification_output::<S::User>(self.exec(), id).await
392 }
393
394 async fn create_user(&self, mut create_user: CreateUser) -> AuthResult<S::User> {
395 create_user.email = create_user.email.map(|email| normalize_user_email(&email));
396 self.create_user_with_connection(
397 self.exec(),
398 None,
399 create_user,
400 alibi_core::store::UserCreationDefaults::default(),
401 )
402 .await
403 }
404
405 async fn create_user_prepared(&self, prepared: PreparedUserCreation) -> AuthResult<S::User> {
406 let (data, defaults) = prepared.into_parts();
407 self.create_user_with_connection(self.exec(), None, data, defaults)
408 .await
409 }
410
411 async fn coerce_user_text_number(&self, input: NumericTextInput) -> AuthResult<String> {
412 let value: SqlValue = match input {
413 NumericTextInput::Integer(value) => value.into(),
414 NumericTextInput::Real(value) if !value.is_nan() => value.into(),
415 NumericTextInput::Real(_) => {
416 return Err(AuthError::bad_request("Numeric text input must not be NaN"));
417 }
418 };
419 let backend = self.exec().engine();
420 let mut sql = Sql::with(backend, "SELECT CAST(");
421 sql.bind(value);
422 sql.push(" AS TEXT) AS value");
423 self.exec()
425 .fetch_scalar::<String>(sql)
426 .await?
427 .ok_or_else(|| AuthError::internal("Numeric text coercion returned no value"))
428 }
429
430 async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<S::User>> {
431 find_user_by_id::<S::User>(self.exec(), id, Lock::None).await
432 }
433
434 async fn list_users_by_ids(&self, ids: &[String]) -> AuthResult<Vec<S::User>> {
435 self.users_by_ids(ids, None).await
436 }
437
438 async fn list_users_by_ids_page(&self, ids: &[String], limit: f64) -> AuthResult<Vec<S::User>> {
439 self.users_by_ids(ids, Some(limit)).await
440 }
441
442 async fn get_user_by_email(&self, email: &str) -> AuthResult<Option<S::User>> {
443 let email = normalize_user_email(email);
444 let sql = self.user_lookup(S::User::email_column(), email);
445 self.exec().fetch_optional(sql).await
446 }
447
448 async fn get_user_by_username(&self, username: &str) -> AuthResult<Option<S::User>> {
449 let Some(column) = S::User::username_column() else {
450 return Ok(None);
451 };
452 let sql = self.user_lookup(column, username);
453 self.exec().fetch_optional(sql).await
454 }
455
456 async fn get_user_by_phone_number(&self, phone_number: &str) -> AuthResult<Option<S::User>> {
457 let column = S::User::phone_number_column()
458 .ok_or_else(|| AuthError::internal("the user schema has no phone-number field"))?;
459 let sql = self.user_lookup(column, phone_number);
460 self.exec().fetch_optional(sql).await
461 }
462
463 async fn update_user(&self, id: &str, mut update: UpdateUser) -> AuthResult<S::User> {
464 update.email = update.email.map(|email| normalize_user_email(&email));
465 _ = S::User::parse_id(id)?;
466 let hook_context = self.hook_context(None);
467 for hook in self.hooks() {
468 if hook
469 .before_update_user(id, &mut update, &hook_context)
470 .await
471 .map_err(alibi_core::store::adapter::callback_error)?
472 .is_cancelled()
473 {
474 return Err(cancelled_by_hook("user update"));
475 }
476 }
477 if let Some(username) = update.username.as_mut() {
478 *username = username.to_lowercase();
479 }
480 let Some(model) = find_user_by_id::<S::User>(self.exec(), id, Lock::None).await? else {
481 return Err(AuthError::UserNotFound);
482 };
483
484 let mut active = model.into_active();
485 let mut fields = std::mem::take(&mut update.additional_fields);
486 fields.apply_adapter_transforms_async().await?;
487 let provider_name = update.provider_name.take();
488 let provider_image = update.provider_image.take();
489 let provider_verification = update.provider_email_verified.take();
490 S::User::apply_update(&mut active, update, Utc::now());
491 stage_provider_verification::<S::User>(
492 &mut active,
493 provider_verification,
494 self.exec().engine(),
495 )?;
496 stage_provider_text::<S::User>(self.exec(), &mut active, provider_name, provider_image)
497 .await?;
498 let backend = self.exec().engine();
499 for (column, value) in S::User::additional_field_bindings(&fields, backend)? {
500 let value = crate::additional_fields::prepare_value(
501 self.exec(),
502 <S::User as SqlxModel>::column_kind(column),
503 value,
504 )
505 .await?;
506 S::User::set_additional_field(&mut active, column, value, backend)?;
507 }
508 S::User::prepare_json_metadata(&mut active, backend)?;
509
510 let user = model::update::<S::User>(self.exec(), &active)
511 .await?
512 .ok_or_else(record_not_updated)?;
513 for hook in self.hooks() {
514 hook.after_update_user(&user, &hook_context)
515 .await
516 .map_err(alibi_core::store::adapter::callback_error)?;
517 }
518 Ok(user)
519 }
520
521 async fn delete_user(&self, id: &str) -> AuthResult<()> {
522 _ = S::User::parse_id(id)?;
523 let Some(user) = self.get_user_by_id(id).await? else {
524 return Err(AuthError::UserNotFound);
525 };
526 let hook_context = self.hook_context(None);
527 for hook in self.hooks() {
528 if hook
529 .before_delete_user(&user, &hook_context)
530 .await?
531 .is_cancelled()
532 {
533 return Ok(());
534 }
535 }
536 let owner = user.id().into_owned();
539 let id = id.to_owned();
540 self.in_transaction(true, async move |tx| {
541 let exec = Exec::tx(tx);
542 _ = find_user_by_id::<S::User>(exec, &id, Lock::Exclusive).await?;
543 super::teams::remove_owned_team_members(tx, &owner, None).await?;
544 super::wallets::remove_owned_wallets(tx, &owner).await?;
545 let mut users = Sql::with(exec.engine(), "DELETE FROM ");
546 users.ident(<S::User as SqlxModel>::TABLE);
547 users.push(" WHERE ");
548 users.compare_model::<S::User>(
549 <S::User as SqlxModel>::TABLE,
550 S::User::id_column(),
551 " = ",
552 S::User::parse_id(&id)?,
553 );
554 _ = exec.execute(users).await?;
555 Ok(())
556 })
557 .await?;
558 for hook in self.hooks() {
559 hook.after_delete_user(&user, &hook_context).await?;
560 }
561 Ok(())
562 }
563
564 async fn list_users(&self, mut params: ListUsersParams) -> AuthResult<(Vec<S::User>, usize)> {
565 let table = <S::User as SqlxModel>::TABLE;
566 let mut sql = model::select_model::<S::User>(self.exec());
567 if let Some(value) = ¶ms.filter_value {
568 let operator = params.filter_operator.as_deref().unwrap_or("eq");
569 if matches!(value, UserFilterValue::Multiple(_))
570 || matches!(operator, "in" | "not_in")
571 || !is_projected_field(params.filter_field.as_deref().unwrap_or("email"))
572 {
573 self.push_user_filter(&mut sql, params.filter_field.as_deref(), operator, value)?;
574 params.filter_value = None;
577 }
578 }
579 let physical_sort = params
582 .sort_by
583 .as_deref()
584 .filter(|field| !is_projected_field(field));
585 let presorted = if let Some(field) = physical_sort {
586 let column = S::User::list_users_column(field).ok_or_else(|| {
587 AuthError::bad_request("User sort field has no configured column")
588 })?;
589 sql.push(" ORDER BY ");
590 sql.column(table, column);
591 sql.push(if params.sort_direction.as_deref() == Some("desc") {
592 " DESC"
593 } else {
594 " ASC"
595 });
596 true
597 } else {
598 false
599 };
600 let models = self.exec().fetch_all(sql).await?;
601
602 Ok(if presorted {
603 alibi_core::user_query::apply_list_users_presorted(models, ¶ms)
604 } else {
605 alibi_core::user_query::apply_list_users(models, ¶ms)
606 })
607 }
608}
609
610fn is_projected_field(field: &str) -> bool {
612 matches!(
613 field,
614 "email"
615 | "name"
616 | "username"
617 | "role"
618 | "banned"
619 | "createdAt"
620 | "updatedAt"
621 | "banExpires"
622 )
623}
624
625fn normalize_user_email(email: &str) -> String {
626 email.to_lowercase()
627}
628
629fn numeric_filter_text(values: &[String]) -> Vec<String> {
633 let numbers = values
634 .iter()
635 .map(|value| {
636 (!alibi_core::utils::javascript::trim(value).is_empty())
637 .then(|| alibi_core::utils::javascript::string_to_number(value))
638 .flatten()
639 .filter(|number| !number.is_nan())
640 })
641 .collect::<Option<Vec<_>>>();
642 numbers.map_or_else(
643 || values.to_vec(),
644 |numbers| {
645 numbers
646 .into_iter()
647 .map(|number| ryu_js::Buffer::new().format(number).to_owned())
648 .collect()
649 },
650 )
651}
652
653fn bind_filter_values(sql: &mut Sql, values: Vec<SqlValue>, cast: Option<&str>) {
654 sql.push("(");
655 for (index, value) in values.into_iter().enumerate() {
656 if index > 0 {
657 sql.push(", ");
658 }
659 sql.bind(value);
660 if let Some(cast) = cast {
661 sql.push("::");
662 sql.push(cast);
663 }
664 }
665 sql.push(")");
666}