Skip to main content

reinhardt_auth/
default_user.rs

1#[cfg(feature = "argon2-hasher")]
2use crate::Argon2Hasher;
3#[cfg(feature = "argon2-hasher")]
4use crate::{BaseUser, FullUser, PermissionsMixin, User};
5#[cfg(feature = "argon2-hasher")]
6use chrono::{DateTime, Utc};
7#[cfg(feature = "argon2-hasher")]
8use reinhardt_db::orm::Model;
9#[cfg(feature = "argon2-hasher")]
10use serde::{Deserialize, Serialize};
11#[cfg(feature = "argon2-hasher")]
12use uuid::Uuid;
13
14/// DefaultUser struct - Django's AbstractUser equivalent
15///
16/// A complete, ready-to-use user model that combines BaseUser, FullUser, and PermissionsMixin.
17/// This is the default user model provided by Reinhardt, suitable for most applications.
18///
19/// # Relationship with Django
20///
21/// This struct is equivalent to Django's `django.contrib.auth.models.AbstractUser`.
22/// It provides a full-featured user model with:
23/// - Username-based authentication
24/// - Email address
25/// - First and last name
26/// - Password hashing (Argon2id by default)
27/// - Active/staff/superuser flags
28/// - Timestamps (last_login, date_joined)
29/// - Permissions and groups
30///
31/// # Database Schema
32///
33/// The default table name is `auth_user` with the following columns:
34/// - `id` (UUID, primary key)
35/// - `username` (String, unique)
36/// - `email` (String)
37/// - `first_name` (String)
38/// - `last_name` (String)
39/// - `password_hash` (`Option<String>`)
40/// - `last_login` (`Option<DateTime<Utc>>`)
41/// - `is_active` (bool)
42/// - `is_staff` (bool)
43/// - `is_superuser` (bool)
44/// - `date_joined` (`DateTime<Utc>`)
45/// - `user_permissions` (`Vec<String>`)
46/// - `groups` (`Vec<String>`)
47///
48/// # Examples
49///
50/// Creating a new user with automatic Argon2id password hashing:
51///
52/// ```
53/// use reinhardt_auth::{BaseUser, DefaultUser};
54/// use uuid::Uuid;
55/// use chrono::Utc;
56///
57/// let mut user = DefaultUser {
58///     id: Uuid::now_v7(),
59///     username: "alice".to_string(),
60///     email: "alice@example.com".to_string(),
61///     first_name: "Alice".to_string(),
62///     last_name: "Smith".to_string(),
63///     password_hash: None,
64///     last_login: None,
65///     is_active: true,
66///     is_staff: false,
67///     is_superuser: false,
68///     date_joined: Utc::now(),
69///     user_permissions: Vec::new(),
70///     groups: Vec::new(),
71/// };
72///
73/// // Password is automatically hashed with Argon2id
74/// user.set_password("securepass123").unwrap();
75///
76/// // Verify password
77/// assert!(user.check_password("securepass123").unwrap());
78/// assert!(!user.check_password("wrongpass").unwrap());
79/// ```
80///
81/// Using with permissions:
82///
83/// ```
84/// use reinhardt_auth::{DefaultUser, PermissionsMixin};
85/// use uuid::Uuid;
86/// use chrono::Utc;
87///
88/// let mut user = DefaultUser {
89///     id: Uuid::now_v7(),
90///     username: "bob".to_string(),
91///     email: "bob@example.com".to_string(),
92///     first_name: "Bob".to_string(),
93///     last_name: "Johnson".to_string(),
94///     password_hash: None,
95///     last_login: None,
96///     is_active: true,
97///     is_staff: true,
98///     is_superuser: false,
99///     date_joined: Utc::now(),
100///     user_permissions: vec![
101///         "blog.add_post".to_string(),
102///         "blog.change_post".to_string(),
103///     ],
104///     groups: vec!["editors".to_string()],
105/// };
106///
107/// // Check permissions
108/// assert!(user.has_perm("blog.add_post"));
109/// assert!(user.has_perm("blog.change_post"));
110/// assert!(!user.has_perm("blog.delete_post"));
111/// assert!(user.has_module_perms("blog"));
112/// ```
113#[cfg(feature = "argon2-hasher")]
114#[deprecated(
115	since = "0.1.0-rc.15",
116	note = "Use the `user` attribute macro to define your own user struct instead"
117)]
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct DefaultUser {
120	/// Unique identifier (primary key)
121	pub id: Uuid,
122
123	/// Username (unique, used for login)
124	pub username: String,
125
126	/// Email address
127	pub email: String,
128
129	/// First name
130	pub first_name: String,
131
132	/// Last name
133	pub last_name: String,
134
135	/// Password hash (hashed with Argon2id by default)
136	pub password_hash: Option<String>,
137
138	/// Last login timestamp
139	pub last_login: Option<DateTime<Utc>>,
140
141	/// Whether this user account is active
142	pub is_active: bool,
143
144	/// Whether this user can access the admin site
145	pub is_staff: bool,
146
147	/// Whether this user has all permissions (superuser)
148	pub is_superuser: bool,
149
150	/// When this user account was created
151	pub date_joined: DateTime<Utc>,
152
153	/// List of permissions (format: "app_label.permission_name")
154	pub user_permissions: Vec<String>,
155
156	/// List of groups this user belongs to
157	pub groups: Vec<String>,
158}
159
160#[cfg(feature = "argon2-hasher")]
161impl BaseUser for DefaultUser {
162	type PrimaryKey = Uuid;
163	type Hasher = Argon2Hasher;
164
165	fn get_username_field() -> &'static str {
166		"username"
167	}
168
169	fn get_username(&self) -> &str {
170		&self.username
171	}
172
173	fn password_hash(&self) -> Option<&str> {
174		self.password_hash.as_deref()
175	}
176
177	fn set_password_hash(&mut self, hash: String) {
178		self.password_hash = Some(hash);
179	}
180
181	fn last_login(&self) -> Option<DateTime<Utc>> {
182		self.last_login
183	}
184
185	fn set_last_login(&mut self, time: DateTime<Utc>) {
186		self.last_login = Some(time);
187	}
188
189	fn is_active(&self) -> bool {
190		self.is_active
191	}
192}
193
194#[cfg(feature = "argon2-hasher")]
195impl FullUser for DefaultUser {
196	fn username(&self) -> &str {
197		&self.username
198	}
199
200	fn email(&self) -> &str {
201		&self.email
202	}
203
204	fn first_name(&self) -> &str {
205		&self.first_name
206	}
207
208	fn last_name(&self) -> &str {
209		&self.last_name
210	}
211
212	fn is_staff(&self) -> bool {
213		self.is_staff
214	}
215
216	fn is_superuser(&self) -> bool {
217		self.is_superuser
218	}
219
220	fn date_joined(&self) -> DateTime<Utc> {
221		self.date_joined
222	}
223}
224
225#[cfg(feature = "argon2-hasher")]
226impl PermissionsMixin for DefaultUser {
227	fn is_superuser(&self) -> bool {
228		self.is_superuser
229	}
230
231	fn user_permissions(&self) -> &[String] {
232		&self.user_permissions
233	}
234
235	fn groups(&self) -> &[String] {
236		&self.groups
237	}
238}
239
240/// Query field descriptors for the `DefaultUser` model.
241#[cfg(feature = "argon2-hasher")]
242#[derive(Debug, Clone)]
243pub struct DefaultUserFields {
244	/// The user's unique identifier field.
245	pub id: reinhardt_db::orm::query_fields::Field<DefaultUser, Uuid>,
246	/// The username field.
247	pub username: reinhardt_db::orm::query_fields::Field<DefaultUser, String>,
248	/// The email address field.
249	pub email: reinhardt_db::orm::query_fields::Field<DefaultUser, String>,
250	/// The first name field.
251	pub first_name: reinhardt_db::orm::query_fields::Field<DefaultUser, String>,
252	/// The last name field.
253	pub last_name: reinhardt_db::orm::query_fields::Field<DefaultUser, String>,
254	/// The password hash field.
255	pub password_hash: reinhardt_db::orm::query_fields::Field<DefaultUser, Option<String>>,
256	/// The last login timestamp field.
257	pub last_login: reinhardt_db::orm::query_fields::Field<DefaultUser, Option<DateTime<Utc>>>,
258	/// The active status field.
259	pub is_active: reinhardt_db::orm::query_fields::Field<DefaultUser, bool>,
260	/// The staff status field.
261	pub is_staff: reinhardt_db::orm::query_fields::Field<DefaultUser, bool>,
262	/// The superuser status field.
263	pub is_superuser: reinhardt_db::orm::query_fields::Field<DefaultUser, bool>,
264	/// The date joined field.
265	pub date_joined: reinhardt_db::orm::query_fields::Field<DefaultUser, DateTime<Utc>>,
266	/// The user permissions field.
267	pub user_permissions: reinhardt_db::orm::query_fields::Field<DefaultUser, Vec<String>>,
268	/// The groups field.
269	pub groups: reinhardt_db::orm::query_fields::Field<DefaultUser, Vec<String>>,
270}
271
272#[cfg(feature = "argon2-hasher")]
273impl Default for DefaultUserFields {
274	fn default() -> Self {
275		Self::new()
276	}
277}
278
279#[cfg(feature = "argon2-hasher")]
280impl DefaultUserFields {
281	/// Creates a new `DefaultUserFields` with default field mappings.
282	pub fn new() -> Self {
283		Self {
284			id: reinhardt_db::orm::query_fields::Field::new(vec!["id"]),
285			username: reinhardt_db::orm::query_fields::Field::new(vec!["username"]),
286			email: reinhardt_db::orm::query_fields::Field::new(vec!["email"]),
287			first_name: reinhardt_db::orm::query_fields::Field::new(vec!["first_name"]),
288			last_name: reinhardt_db::orm::query_fields::Field::new(vec!["last_name"]),
289			password_hash: reinhardt_db::orm::query_fields::Field::new(vec!["password_hash"]),
290			last_login: reinhardt_db::orm::query_fields::Field::new(vec!["last_login"]),
291			is_active: reinhardt_db::orm::query_fields::Field::new(vec!["is_active"]),
292			is_staff: reinhardt_db::orm::query_fields::Field::new(vec!["is_staff"]),
293			is_superuser: reinhardt_db::orm::query_fields::Field::new(vec!["is_superuser"]),
294			date_joined: reinhardt_db::orm::query_fields::Field::new(vec!["date_joined"]),
295			user_permissions: reinhardt_db::orm::query_fields::Field::new(vec!["user_permissions"]),
296			groups: reinhardt_db::orm::query_fields::Field::new(vec!["groups"]),
297		}
298	}
299}
300
301#[cfg(feature = "argon2-hasher")]
302impl reinhardt_db::orm::FieldSelector for DefaultUserFields {
303	fn with_alias(mut self, alias: &str) -> Self {
304		self.id = self.id.with_alias(alias);
305		self.username = self.username.with_alias(alias);
306		self.email = self.email.with_alias(alias);
307		self.first_name = self.first_name.with_alias(alias);
308		self.last_name = self.last_name.with_alias(alias);
309		self.password_hash = self.password_hash.with_alias(alias);
310		self.last_login = self.last_login.with_alias(alias);
311		self.is_active = self.is_active.with_alias(alias);
312		self.is_staff = self.is_staff.with_alias(alias);
313		self.is_superuser = self.is_superuser.with_alias(alias);
314		self.date_joined = self.date_joined.with_alias(alias);
315		self.user_permissions = self.user_permissions.with_alias(alias);
316		self.groups = self.groups.with_alias(alias);
317		self
318	}
319}
320
321#[cfg(feature = "argon2-hasher")]
322impl Model for DefaultUser {
323	type PrimaryKey = Uuid;
324	type Fields = DefaultUserFields;
325
326	fn table_name() -> &'static str {
327		"auth_user"
328	}
329
330	fn new_fields() -> Self::Fields {
331		DefaultUserFields::new()
332	}
333
334	fn primary_key(&self) -> Option<Self::PrimaryKey> {
335		Some(self.id)
336	}
337
338	fn set_primary_key(&mut self, value: Self::PrimaryKey) {
339		self.id = value;
340	}
341
342	fn primary_key_field() -> &'static str {
343		"id"
344	}
345}
346
347#[cfg(feature = "argon2-hasher")]
348impl Default for DefaultUser {
349	fn default() -> Self {
350		Self {
351			id: Uuid::nil(),
352			username: String::new(),
353			email: String::new(),
354			first_name: String::new(),
355			last_name: String::new(),
356			password_hash: None,
357			last_login: None,
358			is_active: true,
359			is_staff: false,
360			is_superuser: false,
361			date_joined: Utc::now(),
362			user_permissions: Vec::new(),
363			groups: Vec::new(),
364		}
365	}
366}
367
368#[cfg(feature = "argon2-hasher")]
369#[allow(deprecated)] // Implementing deprecated User trait for backward compatibility
370impl User for DefaultUser {
371	fn id(&self) -> String {
372		self.id.to_string()
373	}
374
375	fn username(&self) -> &str {
376		&self.username
377	}
378
379	fn get_username(&self) -> &str {
380		&self.username
381	}
382
383	fn is_authenticated(&self) -> bool {
384		true
385	}
386
387	fn is_active(&self) -> bool {
388		self.is_active
389	}
390
391	fn is_admin(&self) -> bool {
392		self.is_superuser
393	}
394
395	fn is_staff(&self) -> bool {
396		self.is_staff
397	}
398
399	fn is_superuser(&self) -> bool {
400		self.is_superuser
401	}
402}