better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
mod routes;
pub(crate) use routes::*;

use better_auth_core::{AuthRequest, AuthResult};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};

use super::json_body::{invalid_type, validation_error};

/// Deserialize the effective input at an explicit JSON-only consumer boundary.
pub(crate) fn parse<T: Default + DeserializeOwned>(request: &AuthRequest) -> AuthResult<T> {
    parse_value(request.input_query()?)
}

fn parse_value<T: Default + DeserializeOwned>(query: Option<Value>) -> AuthResult<T> {
    query
        .map(serde_json::from_value)
        .transpose()
        .map(|value| value.unwrap_or_default())
        .map_err(Into::into)
}

fn object(query: Option<Value>, optional: bool) -> AuthResult<Option<Map<String, Value>>> {
    match query {
        None if optional => Ok(None),
        Some(Value::Object(query)) => Ok(Some(query)),
        value => Err(validation_error(&invalid_type(
            "query",
            "object",
            value.as_ref(),
        ))),
    }
}

pub(crate) fn strings(
    query: Option<Value>,
    optional: bool,
    fields: &[(&str, bool)],
) -> AuthResult<Option<Value>> {
    let Some(mut input) = object(query, optional)? else {
        return Ok(None);
    };
    let mut output = Map::new();
    let mut errors = Vec::new();
    for (name, required) in fields {
        match input.remove(*name) {
            Some(value @ Value::String(_)) => {
                let _ = output.insert((*name).to_owned(), value);
            }
            None if !required => {}
            value => errors.push(invalid_type(
                &format!("query.{name}"),
                "string",
                value.as_ref(),
            )),
        }
    }
    finish(output, errors)
}

fn finish(output: Map<String, Value>, errors: Vec<String>) -> AuthResult<Option<Value>> {
    if errors.is_empty() {
        Ok(Some(Value::Object(output)))
    } else {
        Err(validation_error(&errors.join("; ")))
    }
}

pub(crate) fn string_map(
    request: &AuthRequest,
) -> AuthResult<std::collections::HashMap<String, String>> {
    parse(request)
}

pub(crate) fn string_map_value(
    query: Option<Value>,
) -> AuthResult<std::collections::HashMap<String, String>> {
    parse_value(query)
}

pub(crate) fn callback(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(
        query,
        true,
        &[
            ("code", false),
            ("error", false),
            ("device_id", false),
            ("error_description", false),
            ("state", false),
            ("user", false),
            ("iss", false),
        ],
    )
}

pub(crate) fn get_user(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(query, false, &[("id", true)])
}

pub(crate) fn popup(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(
        query,
        false,
        &[
            ("provider", true),
            ("popupOrigin", true),
            ("popupNonce", false),
            ("callbackURL", false),
            ("errorCallbackURL", false),
            ("newUserCallbackURL", false),
            ("scopes", false),
            ("requestSignUp", false),
            ("additionalData", false),
        ],
    )
}

pub(crate) fn proxy(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(query, false, &[("callbackURL", true), ("profile", false)])
}

pub(crate) fn magic_link(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(
        query,
        false,
        &[
            ("token", true),
            ("callbackURL", false),
            ("errorCallbackURL", false),
            ("newUserCallbackURL", false),
        ],
    )
}

pub(crate) fn verify_email(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(query, false, &[("token", true), ("callbackURL", false)])
}

pub(crate) fn device(query: Option<Value>) -> AuthResult<Option<Value>> {
    strings(query, false, &[("user_code", true)])
}

pub(crate) fn list_members(query: Option<Value>) -> AuthResult<Option<Value>> {
    list(query, false)
}
pub(crate) fn list_users(query: Option<Value>) -> AuthResult<Option<Value>> {
    list(query, true)
}

fn list(query: Option<Value>, admin: bool) -> AuthResult<Option<Value>> {
    let Some(mut input) = object(query, !admin)? else {
        return Ok(None);
    };
    let fields = if admin {
        &[
            "searchValue",
            "searchField",
            "searchOperator",
            "limit",
            "offset",
            "sortBy",
            "sortDirection",
            "filterField",
            "filterValue",
            "filterOperator",
        ][..]
    } else {
        &[
            "limit",
            "offset",
            "sortBy",
            "sortDirection",
            "filterField",
            "filterValue",
            "filterOperator",
            "organizationId",
            "organizationSlug",
        ][..]
    };
    let mut output = Map::new();
    let mut errors = Vec::new();
    for name in fields {
        let Some(value) = input.remove(*name) else {
            continue;
        };
        let choices: &[&str] = match *name {
            "searchField" => &["email", "name"],
            "searchOperator" => &["contains", "starts_with", "ends_with"],
            "sortDirection" => &["asc", "desc"],
            "filterOperator" => &[
                "eq",
                "ne",
                "lt",
                "lte",
                "gt",
                "gte",
                "in",
                "not_in",
                "contains",
                "starts_with",
                "ends_with",
            ],
            _ => &[],
        };
        let error = if !choices.is_empty() {
            if value.as_str().is_some_and(|value| choices.contains(&value)) {
                None
            } else {
                Some(format!(
                    "[query.{name}] Invalid option: expected one of {}",
                    choices
                        .iter()
                        .map(|choice| format!("\"{choice}\""))
                        .collect::<Vec<_>>()
                        .join("|")
                ))
            }
        } else if matches!(*name, "limit" | "offset") {
            (!value.is_string() && !value.is_number())
                .then(|| format!("[query.{name}] Invalid input"))
        } else if *name == "filterValue" {
            let valid = value.is_string()
                || value.is_number()
                || value.is_boolean()
                || value.as_array().is_some_and(|values| {
                    values.iter().all(Value::is_string) || values.iter().all(Value::is_number)
                });
            (!valid).then(|| format!("[query.{name}] Invalid input"))
        } else {
            (!value.is_string())
                .then(|| invalid_type(&format!("query.{name}"), "string", Some(&value)))
        };
        if let Some(error) = error {
            errors.push(error);
        } else {
            let _ = output.insert((*name).to_owned(), value);
        }
    }
    finish(output, errors)
}