alibi-core 0.10.0

Shared authentication contracts and backend-independent services for Alibi
Documentation
//! Internal helpers for applying admin user-list query semantics.

use crate::entity::AuthUser;
use crate::types::{ListUsersParams, UserFilterValue};
use chrono::{DateTime, Utc};
use std::cmp::Ordering;

fn string_field(user: &impl AuthUser, field: &str) -> Option<String> {
    match field {
        "id" | "_id" => Some(user.id().into_owned()),
        "email" => user.email().map(str::to_owned),
        "name" => user.name().map(str::to_owned),
        "username" => user.username().map(str::to_owned),
        "role" => user.role().map(str::to_owned),
        _ => None,
    }
}

fn bool_field(user: &impl AuthUser, field: &str) -> Option<bool> {
    (field == "banned").then(|| user.banned())
}

fn date_field(user: &impl AuthUser, field: &str) -> Option<DateTime<Utc>> {
    match field {
        "createdAt" => Some(user.created_at()),
        "updatedAt" => Some(user.updated_at()),
        "banExpires" => user.ban_expires(),
        _ => None,
    }
}

fn matches_search(user: &impl AuthUser, params: &ListUsersParams) -> bool {
    let Some(search_value) = params.search_value.as_deref() else {
        return true;
    };

    let field = params.search_field.as_deref().unwrap_or("email");
    let operator = params.search_operator.as_deref().unwrap_or("contains");
    let Some(haystack) = string_field(user, field) else {
        return false;
    };

    match operator {
        "contains" => haystack.contains(search_value),
        "starts_with" => haystack.starts_with(search_value),
        "ends_with" => haystack.ends_with(search_value),
        _ => false,
    }
}

fn parse_bool(value: &str) -> Option<bool> {
    match value {
        "true" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

fn parse_date(value: &str) -> Option<DateTime<Utc>> {
    DateTime::parse_from_rfc3339(value)
        .ok()
        .map(|value| value.with_timezone(&Utc))
}

fn compare_string(lhs: &str, rhs: &str, operator: &str) -> bool {
    match operator {
        "eq" => lhs == rhs,
        "ne" | "not_in" => lhs != rhs,
        "lt" => lhs < rhs,
        "lte" => lhs <= rhs,
        "gt" => lhs > rhs,
        "gte" => lhs >= rhs,
        "contains" => lhs.contains(rhs),
        "starts_with" => lhs.starts_with(rhs),
        "ends_with" => lhs.ends_with(rhs),
        _ => false,
    }
}

fn compare_bool(lhs: bool, rhs: bool, operator: &str) -> bool {
    match operator {
        "eq" => lhs == rhs,
        "ne" => lhs != rhs,
        _ => false,
    }
}

fn compare_date(lhs: DateTime<Utc>, rhs: DateTime<Utc>, operator: &str) -> bool {
    match operator {
        "eq" => lhs == rhs,
        "ne" => lhs != rhs,
        "lt" => lhs < rhs,
        "lte" => lhs <= rhs,
        "gt" => lhs > rhs,
        "gte" => lhs >= rhs,
        _ => false,
    }
}

fn matches_filter(user: &impl AuthUser, params: &ListUsersParams) -> bool {
    let Some(filter_value) = params.filter_value.as_ref() else {
        return true;
    };

    let field = params.filter_field.as_deref().unwrap_or("email");
    let operator = params.filter_operator.as_deref().unwrap_or("eq");

    let filter_value = match filter_value {
        UserFilterValue::Scalar(value) => value.as_str(),
        UserFilterValue::Multiple(values) => {
            let Some(value) = string_field(user, field) else {
                return false;
            };
            return match operator {
                "in" => values.contains(&value),
                "not_in" => !values.contains(&value),
                "contains" | "starts_with" | "ends_with" => {
                    compare_string(&value, &values.join(","), operator)
                }
                _ => false,
            };
        }
    };

    if let Some(value) = string_field(user, field) {
        return compare_string(&value, filter_value, operator);
    }

    if let Some(value) = bool_field(user, field) {
        return parse_bool(filter_value)
            .is_some_and(|filter| compare_bool(value, filter, operator));
    }

    if let Some(value) = date_field(user, field) {
        return parse_date(filter_value)
            .is_some_and(|filter| compare_date(value, filter, operator));
    }

    false
}

fn compare_options<T: Ord + Copy>(lhs: Option<T>, rhs: Option<T>, direction: &str) -> Ordering {
    match direction {
        "asc" => lhs.cmp(&rhs),
        _ => rhs.cmp(&lhs),
    }
}

/// Apply Better Auth admin list-users semantics to a user collection.
#[must_use]
pub fn apply_list_users<T: AuthUser + Clone>(
    users: Vec<T>,
    params: &ListUsersParams,
) -> (Vec<T>, usize) {
    apply(users, params, false)
}

/// Filter and page users whose configured column order was applied by storage.
#[must_use]
pub fn apply_list_users_presorted<T: AuthUser + Clone>(
    users: Vec<T>,
    params: &ListUsersParams,
) -> (Vec<T>, usize) {
    apply(users, params, true)
}

fn apply<T: AuthUser + Clone>(
    mut users: Vec<T>,
    params: &ListUsersParams,
    presorted: bool,
) -> (Vec<T>, usize) {
    users.retain(|user| matches_search(user, params) && matches_filter(user, params));

    let sort_by = params.sort_by.as_deref().unwrap_or("createdAt");
    // An explicit sort field defaults to ascending in the public admin API.
    let default_direction = if params.sort_by.is_some() {
        "asc"
    } else {
        "desc"
    };
    let sort_direction = params
        .sort_direction
        .as_deref()
        .unwrap_or(default_direction);

    if !presorted {
        users.sort_by(|lhs, rhs| match sort_by {
            "id" | "_id" | "email" | "name" | "username" | "role" => compare_options(
                string_field(lhs, sort_by).as_deref(),
                string_field(rhs, sort_by).as_deref(),
                sort_direction,
            ),
            "createdAt" | "updatedAt" | "banExpires" => compare_options(
                date_field(lhs, sort_by),
                date_field(rhs, sort_by),
                sort_direction,
            ),
            "banned" => match sort_direction {
                "asc" => bool_field(lhs, sort_by).cmp(&bool_field(rhs, sort_by)),
                _ => bool_field(rhs, sort_by).cmp(&bool_field(lhs, sort_by)),
            },
            _ => compare_options(
                date_field(lhs, "createdAt"),
                date_field(rhs, "createdAt"),
                sort_direction,
            ),
        });
    }

    let total = users.len();
    let offset = params.offset.unwrap_or(0);
    let limit = params.limit.unwrap_or(total);
    let paged = users.into_iter().skip(offset).take(limit).collect();

    (paged, total)
}