Skip to main content

alibi_sqlx/store/
users.rs

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/// Row lock applied to a user lookup inside a transaction.
18#[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    // Boolean inputs retain the ordinary typed-model path on every engine.
83    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    /// Bind an admin user-list predicate with the configured SQL semantics.
260    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        // Upstream coerces a scalar string on a boolean field before binding;
281        // array operands keep their original strings.
282        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            // The pinned adapter interpolates the complete array's
327            // comma-joined value into a bound LIKE pattern. Actual SQL
328            // retains backend case, wildcard and NULL semantics.
329            "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) = &params.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        // The database's own CAST decides the stored text.
452        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        let email = normalize_user_email(email);
472        let sql = self.user_lookup(S::User::email_column(), email);
473        self.exec().fetch_optional(sql).await
474    }
475
476    async fn get_user_by_username(&self, username: &str) -> AuthResult<Option<S::User>> {
477        let Some(column) = S::User::username_column() else {
478            return Ok(None);
479        };
480        let sql = self.user_lookup(column, username);
481        self.exec().fetch_optional(sql).await
482    }
483
484    async fn get_user_by_phone_number(&self, phone_number: &str) -> AuthResult<Option<S::User>> {
485        let column = S::User::phone_number_column()
486            .ok_or_else(|| AuthError::internal("the user schema has no phone-number field"))?;
487        let sql = self.user_lookup(column, phone_number);
488        self.exec().fetch_optional(sql).await
489    }
490
491    async fn update_user(&self, id: &str, mut update: UpdateUser) -> AuthResult<S::User> {
492        update.email = update.email.map(|email| normalize_user_email(&email));
493        _ = S::User::parse_id(id)?;
494        let hook_context = self.hook_context(None);
495        for hook in self.hooks() {
496            if hook
497                .before_update_user(id, &mut update, &hook_context)
498                .await
499                .map_err(alibi_core::store::adapter::callback_error)?
500                .is_cancelled()
501            {
502                return Err(cancelled_by_hook("user update"));
503            }
504        }
505        if let Some(username) = update.username.as_mut() {
506            *username = username.to_lowercase();
507        }
508        let Some(model) = find_user_by_id::<S::User>(self.exec(), id, Lock::None).await? else {
509            return Err(AuthError::UserNotFound);
510        };
511
512        let mut active = model.into_active();
513        let mut fields = std::mem::take(&mut update.additional_fields);
514        fields.apply_adapter_transforms_async().await?;
515        let provider_name = update.provider_name.take();
516        let provider_image = update.provider_image.take();
517        let provider_verification = update.provider_email_verified.take();
518        S::User::apply_update(&mut active, update, Utc::now());
519        stage_provider_verification::<S::User>(
520            &mut active,
521            provider_verification,
522            self.exec().engine(),
523        )?;
524        stage_provider_text::<S::User>(self.exec(), &mut active, provider_name, provider_image)
525            .await?;
526        let backend = self.exec().engine();
527        for (column, value) in S::User::additional_field_bindings(&fields, backend)? {
528            let value = crate::additional_fields::prepare_value(
529                self.exec(),
530                <S::User as SqlxModel>::column_kind(column),
531                value,
532            )
533            .await?;
534            S::User::set_additional_field(&mut active, column, value, backend)?;
535        }
536        S::User::prepare_json_metadata(&mut active, backend)?;
537
538        let user = model::update::<S::User>(self.exec(), &active)
539            .await?
540            .ok_or_else(record_not_updated)?;
541        for hook in self.hooks() {
542            hook.after_update_user(&user, &hook_context)
543                .await
544                .map_err(alibi_core::store::adapter::callback_error)?;
545        }
546        Ok(user)
547    }
548
549    async fn delete_user(&self, id: &str) -> AuthResult<()> {
550        _ = S::User::parse_id(id)?;
551        let Some(user) = self.get_user_by_id(id).await? else {
552            return Err(AuthError::UserNotFound);
553        };
554        let hook_context = self.hook_context(None);
555        for hook in self.hooks() {
556            if hook
557                .before_delete_user(&user, &hook_context)
558                .await?
559                .is_cancelled()
560            {
561                return Ok(());
562            }
563        }
564        // API keys retain their polymorphic reference. Redemption resolves the
565        // owner and rejects a missing user with the public identity error.
566        let owner = user.id().into_owned();
567        let id = id.to_owned();
568        self.in_transaction(true, async move |tx| {
569            let exec = Exec::tx(tx);
570            _ = find_user_by_id::<S::User>(exec, &id, Lock::Exclusive).await?;
571            super::teams::remove_owned_team_members(tx, &owner, None).await?;
572            super::wallets::remove_owned_wallets(tx, &owner).await?;
573            let mut users = Sql::with(exec.engine(), "DELETE FROM ");
574            users.ident(<S::User as SqlxModel>::TABLE);
575            users.push(" WHERE ");
576            users.compare_model::<S::User>(
577                <S::User as SqlxModel>::TABLE,
578                S::User::id_column(),
579                " = ",
580                S::User::parse_id(&id)?,
581            );
582            _ = exec.execute(users).await?;
583            Ok(())
584        })
585        .await?;
586        for hook in self.hooks() {
587            hook.after_delete_user(&user, &hook_context).await?;
588        }
589        Ok(())
590    }
591
592    async fn list_users(&self, params: ListUsersParams) -> AuthResult<(Vec<S::User>, usize)> {
593        let table = <S::User as SqlxModel>::TABLE;
594        let mut sql = model::select_model::<S::User>(self.exec());
595        self.user_list_predicates(&mut sql, &params)?;
596        let field = params
597            .sort_by
598            .as_deref()
599            .filter(|field| !field.is_empty())
600            .unwrap_or("createdAt");
601        let column = S::User::list_users_column(field)
602            .ok_or_else(|| AuthError::bad_request("User sort field has no configured column"))?;
603        sql.push(" ORDER BY ");
604        sql.column(table, column);
605        let direction = params.sort_direction.as_deref().unwrap_or_else(|| {
606            if params.sort_by.is_some() {
607                "asc"
608            } else {
609                "desc"
610            }
611        });
612        sql.push(if direction == "desc" { " DESC" } else { " ASC" });
613        let number = |value: usize| value.to_string().parse::<f64>().ok();
614        let limit = params
615            .source_limit
616            .or_else(|| params.limit.and_then(number))
617            .or_else(|| self.find_many_limit().to_string().parse().ok());
618        let offset = params
619            .source_offset
620            .or_else(|| params.offset.and_then(number));
621        super::bind_page(&mut sql, limit, offset);
622        let users = self.exec().fetch_all(sql).await?;
623        let mut count = Sql::with(self.exec().engine(), "SELECT COUNT(*) FROM ");
624        count.ident(table);
625        self.user_list_predicates(&mut count, &params)?;
626        let total = usize::try_from(
627            self.exec()
628                .fetch_scalar::<i64>(count)
629                .await?
630                .unwrap_or_default(),
631        )
632        .map_err(|_| AuthError::internal("User count exceeds usize"))?;
633        Ok((users, total))
634    }
635}
636
637fn normalize_user_email(email: &str) -> String {
638    email.to_lowercase()
639}
640
641// Source converts a nonempty numeric scalar, or every numeric array operand,
642// to Number before node-postgres serializes the parameter. An invalid array
643// keeps all its original strings. PostgreSQL validates the resulting text.
644fn numeric_filter_text(values: &[String]) -> Vec<String> {
645    let numbers = values
646        .iter()
647        .map(|value| {
648            (!alibi_core::utils::javascript::trim(value).is_empty())
649                .then(|| alibi_core::utils::javascript::string_to_number(value))
650                .flatten()
651                .filter(|number| !number.is_nan())
652        })
653        .collect::<Option<Vec<_>>>();
654    numbers.map_or_else(
655        || values.to_vec(),
656        |numbers| {
657            numbers
658                .into_iter()
659                .map(|number| ryu_js::Buffer::new().format(number).to_owned())
660                .collect()
661        },
662    )
663}
664
665fn bind_filter_values(sql: &mut Sql, values: Vec<SqlValue>, cast: Option<&str>) {
666    sql.push("(");
667    for (index, value) in values.into_iter().enumerate() {
668        if index > 0 {
669            sql.push(", ");
670        }
671        sql.bind(value);
672        if let Some(cast) = cast {
673            sql.push("::");
674            sql.push(cast);
675        }
676    }
677    sql.push(")");
678}