better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use better_auth_core::endpoint_input::ValidatedBody;
use better_auth_core::{AuthError, AuthRequest, AuthResult};
use serde_json::{Map, Value};

use super::EmailOtpType;
use crate::plugins::json_body;

#[derive(Clone)]
pub(super) struct Body(Map<String, Value>);

impl Body {
    pub(super) fn parse(req: &AuthRequest) -> AuthResult<Self> {
        req.validated_body::<Self>()
            .cloned()
            .map_or_else(|| parse(req), Ok)
    }
    pub(super) fn value(&self) -> Value {
        Value::Object(self.0.clone())
    }
    pub(super) fn get(&self, field: &str) -> &str {
        self.optional(field).unwrap_or_default()
    }
    pub(super) fn optional(&self, field: &str) -> Option<&str> {
        self.0.get(field).and_then(Value::as_str)
    }
    pub(super) fn fields(&self) -> &Map<String, Value> {
        &self.0
    }
    pub(super) fn kind(&self) -> EmailOtpType {
        match self.get("type") {
            "sign-in" => EmailOtpType::SignIn,
            "forget-password" => EmailOtpType::ForgetPassword,
            "change-email" => EmailOtpType::ChangeEmail,
            _ => EmailOtpType::EmailVerification,
        }
    }
}

pub(super) fn validate(req: &AuthRequest) -> AuthResult<ValidatedBody> {
    let body = parse(req)?;
    Ok(ValidatedBody::new(Some(body.value()), body))
}

fn parse(req: &AuthRequest) -> AuthResult<Body> {
    let (required, optional): (&[&str], &[&str]) = match req.path() {
        "/email-otp/send-verification-otp" => (&["email", "type"], &[]),
        "/email-otp/check-verification-otp" => (&["email", "type", "otp"], &[]),
        "/email-otp/verify-email" => (&["email", "otp"], &[]),
        "/sign-in/email-otp" => (&["email", "otp"], &["name", "image"]),
        "/email-otp/request-password-reset" | "/forget-password/email-otp" => (&["email"], &[]),
        "/email-otp/reset-password" => (&["email", "otp", "password"], &[]),
        "/email-otp/request-email-change" => (&["newEmail"], &["otp"]),
        "/email-otp/change-email" => (&["newEmail", "otp"], &[]),
        _ => return Err(AuthError::internal("Unknown Email OTP body schema")),
    };
    let record = req.path() == "/sign-in/email-otp";
    let input = req.input_body()?;
    let body = input.as_ref().and_then(Value::as_object).ok_or_else(|| {
        let mut message = json_body::invalid_type("body", "object", input.as_ref());
        if record {
            message.push_str("; ");
            message.push_str(&json_body::invalid_type("body", "record", input.as_ref()));
        }
        json_body::validation_error(&message)
    })?;
    let mut errors = Vec::new();
    for field in required.iter().chain(optional) {
        let value = body.get(*field);
        if *field == "type" {
            if !matches!(
                value.and_then(Value::as_str),
                Some("email-verification" | "sign-in" | "forget-password" | "change-email")
            ) {
                errors.push(r#"[body.type] Invalid option: expected one of "email-verification"|"sign-in"|"forget-password"|"change-email""#.to_owned());
            }
        } else if (required.contains(field) || value.is_some())
            && !value.is_some_and(Value::is_string)
        {
            errors.push(json_body::invalid_type(
                &format!("body.{field}"),
                "string",
                value,
            ));
        }
    }
    if !errors.is_empty() {
        return Err(json_body::validation_error(&errors.join("; ")));
    }
    Ok(Body(
        body.iter()
            .filter(|(key, _)| {
                key.as_str() != "__proto__"
                    && (record
                        || required.contains(&key.as_str())
                        || optional.contains(&key.as_str()))
            })
            .map(|(key, value)| (key.clone(), value.clone()))
            .collect(),
    ))
}

pub(super) fn validate_email(email: &str) -> AuthResult<()> {
    if json_body::valid_email(email)? {
        Ok(())
    } else {
        Err(AuthError::from_code(
            "BAD_REQUEST",
            "INVALID_EMAIL",
            "Invalid email",
        ))
    }
}