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}