1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use chrono::{Utc, Duration};
4use uuid::Uuid;
5
6use crate::core::{AuthPlugin, AuthRoute, AuthContext};
7use crate::types::{AuthRequest, AuthResponse, HttpMethod, User, UpdateUser, CreateVerification};
8use crate::error::{AuthError, AuthResult};
9
10pub struct PasswordManagementPlugin {
12 config: PasswordManagementConfig,
13}
14
15#[derive(Debug, Clone)]
16pub struct PasswordManagementConfig {
17 pub reset_token_expiry_hours: i64,
18 pub require_current_password: bool,
19 pub send_email_notifications: bool,
20}
21
22#[derive(Debug, Deserialize)]
24struct ForgetPasswordRequest {
25 email: String,
26 #[serde(rename = "redirectTo")]
27 redirect_to: Option<String>,
28}
29
30#[derive(Debug, Deserialize)]
31struct ResetPasswordRequest {
32 #[serde(rename = "newPassword")]
33 new_password: String,
34 token: Option<String>,
35}
36
37#[derive(Debug, Deserialize)]
38struct ChangePasswordRequest {
39 #[serde(rename = "newPassword")]
40 new_password: String,
41 #[serde(rename = "currentPassword")]
42 current_password: String,
43 #[serde(rename = "revokeOtherSessions")]
44 revoke_other_sessions: Option<String>,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
49struct StatusResponse {
50 status: bool,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54struct ChangePasswordResponse {
55 token: Option<String>,
56 user: User,
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60struct ResetPasswordTokenResponse {
61 token: String,
62}
63
64impl PasswordManagementPlugin {
65 pub fn new() -> Self {
66 Self {
67 config: PasswordManagementConfig::default(),
68 }
69 }
70
71 pub fn with_config(config: PasswordManagementConfig) -> Self {
72 Self { config }
73 }
74
75 pub fn reset_token_expiry_hours(mut self, hours: i64) -> Self {
76 self.config.reset_token_expiry_hours = hours;
77 self
78 }
79
80 pub fn require_current_password(mut self, require: bool) -> Self {
81 self.config.require_current_password = require;
82 self
83 }
84
85 pub fn send_email_notifications(mut self, send: bool) -> Self {
86 self.config.send_email_notifications = send;
87 self
88 }
89}
90
91impl Default for PasswordManagementPlugin {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl Default for PasswordManagementConfig {
98 fn default() -> Self {
99 Self {
100 reset_token_expiry_hours: 24, require_current_password: true,
102 send_email_notifications: true,
103 }
104 }
105}
106
107#[async_trait]
108impl AuthPlugin for PasswordManagementPlugin {
109 fn name(&self) -> &'static str {
110 "password-management"
111 }
112
113 fn routes(&self) -> Vec<AuthRoute> {
114 vec![
115 AuthRoute::post("/forget-password", "forget_password"),
116 AuthRoute::post("/reset-password", "reset_password"),
117 AuthRoute::get("/reset-password/{token}", "reset_password_token"),
118 AuthRoute::post("/change-password", "change_password"),
119 ]
120 }
121
122 async fn on_request(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<AuthResponse>> {
123 match (req.method(), req.path()) {
124 (HttpMethod::Post, "/forget-password") => {
125 Ok(Some(self.handle_forget_password(req, ctx).await?))
126 },
127 (HttpMethod::Post, "/reset-password") => {
128 Ok(Some(self.handle_reset_password(req, ctx).await?))
129 },
130 (HttpMethod::Post, "/change-password") => {
131 Ok(Some(self.handle_change_password(req, ctx).await?))
132 },
133 (HttpMethod::Get, path) if path.starts_with("/reset-password/") => {
134 let token = &path[16..]; Ok(Some(self.handle_reset_password_token(token, req, ctx).await?))
136 },
137 _ => Ok(None),
138 }
139 }
140}
141
142impl PasswordManagementPlugin {
144 async fn handle_forget_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
145 let forget_req: ForgetPasswordRequest = match req.body_as_json() {
146 Ok(req) => req,
147 Err(e) => {
148 return Ok(AuthResponse::json(400, &serde_json::json!({
149 "error": "Invalid request",
150 "message": format!("Invalid JSON: {}", e)
151 }))?);
152 }
153 };
154
155 let user = match ctx.database.get_user_by_email(&forget_req.email).await? {
157 Some(user) => user,
158 None => {
159 let response = StatusResponse { status: true };
161 return Ok(AuthResponse::json(200, &response)?);
162 }
163 };
164
165 let reset_token = format!("reset_{}", Uuid::new_v4());
167 let expires_at = Utc::now() + Duration::hours(self.config.reset_token_expiry_hours);
168
169 let create_verification = CreateVerification {
171 identifier: user.email.clone().unwrap_or_default(),
172 value: reset_token.clone(),
173 expires_at,
174 };
175
176 ctx.database.create_verification(create_verification).await?;
177
178 if self.config.send_email_notifications {
181 let reset_url = if let Some(redirect_to) = &forget_req.redirect_to {
182 format!("{}?token={}", redirect_to, reset_token)
183 } else {
184 format!("{}/reset-password?token={}", ctx.config.base_url, reset_token)
185 };
186
187 println!("Password reset email would be sent to {} with URL: {}", forget_req.email, reset_url);
188 }
189
190 let response = StatusResponse { status: true };
191 Ok(AuthResponse::json(200, &response)?)
192 }
193
194 async fn handle_reset_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
195 let reset_req: ResetPasswordRequest = match req.body_as_json() {
196 Ok(req) => req,
197 Err(e) => {
198 return Ok(AuthResponse::json(400, &serde_json::json!({
199 "error": "Invalid request",
200 "message": format!("Invalid JSON: {}", e)
201 }))?);
202 }
203 };
204
205 if let Err(e) = self.validate_password(&reset_req.new_password, ctx) {
207 return Ok(AuthResponse::json(400, &serde_json::json!({
208 "error": "Invalid password",
209 "message": e.to_string()
210 }))?);
211 }
212
213 let token = reset_req.token.as_deref().unwrap_or("");
215 if token.is_empty() {
216 return Ok(AuthResponse::json(400, &serde_json::json!({
217 "error": "Invalid request",
218 "message": "Reset token is required"
219 }))?);
220 }
221
222 let (user, verification) = match self.find_user_by_reset_token(token, ctx).await? {
223 Some((user, verification)) => (user, verification),
224 None => {
225 return Ok(AuthResponse::json(400, &serde_json::json!({
226 "error": "Invalid token",
227 "message": "Invalid or expired reset token"
228 }))?);
229 }
230 };
231
232 let password_hash = self.hash_password(&reset_req.new_password)?;
234
235 let mut metadata = user.metadata.clone();
237 metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
238
239 let update_user = UpdateUser {
240 email: None,
241 name: None,
242 image: None,
243 email_verified: None,
244 username: None,
245 display_username: None,
246 role: None,
247 banned: None,
248 ban_reason: None,
249 ban_expires: None,
250 two_factor_enabled: None,
251 metadata: Some(metadata),
252 };
253
254 ctx.database.update_user(&user.id, update_user).await?;
255
256 ctx.database.delete_verification(&verification.id).await?;
258
259 ctx.database.delete_user_sessions(&user.id).await?;
261
262 let response = StatusResponse { status: true };
263 Ok(AuthResponse::json(200, &response)?)
264 }
265
266 async fn handle_change_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
267 let change_req: ChangePasswordRequest = match req.body_as_json() {
268 Ok(req) => req,
269 Err(e) => {
270 return Ok(AuthResponse::json(400, &serde_json::json!({
271 "error": "Invalid request",
272 "message": format!("Invalid JSON: {}", e)
273 }))?);
274 }
275 };
276
277 let user = match self.get_current_user(req, ctx).await? {
280 Some(user) => user,
281 None => {
282 return Ok(AuthResponse::json(401, &serde_json::json!({
283 "error": "Unauthorized",
284 "message": "No valid session found"
285 }))?);
286 }
287 };
288
289 if self.config.require_current_password {
291 let stored_hash = match user.metadata.get("password_hash").and_then(|v| v.as_str()) {
292 Some(hash) => hash,
293 None => {
294 return Ok(AuthResponse::json(400, &serde_json::json!({
295 "error": "Invalid request",
296 "message": "No password set for this user"
297 }))?);
298 }
299 };
300
301 if let Err(_) = self.verify_password(&change_req.current_password, stored_hash) {
302 return Ok(AuthResponse::json(400, &serde_json::json!({
303 "error": "Invalid password",
304 "message": "Current password is incorrect"
305 }))?);
306 }
307 }
308
309 if let Err(e) = self.validate_password(&change_req.new_password, ctx) {
311 return Ok(AuthResponse::json(400, &serde_json::json!({
312 "error": "Invalid password",
313 "message": e.to_string()
314 }))?);
315 }
316
317 let password_hash = self.hash_password(&change_req.new_password)?;
319
320 let mut metadata = user.metadata.clone();
322 metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
323
324 let update_user = UpdateUser {
325 email: None,
326 name: None,
327 image: None,
328 email_verified: None,
329 username: None,
330 display_username: None,
331 role: None,
332 banned: None,
333 ban_reason: None,
334 ban_expires: None,
335 two_factor_enabled: None,
336 metadata: Some(metadata),
337 };
338
339 let updated_user = ctx.database.update_user(&user.id, update_user).await?;
340
341 let new_token = if change_req.revoke_other_sessions.as_deref() == Some("true") {
343 ctx.database.delete_user_sessions(&user.id).await?;
345
346 let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
348 let session = session_manager.create_session(&updated_user, None, None).await?;
349 Some(session.token)
350 } else {
351 None
352 };
353
354 let response = ChangePasswordResponse {
355 token: new_token,
356 user: updated_user,
357 };
358
359 Ok(AuthResponse::json(200, &response)?)
360 }
361
362 async fn handle_reset_password_token(&self, token: &str, _req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
363 let callback_url = _req.query.get("callbackURL").cloned();
365
366 let (_user, _verification) = match self.find_user_by_reset_token(token, ctx).await? {
368 Some((user, verification)) => (user, verification),
369 None => {
370 if let Some(callback_url) = callback_url {
372 let redirect_url = format!("{}?error=INVALID_TOKEN", callback_url);
373 let mut headers = std::collections::HashMap::new();
374 headers.insert("Location".to_string(), redirect_url);
375 return Ok(AuthResponse {
376 status: 302,
377 headers,
378 body: Vec::new(),
379 });
380 }
381
382 return Ok(AuthResponse::json(400, &serde_json::json!({
383 "error": "Invalid token",
384 "message": "Invalid or expired reset token"
385 }))?);
386 }
387 };
388
389 if let Some(callback_url) = callback_url {
391 let redirect_url = format!("{}?token={}", callback_url, token);
392 let mut headers = std::collections::HashMap::new();
393 headers.insert("Location".to_string(), redirect_url);
394 return Ok(AuthResponse {
395 status: 302,
396 headers,
397 body: Vec::new(),
398 });
399 }
400
401 let response = ResetPasswordTokenResponse {
403 token: token.to_string(),
404 };
405 Ok(AuthResponse::json(200, &response)?)
406 }
407
408 async fn find_user_by_reset_token(&self, token: &str, ctx: &AuthContext) -> AuthResult<Option<(User, crate::types::Verification)>> {
409 let verification = match ctx.database.get_verification_by_value(token).await? {
411 Some(verification) => verification,
412 None => return Ok(None),
413 };
414
415 let user = match ctx.database.get_user_by_email(&verification.identifier).await? {
417 Some(user) => user,
418 None => return Ok(None),
419 };
420
421 Ok(Some((user, verification)))
422 }
423
424 async fn get_current_user(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<User>> {
425 let token = if let Some(auth_header) = req.headers.get("authorization") {
427 if auth_header.starts_with("Bearer ") {
428 Some(&auth_header[7..])
429 } else {
430 None
431 }
432 } else {
433 None
434 };
435
436 if let Some(token) = token {
437 let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
438 if let Some(session) = session_manager.get_session(token).await? {
439 return ctx.database.get_user_by_id(&session.user_id).await;
440 }
441 }
442
443 Ok(None)
444 }
445
446 fn validate_password(&self, password: &str, ctx: &AuthContext) -> AuthResult<()> {
447 let config = &ctx.config.password;
448
449 if password.len() < config.min_length {
450 return Err(AuthError::InvalidRequest(format!(
451 "Password must be at least {} characters long",
452 config.min_length
453 )));
454 }
455
456 if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
457 return Err(AuthError::InvalidRequest(
458 "Password must contain at least one uppercase letter".to_string()
459 ));
460 }
461
462 if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
463 return Err(AuthError::InvalidRequest(
464 "Password must contain at least one lowercase letter".to_string()
465 ));
466 }
467
468 if config.require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
469 return Err(AuthError::InvalidRequest(
470 "Password must contain at least one number".to_string()
471 ));
472 }
473
474 if config.require_special && !password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)) {
475 return Err(AuthError::InvalidRequest(
476 "Password must contain at least one special character".to_string()
477 ));
478 }
479
480 Ok(())
481 }
482
483 fn hash_password(&self, password: &str) -> AuthResult<String> {
484 use argon2::{Argon2, PasswordHasher};
485 use argon2::password_hash::{SaltString, rand_core::OsRng};
486
487 let salt = SaltString::generate(&mut OsRng);
488 let argon2 = Argon2::default();
489
490 let password_hash = argon2.hash_password(password.as_bytes(), &salt)
491 .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
492
493 Ok(password_hash.to_string())
494 }
495
496 fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
497 use argon2::{Argon2, PasswordVerifier};
498 use argon2::password_hash::PasswordHash;
499
500 let parsed_hash = PasswordHash::new(hash)
501 .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
502
503 let argon2 = Argon2::default();
504 argon2.verify_password(password.as_bytes(), &parsed_hash)
505 .map_err(|_| AuthError::InvalidCredentials)?;
506
507 Ok(())
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::core::config::{AuthConfig, PasswordConfig, Argon2Config};
515 use crate::adapters::{MemoryDatabaseAdapter, DatabaseAdapter};
516 use crate::types::{CreateUser, CreateSession, CreateVerification, Session, User};
517 use chrono::{Utc, Duration};
518 use std::collections::HashMap;
519 use std::sync::Arc;
520
521 async fn create_test_context_with_user() -> (AuthContext, User, Session) {
522 let mut config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
523 config.password = PasswordConfig {
524 min_length: 8,
525 require_uppercase: true,
526 require_lowercase: true,
527 require_numbers: true,
528 require_special: true,
529 argon2_config: Argon2Config::default(),
530 };
531
532 let config = Arc::new(config);
533 let database = Arc::new(MemoryDatabaseAdapter::new());
534 let ctx = AuthContext::new(config.clone(), database.clone());
535
536 let plugin = PasswordManagementPlugin::new();
538 let password_hash = plugin.hash_password("Password123!").unwrap();
539
540 let mut metadata = HashMap::new();
541 metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
542
543 let create_user = CreateUser::new()
544 .with_email("test@example.com")
545 .with_name("Test User")
546 .with_metadata(metadata);
547 let user = database.create_user(create_user).await.unwrap();
548
549 let create_session = CreateSession {
551 user_id: user.id.clone(),
552 expires_at: Utc::now() + Duration::hours(24),
553 ip_address: Some("127.0.0.1".to_string()),
554 user_agent: Some("test-agent".to_string()),
555 impersonated_by: None,
556 active_organization_id: None,
557 };
558 let session = database.create_session(create_session).await.unwrap();
559
560 (ctx, user, session)
561 }
562
563 fn create_auth_request(method: HttpMethod, path: &str, token: Option<&str>, body: Option<Vec<u8>>) -> AuthRequest {
564 let mut headers = HashMap::new();
565 if let Some(token) = token {
566 headers.insert("authorization".to_string(), format!("Bearer {}", token));
567 }
568
569 AuthRequest {
570 method,
571 path: path.to_string(),
572 headers,
573 body,
574 query: HashMap::new(),
575 }
576 }
577
578 #[tokio::test]
579 async fn test_forget_password_success() {
580 let plugin = PasswordManagementPlugin::new();
581 let (ctx, _user, _session) = create_test_context_with_user().await;
582
583 let body = serde_json::json!({
584 "email": "test@example.com",
585 "redirectTo": "http://localhost:3000/reset"
586 });
587
588 let req = create_auth_request(
589 HttpMethod::Post,
590 "/forget-password",
591 None,
592 Some(body.to_string().into_bytes())
593 );
594
595 let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
596 assert_eq!(response.status, 200);
597
598 let body_str = String::from_utf8(response.body).unwrap();
599 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
600 assert!(response_data.status);
601 }
602
603 #[tokio::test]
604 async fn test_forget_password_unknown_email() {
605 let plugin = PasswordManagementPlugin::new();
606 let (ctx, _user, _session) = create_test_context_with_user().await;
607
608 let body = serde_json::json!({
609 "email": "unknown@example.com"
610 });
611
612 let req = create_auth_request(
613 HttpMethod::Post,
614 "/forget-password",
615 None,
616 Some(body.to_string().into_bytes())
617 );
618
619 let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
620 assert_eq!(response.status, 200);
621
622 let body_str = String::from_utf8(response.body).unwrap();
624 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
625 assert!(response_data.status);
626 }
627
628 #[tokio::test]
629 async fn test_reset_password_success() {
630 let plugin = PasswordManagementPlugin::new();
631 let (ctx, user, _session) = create_test_context_with_user().await;
632
633 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
635 let create_verification = CreateVerification {
636 identifier: user.email.clone().unwrap(),
637 value: reset_token.clone(),
638 expires_at: Utc::now() + Duration::hours(24),
639 };
640 ctx.database.create_verification(create_verification).await.unwrap();
641
642 let body = serde_json::json!({
643 "newPassword": "NewPassword123!",
644 "token": reset_token
645 });
646
647 let req = create_auth_request(
648 HttpMethod::Post,
649 "/reset-password",
650 None,
651 Some(body.to_string().into_bytes())
652 );
653
654 let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
655 assert_eq!(response.status, 200);
656
657 let body_str = String::from_utf8(response.body).unwrap();
658 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
659 assert!(response_data.status);
660
661 let updated_user = ctx.database.get_user_by_id(&user.id).await.unwrap().unwrap();
663 let stored_hash = updated_user.metadata.get("password_hash").unwrap().as_str().unwrap();
664 assert!(plugin.verify_password("NewPassword123!", stored_hash).is_ok());
665
666 let verification_check = ctx.database.get_verification_by_value(&reset_token).await.unwrap();
668 assert!(verification_check.is_none());
669 }
670
671 #[tokio::test]
672 async fn test_reset_password_invalid_token() {
673 let plugin = PasswordManagementPlugin::new();
674 let (ctx, _user, _session) = create_test_context_with_user().await;
675
676 let body = serde_json::json!({
677 "newPassword": "NewPassword123!",
678 "token": "invalid_token"
679 });
680
681 let req = create_auth_request(
682 HttpMethod::Post,
683 "/reset-password",
684 None,
685 Some(body.to_string().into_bytes())
686 );
687
688 let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
689 assert_eq!(response.status, 400);
690 }
691
692 #[tokio::test]
693 async fn test_reset_password_weak_password() {
694 let plugin = PasswordManagementPlugin::new();
695 let (ctx, user, _session) = create_test_context_with_user().await;
696
697 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
699 let create_verification = CreateVerification {
700 identifier: user.email.clone().unwrap(),
701 value: reset_token.clone(),
702 expires_at: Utc::now() + Duration::hours(24),
703 };
704 ctx.database.create_verification(create_verification).await.unwrap();
705
706 let body = serde_json::json!({
707 "newPassword": "weak",
708 "token": reset_token
709 });
710
711 let req = create_auth_request(
712 HttpMethod::Post,
713 "/reset-password",
714 None,
715 Some(body.to_string().into_bytes())
716 );
717
718 let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
719 assert_eq!(response.status, 400);
720 }
721
722 #[tokio::test]
723 async fn test_change_password_success() {
724 let plugin = PasswordManagementPlugin::new();
725 let (ctx, _user, session) = create_test_context_with_user().await;
726
727 let body = serde_json::json!({
728 "currentPassword": "Password123!",
729 "newPassword": "NewPassword123!",
730 "revokeOtherSessions": "false"
731 });
732
733 let req = create_auth_request(
734 HttpMethod::Post,
735 "/change-password",
736 Some(&session.token),
737 Some(body.to_string().into_bytes())
738 );
739
740 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
741 assert_eq!(response.status, 200);
742
743 let body_str = String::from_utf8(response.body).unwrap();
744 let response_data: ChangePasswordResponse = serde_json::from_str(&body_str).unwrap();
745 assert!(response_data.token.is_none()); let updated_user = ctx.database.get_user_by_id(&response_data.user.id).await.unwrap().unwrap();
749 let stored_hash = updated_user.metadata.get("password_hash").unwrap().as_str().unwrap();
750 assert!(plugin.verify_password("NewPassword123!", stored_hash).is_ok());
751 }
752
753 #[tokio::test]
754 async fn test_change_password_with_session_revocation() {
755 let plugin = PasswordManagementPlugin::new();
756 let (ctx, _user, session) = create_test_context_with_user().await;
757
758 let body = serde_json::json!({
759 "currentPassword": "Password123!",
760 "newPassword": "NewPassword123!",
761 "revokeOtherSessions": "true"
762 });
763
764 let req = create_auth_request(
765 HttpMethod::Post,
766 "/change-password",
767 Some(&session.token),
768 Some(body.to_string().into_bytes())
769 );
770
771 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
772 assert_eq!(response.status, 200);
773
774 let body_str = String::from_utf8(response.body).unwrap();
775 let response_data: ChangePasswordResponse = serde_json::from_str(&body_str).unwrap();
776 assert!(response_data.token.is_some()); }
778
779 #[tokio::test]
780 async fn test_change_password_wrong_current_password() {
781 let plugin = PasswordManagementPlugin::new();
782 let (ctx, _user, session) = create_test_context_with_user().await;
783
784 let body = serde_json::json!({
785 "currentPassword": "WrongPassword123!",
786 "newPassword": "NewPassword123!"
787 });
788
789 let req = create_auth_request(
790 HttpMethod::Post,
791 "/change-password",
792 Some(&session.token),
793 Some(body.to_string().into_bytes())
794 );
795
796 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
797 assert_eq!(response.status, 400);
798 }
799
800 #[tokio::test]
801 async fn test_change_password_unauthorized() {
802 let plugin = PasswordManagementPlugin::new();
803 let (ctx, _user, _session) = create_test_context_with_user().await;
804
805 let body = serde_json::json!({
806 "currentPassword": "Password123!",
807 "newPassword": "NewPassword123!"
808 });
809
810 let req = create_auth_request(
811 HttpMethod::Post,
812 "/change-password",
813 None,
814 Some(body.to_string().into_bytes())
815 );
816
817 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
818 assert_eq!(response.status, 401);
819 }
820
821 #[tokio::test]
822 async fn test_reset_password_token_endpoint_success() {
823 let plugin = PasswordManagementPlugin::new();
824 let (ctx, user, _session) = create_test_context_with_user().await;
825
826 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
828 let create_verification = CreateVerification {
829 identifier: user.email.clone().unwrap(),
830 value: reset_token.clone(),
831 expires_at: Utc::now() + Duration::hours(24),
832 };
833 ctx.database.create_verification(create_verification).await.unwrap();
834
835 let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
836
837 let response = plugin.handle_reset_password_token(&reset_token, &req, &ctx).await.unwrap();
838 assert_eq!(response.status, 200);
839
840 let body_str = String::from_utf8(response.body).unwrap();
841 let response_data: ResetPasswordTokenResponse = serde_json::from_str(&body_str).unwrap();
842 assert_eq!(response_data.token, reset_token);
843 }
844
845 #[tokio::test]
846 async fn test_reset_password_token_endpoint_with_callback() {
847 let plugin = PasswordManagementPlugin::new();
848 let (ctx, user, _session) = create_test_context_with_user().await;
849
850 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
852 let create_verification = CreateVerification {
853 identifier: user.email.clone().unwrap(),
854 value: reset_token.clone(),
855 expires_at: Utc::now() + Duration::hours(24),
856 };
857 ctx.database.create_verification(create_verification).await.unwrap();
858
859 let mut query = HashMap::new();
860 query.insert("callbackURL".to_string(), "http://localhost:3000/reset".to_string());
861
862 let req = AuthRequest {
863 method: HttpMethod::Get,
864 path: "/reset-password/token".to_string(),
865 headers: HashMap::new(),
866 body: None,
867 query,
868 };
869
870 let response = plugin.handle_reset_password_token(&reset_token, &req, &ctx).await.unwrap();
871 assert_eq!(response.status, 302);
872
873 let location_header = response.headers.iter()
875 .find(|(key, _)| *key == "Location")
876 .map(|(_, value)| value);
877 assert!(location_header.is_some());
878 assert!(location_header.unwrap().contains("http://localhost:3000/reset"));
879 assert!(location_header.unwrap().contains(&reset_token));
880 }
881
882 #[tokio::test]
883 async fn test_reset_password_token_endpoint_invalid_token() {
884 let plugin = PasswordManagementPlugin::new();
885 let (ctx, _user, _session) = create_test_context_with_user().await;
886
887 let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
888
889 let response = plugin.handle_reset_password_token("invalid_token", &req, &ctx).await.unwrap();
890 assert_eq!(response.status, 400);
891 }
892
893 #[tokio::test]
894 async fn test_password_validation() {
895 let plugin = PasswordManagementPlugin::new();
896 let mut config = AuthConfig::new("test-secret");
897 config.password = PasswordConfig {
898 min_length: 8,
899 require_uppercase: true,
900 require_lowercase: true,
901 require_numbers: true,
902 require_special: true,
903 argon2_config: Argon2Config::default(),
904 };
905 let ctx = AuthContext::new(Arc::new(config), Arc::new(MemoryDatabaseAdapter::new()));
906
907 assert!(plugin.validate_password("Password123!", &ctx).is_ok());
909
910 assert!(plugin.validate_password("Pass1!", &ctx).is_err());
912
913 assert!(plugin.validate_password("password123!", &ctx).is_err());
915
916 assert!(plugin.validate_password("PASSWORD123!", &ctx).is_err());
918
919 assert!(plugin.validate_password("Password!", &ctx).is_err());
921
922 assert!(plugin.validate_password("Password123", &ctx).is_err());
924 }
925
926 #[tokio::test]
927 async fn test_password_hashing_and_verification() {
928 let plugin = PasswordManagementPlugin::new();
929
930 let password = "TestPassword123!";
931 let hash = plugin.hash_password(password).unwrap();
932
933 assert!(plugin.verify_password(password, &hash).is_ok());
935
936 assert!(plugin.verify_password("WrongPassword123!", &hash).is_err());
938 }
939
940 #[tokio::test]
941 async fn test_plugin_routes() {
942 let plugin = PasswordManagementPlugin::new();
943 let routes = plugin.routes();
944
945 assert_eq!(routes.len(), 4);
946 assert!(routes.iter().any(|r| r.path == "/forget-password" && r.method == HttpMethod::Post));
947 assert!(routes.iter().any(|r| r.path == "/reset-password" && r.method == HttpMethod::Post));
948 assert!(routes.iter().any(|r| r.path == "/reset-password/{token}" && r.method == HttpMethod::Get));
949 assert!(routes.iter().any(|r| r.path == "/change-password" && r.method == HttpMethod::Post));
950 }
951
952 #[tokio::test]
953 async fn test_plugin_on_request_routing() {
954 let plugin = PasswordManagementPlugin::new();
955 let (ctx, _user, session) = create_test_context_with_user().await;
956
957 let body = serde_json::json!({"email": "test@example.com"});
959 let req = create_auth_request(
960 HttpMethod::Post,
961 "/forget-password",
962 None,
963 Some(body.to_string().into_bytes())
964 );
965 let response = plugin.on_request(&req, &ctx).await.unwrap();
966 assert!(response.is_some());
967 assert_eq!(response.unwrap().status, 200);
968
969 let body = serde_json::json!({
971 "currentPassword": "Password123!",
972 "newPassword": "NewPassword123!"
973 });
974 let req = create_auth_request(
975 HttpMethod::Post,
976 "/change-password",
977 Some(&session.token),
978 Some(body.to_string().into_bytes())
979 );
980 let response = plugin.on_request(&req, &ctx).await.unwrap();
981 assert!(response.is_some());
982 assert_eq!(response.unwrap().status, 200);
983
984 let req = create_auth_request(HttpMethod::Get, "/invalid-route", None, None);
986 let response = plugin.on_request(&req, &ctx).await.unwrap();
987 assert!(response.is_none());
988 }
989
990 #[tokio::test]
991 async fn test_configuration() {
992 let config = PasswordManagementConfig {
993 reset_token_expiry_hours: 48,
994 require_current_password: false,
995 send_email_notifications: false,
996 };
997
998 let plugin = PasswordManagementPlugin::with_config(config);
999 assert_eq!(plugin.config.reset_token_expiry_hours, 48);
1000 assert!(!plugin.config.require_current_password);
1001 assert!(!plugin.config.send_email_notifications);
1002 }
1003}