use async_trait::async_trait;
use chrono::Duration;
use std::sync::Arc;
use better_auth_core::{AuthContext, AuthPlugin, AuthRoute, FieldValue};
use better_auth_core::{AuthError, AuthResult};
use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
mod callbacks;
pub(super) mod handlers;
pub use callbacks::{ChangeEmailConfirmation, UserManagementCallbacks};
mod request;
pub(super) mod types;
#[cfg(test)]
mod native_tests;
#[cfg(test)]
mod tests;
use handlers::*;
use types::*;
pub type UserInfo = FieldValue;
#[async_trait]
pub trait SendChangeEmailConfirmation: Send + Sync {
async fn send(
&self,
user: &UserInfo,
new_email: &str,
url: &str,
token: &str,
) -> AuthResult<()>;
}
#[async_trait]
pub trait BeforeDeleteUser: Send + Sync {
async fn before_delete(
&self,
user: &FieldValue,
request: Option<&AuthRequest>,
) -> AuthResult<()>;
}
#[async_trait]
pub trait AfterDeleteUser: Send + Sync {
async fn after_delete(
&self,
user: &FieldValue,
request: Option<&AuthRequest>,
) -> AuthResult<()>;
}
#[async_trait]
pub trait SendDeleteAccountVerification: Send + Sync {
async fn send(
&self,
user: &FieldValue,
url: &str,
token: &str,
request: Option<&AuthRequest>,
) -> AuthResult<()>;
}
#[derive(Clone, Default)]
pub struct ChangeEmailConfig {
pub enabled: Option<bool>,
pub update_without_verification: bool,
pub send_change_email_confirmation: Option<Arc<dyn SendChangeEmailConfirmation>>,
}
impl ChangeEmailConfig {
pub fn enabled(&self) -> bool {
self.enabled.unwrap_or(false)
}
}
impl std::fmt::Debug for ChangeEmailConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChangeEmailConfig")
.field("enabled", &self.enabled)
.field(
"update_without_verification",
&self.update_without_verification,
)
.field(
"send_change_email_confirmation",
&self.send_change_email_confirmation.is_some(),
)
.finish()
}
}
#[derive(Clone)]
pub struct DeleteUserConfig {
pub enabled: bool,
pub delete_token_expires_in: Duration,
pub send_delete_account_verification: Option<Arc<dyn SendDeleteAccountVerification>>,
pub before_delete: Option<Arc<dyn BeforeDeleteUser>>,
pub after_delete: Option<Arc<dyn AfterDeleteUser>>,
}
impl std::fmt::Debug for DeleteUserConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeleteUserConfig")
.field("enabled", &self.enabled)
.field("delete_token_expires_in", &self.delete_token_expires_in)
.field(
"send_delete_account_verification",
&self.send_delete_account_verification.is_some(),
)
.field("before_delete", &self.before_delete.is_some())
.field("after_delete", &self.after_delete.is_some())
.finish()
}
}
impl Default for DeleteUserConfig {
fn default() -> Self {
Self {
enabled: false,
delete_token_expires_in: Duration::hours(24),
send_delete_account_verification: None,
before_delete: None,
after_delete: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct UserManagementConfig {
pub change_email: ChangeEmailConfig,
pub delete_user: DeleteUserConfig,
}
pub struct UserManagementPlugin {
config: UserManagementConfig,
}
impl UserManagementPlugin {
pub fn new() -> Self {
Self {
config: UserManagementConfig::default(),
}
}
pub fn with_config(config: UserManagementConfig) -> Self {
Self { config }
}
pub fn change_email_enabled(mut self, enabled: bool) -> Self {
self.config.change_email.enabled = Some(enabled);
self
}
pub fn update_without_verification(mut self, flag: bool) -> Self {
self.config.change_email.update_without_verification = flag;
self
}
pub fn send_change_email_confirmation(
mut self,
cb: Arc<dyn SendChangeEmailConfirmation>,
) -> Self {
self.config.change_email.send_change_email_confirmation = Some(cb);
self
}
pub fn delete_user_enabled(mut self, enabled: bool) -> Self {
self.config.delete_user.enabled = enabled;
self
}
pub fn delete_token_expires_in(mut self, duration: Duration) -> Self {
self.config.delete_user.delete_token_expires_in = duration;
self
}
pub fn send_delete_account_verification(
mut self,
sender: Arc<dyn SendDeleteAccountVerification>,
) -> Self {
self.config.delete_user.send_delete_account_verification = Some(sender);
self
}
pub fn before_delete(mut self, hook: Arc<dyn BeforeDeleteUser>) -> Self {
self.config.delete_user.before_delete = Some(hook);
self
}
pub fn after_delete(mut self, hook: Arc<dyn AfterDeleteUser>) -> Self {
self.config.delete_user.after_delete = Some(hook);
self
}
}
impl Default for UserManagementPlugin {
fn default() -> Self {
Self::new()
}
}
impl UserManagementPlugin {
async fn handle_change_email(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let data = ctx
.require_authoritative_native_session(req)
.await
.map_err(|error| match error {
AuthError::Unauthenticated => {
AuthError::from_code("UNAUTHORIZED", "UNAUTHORIZED", "Unauthorized")
}
error => error,
})?;
let body = request::change_email(req)?;
let response = change_email_core(&body, &data, req, &self.config, ctx).await?;
AuthResponse::json(None, &response)
}
async fn handle_delete_user(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let data = ctx.require_native_session(req).await?;
let body = request::delete_user(req)?;
let response = delete_user_core(&body, &data, req, &self.config, ctx).await?;
let mut response = AuthResponse::json(None, &response)?;
for (name, value) in req.take_response_headers()? {
response.headers.append(name, value);
}
Ok(response)
}
async fn handle_delete_user_callback(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let data = ctx
.require_authoritative_native_session(req)
.await
.map_err(|error| match error {
AuthError::Unauthenticated => AuthError::from_code(
"NOT_FOUND",
"FAILED_TO_GET_USER_INFO",
"Failed to get user info",
),
error => error,
})?;
let query: TokenQuery = serde_json::from_value(serde_json::json!({
"token": req.query_string("token")?,
"callbackURL": req.query_string("callbackURL")?,
}))
.map_err(|_| AuthError::bad_request("Verification token is required"))?;
let response =
delete_user_callback_core(&query.token, &data, req, &self.config, ctx).await?;
if let Some(callback_url) = query.callback_url.filter(|url| !url.is_empty()) {
let mut headers = better_auth_core::Headers::new();
_ = headers.insert("Location".to_string(), callback_url);
let mut response = AuthResponse::new(302);
response.headers = headers;
for (name, value) in req.take_response_headers()? {
response.headers.append(name, value);
}
return Ok(response);
}
let mut response = AuthResponse::json(None, &response)?;
for (name, value) in req.take_response_headers()? {
response.headers.append(name, value);
}
Ok(response)
}
}
#[async_trait]
impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for UserManagementPlugin {
fn telemetry(&self, options: &mut better_auth_core::observability::telemetry::PluginTelemetry) {
options.change_email_enabled = self.config.change_email.enabled;
options.send_change_email_confirmation = self
.config
.change_email
.send_change_email_confirmation
.is_some();
}
fn name(&self) -> &'static str {
"user-management"
}
fn telemetry_plugin_id(&self) -> Option<&'static str> {
None
}
fn routes(&self) -> Vec<AuthRoute> {
let mut routes = vec![
AuthRoute::post("/change-email", "changeEmail")
.body_validator(request::change_email_body),
];
if self.config.delete_user.enabled {
routes.push(
AuthRoute::post("/delete-user", "deleteUser")
.body_validator(request::delete_user_body),
);
routes.push(
AuthRoute::get("/delete-user/callback", "deleteUserCallback")
.query_validator(crate::plugins::query_input::verify_email),
);
}
routes
}
async fn on_request(
&self,
req: &AuthRequest,
ctx: &AuthContext<S>,
) -> AuthResult<Option<AuthResponse>> {
match (req.method(), req.path()) {
(HttpMethod::Post, "/change-email") => {
Ok(Some(self.handle_change_email(req, ctx).await?))
}
(HttpMethod::Post, "/delete-user") if self.config.delete_user.enabled => {
Ok(Some(self.handle_delete_user(req, ctx).await?))
}
(HttpMethod::Get, "/delete-user/callback") if self.config.delete_user.enabled => {
Ok(Some(self.handle_delete_user_callback(req, ctx).await?))
}
_ => Ok(None),
}
}
}