Skip to main content

reinhardt_auth/
base_user_manager.rs

1use crate::BaseUser;
2use async_trait::async_trait;
3use reinhardt_core::exception::Error;
4
5type Result<T> = std::result::Result<T, Error>;
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// BaseUserManager trait - Django's BaseUserManager equivalent
10///
11/// Provides an interface for creating and managing user objects. This trait defines
12/// the essential methods needed for user management, including user and superuser creation.
13///
14/// # Relationship with Django
15///
16/// This trait corresponds to Django's `BaseUserManager`, which provides:
17/// - `create_user()` - Creates a normal user
18/// - `create_superuser()` - Creates a superuser/admin
19/// - `normalize_email()` - Normalizes email addresses
20///
21/// # Examples
22///
23/// Implementing a simple in-memory user manager:
24///
25/// ```no_run
26/// # #[tokio::main]
27/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28/// use reinhardt_auth::{BaseUser, BaseUserManager, Argon2Hasher};
29/// use reinhardt_core::exception::Result;
30/// use async_trait::async_trait;
31/// use std::collections::HashMap;
32/// use serde_json::Value;
33/// use uuid::Uuid;
34/// use chrono::Utc;
35/// use serde::{Serialize, Deserialize};
36///
37/// #[derive(Clone, Serialize, Deserialize)]
38/// struct MyUser {
39///     id: Uuid,
40///     email: String,
41///     password_hash: Option<String>,
42///     last_login: Option<chrono::DateTime<Utc>>,
43///     is_active: bool,
44///     is_admin: bool,
45/// }
46///
47/// impl BaseUser for MyUser {
48///     type PrimaryKey = Uuid;
49///     type Hasher = Argon2Hasher;
50///
51///     fn get_username_field() -> &'static str { "email" }
52///     fn get_username(&self) -> &str { &self.email }
53///     fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
54///     fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
55///     fn last_login(&self) -> Option<chrono::DateTime<Utc>> { self.last_login }
56///     fn set_last_login(&mut self, time: chrono::DateTime<Utc>) { self.last_login = Some(time); }
57///     fn is_active(&self) -> bool { self.is_active }
58/// }
59///
60/// struct MyUserManager {
61///     users: HashMap<Uuid, MyUser>,
62/// }
63///
64/// #[async_trait]
65/// impl BaseUserManager<MyUser> for MyUserManager {
66///     async fn create_user(
67///         &mut self,
68///         username: &str,
69///         password: Option<&str>,
70///         extra: HashMap<String, Value>,
71///     ) -> Result<MyUser> {
72///         let mut user = MyUser {
73///             id: Uuid::now_v7(),
74///             email: username.to_string(),
75///             password_hash: None,
76///             last_login: None,
77///             is_active: true,
78///             is_admin: false,
79///         };
80///
81///         if let Some(pwd) = password {
82///             user.set_password(pwd)?;
83///         }
84///
85///         self.users.insert(user.id, user.clone());
86///         Ok(user)
87///     }
88///
89///     async fn create_superuser(
90///         &mut self,
91///         username: &str,
92///         password: Option<&str>,
93///         extra: HashMap<String, Value>,
94///     ) -> Result<MyUser> {
95///         let mut user = self.create_user(username, password, extra).await?;
96///         user.is_admin = true;
97///         self.users.insert(user.id, user.clone());
98///         Ok(user)
99///     }
100/// }
101/// # Ok(())
102/// # }
103/// ```
104#[async_trait]
105pub trait BaseUserManager<U: BaseUser>: Send + Sync {
106	/// Creates a new user with the given username and password
107	///
108	/// This method should:
109	/// 1. Validate the username (check uniqueness, format, etc.)
110	/// 2. Create a new user instance
111	/// 3. Set the password using `set_password()` (which automatically hashes it)
112	/// 4. Apply any additional fields from `extra`
113	/// 5. Save the user to the backing store
114	///
115	/// # Arguments
116	///
117	/// * `username` - The username/email for the new user
118	/// * `password` - Optional password (will be hashed automatically)
119	/// * `extra` - Additional fields to set on the user
120	///
121	/// # Examples
122	///
123	/// ```ignore
124	/// let mut manager = MyUserManager::new();
125	/// let user = manager.create_user(
126	///     "alice@example.com",
127	///     Some("securepass123"),
128	///     HashMap::new()
129	/// ).await?;
130	/// ```
131	async fn create_user(
132		&mut self,
133		username: &str,
134		password: Option<&str>,
135		extra: HashMap<String, Value>,
136	) -> Result<U>;
137
138	/// Creates a new superuser with the given username and password
139	///
140	/// This method should:
141	/// 1. Call `create_user()` to create the base user
142	/// 2. Set superuser flags (is_staff=true, is_superuser=true, etc.)
143	/// 3. Save the updated user
144	///
145	/// # Arguments
146	///
147	/// * `username` - The username/email for the new superuser
148	/// * `password` - Optional password (will be hashed automatically)
149	/// * `extra` - Additional fields to set on the user
150	///
151	/// # Examples
152	///
153	/// ```ignore
154	/// let mut manager = MyUserManager::new();
155	/// let superuser = manager.create_superuser(
156	///     "admin@example.com",
157	///     Some("adminsecret"),
158	///     HashMap::new()
159	/// ).await?;
160	/// ```
161	async fn create_superuser(
162		&mut self,
163		username: &str,
164		password: Option<&str>,
165		extra: HashMap<String, Value>,
166	) -> Result<U>;
167
168	/// Normalizes an email address
169	///
170	/// Converts the domain part of the email to lowercase to prevent case-sensitivity issues.
171	/// This is the same normalization used by Django.
172	///
173	/// # Arguments
174	///
175	/// * `email` - The email address to normalize
176	///
177	/// # Examples
178	///
179	/// ```
180	/// use reinhardt_auth::BaseUserManager;
181	///
182	/// # struct DummyManager;
183	/// # impl DummyManager {
184	/// #     fn normalize_email(email: &str) -> String {
185	/// #         let parts: Vec<&str> = email.split('@').collect();
186	/// #         if parts.len() == 2 {
187	/// #             format!("{}@{}", parts[0], parts[1].to_lowercase())
188	/// #         } else {
189	/// #             email.to_string()
190	/// #         }
191	/// #     }
192	/// # }
193	/// let normalized = DummyManager::normalize_email("Alice@EXAMPLE.COM");
194	/// assert_eq!(normalized, "Alice@example.com");
195	///
196	/// let already_normal = DummyManager::normalize_email("bob@example.com");
197	/// assert_eq!(already_normal, "bob@example.com");
198	/// ```
199	fn normalize_email(email: &str) -> String {
200		let parts: Vec<&str> = email.split('@').collect();
201		if parts.len() == 2 {
202			format!("{}@{}", parts[0], parts[1].to_lowercase())
203		} else {
204			email.to_string()
205		}
206	}
207}