better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use better_auth_core::plugin_runtime::PluginRuntime;
use better_auth_core::store::database_hooks::{
    DatabaseHookContext, DatabaseHookUpdate, DatabaseHooks,
};
use better_auth_core::{AuthError, AuthResult, AuthSchema, FieldMap, FieldValue};

use super::config::UsernameConfig;

pub(super) struct UsernameHooks<S: AuthSchema> {
    pub(super) config: UsernameConfig,
    pub(super) runtime: PluginRuntime<S>,
}

impl<S: AuthSchema> UsernameHooks<S> {
    async fn validate(
        &self,
        username: &FieldValue,
        display: Option<&FieldValue>,
        current_user_id: Option<&FieldValue>,
        context: &DatabaseHookContext<'_, S>,
    ) -> AuthResult<()> {
        if let Some((code, message)) = self.config.validate_input(username).await? {
            return Err(AuthError::from_code("BAD_REQUEST", code, message));
        }
        let normalized = self.config.normalize(username)?;
        let existing = if let Some(transaction) = context.transaction {
            transaction
                .get_user_by_field_value("username", &normalized)
                .await?
        } else {
            self.runtime
                .context()?
                .database
                .get_user_by_field_value("username", &normalized)
                .await?
        };
        if let Some(user) = existing
            && current_user_id
                .is_none_or(|id| !id.is_truthy() || !id.strict_equals(&user.id.field_value()))
        {
            return Err(AuthError::from_code(
                "BAD_REQUEST",
                "USERNAME_IS_ALREADY_TAKEN",
                "Username is already taken. Please try another.",
            ));
        }
        if let Some(display) = display.filter(|value| value.is_truthy()) {
            self.config.validate_display(display).await?;
        }
        Ok(())
    }
}

fn endpoint_validated<S: AuthSchema>(context: &DatabaseHookContext<'_, S>) -> bool {
    context.request.as_ref().is_some_and(|request| {
        matches!(
            request.path.as_deref(),
            Some("/sign-up/email" | "/update-user")
        )
    })
}

#[better_auth_core::database_hooks("plugin:username")]
impl<S: AuthSchema> DatabaseHooks<S> for UsernameHooks<S> {
    async fn before_create_user(
        &self,
        user: &mut FieldMap,
        context: &DatabaseHookContext<'_, S>,
    ) -> AuthResult<DatabaseHookUpdate<FieldMap>> {
        let username = user.get("username").filter(|value| value.is_truthy());
        let display = user
            .get("displayUsername")
            .filter(|value| value.is_truthy());
        let mut patch = user.clone();
        if let Some(username) = username {
            if !endpoint_validated(context) {
                self.validate(username, display, None, context).await?;
            }
            let _ = patch.insert("username".into(), self.config.normalize(username)?);
            if self.config.display_username {
                let display = match display {
                    Some(display) => self.config.normalize_display(display)?,
                    None => username.clone(),
                };
                let _ = patch.insert("displayUsername".into(), display);
            }
        } else if self.config.display_username
            && let Some(display) = display
        {
            let _ = patch.insert(
                "displayUsername".into(),
                self.config.normalize_display(display)?,
            );
        }
        Ok(DatabaseHookUpdate::Patch(patch))
    }

    async fn before_update_user(
        &self,
        user: &mut FieldMap,
        context: &DatabaseHookContext<'_, S>,
    ) -> AuthResult<DatabaseHookUpdate<FieldMap>> {
        let username = user.get("username").filter(|value| value.is_truthy());
        let display = user
            .get("displayUsername")
            .filter(|value| value.is_truthy());
        let mut patch = user.clone();
        if let Some(username) = username {
            if !endpoint_validated(context) {
                let session_id = context
                    .request
                    .as_ref()
                    .map(|request| request.request.native_session_snapshot())
                    .transpose()?
                    .flatten()
                    .map(|session| session.user_field("id"))
                    .transpose()?;
                let current_id = session_id
                    .as_ref()
                    .filter(|value| value.is_truthy())
                    .or_else(|| user.get("id"));
                self.validate(username, display, current_id, context)
                    .await?;
            }
            let _ = patch.insert("username".into(), self.config.normalize(username)?);
        }
        if self.config.display_username
            && let Some(display) = display
        {
            let _ = patch.insert(
                "displayUsername".into(),
                self.config.normalize_display(display)?,
            );
        }
        Ok(DatabaseHookUpdate::Patch(patch))
    }
}