Skip to main content

reinhardt_auth/
default_user_manager.rs

1// This module uses the deprecated DefaultUser and User types for backward compatibility.
2// DefaultUserManager operates on DefaultUser which is deprecated in favor of #[user] macro.
3#![allow(deprecated)]
4#[cfg(feature = "argon2-hasher")]
5use crate::BaseUser;
6#[cfg(feature = "argon2-hasher")]
7use crate::base_user_manager::BaseUserManager;
8#[cfg(feature = "argon2-hasher")]
9use crate::default_user::DefaultUser;
10#[cfg(feature = "argon2-hasher")]
11use async_trait::async_trait;
12#[cfg(feature = "argon2-hasher")]
13use chrono::Utc;
14#[cfg(feature = "argon2-hasher")]
15use reinhardt_core::exception::Error;
16
17#[cfg(feature = "argon2-hasher")]
18type Result<T> = std::result::Result<T, Error>;
19#[cfg(feature = "argon2-hasher")]
20use serde_json::Value;
21#[cfg(feature = "argon2-hasher")]
22use std::collections::HashMap;
23#[cfg(feature = "argon2-hasher")]
24use std::sync::{Arc, RwLock};
25#[cfg(feature = "argon2-hasher")]
26use uuid::Uuid;
27
28/// DefaultUserManager - In-memory user manager for DefaultUser
29///
30/// A simple in-memory implementation of BaseUserManager for DefaultUser.
31/// This is primarily for demonstration and testing purposes.
32///
33/// For production use, you should implement your own manager that persists
34/// users to a database using Reinhardt's ORM.
35///
36/// # Relationship with Django
37///
38/// This corresponds to Django's `UserManager` class, but with in-memory storage
39/// instead of database persistence. In Django, UserManager is typically bound
40/// to a model via `objects = UserManager()`.
41///
42/// # Examples
43///
44/// Creating and managing users:
45///
46/// ```
47/// use reinhardt_auth::{BaseUserManager, DefaultUserManager};
48/// use std::collections::HashMap;
49///
50/// # tokio_test::block_on(async {
51/// let mut manager = DefaultUserManager::new();
52///
53/// // Create a regular user
54/// let user = manager.create_user(
55///     "alice",
56///     Some("securepass123"),
57///     HashMap::new()
58/// ).await.unwrap();
59///
60/// assert_eq!(user.username, "alice");
61/// assert!(user.is_active);
62/// assert!(!user.is_staff);
63/// assert!(!user.is_superuser);
64///
65/// // Create a superuser
66/// let admin = manager.create_superuser(
67///     "admin",
68///     Some("adminsecret"),
69///     HashMap::new()
70/// ).await.unwrap();
71///
72/// assert!(admin.is_staff);
73/// assert!(admin.is_superuser);
74/// # })
75/// ```
76///
77/// Creating user with extra fields:
78///
79/// ```
80/// use reinhardt_auth::{BaseUserManager, DefaultUserManager};
81/// use std::collections::HashMap;
82/// use serde_json::json;
83///
84/// # tokio_test::block_on(async {
85/// let mut manager = DefaultUserManager::new();
86/// let mut extra = HashMap::new();
87/// extra.insert("email".to_string(), json!("bob@example.com"));
88/// extra.insert("first_name".to_string(), json!("Bob"));
89/// extra.insert("last_name".to_string(), json!("Johnson"));
90///
91/// let user = manager.create_user(
92///     "bob",
93///     Some("password"),
94///     extra
95/// ).await.unwrap();
96///
97/// assert_eq!(user.email, "bob@example.com");
98/// assert_eq!(user.first_name, "Bob");
99/// assert_eq!(user.last_name, "Johnson");
100/// # })
101/// ```
102#[cfg(feature = "argon2-hasher")]
103pub struct DefaultUserManager {
104	users: Arc<RwLock<HashMap<Uuid, DefaultUser>>>,
105}
106
107#[cfg(feature = "argon2-hasher")]
108impl DefaultUserManager {
109	/// Creates a new DefaultUserManager with empty user storage
110	pub fn new() -> Self {
111		Self {
112			users: Arc::new(RwLock::new(HashMap::new())),
113		}
114	}
115
116	/// Gets a user by ID (for internal use)
117	pub fn get_by_id(&self, id: Uuid) -> Option<DefaultUser> {
118		let users = self.users.read().unwrap_or_else(|e| e.into_inner());
119		users.get(&id).cloned()
120	}
121
122	/// Gets a user by username (for internal use)
123	pub fn get_by_username(&self, username: &str) -> Option<DefaultUser> {
124		let users = self.users.read().unwrap_or_else(|e| e.into_inner());
125		users.values().find(|u| u.username == username).cloned()
126	}
127
128	/// Lists all users (for internal use)
129	pub fn list_all(&self) -> Vec<DefaultUser> {
130		let users = self.users.read().unwrap_or_else(|e| e.into_inner());
131		users.values().cloned().collect()
132	}
133}
134
135#[cfg(feature = "argon2-hasher")]
136impl Default for DefaultUserManager {
137	fn default() -> Self {
138		Self::new()
139	}
140}
141
142#[cfg(feature = "argon2-hasher")]
143#[async_trait]
144impl BaseUserManager<DefaultUser> for DefaultUserManager {
145	async fn create_user(
146		&mut self,
147		username: &str,
148		password: Option<&str>,
149		extra: HashMap<String, Value>,
150	) -> Result<DefaultUser> {
151		// Check if username already exists
152		if self.get_by_username(username).is_some() {
153			return Err(reinhardt_core::exception::Error::Validation(format!(
154				"Username '{}' already exists",
155				username
156			)));
157		}
158
159		// Create user with default values
160		let mut user = DefaultUser {
161			id: Uuid::new_v4(),
162			username: username.to_string(),
163			email: String::new(),
164			first_name: String::new(),
165			last_name: String::new(),
166			password_hash: None,
167			last_login: None,
168			is_active: true,
169			is_staff: false,
170			is_superuser: false,
171			date_joined: Utc::now(),
172			user_permissions: Vec::new(),
173			groups: Vec::new(),
174		};
175
176		// Apply extra fields
177		if let Some(email) = extra.get("email")
178			&& let Some(email_str) = email.as_str()
179		{
180			user.email = Self::normalize_email(email_str);
181		}
182
183		if let Some(first_name) = extra.get("first_name")
184			&& let Some(name) = first_name.as_str()
185		{
186			user.first_name = name.to_string();
187		}
188
189		if let Some(last_name) = extra.get("last_name")
190			&& let Some(name) = last_name.as_str()
191		{
192			user.last_name = name.to_string();
193		}
194
195		if let Some(is_active) = extra.get("is_active")
196			&& let Some(active) = is_active.as_bool()
197		{
198			user.is_active = active;
199		}
200
201		// Set password if provided (automatically hashed via BaseUser trait)
202		if let Some(pwd) = password {
203			user.set_password(pwd)?;
204		}
205
206		// Store user
207		let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
208		users.insert(user.id, user.clone());
209
210		Ok(user)
211	}
212
213	async fn create_superuser(
214		&mut self,
215		username: &str,
216		password: Option<&str>,
217		extra: HashMap<String, Value>,
218	) -> Result<DefaultUser> {
219		// Create base user first
220		let mut user = self.create_user(username, password, extra).await?;
221
222		// Promote to superuser
223		user.is_staff = true;
224		user.is_superuser = true;
225
226		// Update storage
227		let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
228		users.insert(user.id, user.clone());
229
230		Ok(user)
231	}
232}
233
234#[cfg(feature = "argon2-hasher")]
235#[cfg(test)]
236mod tests {
237	use super::*;
238	use rstest::rstest;
239	use std::collections::HashMap;
240
241	#[rstest]
242	#[tokio::test]
243	async fn test_rwlock_poison_recovery_default_user_manager() {
244		// Arrange
245		let mut manager = DefaultUserManager::new();
246		let user = manager
247			.create_user("pre_poison", Some("password123"), HashMap::new())
248			.await
249			.unwrap();
250		let user_id = user.id;
251
252		// Act - poison the RwLock by panicking while holding a write guard
253		let users_clone = Arc::clone(&manager.users);
254		let _ = std::thread::spawn(move || {
255			let _guard = users_clone.write().unwrap();
256			panic!("intentional panic to poison lock");
257		})
258		.join();
259
260		// Assert - operations still work after poison recovery
261		let found = manager.get_by_id(user_id);
262		assert!(found.is_some());
263		assert_eq!(found.unwrap().username, "pre_poison");
264
265		let found_by_name = manager.get_by_username("pre_poison");
266		assert!(found_by_name.is_some());
267
268		let all_users = manager.list_all();
269		assert_eq!(all_users.len(), 1);
270
271		// Create a new user after poison recovery
272		let new_user = manager
273			.create_user("post_poison", Some("password456"), HashMap::new())
274			.await
275			.unwrap();
276		assert_eq!(new_user.username, "post_poison");
277		assert_eq!(manager.list_all().len(), 2);
278	}
279}