better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::{
    OrganizationFields,
    handlers::{roles, team},
    input::{self, BaseField},
    types::*,
};
use better_auth_core::{
    AuthError, AuthRequest, AuthResult, FieldMap, FieldValue, FromFieldMap, SchemaField,
    endpoint_input::ValidatedBody, user_fields::UserConfig,
};
use serde_json::{Map, Value};

macro_rules! from_fields {
    ($type:ty { $($field:ident: $name:literal),* $(,)? } $(; $additional:ident)?) => {
        impl better_auth_core::FromFieldMap for $type {
            fn from_field_values(mut fields: better_auth_core::FieldMap) -> better_auth_core::AuthResult<Self> {
                Ok(Self {
                    $($field: crate::plugins::organization::request::take(&mut fields, $name)?,)*
                    $($additional: fields,)?
                })
            }
        }
    };
}
pub(super) use from_fields;

pub(super) fn typed<T: FromFieldMap + Send + Sync + 'static>(
    fields: FieldMap,
) -> AuthResult<ValidatedBody> {
    Ok(ValidatedBody::native(
        fields.clone().into(),
        T::from_field_values(fields)?,
    ))
}

pub(super) fn take<T: SchemaField>(fields: &mut FieldMap, name: &str) -> AuthResult<T> {
    fields.remove(name).unwrap_or_default().decode()
}

pub(super) fn object(fields: &mut FieldMap, name: &str) -> AuthResult<FieldMap> {
    match fields.remove(name) {
        Some(FieldValue::Object(value)) => value.snapshot_fields(),
        _ => Err(AuthError::internal(format!(
            "Validated Organization field {name} is not an object"
        ))),
    }
}

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

pub(super) fn prepared_schema(path: &str, schema: &OrganizationFields) -> OrganizationFields {
    if path == "/organization/update-role" {
        schema.initialized_options()
    } else {
        schema.clone()
    }
}

fn schema_fields(
    schema: &UserConfig,
    body: &Map<String, Value>,
    base: &[(&str, BaseField, bool)],
    prefix: &str,
    partial: bool,
    errors: &mut Vec<String>,
) -> AuthResult<FieldMap> {
    input::fields(schema, body, base, prefix, partial, false, errors)
}

fn static_fields(
    body: &Map<String, Value>,
    base: &[(&str, BaseField, bool)],
    errors: &mut Vec<String>,
) -> AuthResult<FieldMap> {
    schema_fields(&UserConfig::default(), body, base, "body", false, errors)
}

fn selector(body: &Map<String, Value>, output: &mut FieldMap, errors: &mut Vec<String>) {
    if let Some((name, value)) = ["roleName", "roleId"].into_iter().find_map(|name| {
        body.get(name)
            .and_then(Value::as_str)
            .filter(|value| !value.is_empty())
            .map(|value| (name, value))
    }) {
        let _ = output.insert(name.into(), value.into());
    } else {
        let name = match (
            body.get("roleName").and_then(Value::as_str),
            body.get("roleId").and_then(Value::as_str),
        ) {
            (Some(""), None) => Some("roleName"),
            (None, Some("")) => Some("roleId"),
            _ => None,
        };
        errors.push(name.map_or_else(
            || "[body] Invalid input".into(),
            |name| format!("[body.{name}] Too small: expected string to have >=1 characters"),
        ));
    }
}

pub(super) fn validate(
    req: &AuthRequest,
    schema: &OrganizationFields,
) -> AuthResult<ValidatedBody> {
    use BaseField::*;
    let raw = req.input_body()?;
    let mut errors = Vec::new();
    let Some(body) = input::object(raw.as_ref(), "body", &mut errors) else {
        if matches!(
            req.path(),
            "/organization/delete-role"
                | "/organization/update-role"
                | "/organization/has-permission"
        ) {
            errors.push("[body] Invalid input".into());
        }
        input::finish(errors)?;
        return Err(AuthError::internal("Invalid Organization body"));
    };
    let output = match req.path() {
        "/organization/create" => schema_fields(
            &schema.organization,
            body,
            &[
                ("name", NonemptyString, true),
                ("slug", NonemptyString, true),
                ("userId", CoercedString, false),
                ("logo", NullableString, false),
                ("metadata", Record, false),
                ("keepCurrentActiveOrganization", Boolean, false),
            ],
            "body",
            false,
            &mut errors,
        )?,
        "/organization/update" => {
            let mut output = FieldMap::new();
            if let Some(data) = input::object(body.get("data"), "body.data", &mut errors) {
                let data = input::fields(
                    &schema.organization,
                    data,
                    &[
                        ("name", NonemptyString, false),
                        ("slug", NonemptyString, false),
                        ("logo", NullableString, false),
                        ("metadata", Record, false),
                    ],
                    "body.data",
                    true,
                    true,
                    &mut errors,
                )?;
                let _ = output.insert("data".into(), data.into());
            }
            output.extend(static_fields(
                body,
                &[("organizationId", String, false)],
                &mut errors,
            )?);
            output
        }
        "/organization/delete" | "/organization/leave" => {
            static_fields(body, &[("organizationId", String, true)], &mut errors)?
        }
        "/organization/check-slug" => static_fields(body, &[("slug", String, true)], &mut errors)?,
        "/organization/set-active" => static_fields(
            body,
            &[
                ("organizationId", NullableString, false),
                ("organizationSlug", String, false),
            ],
            &mut errors,
        )?,
        "/organization/remove-member" => static_fields(
            body,
            &[
                ("memberIdOrEmail", String, true),
                ("organizationId", String, false),
            ],
            &mut errors,
        )?,
        "/organization/update-member-role" => static_fields(
            body,
            &[
                ("role", Roles, true),
                ("memberId", String, true),
                ("organizationId", String, false),
            ],
            &mut errors,
        )?,
        "/organization/invite-member" => schema_fields(
            &schema.invitation,
            body,
            &[
                ("email", String, true),
                ("role", Roles, true),
                ("organizationId", String, false),
                ("resend", Boolean, false),
                ("teamId", Roles, false),
            ],
            "body",
            false,
            &mut errors,
        )?,
        "/organization/accept-invitation"
        | "/organization/reject-invitation"
        | "/organization/cancel-invitation" => {
            static_fields(body, &[("invitationId", String, true)], &mut errors)?
        }
        "/organization/create-team" => schema_fields(
            &schema.team,
            body,
            &[("name", String, true), ("organizationId", String, false)],
            "body",
            false,
            &mut errors,
        )?,
        "/organization/update-team" => {
            let mut output = static_fields(body, &[("teamId", String, true)], &mut errors)?;
            if let Some(data) = input::object(body.get("data"), "body.data", &mut errors) {
                let data = schema_fields(
                    &schema.team,
                    data,
                    &[("name", String, true), ("organizationId", String, true)],
                    "body.data",
                    true,
                    &mut errors,
                )?;
                let _ = output.insert("data".into(), data.into());
            }
            output
        }
        "/organization/remove-team" => static_fields(
            body,
            &[("teamId", String, true), ("organizationId", String, false)],
            &mut errors,
        )?,
        "/organization/set-active-team" => {
            static_fields(body, &[("teamId", NullableString, false)], &mut errors)?
        }
        "/organization/add-team-member" | "/organization/remove-team-member" => static_fields(
            body,
            &[
                ("teamId", String, true),
                ("userId", CoercedString, true),
                ("organizationId", String, false),
            ],
            &mut errors,
        )?,
        "/organization/create-role" => {
            let mut output = static_fields(
                body,
                &[
                    ("organizationId", String, false),
                    ("role", String, true),
                    ("permission", Permissions, true),
                ],
                &mut errors,
            )?;
            if let Some(value) = body.get("additionalFields")
                && let Some(data) = input::object(Some(value), "body.additionalFields", &mut errors)
            {
                let data = schema_fields(
                    &schema.organization_role,
                    data,
                    &[],
                    "body.additionalFields",
                    false,
                    &mut errors,
                )?;
                let _ = output.insert("additionalFields".into(), data.into());
            }
            output
        }
        "/organization/delete-role" => {
            let mut output =
                static_fields(body, &[("organizationId", String, false)], &mut errors)?;
            selector(body, &mut output, &mut errors);
            output
        }
        "/organization/update-role" => {
            let mut output =
                static_fields(body, &[("organizationId", String, false)], &mut errors)?;
            if let Some(data) = input::object(body.get("data"), "body.data", &mut errors) {
                // The upstream role update endpoint makes configured fields nullish at initialization.
                let data = schema_fields(
                    &schema.organization_role,
                    data,
                    &[
                        ("permission", Permissions, false),
                        ("roleName", String, false),
                    ],
                    "body.data",
                    false,
                    &mut errors,
                )?;
                let _ = output.insert("data".into(), data.into());
            }
            selector(body, &mut output, &mut errors);
            output
        }
        "/organization/has-permission" => {
            let mut output =
                static_fields(body, &[("organizationId", String, false)], &mut errors)?;
            let mut branches = Vec::new();
            for field in ["permission", "permissions"] {
                let mut branch_errors = Vec::new();
                let branch =
                    static_fields(body, &[(field, Permissions, true)], &mut branch_errors)?;
                if branch_errors.is_empty() {
                    branches.push(branch);
                }
            }
            if branches.len() == 1 {
                if let Some(branch) = branches.pop() {
                    output.extend(branch);
                }
            } else {
                errors.push(
                    if branches.is_empty() {
                        "[body] Invalid input"
                    } else {
                        "[body] Invalid input: more than one option matched"
                    }
                    .into(),
                );
            }
            output
        }
        _ => {
            return Err(AuthError::internal(
                "Organization endpoint has no body schema",
            ));
        }
    };
    input::finish(errors)?;
    match req.path() {
        "/organization/create" => typed::<CreateOrganizationRequest>(output),
        "/organization/update" => typed::<UpdateOrganizationRequest>(output),
        "/organization/delete" => typed::<DeleteOrganizationRequest>(output),
        "/organization/leave" => typed::<LeaveOrganizationRequest>(output),
        "/organization/check-slug" => typed::<CheckSlugRequest>(output),
        "/organization/set-active" => typed::<SetActiveOrganizationRequest>(output),
        "/organization/remove-member" => typed::<RemoveMemberRequest>(output),
        "/organization/update-member-role" => typed::<UpdateMemberRoleRequest>(output),
        "/organization/invite-member" => typed::<InviteMemberRequest>(output),
        "/organization/accept-invitation" => typed::<AcceptInvitationRequest>(output),
        "/organization/reject-invitation" => typed::<RejectInvitationRequest>(output),
        "/organization/cancel-invitation" => typed::<CancelInvitationRequest>(output),
        "/organization/create-team" => typed::<team::CreateBody>(output),
        "/organization/update-team" => typed::<team::UpdateBody>(output),
        "/organization/remove-team" => typed::<team::TeamBody>(output),
        "/organization/set-active-team" => typed::<team::ActiveBody>(output),
        "/organization/add-team-member" | "/organization/remove-team-member" => {
            typed::<team::MemberBody>(output)
        }
        "/organization/create-role" => typed::<roles::CreateRole>(output),
        "/organization/delete-role" => typed::<roles::RoleSelector>(output),
        "/organization/update-role" => Ok(ValidatedBody::native(
            output.clone().into(),
            roles::UpdateRole::validated(output, &schema.organization_role)?,
        )),
        "/organization/has-permission" => typed::<HasPermissionRequest>(output),
        _ => Err(AuthError::internal(
            "Organization endpoint has no typed body",
        )),
    }
}