better-auth-api 1.0.0-alpha.5

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

pub(super) fn read<T: Clone + Send + Sync + 'static>(
    req: &AuthRequest,
    password_optional: bool,
) -> AuthResult<T> {
    if let Some(value) = req.validated_body::<T>() {
        return Ok(value.clone());
    }
    validate(req, password_optional)?
        .get::<T>()
        .cloned()
        .ok_or_else(|| {
            AuthError::internal("Two Factor 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)?,
    ))
}

pub(super) fn validate(req: &AuthRequest, password_optional: bool) -> AuthResult<ValidatedBody> {
    let input = req.input_body()?;
    if req.path() == "/two-factor/send-otp" && input.is_none() {
        return Ok(ValidatedBody::new(None, Value::Null));
    }
    let body = input
        .as_ref()
        .and_then(Value::as_object)
        .ok_or_else(|| validation_error(&invalid_type("body", "object", input.as_ref())))?;
    let fields: &[(&str, &str, bool)] = match req.path() {
        "/two-factor/enable" => &[
            ("password", "string", !password_optional),
            ("method", "method", false),
            ("issuer", "string", false),
        ],
        "/two-factor/disable"
        | "/two-factor/get-totp-uri"
        | "/two-factor/generate-backup-codes" => &[("password", "string", !password_optional)],
        "/two-factor/verify-totp" | "/two-factor/verify-otp" => {
            &[("code", "string", true), ("trustDevice", "boolean", false)]
        }
        "/two-factor/verify-backup-code" => &[
            ("code", "string", true),
            ("disableSession", "boolean", false),
            ("trustDevice", "boolean", false),
        ],
        "/two-factor/send-otp" => &[("trustDevice", "boolean", false)],
        "generateTOTP" => &[("secret", "string", true)],
        "viewBackupCodes" => &[("userId", "id", true)],
        _ => return Err(AuthError::internal("Unknown Two Factor body schema")),
    };
    let mut output = Map::new();
    let mut errors = Vec::new();
    for &(name, kind, required) in fields {
        let value = body.get(name);
        if value.is_none() && !required {
            if kind == "method" {
                let _ = output.insert(name.into(), Value::String("totp".into()));
            }
            continue;
        }
        let valid = match kind {
            "id" => value.is_some(),
            "string" => value.is_some_and(Value::is_string),
            "boolean" => value.is_some_and(Value::is_boolean),
            "method" => matches!(value.and_then(Value::as_str), Some("otp" | "totp")),
            _ => false,
        };
        if valid {
            if let Some(value) = value {
                let value = if kind == "id" {
                    better_auth_core::SchemaValue::<better_auth_core::FieldValue>::from_json(Some(
                        value.clone(),
                    ))?
                    .display_string()?
                    .into()
                } else {
                    value.clone()
                };
                let _ = output.insert(name.into(), value);
            }
        } else {
            errors.push(if kind == "method" {
                "[body.method] Invalid option: expected one of \"otp\"|\"totp\"".into()
            } else {
                invalid_type(
                    &format!("body.{name}"),
                    if kind == "id" { "nonoptional" } else { kind },
                    value,
                )
            });
        }
    }
    if !errors.is_empty() {
        return Err(validation_error(&errors.join("; ")));
    }
    match req.path() {
        "/two-factor/enable" => typed::<EnableRequest>(output),
        "/two-factor/disable" => typed::<DisableRequest>(output),
        "/two-factor/get-totp-uri" => typed::<GetTotpUriRequest>(output),
        "/two-factor/verify-totp" => typed::<VerifyTotpRequest>(output),
        "/two-factor/verify-otp" => typed::<VerifyOtpRequest>(output),
        "/two-factor/generate-backup-codes" => typed::<GenerateBackupCodesRequest>(output),
        "/two-factor/verify-backup-code" => typed::<VerifyBackupCodeRequest>(output),
        "generateTOTP" => typed::<GenerateTotpRequest>(output),
        "viewBackupCodes" => typed::<ViewBackupCodesRequest>(output),
        _ => typed::<Value>(output),
    }
}

#[derive(Clone, Deserialize)]
pub(super) struct GenerateTotpRequest {
    pub secret: String,
}
#[derive(Clone, Deserialize)]
pub(super) struct ViewBackupCodesRequest {
    #[serde(rename = "userId")]
    pub user_id: String,
}

pub(super) fn validate_native(request: &AuthRequest, operation: &str) -> AuthResult<ValidatedBody> {
    let mut input = request.clone();
    input.path = operation.into();
    validate(&input, false)
}