use std::sync::Arc;
use async_trait::async_trait;
use better_auth_core::{AuthError, AuthResult, FieldValue};
pub type UsernameNormalizer = dyn Fn(&FieldValue) -> AuthResult<FieldValue> + Send + Sync;
#[async_trait]
pub trait UsernameValidator: Send + Sync {
async fn validate(&self, value: &FieldValue) -> AuthResult<bool>;
}
#[derive(Clone, Default)]
pub enum UsernameNormalization {
#[default]
Default,
Disabled,
Custom(Arc<UsernameNormalizer>),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum UsernameValidationOrder {
PreNormalization,
PostNormalization,
}
#[derive(Clone)]
pub struct UsernameConfig {
pub min_username_length: f64,
pub max_username_length: f64,
pub username_validator: Option<Arc<dyn UsernameValidator>>,
pub display_username_validator: Option<Arc<dyn UsernameValidator>>,
pub username_normalization: UsernameNormalization,
pub display_username_normalization: UsernameNormalization,
pub username_validation_order: Option<UsernameValidationOrder>,
pub display_username_validation_order: Option<UsernameValidationOrder>,
pub immutable_username: bool,
pub display_username: bool,
pub username_field_name: Option<String>,
pub display_username_field_name: Option<String>,
}
impl Default for UsernameConfig {
fn default() -> Self {
Self {
min_username_length: 3.0,
max_username_length: 30.0,
username_validator: None,
display_username_validator: None,
username_normalization: UsernameNormalization::Default,
display_username_normalization: UsernameNormalization::Default,
username_validation_order: None,
display_username_validation_order: None,
immutable_username: false,
display_username: true,
username_field_name: None,
display_username_field_name: None,
}
}
}
impl UsernameConfig {
pub(super) fn normalize(&self, value: &FieldValue) -> AuthResult<FieldValue> {
match &self.username_normalization {
UsernameNormalization::Default => match value {
FieldValue::String(value) => Ok(value.to_lowercase().into()),
FieldValue::Utf16String(value) => Ok(value.to_lowercase().into()),
_ => Err(AuthError::internal(
"username.toLowerCase is not a function",
)),
},
UsernameNormalization::Disabled => Ok(value.clone()),
UsernameNormalization::Custom(callback) => callback(value),
}
}
pub(super) fn normalize_display(&self, value: &FieldValue) -> AuthResult<FieldValue> {
match &self.display_username_normalization {
UsernameNormalization::Default | UsernameNormalization::Disabled => Ok(value.clone()),
UsernameNormalization::Custom(callback) => callback(value),
}
}
pub(super) async fn validate_raw(
&self,
value: &FieldValue,
) -> AuthResult<Option<(&'static str, &'static str)>> {
let length = match value {
FieldValue::Undefined | FieldValue::Null => {
return Err(AuthError::internal("Cannot read username length"));
}
FieldValue::String(value) => value.encode_utf16().count() as f64,
FieldValue::Utf16String(value) => value.as_utf16().len() as f64,
FieldValue::Array(value) => value.len()? as f64,
value => better_auth_core::query::field_number(
&value
.as_object()
.map(|value| value.get("length"))
.transpose()?
.flatten()
.unwrap_or_default(),
)?,
};
let minimum = if self.min_username_length == 0.0 || self.min_username_length.is_nan() {
3.0
} else {
self.min_username_length
};
let maximum = if self.max_username_length == 0.0 || self.max_username_length.is_nan() {
30.0
} else {
self.max_username_length
};
if length < minimum {
return Ok(Some(("USERNAME_TOO_SHORT", "Username is too short")));
}
if length > maximum {
return Ok(Some(("USERNAME_TOO_LONG", "Username is too long")));
}
let valid = match &self.username_validator {
Some(callback) => callback.validate(value).await?,
None => {
let value = value.display_utf16()?;
!value.as_utf16().is_empty()
&& value
.as_utf16()
.iter()
.all(|unit| matches!(unit, 48..=57 | 65..=90 | 97..=122 | 95 | 46))
}
};
Ok((!valid).then_some(("INVALID_USERNAME", "Username is invalid")))
}
pub(super) async fn validate_input(
&self,
value: &FieldValue,
) -> AuthResult<Option<(&'static str, &'static str)>> {
if self.username_validation_order == Some(UsernameValidationOrder::PostNormalization) {
self.validate_raw(&self.normalize(value)?).await
} else {
self.validate_raw(value).await
}
}
pub(super) async fn validate_display(&self, value: &FieldValue) -> AuthResult<()> {
if let Some(callback) = &self.display_username_validator {
let valid = if self.display_username_validation_order
== Some(UsernameValidationOrder::PostNormalization)
{
callback.validate(&self.normalize_display(value)?).await?
} else {
callback.validate(value).await?
};
if !valid {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVALID_DISPLAY_USERNAME",
"Display username is invalid",
));
}
}
Ok(())
}
}