better_auth_api/plugins/user_management/
mod.rs1use async_trait::async_trait;
2use chrono::Duration;
3use std::sync::Arc;
4
5use better_auth_core::adapters::DatabaseAdapter;
6use better_auth_core::entity::AuthUser;
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
10
11pub(super) mod handlers;
12pub(super) mod types;
13
14#[cfg(test)]
15mod tests;
16
17use handlers::*;
18use types::*;
19
20#[derive(Debug, Clone)]
29pub struct UserInfo {
30 pub id: String,
31 pub email: Option<String>,
32 pub name: Option<String>,
33 pub email_verified: bool,
34}
35
36impl UserInfo {
37 fn from_auth_user(user: &impl AuthUser) -> Self {
39 Self {
40 id: user.id().to_string(),
41 email: user.email().map(|s| s.to_string()),
42 name: user.name().map(|s| s.to_string()),
43 email_verified: user.email_verified(),
44 }
45 }
46}
47
48#[async_trait]
58pub trait SendChangeEmailConfirmation: Send + Sync {
59 async fn send(
60 &self,
61 user: &UserInfo,
62 new_email: &str,
63 url: &str,
64 token: &str,
65 ) -> AuthResult<()>;
66}
67
68#[async_trait]
73pub trait BeforeDeleteUser: Send + Sync {
74 async fn before_delete(&self, user: &UserInfo) -> AuthResult<()>;
75}
76
77#[async_trait]
79pub trait AfterDeleteUser: Send + Sync {
80 async fn after_delete(&self, user: &UserInfo) -> AuthResult<()>;
81}
82
83#[derive(Clone, Default)]
89pub struct ChangeEmailConfig {
90 pub enabled: bool,
92 pub update_without_verification: bool,
95 pub send_change_email_confirmation: Option<Arc<dyn SendChangeEmailConfirmation>>,
97}
98
99impl std::fmt::Debug for ChangeEmailConfig {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("ChangeEmailConfig")
102 .field("enabled", &self.enabled)
103 .field(
104 "update_without_verification",
105 &self.update_without_verification,
106 )
107 .field(
108 "send_change_email_confirmation",
109 &self.send_change_email_confirmation.is_some(),
110 )
111 .finish()
112 }
113}
114
115#[derive(Clone)]
117pub struct DeleteUserConfig {
118 pub enabled: bool,
120 pub delete_token_expires_in: Duration,
122 pub require_verification: bool,
125 pub before_delete: Option<Arc<dyn BeforeDeleteUser>>,
127 pub after_delete: Option<Arc<dyn AfterDeleteUser>>,
129}
130
131impl std::fmt::Debug for DeleteUserConfig {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("DeleteUserConfig")
134 .field("enabled", &self.enabled)
135 .field("delete_token_expires_in", &self.delete_token_expires_in)
136 .field("require_verification", &self.require_verification)
137 .field("before_delete", &self.before_delete.is_some())
138 .field("after_delete", &self.after_delete.is_some())
139 .finish()
140 }
141}
142
143impl Default for DeleteUserConfig {
144 fn default() -> Self {
145 Self {
146 enabled: false,
147 delete_token_expires_in: Duration::hours(24),
148 require_verification: true,
149 before_delete: None,
150 after_delete: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct UserManagementConfig {
158 pub change_email: ChangeEmailConfig,
159 pub delete_user: DeleteUserConfig,
160}
161
162pub struct UserManagementPlugin {
168 config: UserManagementConfig,
169}
170
171impl UserManagementPlugin {
172 pub fn new() -> Self {
173 Self {
174 config: UserManagementConfig::default(),
175 }
176 }
177
178 pub fn with_config(config: UserManagementConfig) -> Self {
179 Self { config }
180 }
181
182 pub fn change_email_enabled(mut self, enabled: bool) -> Self {
185 self.config.change_email.enabled = enabled;
186 self
187 }
188
189 pub fn update_without_verification(mut self, flag: bool) -> Self {
190 self.config.change_email.update_without_verification = flag;
191 self
192 }
193
194 pub fn send_change_email_confirmation(
195 mut self,
196 cb: Arc<dyn SendChangeEmailConfirmation>,
197 ) -> Self {
198 self.config.change_email.send_change_email_confirmation = Some(cb);
199 self
200 }
201
202 pub fn delete_user_enabled(mut self, enabled: bool) -> Self {
203 self.config.delete_user.enabled = enabled;
204 self
205 }
206
207 pub fn delete_token_expires_in(mut self, duration: Duration) -> Self {
208 self.config.delete_user.delete_token_expires_in = duration;
209 self
210 }
211
212 pub fn require_delete_verification(mut self, require: bool) -> Self {
213 self.config.delete_user.require_verification = require;
214 self
215 }
216
217 pub fn before_delete(mut self, hook: Arc<dyn BeforeDeleteUser>) -> Self {
218 self.config.delete_user.before_delete = Some(hook);
219 self
220 }
221
222 pub fn after_delete(mut self, hook: Arc<dyn AfterDeleteUser>) -> Self {
223 self.config.delete_user.after_delete = Some(hook);
224 self
225 }
226}
227
228impl Default for UserManagementPlugin {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl UserManagementPlugin {
239 async fn handle_change_email<DB: DatabaseAdapter>(
241 &self,
242 req: &AuthRequest,
243 ctx: &AuthContext<DB>,
244 ) -> AuthResult<AuthResponse> {
245 let (user, _session) = ctx.require_session(req).await?;
246 let body: ChangeEmailRequest = match better_auth_core::validate_request_body(req) {
247 Ok(v) => v,
248 Err(resp) => return Ok(resp),
249 };
250 let response = change_email_core(&body, &user, &self.config, ctx).await?;
251 Ok(AuthResponse::json(200, &response)?)
252 }
253
254 async fn handle_change_email_verify<DB: DatabaseAdapter>(
256 &self,
257 req: &AuthRequest,
258 ctx: &AuthContext<DB>,
259 ) -> AuthResult<AuthResponse> {
260 let token = req
261 .query
262 .get("token")
263 .ok_or_else(|| AuthError::bad_request("Verification token is required"))?;
264 let response = change_email_verify_core(token, ctx).await?;
265 Ok(AuthResponse::json(200, &response)?)
266 }
267
268 async fn handle_delete_user<DB: DatabaseAdapter>(
270 &self,
271 req: &AuthRequest,
272 ctx: &AuthContext<DB>,
273 ) -> AuthResult<AuthResponse> {
274 let (user, _session) = ctx.require_session(req).await?;
275 let response = delete_user_core(&user, &self.config, ctx).await?;
276 Ok(AuthResponse::json(200, &response)?)
277 }
278
279 async fn handle_delete_user_verify<DB: DatabaseAdapter>(
281 &self,
282 req: &AuthRequest,
283 ctx: &AuthContext<DB>,
284 ) -> AuthResult<AuthResponse> {
285 let token = req
286 .query
287 .get("token")
288 .ok_or_else(|| AuthError::bad_request("Verification token is required"))?;
289 let response = delete_user_verify_core(token, &self.config, ctx).await?;
290 Ok(AuthResponse::json(200, &response)?)
291 }
292}
293
294#[async_trait]
299impl<DB: DatabaseAdapter> AuthPlugin<DB> for UserManagementPlugin {
300 fn name(&self) -> &'static str {
301 "user-management"
302 }
303
304 fn routes(&self) -> Vec<AuthRoute> {
305 let mut routes = Vec::new();
306 if self.config.change_email.enabled {
307 routes.push(AuthRoute::post("/change-email", "change_email"));
308 routes.push(AuthRoute::get(
309 "/change-email/verify",
310 "change_email_verify",
311 ));
312 }
313 if self.config.delete_user.enabled {
314 routes.push(AuthRoute::post("/delete-user", "delete_user"));
315 routes.push(AuthRoute::get("/delete-user/verify", "delete_user_verify"));
316 }
317 routes
318 }
319
320 async fn on_request(
321 &self,
322 req: &AuthRequest,
323 ctx: &AuthContext<DB>,
324 ) -> AuthResult<Option<AuthResponse>> {
325 match (req.method(), req.path()) {
326 (HttpMethod::Post, "/change-email") if self.config.change_email.enabled => {
328 Ok(Some(self.handle_change_email(req, ctx).await?))
329 }
330 (HttpMethod::Get, "/change-email/verify") if self.config.change_email.enabled => {
331 Ok(Some(self.handle_change_email_verify(req, ctx).await?))
332 }
333 (HttpMethod::Post, "/delete-user") if self.config.delete_user.enabled => {
335 Ok(Some(self.handle_delete_user(req, ctx).await?))
336 }
337 (HttpMethod::Get, "/delete-user/verify") if self.config.delete_user.enabled => {
338 Ok(Some(self.handle_delete_user_verify(req, ctx).await?))
339 }
340 _ => Ok(None),
341 }
342 }
343}
344
345#[cfg(feature = "axum")]
350mod axum_impl {
351 use super::*;
352 use std::sync::Arc;
353
354 use axum::Json;
355 use axum::extract::{Extension, Query, State};
356 use better_auth_core::{
357 AuthError, AuthState, CurrentSession, SuccessMessageResponse, ValidatedJson,
358 };
359
360 #[derive(Clone)]
361 struct PluginState {
362 config: UserManagementConfig,
363 }
364
365 async fn handle_change_email<DB: DatabaseAdapter>(
366 State(state): State<AuthState<DB>>,
367 Extension(ps): Extension<Arc<PluginState>>,
368 CurrentSession { user, .. }: CurrentSession<DB>,
369 ValidatedJson(body): ValidatedJson<ChangeEmailRequest>,
370 ) -> Result<Json<StatusMessageResponse>, AuthError> {
371 if !ps.config.change_email.enabled {
372 return Err(AuthError::not_found("Not found"));
373 }
374 let ctx = state.to_context();
375 let response = change_email_core(&body, &user, &ps.config, &ctx).await?;
376 Ok(Json(response))
377 }
378
379 async fn handle_change_email_verify<DB: DatabaseAdapter>(
380 State(state): State<AuthState<DB>>,
381 Extension(ps): Extension<Arc<PluginState>>,
382 Query(query): Query<TokenQuery>,
383 ) -> Result<Json<StatusMessageResponse>, AuthError> {
384 if !ps.config.change_email.enabled {
385 return Err(AuthError::not_found("Not found"));
386 }
387 let ctx = state.to_context();
388 let response = change_email_verify_core(&query.token, &ctx).await?;
389 Ok(Json(response))
390 }
391
392 async fn handle_delete_user<DB: DatabaseAdapter>(
393 State(state): State<AuthState<DB>>,
394 Extension(ps): Extension<Arc<PluginState>>,
395 CurrentSession { user, .. }: CurrentSession<DB>,
396 ) -> Result<Json<SuccessMessageResponse>, AuthError> {
397 if !ps.config.delete_user.enabled {
398 return Err(AuthError::not_found("Not found"));
399 }
400 let ctx = state.to_context();
401 let response = delete_user_core(&user, &ps.config, &ctx).await?;
402 Ok(Json(response))
403 }
404
405 async fn handle_delete_user_verify<DB: DatabaseAdapter>(
406 State(state): State<AuthState<DB>>,
407 Extension(ps): Extension<Arc<PluginState>>,
408 Query(query): Query<TokenQuery>,
409 ) -> Result<Json<SuccessMessageResponse>, AuthError> {
410 if !ps.config.delete_user.enabled {
411 return Err(AuthError::not_found("Not found"));
412 }
413 let ctx = state.to_context();
414 let response = delete_user_verify_core(&query.token, &ps.config, &ctx).await?;
415 Ok(Json(response))
416 }
417
418 impl<DB: DatabaseAdapter> better_auth_core::AxumPlugin<DB> for UserManagementPlugin {
419 fn name(&self) -> &'static str {
420 "user-management"
421 }
422
423 fn router(&self) -> axum::Router<AuthState<DB>> {
424 use axum::routing::{get, post};
425
426 let plugin_state = Arc::new(PluginState {
427 config: self.config.clone(),
428 });
429
430 axum::Router::new()
431 .route("/change-email", post(handle_change_email::<DB>))
432 .route(
433 "/change-email/verify",
434 get(handle_change_email_verify::<DB>),
435 )
436 .route("/delete-user", post(handle_delete_user::<DB>))
437 .route("/delete-user/verify", get(handle_delete_user_verify::<DB>))
438 .layer(Extension(plugin_state))
439 }
440 }
441}