reinhardt_auth/
auth_user.rs1use crate::BaseUser;
7use async_trait::async_trait;
8use reinhardt_db::orm::{CustomManager, DatabaseConnection, Model};
9use reinhardt_di::{DiError, DiResult, Injectable, InjectionContext};
10use reinhardt_http::AuthState;
11use std::sync::Arc;
12
13#[derive(Debug, Clone)]
44pub struct CurrentUser<U: BaseUser>(pub U);
45
46#[cfg(feature = "params")]
47async fn resolve_current_user<U>(ctx: &InjectionContext) -> DiResult<U>
48where
49 U: BaseUser + Model + Clone + Send + Sync + 'static,
50 <U as BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
51 <<U as BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
52 <U as Model>::PrimaryKey: From<<U as BaseUser>::PrimaryKey>,
53{
54 let request = ctx.get_http_request().ok_or_else(|| {
56 DiError::NotFound("CurrentUser: No HTTP request available in InjectionContext".to_string())
57 })?;
58
59 let auth_state: AuthState = request.extensions.get().ok_or_else(|| {
61 DiError::NotFound("CurrentUser: No AuthState found in request extensions".to_string())
62 })?;
63
64 if !auth_state.is_authenticated() {
65 return Err(DiError::Authentication(
66 "CurrentUser: User is not authenticated".to_string(),
67 ));
68 }
69
70 let user_pk = auth_state
72 .user_id()
73 .parse::<<U as BaseUser>::PrimaryKey>()
74 .map_err(|e| {
75 ::tracing::warn!(
76 user_id = %auth_state.user_id(),
77 error = ?e,
78 "CurrentUser: failed to parse user_id from AuthState"
79 );
80 DiError::Authentication("CurrentUser: Invalid user_id format in AuthState".to_string())
81 })?;
82
83 let model_pk = <U as Model>::PrimaryKey::from(user_pk);
84
85 let db: Arc<DatabaseConnection> = ctx
90 .get_singleton::<DatabaseConnection>()
91 .or_else(|| ctx.get_request::<DatabaseConnection>())
92 .ok_or_else(|| {
93 ::tracing::warn!("CurrentUser: DatabaseConnection not available for user resolution");
94 DiError::Internal {
95 message: "CurrentUser: DatabaseConnection not registered in DI context".to_string(),
96 }
97 })?;
98
99 let user = U::objects()
100 .get(model_pk)
101 .first_with_db(&db)
102 .await
103 .map_err(|e| {
104 ::tracing::warn!(error = ?e, "CurrentUser: Failed to load user from database");
105 DiError::Internal {
106 message: "CurrentUser: Database query failed".to_string(),
107 }
108 })?
109 .ok_or_else(|| {
110 ::tracing::warn!(
111 user_id = %auth_state.user_id(),
112 "CurrentUser: User not found in database"
113 );
114 DiError::NotFound("CurrentUser: User not found".to_string())
115 })?;
116
117 if !user.is_active() {
118 ::tracing::warn!(user_id = %auth_state.user_id(), "CurrentUser: User account is inactive");
119 return Err(DiError::Authentication(
120 "CurrentUser: User account is inactive".to_string(),
121 ));
122 }
123
124 Ok(user)
125}
126
127#[cfg(feature = "params")]
128#[async_trait]
129impl<U> Injectable for CurrentUser<U>
130where
131 U: BaseUser + Model + Clone + Send + Sync + 'static,
132 <U as BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
133 <<U as BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
134 <U as Model>::PrimaryKey: From<<U as BaseUser>::PrimaryKey>,
135{
136 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
137 resolve_current_user(ctx).await.map(CurrentUser)
138 }
139}
140
141#[cfg(not(feature = "params"))]
142#[async_trait]
143impl<U> Injectable for CurrentUser<U>
144where
145 U: BaseUser + Model + Clone + Send + Sync + 'static,
146 <U as BaseUser>::PrimaryKey: std::str::FromStr + ToString + Send + Sync,
147 <<U as BaseUser>::PrimaryKey as std::str::FromStr>::Err: std::fmt::Debug,
148 <U as Model>::PrimaryKey: From<<U as BaseUser>::PrimaryKey>,
149{
150 async fn inject(_ctx: &InjectionContext) -> DiResult<Self> {
151 Err(DiError::NotFound(
152 "CurrentUser requires the 'params' feature to be enabled".to_string(),
153 ))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::CurrentUser;
160 use crate::{BaseUser, PasswordHasher};
161 use chrono::{DateTime, Utc};
162 use serde::{Deserialize, Serialize};
163
164 #[derive(Default)]
165 struct TestHasher;
166
167 impl PasswordHasher for TestHasher {
168 fn hash(&self, password: &str) -> Result<String, reinhardt_core::exception::Error> {
169 Ok(password.to_string())
170 }
171
172 fn verify(
173 &self,
174 password: &str,
175 hash: &str,
176 ) -> Result<bool, reinhardt_core::exception::Error> {
177 Ok(password == hash)
178 }
179 }
180
181 #[derive(Clone, Serialize, Deserialize)]
182 struct TestUser {
183 username: String,
184 password_hash: Option<String>,
185 last_login: Option<DateTime<Utc>>,
186 is_active: bool,
187 }
188
189 impl BaseUser for TestUser {
190 type PrimaryKey = String;
191 type Hasher = TestHasher;
192
193 fn get_username_field() -> &'static str {
194 "username"
195 }
196
197 fn get_username(&self) -> &str {
198 &self.username
199 }
200
201 fn password_hash(&self) -> Option<&str> {
202 self.password_hash.as_deref()
203 }
204
205 fn set_password_hash(&mut self, hash: String) {
206 self.password_hash = Some(hash);
207 }
208
209 fn last_login(&self) -> Option<DateTime<Utc>> {
210 self.last_login
211 }
212
213 fn set_last_login(&mut self, time: DateTime<Utc>) {
214 self.last_login = Some(time);
215 }
216
217 fn is_active(&self) -> bool {
218 self.is_active
219 }
220 }
221
222 fn test_user(username: &str) -> TestUser {
223 TestUser {
224 username: username.to_string(),
225 password_hash: None,
226 last_login: None,
227 is_active: true,
228 }
229 }
230
231 #[test]
232 fn current_user_supports_tuple_struct_destructuring() {
233 let CurrentUser(user): CurrentUser<TestUser> = CurrentUser(test_user("alice"));
234
235 assert_eq!(user.get_username(), "alice");
236 }
237}