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        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        // API keys retain their polymorphic reference. Redemption resolves the
511        // owner and rejects a missing user with the public identity error.
512        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, &params)?;
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, &params)?;
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
587// Source converts a nonempty numeric scalar, or every numeric array operand,
588// to Number before node-postgres serializes the parameter. An invalid array
589// keeps all its original strings. PostgreSQL validates the resulting text.
590fn 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}