use better_auth_core::config::OAuthStateStrategy;
use better_auth_core::entity::{AuthAccount, AuthApiKey, AuthUser};
use better_auth_core::{AuthContext, AuthError, AuthRequest, AuthResult, CreateUser, UpdateUser};
use chrono::Utc;
pub fn expires_in_to_at(expires_in_secs: Option<i64>) -> AuthResult<Option<String>> {
match expires_in_secs {
Some(secs) => {
let duration = chrono::Duration::try_seconds(secs)
.ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
let dt = chrono::Utc::now()
.checked_add_signed(duration)
.ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
Ok(Some(dt.to_rfc3339()))
}
None => Ok(None),
}
}
pub async fn get_owned_api_key(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
key_id: &str,
user_id: impl AsRef<str>,
) -> AuthResult<better_auth_core::ApiKey> {
let api_key = ctx
.database
.get_api_key_by_id(key_id)
.await?
.ok_or_else(|| AuthError::not_found("API key not found"))?;
if api_key.user_id().as_ref() != user_id.as_ref() {
return Err(AuthError::not_found("API key not found"));
}
Ok(api_key)
}
pub async fn get_credential_account<S: better_auth_core::AuthSchema>(
ctx: &AuthContext<S>,
user_id: impl AsRef<str>,
) -> AuthResult<Option<S::Account>> {
Ok(ctx
.database
.get_user_accounts(user_id.as_ref())
.await?
.into_iter()
.find(|account| account.provider_id() == "credential"))
}
pub async fn get_credential_password_hash(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
user: &impl AuthUser,
) -> AuthResult<Option<String>> {
Ok(get_credential_account(ctx, user.id())
.await?
.and_then(|account| account.password().map(str::to_string)))
}
pub async fn user_has_password(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
user: &impl AuthUser,
) -> AuthResult<bool> {
Ok(get_credential_password_hash(ctx, user).await?.is_some())
}
pub fn apply_default_role(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
create_user: &mut CreateUser,
) {
if create_user.role.is_some() {
return;
}
if let Some(default_role) = ctx
.get_metadata("admin.default_role")
.and_then(|value| value.as_str())
{
create_user.role = Some(default_role.to_string());
}
}
pub struct IssuedSession<S: better_auth_core::AuthSchema> {
pub user: S::User,
pub session: S::Session,
}
pub enum SessionIssueError {
Auth(AuthError),
Banned { message: String },
}
impl SessionIssueError {
pub fn into_auth_error(self) -> AuthError {
match self {
Self::Auth(error) => error,
Self::Banned { message } => AuthError::banned_user(message),
}
}
pub fn banned_message(&self) -> Option<&str> {
match self {
Self::Banned { message } => Some(message.as_str()),
Self::Auth(_) => None,
}
}
}
impl From<AuthError> for SessionIssueError {
fn from(value: AuthError) -> Self {
Self::Auth(value)
}
}
pub fn admin_plugin_enabled(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> bool {
ctx.get_metadata("admin.enabled")
.and_then(|value| value.as_bool())
.unwrap_or(false)
}
pub fn admin_banned_user_message(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> Option<String> {
ctx.get_metadata("admin.banned_user_message")
.and_then(|value| value.as_str())
.map(ToOwned::to_owned)
}
pub async fn issue_user_session<S: better_auth_core::AuthSchema>(
ctx: &AuthContext<S>,
user_id: &str,
ip_address: Option<String>,
user_agent: Option<String>,
) -> Result<IssuedSession<S>, SessionIssueError> {
let mut user = ctx
.database
.get_user_by_id(user_id)
.await?
.ok_or(AuthError::UserNotFound)?;
if admin_plugin_enabled(ctx) && user.banned() {
if user
.ban_expires()
.is_some_and(|expires| expires <= Utc::now())
{
user = ctx
.database
.update_user(
user_id,
UpdateUser {
banned: Some(false),
ban_reason: None,
ban_expires: None,
..Default::default()
},
)
.await?;
} else {
return Err(SessionIssueError::Banned {
message: admin_banned_user_message(ctx).unwrap_or_else(|| {
"You have been banned from this application. Please contact support if you believe this is an error.".to_string()
}),
});
}
}
let session = ctx
.session_manager()
.create_session(&user, ip_address, user_agent)
.await?;
Ok(IssuedSession { user, session })
}
pub fn get_cookie(req: &AuthRequest, name: &str) -> Option<String> {
let header = req.headers.get("cookie")?;
header
.split(';')
.filter_map(|cookie| {
let trimmed = cookie.trim();
let (cookie_name, cookie_value) = trimmed.split_once('=')?;
(cookie_name == name).then_some(cookie_value.to_string())
})
.next()
}
pub fn delete_session_cookie_headers(config: &better_auth_core::AuthConfig) -> Vec<String> {
let mut cookies = vec![
better_auth_core::utils::cookie_utils::create_clear_session_cookie(config),
better_auth_core::utils::cookie_utils::create_clear_cookie(
&better_auth_core::utils::cookie_utils::related_cookie_name(config, "session_data"),
config,
),
better_auth_core::utils::cookie_utils::create_clear_cookie(
&better_auth_core::utils::cookie_utils::related_cookie_name(config, "dont_remember"),
config,
),
];
if config.account.store_account_cookie {
cookies.push(better_auth_core::utils::cookie_utils::create_clear_cookie(
&better_auth_core::utils::cookie_utils::related_cookie_name(config, "account_data"),
config,
));
}
if matches!(
config.account.store_state_strategy,
OAuthStateStrategy::Cookie
) {
cookies.push(better_auth_core::utils::cookie_utils::create_clear_cookie(
&better_auth_core::utils::cookie_utils::related_cookie_name(config, "oauth_state"),
config,
));
}
cookies
}