better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::types::*;
use crate::plugins::json_body::{invalid_type, validation_error};
use better_auth_core::{
    AuthError, AuthRequest, AuthResult, SchemaValue, endpoint_input::ValidatedBody,
};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};

pub(super) fn read<T: Clone + Send + Sync + 'static>(req: &AuthRequest) -> AuthResult<T> {
    if let Some(value) = req.validated_body::<T>() {
        return Ok(value.clone());
    }
    validate(req)?
        .get::<T>()
        .cloned()
        .ok_or_else(|| AuthError::internal("Admin body validator returned a different input type"))
}

fn typed<T: DeserializeOwned + Send + Sync + 'static>(
    output: Map<String, Value>,
) -> AuthResult<ValidatedBody> {
    let output = Value::Object(output);
    Ok(ValidatedBody::new(
        Some(output.clone()),
        serde_json::from_value::<T>(output)?,
    ))
}

#[derive(Clone, Copy)]
enum Field {
    String,
    Id,
    Role,
    Record,
    Number,
}
fn field(
    body: &Map<String, Value>,
    output: &mut Map<String, Value>,
    errors: &mut Vec<String>,
    name: &str,
    kind: Field,
    required: bool,
) -> AuthResult<()> {
    let value = body.get(name);
    if value.is_none() && !required {
        return Ok(());
    }
    if matches!(kind, Field::Id) {
        if value.is_none() {
            errors.push(invalid_type(&format!("body.{name}"), "nonoptional", None));
            return Ok(());
        }
        let value = SchemaValue::<better_auth_core::FieldValue>::from_json(value.cloned())?
            .display_string()?;
        let _ = output.insert(name.into(), value.into());
        return Ok(());
    }
    let (valid, expected) = match kind {
        Field::String => (value.is_some_and(Value::is_string), "string"),
        Field::Number => (value.is_some_and(Value::is_number), "number"),
        Field::Record => (value.is_some_and(Value::is_object), "record"),
        Field::Role => (
            value.is_some_and(|v| {
                v.is_string()
                    || v.as_array()
                        .is_some_and(|values| values.iter().all(Value::is_string))
            }),
            "union",
        ),
        Field::Id => (true, "string"),
    };
    if valid {
        if let Some(value) = value {
            let _ = output.insert(name.into(), value.clone());
        }
    } else {
        errors.push(if matches!(kind, Field::Role) {
            format!("[body.{name}] Invalid input")
        } else {
            invalid_type(&format!("body.{name}"), expected, value)
        });
    }
    Ok(())
}

fn permission_branch(body: &Map<String, Value>, name: &str) -> Option<Value> {
    body.get(name)
        .filter(|value| {
            value.as_object().is_some_and(|values| {
                values.values().all(|value| {
                    value
                        .as_array()
                        .is_some_and(|values| values.iter().all(Value::is_string))
                })
            })
        })
        .cloned()
}

pub(super) fn validate(req: &AuthRequest) -> AuthResult<ValidatedBody> {
    use Field::*;
    let input = req.input_body()?;
    let body = input.as_ref().and_then(Value::as_object).ok_or_else(|| {
        let mut message = invalid_type("body", "object", input.as_ref());
        if req.path() == "/admin/has-permission" {
            message.push_str("; [body] Invalid input");
        }
        validation_error(&message)
    })?;
    let mut output = Map::new();
    let mut errors = Vec::new();
    let fields: &[(&str, Field, bool)] = match req.path() {
        "/admin/set-role" => &[("userId", Id, true), ("role", Role, true)],
        "/admin/create-user" => &[
            ("email", String, true),
            ("password", String, false),
            ("name", String, true),
            ("role", Role, false),
            ("data", Record, false),
        ],
        "/admin/update-user" => &[("userId", Id, true), ("data", Record, true)],
        "/admin/ban-user" => &[
            ("userId", Id, true),
            ("banReason", String, false),
            ("banExpiresIn", Number, false),
        ],
        "/admin/revoke-user-session" => &[("sessionToken", String, true)],
        "/admin/set-user-password" => &[("newPassword", String, true), ("userId", Id, true)],
        "/admin/has-permission" => &[("userId", Id, false), ("role", String, false)],
        _ => &[("userId", Id, true)],
    };
    for (name, kind, required) in fields {
        field(body, &mut output, &mut errors, name, *kind, *required)?;
        if req.path() == "/admin/set-user-password"
            && output.get(*name).and_then(Value::as_str) == Some("")
        {
            errors.push(format!("[body.{name}] {name} cannot be empty"));
        }
    }
    if req.path() == "/admin/has-permission" {
        match (
            permission_branch(body, "permission"),
            permission_branch(body, "permissions"),
        ) {
            (Some(_), Some(_)) => {
                errors.push("[body] Invalid input: more than one option matched".into())
            }
            (Some(value), None) => {
                let _ = output.insert("permission".into(), value);
            }
            (None, Some(value)) => {
                let _ = output.insert("permissions".into(), value);
            }
            (None, None) => errors.push("[body] Invalid input".into()),
        }
    }
    if !errors.is_empty() {
        return Err(validation_error(&errors.join("; ")));
    }
    match req.path() {
        "/admin/set-role" => typed::<SetRoleRequest>(output),
        "/admin/create-user" => typed::<CreateUserRequest>(output),
        "/admin/update-user" => typed::<AdminUpdateUserRequest>(output),
        "/admin/ban-user" => typed::<BanUserRequest>(output),
        "/admin/revoke-user-session" => typed::<RevokeSessionRequest>(output),
        "/admin/set-user-password" => typed::<SetUserPasswordRequest>(output),
        "/admin/has-permission" => typed::<HasPermissionRequest>(output),
        _ => typed::<UserIdRequest>(output),
    }
}