use chrono::Utc;
use better_auth_core::entity::{AuthSession, AuthUser};
use better_auth_core::session::NativeSessionData;
use better_auth_core::utils::cookie_utils::{get_cookie, related_cookie_name, verify_cookie_value};
use better_auth_core::wire::{SessionView, UserView};
use better_auth_core::{
AuthContext, AuthError, AuthResult, CreateAccount, CreateSession, UpdateUser,
};
use crate::plugins::StatusResponse;
use super::access::has_permission;
use super::types::*;
use super::{AdminConfig, target_is_admin};
const MESSAGE_USER_NOT_FOUND: &str = "User not found";
const MESSAGE_NON_EXISTENT_ROLE: &str = "You are not allowed to set a non-existent role value";
const MESSAGE_INVALID_ROLE_TYPE: &str = "Invalid role type";
const MESSAGE_NO_DATA_TO_UPDATE: &str = "No data to update";
const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
const MESSAGE_CANNOT_IMPERSONATE_ADMINS: &str = "You cannot impersonate admins";
const MESSAGE_NOT_IMPERSONATING: &str = "You are not impersonating anyone";
const MESSAGE_FAILED_TO_FIND_USER: &str = "Failed to find user";
const MESSAGE_FAILED_TO_FIND_ADMIN_SESSION: &str = "Failed to find admin session";
fn impersonation_error(message: &str) -> AuthError {
AuthError::from_status(
"INTERNAL_SERVER_ERROR",
better_auth_core::FieldMap::from([("message".into(), message.into())]).into(),
)
}
fn joined_role(role: &RoleInput) -> String {
role.joined()
}
fn validate_role_input(role: &RoleInput, config: &AdminConfig) -> AuthResult<()> {
if let Some(roles) = &config.roles
&& role.roles().iter().any(|role| !roles.contains_key(role))
{
return Err(AuthError::from_code(
"BAD_REQUEST",
"YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE",
MESSAGE_NON_EXISTENT_ROLE,
));
}
Ok(())
}
fn require_user_permission(
session: &NativeSessionData,
config: &AdminConfig,
action: &str,
code: &str,
message: &str,
) -> AuthResult<()> {
let permission = std::collections::HashMap::from([("user".into(), vec![action.into()])]);
if has_permission(
&session.user_property("id")?,
&session.user_property("role")?,
config,
&permission,
)? {
Ok(())
} else {
Err(AuthError::from_code("FORBIDDEN", code, message))
}
}
fn has_ban_data(data: &serde_json::Map<String, serde_json::Value>) -> bool {
["banned", "banReason", "banExpires"]
.iter()
.any(|key| data.contains_key(*key))
}
fn ban_expiry(
data: &serde_json::Map<String, serde_json::Value>,
) -> AuthResult<Option<chrono::DateTime<Utc>>> {
data.get("banExpires")
.filter(|value| !value.is_null())
.map(|value| {
serde_json::from_value(value.clone())
.map_err(|_| AuthError::bad_request("Invalid banExpires"))
})
.transpose()
}
pub(crate) async fn set_role_core(
body: &SetRoleRequest,
config: &AdminConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<UserResponse<AdminUserView>> {
validate_role_input(&body.role, config)?;
let _target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
let update = UpdateUser {
role: Some(joined_role(&body.role)),
..Default::default()
};
let updated_user = ctx.database.update_user(&body.user_id, update).await?;
Ok(UserResponse {
user: ctx.user_view(&updated_user).await?,
})
}
pub(crate) async fn get_user_core(
query: &GetUserQuery,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AdminUserView> {
let user = ctx
.database
.get_user_by_id(&query.id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
ctx.user_view(&user).await
}
pub(crate) async fn create_user_core(
body: &CreateUserRequest,
req: Option<&better_auth_core::AuthRequest>,
session: Option<NativeSessionData>,
config: &AdminConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<UserResponse<AdminUserView>> {
let mut data = body.data.clone().unwrap_or_default();
let data_role = data.remove("role");
if (body.role.is_some() || data_role.is_some())
&& let Some(session) = &session
{
require_user_permission(
session,
config,
"set-role",
"YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE",
MESSAGE_CHANGE_ROLE,
)?;
}
let role = match &body.role {
Some(role) => Some(role.clone()),
None => data_role
.map(serde_json::from_value::<RoleInput>)
.transpose()
.map_err(|_| {
AuthError::from_code(
"BAD_REQUEST",
"INVALID_ROLE_TYPE",
MESSAGE_INVALID_ROLE_TYPE,
)
})?,
};
if let Some(role) = &role {
validate_role_input(role, config)?;
}
if has_ban_data(&data)
&& let Some(session) = &session
{
require_user_permission(
session,
config,
"ban",
"YOU_ARE_NOT_ALLOWED_TO_BAN_USERS",
"You are not allowed to ban users",
)?;
}
let email = body.email.to_lowercase();
if !crate::plugins::json_body::valid_email(&email)? {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVALID_EMAIL",
"Invalid email",
));
}
if let Some(password) = body
.password
.as_deref()
.filter(|password| !password.is_empty())
&& password.encode_utf16().count() > ctx.password_policy.max_length
{
return Err(AuthError::from_code(
"BAD_REQUEST",
"PASSWORD_TOO_LONG",
"Password too long",
));
}
if ctx.database.get_user_by_email(&email).await?.is_some() {
return Err(AuthError::from_code(
"BAD_REQUEST",
"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
"User already exists. Use another email.",
));
}
let mut create_user = better_auth_core::CreateUser::new()
.with_email(email)
.with_name(&body.name)
.with_role(
role.as_ref()
.map(joined_role)
.unwrap_or_else(|| config.default_role.clone()),
);
create_user.banned = data.get("banned").and_then(serde_json::Value::as_bool);
create_user.ban_reason = data
.get("banReason")
.and_then(serde_json::Value::as_str)
.map(str::to_owned);
create_user.ban_expires = ban_expiry(&data)?.map(Into::into);
create_user.image = better_auth_core::SchemaValue::from_json(data.get("image").cloned())?;
create_user.email_verified = data
.get("emailVerified")
.and_then(serde_json::Value::as_bool);
create_user.additional_fields = better_auth_core::FieldMap::from_json(data)?;
let mut input: serde_json::Map<String, serde_json::Value> = match req {
Some(req) => req.body_as_json()?,
None => serde_json::from_value(serde_json::to_value(body)?)?,
};
input.retain(|key, _| {
matches!(
key.as_str(),
"email" | "name" | "password" | "role" | "data"
)
});
let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
req,
better_auth_core::FieldValue::from_json(input.into())?,
ctx,
);
endpoint.path = Some("/admin/create-user");
endpoint.session = session;
let user =
crate::plugins::user_admission::create_user_optional(create_user, "admin", &endpoint)
.await?
.ok_or_else(|| {
AuthError::from_code(
"INTERNAL_SERVER_ERROR",
"FAILED_TO_CREATE_USER",
"Failed to create user",
)
})?;
if let Some(password) = body
.password
.as_deref()
.filter(|password| !password.is_empty())
{
let password_hash =
better_auth_core::hash_password(ctx.password_policy.hasher.as_ref(), password).await?;
let _ = ctx
.database
.create_account_optional(CreateAccount {
user_id: user.id().into_owned(),
account_id: user.id().into_owned(),
provider_id: ("credential".to_string()).into(),
access_token: Default::default(),
refresh_token: Default::default(),
id_token: Default::default(),
access_token_expires_at: Default::default(),
refresh_token_expires_at: Default::default(),
scope: Default::default(),
password: (Some(password_hash))
.map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
.unwrap_or_default(),
..Default::default()
})
.await?;
}
Ok(UserResponse {
user: ctx.user_view(&user).await?,
})
}
pub(crate) async fn update_user_core(
body: &AdminUpdateUserRequest,
acting_session: &NativeSessionData,
config: &AdminConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Option<AdminUserView>> {
if body.data.is_empty() {
return Err(AuthError::from_code(
"BAD_REQUEST",
"NO_DATA_TO_UPDATE",
MESSAGE_NO_DATA_TO_UPDATE,
));
}
if body.data.contains_key("password") {
return Err(AuthError::from_code(
"BAD_REQUEST",
"PASSWORD_CANNOT_BE_UPDATED_VIA_UPDATE_USER",
"Password cannot be updated through update-user. Use the set-user-password endpoint instead",
));
}
let mut update = UpdateUser {
additional_fields: better_auth_core::FieldMap::from_json(body.data.clone())?,
..Default::default()
};
if let Some(value) = body.data.get("role") {
let permissions =
std::collections::HashMap::from([("user".to_string(), vec!["set-role".to_string()])]);
if !has_permission(
&acting_session.user_property("id")?,
&acting_session.user_property("role")?,
config,
&permissions,
)? {
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE",
MESSAGE_CHANGE_ROLE,
));
}
let role = serde_json::from_value::<RoleInput>(value.clone()).map_err(|_| {
AuthError::from_code(
"BAD_REQUEST",
"INVALID_ROLE_TYPE",
MESSAGE_INVALID_ROLE_TYPE,
)
})?;
validate_role_input(&role, config)?;
update.role = Some(joined_role(&role));
}
if has_ban_data(&body.data) {
require_user_permission(
acting_session,
config,
"ban",
"YOU_ARE_NOT_ALLOWED_TO_BAN_USERS",
"You are not allowed to ban users",
)?;
if body.data.get("banned") == Some(&serde_json::Value::Bool(true))
&& acting_session
.user_property("id")?
.strict_equals(&body.user_id.clone().into())
{
return Err(AuthError::from_code(
"BAD_REQUEST",
"YOU_CANNOT_BAN_YOURSELF",
"You cannot ban yourself",
));
}
}
if body.data.contains_key("email") || body.data.contains_key("emailVerified") {
require_user_permission(
acting_session,
config,
"set-email",
"YOU_ARE_NOT_ALLOWED_TO_SET_USERS_EMAIL",
"You are not allowed to update users email",
)?;
if let Some(value) = body.data.get("email") {
let email = better_auth_core::SchemaValue::<String>::from_json(Some(value.clone()))?
.display_string()?
.to_lowercase();
if !crate::plugins::json_body::valid_email(&email)? {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVALID_EMAIL",
"Invalid email",
));
}
if let Some(existing) = ctx.database.get_user_by_email(&email).await?
&& existing.id().into_owned() != body.user_id.as_str()
{
return Err(AuthError::from_code(
"BAD_REQUEST",
"USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
"User already exists. Use another email.",
));
}
update.email = Some(email);
}
}
let _target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
update.name = better_auth_core::SchemaValue::from_json(body.data.get("name").cloned())?;
update.image = better_auth_core::SchemaValue::from_json(body.data.get("image").cloned())?;
if let Some(value) = body
.data
.get("emailVerified")
.and_then(|value| value.as_bool())
{
update.email_verified = Some(value);
}
if ctx.get_metadata("anonymous.enabled") == Some(&serde_json::Value::Bool(true)) {
update.is_anonymous = body
.data
.get("isAnonymous")
.and_then(serde_json::Value::as_bool);
}
if ctx.get_metadata("phone-number.enabled") == Some(&serde_json::Value::Bool(true)) {
update.phone_number = body
.data
.get("phoneNumber")
.map(|value| serde_json::from_value(value.clone()))
.transpose()?;
update.phone_number_verified = body
.data
.get("phoneNumberVerified")
.and_then(serde_json::Value::as_bool);
}
if let Some(value) = body.data.get("banned").and_then(|value| value.as_bool()) {
update.banned = Some(value);
}
if let Some(value) = body.data.get("banReason") {
update.ban_reason = Some(
serde_json::from_value(value.clone())
.map_err(|_| AuthError::bad_request("Invalid banReason"))?,
);
}
if body.data.contains_key("banExpires") {
update.ban_expires = Some(ban_expiry(&body.data)?.map(Into::into));
}
if let Some(value) = body
.data
.get("twoFactorEnabled")
.and_then(|value| value.as_bool())
{
update.two_factor_enabled = Some(value);
}
if let Some(value) = body
.data
.get("metadata")
.and_then(|value| value.as_object())
{
update.metadata = Some(better_auth_core::FieldMap::from_json(value.clone())?.into());
}
let updated_user = ctx
.database
.update_user_optional(&body.user_id, update)
.await?;
if body.data.get("banned") == Some(&serde_json::Value::Bool(true)) {
ctx.database.delete_user_sessions(&body.user_id).await?;
}
match updated_user {
Some(user) => ctx.user_view(&user).await.map(Some),
None => Ok(None),
}
}
pub(crate) async fn list_users_core(
query: &ListUsersQueryParams,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<ListUsersResponse<AdminUserView>> {
let params = better_auth_core::ListUsersParams {
limit: query.limit,
offset: query.offset,
search_field: query.search_field.clone(),
search_value: query.search_value.clone(),
search_operator: query.search_operator.clone(),
sort_by: query.sort_by.clone(),
sort_direction: query.sort_direction.clone(),
filter_field: query.filter_field.clone(),
filter_value: query
.filter_value
.clone()
.map(better_auth_core::FieldValue::from_json)
.transpose()?,
filter_operator: query.filter_operator.clone(),
};
let (users, total) = ctx.database.list_users(params).await?;
let mut projected = Vec::with_capacity(users.len());
for user in &users {
projected.push(ctx.user_view(user).await?);
}
Ok(ListUsersResponse {
users: projected,
total,
limit: query.limit,
offset: query.offset,
})
}
pub(crate) async fn list_user_sessions_core(
body: &UserIdRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<ListSessionsResponse<SessionView>> {
Ok(ListSessionsResponse {
sessions: ctx
.session_manager()
.list_user_session_views(&body.user_id)
.await?,
})
}
pub(crate) async fn ban_user_core(
body: &BanUserRequest,
admin_user_id: &better_auth_core::FieldValue,
config: &AdminConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<UserResponse<AdminUserView>> {
let _target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
if admin_user_id.strict_equals(&body.user_id.clone().into()) {
return Err(AuthError::from_code(
"BAD_REQUEST",
"YOU_CANNOT_BAN_YOURSELF",
"You cannot ban yourself",
));
}
let seconds = body
.ban_expires_in
.filter(|value| *value != 0.0)
.or_else(|| {
config
.default_ban_expires_in
.filter(|value| *value != 0.0 && !value.is_nan())
});
let ban_expires = seconds
.map(|seconds| {
better_auth_core::utils::date::from_milliseconds(
Utc::now().timestamp_millis() as f64 + seconds * 1000.0,
)
.ok_or_else(|| AuthError::internal("Invalid ban expiration date"))
})
.transpose()?;
let update = UpdateUser {
banned: Some(true),
ban_reason: Some(Some(
body.ban_reason
.clone()
.filter(|value| !value.is_empty())
.or_else(|| {
config
.default_ban_reason
.clone()
.filter(|value| !value.is_empty())
})
.unwrap_or_else(|| "No reason".to_string()),
)),
ban_expires: Some(ban_expires.map(Into::into)),
..Default::default()
};
let updated_user = ctx.database.update_user(&body.user_id, update).await?;
let _ = ctx
.session_manager()
.revoke_all_user_sessions(&body.user_id)
.await?;
Ok(UserResponse {
user: ctx.user_view(&updated_user).await?,
})
}
pub(crate) async fn unban_user_core(
body: &UserIdRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<UserResponse<AdminUserView>> {
let _target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
let update = UpdateUser {
banned: Some(false),
ban_reason: Some(None),
ban_expires: Some(None),
..Default::default()
};
let updated_user = ctx.database.update_user(&body.user_id, update).await?;
Ok(UserResponse {
user: ctx.user_view(&updated_user).await?,
})
}
pub(crate) async fn impersonate_user_core(
body: &UserIdRequest,
acting_session: &NativeSessionData,
ip_address: Option<&str>,
user_agent: Option<&str>,
config: &AdminConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<(
SessionUserResponse<SessionView, UserView>,
better_auth_core::session::SessionData,
)> {
let mut target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
if !config.allow_impersonating_admins
&& target_is_admin(
&target.id().field_value(),
&target.role().field_value(),
config,
)?
{
require_user_permission(
acting_session,
config,
"impersonate-admins",
"YOU_CANNOT_IMPERSONATE_ADMINS",
MESSAGE_CANNOT_IMPERSONATE_ADMINS,
)?;
}
if target.banned().is_truthy()? {
let expires = target.ban_expires().field_value();
if expires.is_truthy()
&& better_auth_core::query::field_date(&expires)?.milliseconds()
< Utc::now().timestamp_millis() as f64
{
target = ctx
.database
.update_user(
&body.user_id,
UpdateUser {
banned: Some(false),
ban_reason: Some(None),
ban_expires: Some(None),
..Default::default()
},
)
.await?;
} else {
return Err(AuthError::from_code(
"FORBIDDEN",
"BANNED_USER",
config
.banned_user_message
.resolve(&ctx.internal_user_view(&target).await?)
.await?,
));
}
}
let seconds = config
.impersonation_session_duration
.filter(|value| *value != 0.0 && !value.is_nan())
.unwrap_or(3600.0);
let expires_at = better_auth_core::utils::date::from_milliseconds(
Utc::now().timestamp_millis() as f64 + seconds * 1000.0,
)
.ok_or_else(|| AuthError::internal("Invalid impersonation expiration date"))?;
let create_session = CreateSession {
inherited_fields: Default::default(),
additional_fields: [("impersonatedBy".into(), acting_session.user_property("id")?)].into(),
user_id: target.id().into_owned(),
expires_at: expires_at.into(),
ip_address: ip_address.map(|value| value.to_string()),
user_agent: user_agent.map(|value| value.to_string()),
impersonated_by: None,
active_organization_id: None,
};
let session = ctx
.database
.create_session_optional(create_session)
.await?
.ok_or_else(|| {
AuthError::from_code(
"INTERNAL_SERVER_ERROR",
"FAILED_TO_CREATE_USER",
"Failed to create user",
)
})?;
let data = ctx
.session_manager()
.internal_data(&target, &session)
.await?;
let response = SessionUserResponse {
session,
user: target,
};
Ok((response, data))
}
pub(crate) async fn stop_impersonating_core(
session: &NativeSessionData,
req: &better_auth_core::AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<(NativeSessionData, bool)> {
let admin_id = session.session_property("impersonatedBy")?;
if !admin_id.is_truthy() {
return Err(AuthError::from_status(
"BAD_REQUEST",
better_auth_core::FieldMap::from([(
"message".into(),
MESSAGE_NOT_IMPERSONATING.into(),
)])
.into(),
));
}
let admin_user = ctx
.database
.get_user_by_id_value(&admin_id)
.await?
.ok_or_else(|| impersonation_error(MESSAGE_FAILED_TO_FIND_USER))?;
let admin_cookie = get_cookie(req, &related_cookie_name(&ctx.config, "admin_session"))
.and_then(|value| verify_cookie_value(&value, ctx.config.signing_secret()))
.filter(|value| !value.is_empty())
.ok_or_else(|| impersonation_error(MESSAGE_FAILED_TO_FIND_ADMIN_SESSION))?;
let mut parts = admin_cookie.split(':');
let admin_token = parts.next().unwrap_or_default();
let dont_remember = parts.next().is_some_and(|value| !value.is_empty());
let (admin_session, snapshot) = ctx
.database
.get_session_snapshot(admin_token)
.await?
.ok_or_else(|| impersonation_error(MESSAGE_FAILED_TO_FIND_ADMIN_SESSION))?;
if !admin_session
.user_id()
.field_value()
.strict_equals(&admin_user.id.field_value())
{
return Err(impersonation_error(MESSAGE_FAILED_TO_FIND_ADMIN_SESSION));
}
let data = if let Some(data) = snapshot {
better_auth_core::session::NativeSessionData::from(data)
} else {
ctx.session_manager()
.internal_data(&admin_user, &admin_session)
.await?
.into()
};
if !data.user_value()?.is_truthy() {
return Err(impersonation_error(MESSAGE_FAILED_TO_FIND_ADMIN_SESSION));
}
ctx.database
.delete_session_by_token_value(&session.session_property("token")?)
.await?;
Ok((data, dont_remember))
}
pub(crate) async fn revoke_user_session_core(
body: &RevokeSessionRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<SuccessResponse> {
ctx.session_manager()
.delete_session(&body.session_token)
.await?;
Ok(SuccessResponse { success: true })
}
pub(crate) async fn revoke_user_sessions_core(
body: &UserIdRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<SuccessResponse> {
let _ = ctx
.session_manager()
.revoke_all_user_sessions(&body.user_id)
.await?;
Ok(SuccessResponse { success: true })
}
pub(crate) async fn remove_user_core(
body: &UserIdRequest,
admin_user_id: &better_auth_core::FieldValue,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<SuccessResponse> {
if admin_user_id.strict_equals(&body.user_id.clone().into()) {
return Err(AuthError::from_code(
"BAD_REQUEST",
"YOU_CANNOT_REMOVE_YOURSELF",
"You cannot remove yourself",
));
}
let _target = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
ctx.database.delete_user_sessions(&body.user_id).await?;
let accounts = ctx.database.get_user_accounts(&body.user_id).await?;
for account in &accounts {
ctx.database
.delete_account_value(&account.id.field_value())
.await?;
}
ctx.database.delete_user(&body.user_id).await?;
Ok(SuccessResponse { success: true })
}
pub(crate) async fn set_user_password_core(
body: &SetUserPasswordRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<StatusResponse> {
better_auth_core::utils::password::validate_password(
&body.new_password,
ctx.password_policy.min_length,
ctx.password_policy.max_length,
ctx,
)?;
let user = ctx
.database
.get_user_by_id(&body.user_id)
.await?
.ok_or_else(|| {
AuthError::from_code("NOT_FOUND", "USER_NOT_FOUND", MESSAGE_USER_NOT_FOUND)
})?;
let password_hash =
better_auth_core::hash_password(ctx.password_policy.hasher.as_ref(), &body.new_password)
.await?;
if ctx
.database
.get_credential_account(&body.user_id)
.await?
.is_some()
{
crate::plugins::helpers::update_password(ctx, &body.user_id.as_str().into(), password_hash)
.await?;
} else {
let _ = ctx
.database
.create_account_optional(CreateAccount {
user_id: body.user_id.clone().into(),
account_id: user.id.clone(),
provider_id: "credential".into(),
password: better_auth_core::SchemaValue::Typed(Some(password_hash)),
..Default::default()
})
.await?;
}
Ok(StatusResponse { status: true })
}