1use async_trait::async_trait;
2use chrono::{Duration, Utc};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use uuid::Uuid;
6use validator::Validate;
7
8use better_auth_core::adapters::DatabaseAdapter;
9use better_auth_core::entity::{AuthUser, AuthVerification};
10use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
11use better_auth_core::{AuthError, AuthResult};
12use better_auth_core::{AuthRequest, AuthResponse, CreateVerification, HttpMethod, UpdateUser};
13
14#[derive(Debug, Clone)]
23pub struct UserInfo {
24 pub id: String,
25 pub email: Option<String>,
26 pub name: Option<String>,
27 pub email_verified: bool,
28}
29
30impl UserInfo {
31 fn from_auth_user(user: &impl AuthUser) -> Self {
33 Self {
34 id: user.id().to_string(),
35 email: user.email().map(|s| s.to_string()),
36 name: user.name().map(|s| s.to_string()),
37 email_verified: user.email_verified(),
38 }
39 }
40}
41
42#[async_trait]
52pub trait SendChangeEmailConfirmation: Send + Sync {
53 async fn send(
54 &self,
55 user: &UserInfo,
56 new_email: &str,
57 url: &str,
58 token: &str,
59 ) -> AuthResult<()>;
60}
61
62#[async_trait]
67pub trait BeforeDeleteUser: Send + Sync {
68 async fn before_delete(&self, user: &UserInfo) -> AuthResult<()>;
69}
70
71#[async_trait]
73pub trait AfterDeleteUser: Send + Sync {
74 async fn after_delete(&self, user: &UserInfo) -> AuthResult<()>;
75}
76
77#[derive(Clone, Default)]
83pub struct ChangeEmailConfig {
84 pub enabled: bool,
86 pub update_without_verification: bool,
89 pub send_change_email_confirmation: Option<Arc<dyn SendChangeEmailConfirmation>>,
91}
92
93impl std::fmt::Debug for ChangeEmailConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("ChangeEmailConfig")
96 .field("enabled", &self.enabled)
97 .field(
98 "update_without_verification",
99 &self.update_without_verification,
100 )
101 .field(
102 "send_change_email_confirmation",
103 &self.send_change_email_confirmation.is_some(),
104 )
105 .finish()
106 }
107}
108
109#[derive(Clone)]
111pub struct DeleteUserConfig {
112 pub enabled: bool,
114 pub delete_token_expires_in: Duration,
116 pub require_verification: bool,
119 pub before_delete: Option<Arc<dyn BeforeDeleteUser>>,
121 pub after_delete: Option<Arc<dyn AfterDeleteUser>>,
123}
124
125impl std::fmt::Debug for DeleteUserConfig {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("DeleteUserConfig")
128 .field("enabled", &self.enabled)
129 .field("delete_token_expires_in", &self.delete_token_expires_in)
130 .field("require_verification", &self.require_verification)
131 .field("before_delete", &self.before_delete.is_some())
132 .field("after_delete", &self.after_delete.is_some())
133 .finish()
134 }
135}
136
137impl Default for DeleteUserConfig {
138 fn default() -> Self {
139 Self {
140 enabled: false,
141 delete_token_expires_in: Duration::hours(24),
142 require_verification: true,
143 before_delete: None,
144 after_delete: None,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Default)]
151pub struct UserManagementConfig {
152 pub change_email: ChangeEmailConfig,
153 pub delete_user: DeleteUserConfig,
154}
155
156#[derive(Debug, Deserialize, Validate)]
161struct ChangeEmailRequest {
162 #[serde(rename = "newEmail")]
163 #[validate(email(message = "Invalid email address"))]
164 new_email: String,
165 #[serde(rename = "callbackURL")]
166 callback_url: Option<String>,
167}
168
169#[derive(Debug, Serialize)]
170struct StatusMessageResponse {
171 status: bool,
172 message: String,
173}
174
175pub struct UserManagementPlugin {
181 config: UserManagementConfig,
182}
183
184impl UserManagementPlugin {
185 pub fn new() -> Self {
186 Self {
187 config: UserManagementConfig::default(),
188 }
189 }
190
191 pub fn with_config(config: UserManagementConfig) -> Self {
192 Self { config }
193 }
194
195 pub fn change_email_enabled(mut self, enabled: bool) -> Self {
198 self.config.change_email.enabled = enabled;
199 self
200 }
201
202 pub fn update_without_verification(mut self, flag: bool) -> Self {
203 self.config.change_email.update_without_verification = flag;
204 self
205 }
206
207 pub fn send_change_email_confirmation(
208 mut self,
209 cb: Arc<dyn SendChangeEmailConfirmation>,
210 ) -> Self {
211 self.config.change_email.send_change_email_confirmation = Some(cb);
212 self
213 }
214
215 pub fn delete_user_enabled(mut self, enabled: bool) -> Self {
216 self.config.delete_user.enabled = enabled;
217 self
218 }
219
220 pub fn delete_token_expires_in(mut self, duration: Duration) -> Self {
221 self.config.delete_user.delete_token_expires_in = duration;
222 self
223 }
224
225 pub fn require_delete_verification(mut self, require: bool) -> Self {
226 self.config.delete_user.require_verification = require;
227 self
228 }
229
230 pub fn before_delete(mut self, hook: Arc<dyn BeforeDeleteUser>) -> Self {
231 self.config.delete_user.before_delete = Some(hook);
232 self
233 }
234
235 pub fn after_delete(mut self, hook: Arc<dyn AfterDeleteUser>) -> Self {
236 self.config.delete_user.after_delete = Some(hook);
237 self
238 }
239}
240
241impl Default for UserManagementPlugin {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247#[async_trait]
252impl<DB: DatabaseAdapter> AuthPlugin<DB> for UserManagementPlugin {
253 fn name(&self) -> &'static str {
254 "user-management"
255 }
256
257 fn routes(&self) -> Vec<AuthRoute> {
258 let mut routes = Vec::new();
259 if self.config.change_email.enabled {
260 routes.push(AuthRoute::post("/change-email", "change_email"));
261 routes.push(AuthRoute::get(
262 "/change-email/verify",
263 "change_email_verify",
264 ));
265 }
266 if self.config.delete_user.enabled {
267 routes.push(AuthRoute::post("/delete-user", "delete_user"));
268 routes.push(AuthRoute::get("/delete-user/verify", "delete_user_verify"));
269 }
270 routes
271 }
272
273 async fn on_request(
274 &self,
275 req: &AuthRequest,
276 ctx: &AuthContext<DB>,
277 ) -> AuthResult<Option<AuthResponse>> {
278 match (req.method(), req.path()) {
279 (HttpMethod::Post, "/change-email") if self.config.change_email.enabled => {
281 Ok(Some(self.handle_change_email(req, ctx).await?))
282 }
283 (HttpMethod::Get, "/change-email/verify") if self.config.change_email.enabled => {
284 Ok(Some(self.handle_change_email_verify(req, ctx).await?))
285 }
286 (HttpMethod::Post, "/delete-user") if self.config.delete_user.enabled => {
288 Ok(Some(self.handle_delete_user(req, ctx).await?))
289 }
290 (HttpMethod::Get, "/delete-user/verify") if self.config.delete_user.enabled => {
291 Ok(Some(self.handle_delete_user_verify(req, ctx).await?))
292 }
293 _ => Ok(None),
294 }
295 }
296}
297
298impl UserManagementPlugin {
303 async fn create_verification_token<DB: DatabaseAdapter>(
305 ctx: &AuthContext<DB>,
306 identifier: &str,
307 token_prefix: &str,
308 expires_at: chrono::DateTime<Utc>,
309 callback_url: Option<&str>,
310 default_path: &str,
311 ) -> AuthResult<(String, String)> {
312 let token_value = format!("{}_{}", token_prefix, Uuid::new_v4());
313
314 let create_verification = CreateVerification {
315 identifier: identifier.to_string(),
316 value: token_value.clone(),
317 expires_at,
318 };
319
320 ctx.database
321 .create_verification(create_verification)
322 .await?;
323
324 let verification_url = if let Some(cb_url) = callback_url {
325 format!("{}?token={}", cb_url, token_value)
326 } else {
327 format!(
328 "{}/{}?token={}",
329 ctx.config.base_url, default_path, token_value
330 )
331 };
332
333 Ok((token_value, verification_url))
334 }
335
336 async fn consume_verification_token<DB: DatabaseAdapter>(
342 ctx: &AuthContext<DB>,
343 req: &AuthRequest,
344 expected_prefix: &str,
345 expected_parts: usize,
346 ) -> AuthResult<(Vec<String>, String)> {
347 let token = req
348 .query
349 .get("token")
350 .ok_or_else(|| AuthError::bad_request("Verification token is required"))?;
351
352 let verification = ctx
353 .database
354 .get_verification_by_value(token)
355 .await?
356 .ok_or_else(|| AuthError::bad_request("Invalid or expired verification token"))?;
357
358 if verification.expires_at() < Utc::now() {
359 ctx.database.delete_verification(verification.id()).await?;
360 return Err(AuthError::bad_request("Verification token has expired"));
361 }
362
363 let identifier = verification.identifier();
364 let parts: Vec<String> = identifier
365 .splitn(expected_parts, ':')
366 .map(|s| s.to_string())
367 .collect();
368 if parts.len() != expected_parts || parts[0] != expected_prefix {
369 return Err(AuthError::bad_request("Invalid verification token"));
370 }
371
372 let verification_id = verification.id().to_string();
373 Ok((parts, verification_id))
374 }
375
376 async fn send_email_or_log<DB: DatabaseAdapter>(
378 ctx: &AuthContext<DB>,
379 to: &str,
380 subject: &str,
381 html: &str,
382 text: &str,
383 action: &str,
384 ) {
385 if let Ok(provider) = ctx.email_provider() {
386 if let Err(e) = provider.send(to, subject, html, text).await {
387 tracing::warn!(
388 plugin = "user-management",
389 action = action,
390 email = to,
391 error = %e,
392 "Failed to send email"
393 );
394 }
395 } else {
396 tracing::warn!(
397 plugin = "user-management",
398 action = action,
399 email = to,
400 "No email provider configured, skipping email"
401 );
402 }
403 }
404}
405
406impl UserManagementPlugin {
411 async fn handle_change_email<DB: DatabaseAdapter>(
415 &self,
416 req: &AuthRequest,
417 ctx: &AuthContext<DB>,
418 ) -> AuthResult<AuthResponse> {
419 let (user, _session) = ctx.require_session(req).await?;
420
421 let change_req: ChangeEmailRequest = match better_auth_core::validate_request_body(req) {
422 Ok(v) => v,
423 Err(resp) => return Ok(resp),
424 };
425
426 if user
428 .email()
429 .map(|e| e == change_req.new_email)
430 .unwrap_or(false)
431 {
432 return Err(AuthError::bad_request(
433 "New email must be different from the current email",
434 ));
435 }
436
437 if ctx
439 .database
440 .get_user_by_email(&change_req.new_email)
441 .await?
442 .is_some()
443 {
444 return Err(AuthError::bad_request(
445 "Email is already in use by another account",
446 ));
447 }
448
449 if self.config.change_email.update_without_verification {
452 let update_user = UpdateUser {
453 email: Some(change_req.new_email.clone()),
454 email_verified: Some(false),
455 ..Default::default()
456 };
457 ctx.database.update_user(user.id(), update_user).await?;
458
459 let response = StatusMessageResponse {
460 status: true,
461 message: "Email updated successfully".to_string(),
462 };
463 return Ok(AuthResponse::json(200, &response)?);
464 }
465
466 let identifier = format!("change_email:{}:{}", user.id(), change_req.new_email);
468 let expires_at = Utc::now() + Duration::hours(24);
469 let (verification_token, verification_url) = Self::create_verification_token(
470 ctx,
471 &identifier,
472 "ce",
473 expires_at,
474 change_req.callback_url.as_deref(),
475 "change-email/verify",
476 )
477 .await?;
478
479 if let Some(ref cb) = self.config.change_email.send_change_email_confirmation {
481 let user_info = UserInfo::from_auth_user(&user);
482 cb.send(
483 &user_info,
484 &change_req.new_email,
485 &verification_url,
486 &verification_token,
487 )
488 .await?;
489 } else {
490 let subject = "Confirm your email change";
491 let html = format!(
492 "<p>Click the link below to confirm your new email address:</p>\
493 <p><a href=\"{url}\">Confirm Email Change</a></p>",
494 url = verification_url
495 );
496 let text = format!("Confirm your email change: {}", verification_url);
497
498 Self::send_email_or_log(
499 ctx,
500 &change_req.new_email,
501 subject,
502 &html,
503 &text,
504 "change-email",
505 )
506 .await;
507 }
508
509 let response = StatusMessageResponse {
510 status: true,
511 message: "Verification email sent to your new email address".to_string(),
512 };
513 Ok(AuthResponse::json(200, &response)?)
514 }
515
516 async fn handle_change_email_verify<DB: DatabaseAdapter>(
518 &self,
519 req: &AuthRequest,
520 ctx: &AuthContext<DB>,
521 ) -> AuthResult<AuthResponse> {
522 let (parts, verification_id) =
524 Self::consume_verification_token(ctx, req, "change_email", 3).await?;
525
526 let user_id = &parts[1];
527 let new_email = &parts[2];
528
529 let user = ctx
531 .database
532 .get_user_by_id(user_id)
533 .await?
534 .ok_or_else(|| AuthError::not_found("User not found"))?;
535
536 if ctx.database.get_user_by_email(new_email).await?.is_some() {
538 ctx.database.delete_verification(&verification_id).await?;
539 return Err(AuthError::bad_request(
540 "Email is already in use by another account",
541 ));
542 }
543
544 let new_verified = true;
547
548 let update_user = UpdateUser {
549 email: Some(new_email.to_string()),
550 email_verified: Some(new_verified),
551 ..Default::default()
552 };
553
554 ctx.database.update_user(user.id(), update_user).await?;
555
556 ctx.database.delete_verification(&verification_id).await?;
558
559 let response = StatusMessageResponse {
560 status: true,
561 message: "Email updated successfully".to_string(),
562 };
563 Ok(AuthResponse::json(200, &response)?)
564 }
565
566 async fn handle_delete_user<DB: DatabaseAdapter>(
570 &self,
571 req: &AuthRequest,
572 ctx: &AuthContext<DB>,
573 ) -> AuthResult<AuthResponse> {
574 let (user, _session) = ctx.require_session(req).await?;
575
576 if self.config.delete_user.require_verification {
577 let email = user.email().filter(|e| !e.is_empty()).ok_or_else(|| {
579 AuthError::bad_request("Cannot send verification email: user has no email address")
580 })?;
581 let email = email.to_string();
582
583 let identifier = format!("delete_user:{}", user.id());
585 let expires_at = Utc::now() + self.config.delete_user.delete_token_expires_in;
586 let (_delete_token, verification_url) = Self::create_verification_token(
587 ctx,
588 &identifier,
589 "del",
590 expires_at,
591 None,
592 "delete-user/verify",
593 )
594 .await?;
595
596 let subject = "Confirm account deletion";
598 let html = format!(
599 "<p>Click the link below to confirm the deletion of your account:</p>\
600 <p><a href=\"{url}\">Confirm Account Deletion</a></p>\
601 <p>If you did not request this, please ignore this email.</p>",
602 url = verification_url
603 );
604 let text = format!("Confirm account deletion: {}", verification_url);
605
606 Self::send_email_or_log(ctx, &email, subject, &html, &text, "delete-user").await;
607
608 let response = StatusMessageResponse {
609 status: true,
610 message: "Verification email sent. Please confirm to delete your account."
611 .to_string(),
612 };
613 Ok(AuthResponse::json(200, &response)?)
614 } else {
615 self.perform_user_deletion(&user, ctx).await?;
617
618 let response = StatusMessageResponse {
619 status: true,
620 message: "Account deleted successfully".to_string(),
621 };
622 Ok(AuthResponse::json(200, &response)?)
623 }
624 }
625
626 async fn handle_delete_user_verify<DB: DatabaseAdapter>(
628 &self,
629 req: &AuthRequest,
630 ctx: &AuthContext<DB>,
631 ) -> AuthResult<AuthResponse> {
632 let (parts, verification_id) =
634 Self::consume_verification_token(ctx, req, "delete_user", 2).await?;
635
636 let user_id = &parts[1];
637
638 let user = ctx
640 .database
641 .get_user_by_id(user_id)
642 .await?
643 .ok_or_else(|| AuthError::not_found("User not found"))?;
644
645 self.perform_user_deletion(&user, ctx).await?;
649
650 ctx.database.delete_verification(&verification_id).await?;
652
653 let response = StatusMessageResponse {
654 status: true,
655 message: "Account deleted successfully".to_string(),
656 };
657 Ok(AuthResponse::json(200, &response)?)
658 }
659
660 async fn perform_user_deletion<DB: DatabaseAdapter>(
667 &self,
668 user: &DB::User,
669 ctx: &AuthContext<DB>,
670 ) -> AuthResult<()> {
671 let user_info = UserInfo::from_auth_user(user);
672
673 if let Some(ref hook) = self.config.delete_user.before_delete {
675 hook.before_delete(&user_info).await?;
676 }
677
678 ctx.database.delete_user_sessions(user.id()).await?;
680
681 let accounts = ctx.database.get_user_accounts(user.id()).await?;
683 for account in &accounts {
684 use better_auth_core::entity::AuthAccount;
685 ctx.database.delete_account(account.id()).await?;
686 }
687
688 ctx.database.delete_user(user.id()).await?;
690
691 if let Some(ref hook) = self.config.delete_user.after_delete
695 && let Err(e) = hook.after_delete(&user_info).await
696 {
697 tracing::warn!(
698 error = %e,
699 user_id = %user_info.id,
700 "after_delete hook failed (user already deleted)"
701 );
702 }
703
704 Ok(())
705 }
706}
707
708#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::plugins::test_helpers;
716 use better_auth_core::CreateUser;
717 use better_auth_core::adapters::{MemoryDatabaseAdapter, UserOps, VerificationOps};
718 use chrono::Duration;
719 use std::collections::HashMap;
720 use std::sync::Arc;
721
722 #[tokio::test]
725 async fn test_change_email_success() {
726 let plugin = UserManagementPlugin::new().change_email_enabled(true);
727 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
728 CreateUser::new()
729 .with_email("test@example.com")
730 .with_name("Test User")
731 .with_email_verified(true),
732 Duration::hours(24),
733 )
734 .await;
735
736 let body = serde_json::json!({ "newEmail": "new@example.com" });
737 let req = test_helpers::create_auth_request(
738 HttpMethod::Post,
739 "/change-email",
740 Some(&session.token),
741 Some(body.to_string().into_bytes()),
742 HashMap::new(),
743 );
744
745 let response = plugin.handle_change_email(&req, &ctx).await.unwrap();
746 assert_eq!(response.status, 200);
747 }
748
749 #[tokio::test]
750 async fn test_change_email_same_email() {
751 let plugin = UserManagementPlugin::new().change_email_enabled(true);
752 let (ctx, _user, session) = test_helpers::create_test_context_with_user(
753 CreateUser::new()
754 .with_email("test@example.com")
755 .with_name("Test User")
756 .with_email_verified(true),
757 Duration::hours(24),
758 )
759 .await;
760
761 let body = serde_json::json!({ "newEmail": "test@example.com" });
762 let req = test_helpers::create_auth_request(
763 HttpMethod::Post,
764 "/change-email",
765 Some(&session.token),
766 Some(body.to_string().into_bytes()),
767 HashMap::new(),
768 );
769
770 let err = plugin.handle_change_email(&req, &ctx).await.unwrap_err();
771 assert_eq!(err.status_code(), 400);
772 }
773
774 #[tokio::test]
775 async fn test_change_email_unauthenticated() {
776 let plugin = UserManagementPlugin::new().change_email_enabled(true);
777 let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
778 CreateUser::new()
779 .with_email("test@example.com")
780 .with_name("Test User")
781 .with_email_verified(true),
782 Duration::hours(24),
783 )
784 .await;
785
786 let body = serde_json::json!({ "newEmail": "new@example.com" });
787 let req = test_helpers::create_auth_request(
788 HttpMethod::Post,
789 "/change-email",
790 None,
791 Some(body.to_string().into_bytes()),
792 HashMap::new(),
793 );
794
795 let err = plugin.handle_change_email(&req, &ctx).await.unwrap_err();
796 assert_eq!(err.status_code(), 401);
797 }
798
799 #[tokio::test]
800 async fn test_change_email_verify_success() {
801 let plugin = UserManagementPlugin::new().change_email_enabled(true);
802 let (ctx, user, session) = test_helpers::create_test_context_with_user(
803 CreateUser::new()
804 .with_email("test@example.com")
805 .with_name("Test User")
806 .with_email_verified(true),
807 Duration::hours(24),
808 )
809 .await;
810
811 let body = serde_json::json!({ "newEmail": "new@example.com" });
813 let req = test_helpers::create_auth_request(
814 HttpMethod::Post,
815 "/change-email",
816 Some(&session.token),
817 Some(body.to_string().into_bytes()),
818 HashMap::new(),
819 );
820 plugin.handle_change_email(&req, &ctx).await.unwrap();
821
822 let identifier = format!("change_email:{}:new@example.com", user.id);
824 let verification = ctx
825 .database
826 .get_verification_by_identifier(&identifier)
827 .await
828 .unwrap()
829 .expect("verification should exist");
830
831 let mut query = HashMap::new();
833 query.insert("token".to_string(), verification.value.clone());
834 let req = test_helpers::create_auth_request(
835 HttpMethod::Get,
836 "/change-email/verify",
837 None,
838 None,
839 query,
840 );
841 let response = plugin.handle_change_email_verify(&req, &ctx).await.unwrap();
842 assert_eq!(response.status, 200);
843
844 let updated_user = ctx
846 .database
847 .get_user_by_id(&user.id)
848 .await
849 .unwrap()
850 .unwrap();
851 assert_eq!(updated_user.email.as_deref(), Some("new@example.com"));
852 assert!(updated_user.email_verified);
854 }
855
856 #[tokio::test]
857 async fn test_change_email_immediate_when_update_without_verification() {
858 let plugin = UserManagementPlugin::new()
859 .change_email_enabled(true)
860 .update_without_verification(true);
861 let (ctx, user, session) = test_helpers::create_test_context_with_user(
862 CreateUser::new()
863 .with_email("test@example.com")
864 .with_name("Test User")
865 .with_email_verified(true),
866 Duration::hours(24),
867 )
868 .await;
869
870 let body = serde_json::json!({ "newEmail": "new@example.com" });
872 let req = test_helpers::create_auth_request(
873 HttpMethod::Post,
874 "/change-email",
875 Some(&session.token),
876 Some(body.to_string().into_bytes()),
877 HashMap::new(),
878 );
879 let response = plugin.handle_change_email(&req, &ctx).await.unwrap();
880 assert_eq!(response.status, 200);
881
882 let updated_user = ctx
884 .database
885 .get_user_by_id(&user.id)
886 .await
887 .unwrap()
888 .unwrap();
889 assert_eq!(updated_user.email.as_deref(), Some("new@example.com"));
890 assert!(!updated_user.email_verified);
892
893 let identifier = format!("change_email:{}:new@example.com", user.id);
895 let verification = ctx
896 .database
897 .get_verification_by_identifier(&identifier)
898 .await
899 .unwrap();
900 assert!(
901 verification.is_none(),
902 "no verification token should be created when update_without_verification=true"
903 );
904 }
905
906 #[tokio::test]
907 async fn test_change_email_verify_invalid_token() {
908 let plugin = UserManagementPlugin::new().change_email_enabled(true);
909 let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
910 CreateUser::new()
911 .with_email("test@example.com")
912 .with_name("Test User")
913 .with_email_verified(true),
914 Duration::hours(24),
915 )
916 .await;
917
918 let mut query = HashMap::new();
919 query.insert("token".to_string(), "invalid-token".to_string());
920 let req = test_helpers::create_auth_request(
921 HttpMethod::Get,
922 "/change-email/verify",
923 None,
924 None,
925 query,
926 );
927
928 let err = plugin
929 .handle_change_email_verify(&req, &ctx)
930 .await
931 .unwrap_err();
932 assert_eq!(err.status_code(), 400);
933 }
934
935 #[tokio::test]
938 async fn test_delete_user_immediate() {
939 let plugin = UserManagementPlugin::new()
940 .delete_user_enabled(true)
941 .require_delete_verification(false);
942 let (ctx, user, session) = test_helpers::create_test_context_with_user(
943 CreateUser::new()
944 .with_email("test@example.com")
945 .with_name("Test User")
946 .with_email_verified(true),
947 Duration::hours(24),
948 )
949 .await;
950
951 let req = test_helpers::create_auth_request(
952 HttpMethod::Post,
953 "/delete-user",
954 Some(&session.token),
955 Some(b"{}".to_vec()),
956 HashMap::new(),
957 );
958
959 let response = plugin.handle_delete_user(&req, &ctx).await.unwrap();
960 assert_eq!(response.status, 200);
961
962 let deleted_user = ctx.database.get_user_by_id(&user.id).await.unwrap();
964 assert!(deleted_user.is_none());
965 }
966
967 #[tokio::test]
968 async fn test_delete_user_with_verification() {
969 let plugin = UserManagementPlugin::new()
970 .delete_user_enabled(true)
971 .require_delete_verification(true);
972 let (ctx, user, session) = test_helpers::create_test_context_with_user(
973 CreateUser::new()
974 .with_email("test@example.com")
975 .with_name("Test User")
976 .with_email_verified(true),
977 Duration::hours(24),
978 )
979 .await;
980
981 let req = test_helpers::create_auth_request(
983 HttpMethod::Post,
984 "/delete-user",
985 Some(&session.token),
986 Some(b"{}".to_vec()),
987 HashMap::new(),
988 );
989
990 let response = plugin.handle_delete_user(&req, &ctx).await.unwrap();
991 assert_eq!(response.status, 200);
992
993 let still_exists = ctx.database.get_user_by_id(&user.id).await.unwrap();
995 assert!(still_exists.is_some());
996
997 let identifier = format!("delete_user:{}", user.id);
999 let verification = ctx
1000 .database
1001 .get_verification_by_identifier(&identifier)
1002 .await
1003 .unwrap()
1004 .expect("verification should exist");
1005
1006 let mut query = HashMap::new();
1008 query.insert("token".to_string(), verification.value.clone());
1009 let req = test_helpers::create_auth_request(
1010 HttpMethod::Get,
1011 "/delete-user/verify",
1012 None,
1013 None,
1014 query,
1015 );
1016 let response = plugin.handle_delete_user_verify(&req, &ctx).await.unwrap();
1017 assert_eq!(response.status, 200);
1018
1019 let deleted = ctx.database.get_user_by_id(&user.id).await.unwrap();
1021 assert!(deleted.is_none());
1022 }
1023
1024 #[tokio::test]
1025 async fn test_delete_user_unauthenticated() {
1026 let plugin = UserManagementPlugin::new()
1027 .delete_user_enabled(true)
1028 .require_delete_verification(false);
1029 let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
1030 CreateUser::new()
1031 .with_email("test@example.com")
1032 .with_name("Test User")
1033 .with_email_verified(true),
1034 Duration::hours(24),
1035 )
1036 .await;
1037
1038 let req = test_helpers::create_auth_request(
1039 HttpMethod::Post,
1040 "/delete-user",
1041 None,
1042 Some(b"{}".to_vec()),
1043 HashMap::new(),
1044 );
1045
1046 let err = plugin.handle_delete_user(&req, &ctx).await.unwrap_err();
1047 assert_eq!(err.status_code(), 401);
1048 }
1049
1050 #[tokio::test]
1051 async fn test_delete_user_verify_invalid_token() {
1052 let plugin = UserManagementPlugin::new().delete_user_enabled(true);
1053 let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
1054 CreateUser::new()
1055 .with_email("test@example.com")
1056 .with_name("Test User")
1057 .with_email_verified(true),
1058 Duration::hours(24),
1059 )
1060 .await;
1061
1062 let mut query = HashMap::new();
1063 query.insert("token".to_string(), "invalid-token".to_string());
1064 let req = test_helpers::create_auth_request(
1065 HttpMethod::Get,
1066 "/delete-user/verify",
1067 None,
1068 None,
1069 query,
1070 );
1071
1072 let err = plugin
1073 .handle_delete_user_verify(&req, &ctx)
1074 .await
1075 .unwrap_err();
1076 assert_eq!(err.status_code(), 400);
1077 }
1078
1079 #[tokio::test]
1080 async fn test_delete_user_before_hook_abort() {
1081 use std::sync::atomic::{AtomicBool, Ordering};
1082
1083 struct AbortHook;
1084 #[async_trait]
1085 impl BeforeDeleteUser for AbortHook {
1086 async fn before_delete(&self, _user: &UserInfo) -> AuthResult<()> {
1087 Err(AuthError::forbidden("Deletion blocked by policy"))
1088 }
1089 }
1090
1091 let called = Arc::new(AtomicBool::new(false));
1092 let called_clone = called.clone();
1093
1094 struct AfterHook(Arc<AtomicBool>);
1095 #[async_trait]
1096 impl AfterDeleteUser for AfterHook {
1097 async fn after_delete(&self, _user: &UserInfo) -> AuthResult<()> {
1098 self.0.store(true, Ordering::SeqCst);
1099 Ok(())
1100 }
1101 }
1102
1103 let plugin = UserManagementPlugin::new()
1104 .delete_user_enabled(true)
1105 .require_delete_verification(false)
1106 .before_delete(Arc::new(AbortHook))
1107 .after_delete(Arc::new(AfterHook(called_clone)));
1108 let (ctx, user, session) = test_helpers::create_test_context_with_user(
1109 CreateUser::new()
1110 .with_email("test@example.com")
1111 .with_name("Test User")
1112 .with_email_verified(true),
1113 Duration::hours(24),
1114 )
1115 .await;
1116
1117 let req = test_helpers::create_auth_request(
1118 HttpMethod::Post,
1119 "/delete-user",
1120 Some(&session.token),
1121 Some(b"{}".to_vec()),
1122 HashMap::new(),
1123 );
1124
1125 let err = plugin.handle_delete_user(&req, &ctx).await.unwrap_err();
1126 assert_eq!(err.status_code(), 403);
1127
1128 let still_exists = ctx.database.get_user_by_id(&user.id).await.unwrap();
1130 assert!(still_exists.is_some());
1131
1132 assert!(!called.load(Ordering::SeqCst));
1134 }
1135
1136 #[tokio::test]
1137 async fn test_plugin_routes_conditional() {
1138 let plugin = UserManagementPlugin::new();
1140 assert!(
1141 <UserManagementPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin).is_empty()
1142 );
1143
1144 let plugin = UserManagementPlugin::new().change_email_enabled(true);
1146 let routes = <UserManagementPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin);
1147 assert_eq!(routes.len(), 2);
1148 assert!(routes.iter().any(|r| r.path == "/change-email"));
1149 assert!(routes.iter().any(|r| r.path == "/change-email/verify"));
1150
1151 let plugin = UserManagementPlugin::new().delete_user_enabled(true);
1153 let routes = <UserManagementPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin);
1154 assert_eq!(routes.len(), 2);
1155 assert!(routes.iter().any(|r| r.path == "/delete-user"));
1156 assert!(routes.iter().any(|r| r.path == "/delete-user/verify"));
1157
1158 let plugin = UserManagementPlugin::new()
1160 .change_email_enabled(true)
1161 .delete_user_enabled(true);
1162 assert_eq!(
1163 <UserManagementPlugin as AuthPlugin<MemoryDatabaseAdapter>>::routes(&plugin).len(),
1164 4
1165 );
1166 }
1167
1168 #[tokio::test]
1169 async fn test_on_request_disabled_routes_passthrough() {
1170 let plugin = UserManagementPlugin::new(); let (ctx, _user, session) = test_helpers::create_test_context_with_user(
1172 CreateUser::new()
1173 .with_email("test@example.com")
1174 .with_name("Test User")
1175 .with_email_verified(true),
1176 Duration::hours(24),
1177 )
1178 .await;
1179
1180 let body = serde_json::json!({ "newEmail": "x@y.com" });
1181 let req = test_helpers::create_auth_request(
1182 HttpMethod::Post,
1183 "/change-email",
1184 Some(&session.token),
1185 Some(body.to_string().into_bytes()),
1186 HashMap::new(),
1187 );
1188
1189 let result = plugin.on_request(&req, &ctx).await.unwrap();
1190 assert!(result.is_none(), "disabled routes should return None");
1191 }
1192}