better-auth-api 1.0.0-alpha.5

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

use super::types::{LinkSocialRequest, SocialSignInRequest};
use crate::plugins::json_body::{invalid_type, validation_error};

#[derive(Clone, Copy)]
enum Kind {
    String,
    Provider,
    Boolean,
    Number,
    Strings,
    Record { strings: bool },
    Object(&'static [Field]),
}
type Field = (&'static str, Kind, bool);

const NAME: &[Field] = &[
    ("firstName", Kind::String, false),
    ("lastName", Kind::String, false),
];
const USER: &[Field] = &[
    ("name", Kind::Object(NAME), false),
    ("email", Kind::String, false),
];
const SIGN_IN_TOKEN: &[Field] = &[
    ("token", Kind::String, true),
    ("nonce", Kind::String, false),
    ("accessToken", Kind::String, false),
    ("refreshToken", Kind::String, false),
    ("expiresAt", Kind::Number, false),
    ("user", Kind::Object(USER), false),
];
const LINK_TOKEN: &[Field] = &[
    ("token", Kind::String, true),
    ("nonce", Kind::String, false),
    ("accessToken", Kind::String, false),
    ("refreshToken", Kind::String, false),
];

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

fn typed<T: DeserializeOwned + Send + Sync + 'static>(body: Value) -> AuthResult<ValidatedBody> {
    Ok(ValidatedBody::new(
        Some(body.clone()),
        serde_json::from_value::<T>(body)?,
    ))
}

pub(super) fn validate(req: &AuthRequest) -> AuthResult<ValidatedBody> {
    use Kind::*;
    let fields: &[Field] = match req.path() {
        "/sign-in/social" => &[
            ("callbackURL", String, false),
            ("newUserCallbackURL", String, false),
            ("errorCallbackURL", String, false),
            ("provider", Provider, true),
            ("disableRedirect", Boolean, false),
            ("idToken", Object(SIGN_IN_TOKEN), false),
            ("scopes", Strings, false),
            ("requestSignUp", Boolean, false),
            ("loginHint", String, false),
            ("additionalParams", Record { strings: true }, false),
            ("additionalData", Record { strings: false }, false),
        ],
        "/link-social" => &[
            ("callbackURL", String, false),
            ("provider", Provider, true),
            ("idToken", Object(LINK_TOKEN), false),
            ("requestSignUp", Boolean, false),
            ("scopes", Strings, false),
            ("errorCallbackURL", String, false),
            ("disableRedirect", Boolean, false),
            ("loginHint", String, false),
            ("additionalParams", Record { strings: true }, false),
            ("additionalData", Record { strings: false }, false),
        ],
        _ => return Err(AuthError::internal("Unknown OAuth body schema")),
    };
    let mut errors = Vec::new();
    let body = project_object(req.input_body()?.as_ref(), "body", fields, &mut errors);
    if !errors.is_empty() {
        return Err(validation_error(&errors.join("; ")));
    }
    if req.path() == "/sign-in/social" {
        typed::<SocialSignInRequest>(body)
    } else {
        typed::<LinkSocialRequest>(body)
    }
}

fn project_object(
    input: Option<&Value>,
    location: &str,
    fields: &[Field],
    errors: &mut Vec<String>,
) -> Value {
    let Some(body) = input.and_then(Value::as_object) else {
        errors.push(invalid_type(location, "object", input));
        return Value::Null;
    };
    let mut output = Map::new();
    for &(name, kind, required) in fields {
        let value = body.get(name);
        if value.is_none() && !required {
            continue;
        }
        let location = format!("{location}.{name}");
        let (valid, expected) = match kind {
            Kind::String | Kind::Provider => (value.is_some_and(Value::is_string), "string"),
            Kind::Boolean => (value.is_some_and(Value::is_boolean), "boolean"),
            Kind::Number => (value.is_some_and(Value::is_number), "number"),
            Kind::Strings => (value.is_some_and(Value::is_array), "array"),
            Kind::Record { .. } => (value.is_some_and(Value::is_object), "record"),
            Kind::Object(_) => (value.is_some_and(Value::is_object), "object"),
        };
        if !valid {
            errors.push(if matches!(kind, Kind::Provider) {
                format!("[{location}] Invalid input")
            } else {
                invalid_type(&location, expected, value)
            });
            continue;
        }
        let Some(value) = value else { continue };
        let projected = match kind {
            Kind::Object(fields) => project_object(Some(value), &location, fields, errors),
            Kind::Strings => {
                if let Some(values) = value.as_array() {
                    for (index, value) in values.iter().enumerate() {
                        if !value.is_string() {
                            errors.push(invalid_type(
                                &format!("{location}.{index}"),
                                "string",
                                Some(value),
                            ));
                        }
                    }
                }
                value.clone()
            }
            Kind::Record { strings } => {
                let mut record = Map::new();
                if let Some(values) = value.as_object() {
                    let before = errors.len();
                    for (key, value) in values {
                        if strings && !value.is_string() {
                            errors.push(invalid_type(
                                &format!("{location}.{key}"),
                                "string",
                                Some(value),
                            ));
                        }
                        if key != "__proto__" {
                            let _ = record.insert(key.clone(), value.clone());
                        }
                    }
                    if strings
                        && before == errors.len()
                        && record.keys().any(|key| {
                            super::authorization::RESERVED_PARAMS.contains(&key.as_str())
                        })
                    {
                        errors.push(format!("[{location}] additionalParams cannot include reserved OAuth parameters: {}", super::authorization::RESERVED_PARAMS.join(", ")));
                    }
                }
                Value::Object(record)
            }
            _ => value.clone(),
        };
        let _ = output.insert(name.into(), projected);
    }
    Value::Object(output)
}