koprogo_api/application/use_cases/
auth_use_cases.rs

1use crate::application::dto::{
2    Claims, LoginRequest, LoginResponse, RefreshTokenRequest, RegisterRequest, UserResponse,
3    UserRoleSummary,
4};
5use crate::application::ports::{RefreshTokenRepository, UserRepository, UserRoleRepository};
6use crate::domain::entities::{RefreshToken, User, UserRole, UserRoleAssignment};
7use crate::infrastructure::audit::{log_audit_event, AuditEventType};
8use bcrypt::{hash, verify, DEFAULT_COST};
9use chrono::Utc;
10use jsonwebtoken::{encode, EncodingKey, Header};
11use std::sync::Arc;
12use uuid::Uuid;
13
14pub struct AuthUseCases {
15    user_repo: Arc<dyn UserRepository>,
16    refresh_token_repo: Arc<dyn RefreshTokenRepository>,
17    user_role_repo: Arc<dyn UserRoleRepository>,
18    jwt_secret: String,
19}
20
21impl AuthUseCases {
22    pub fn new(
23        user_repo: Arc<dyn UserRepository>,
24        refresh_token_repo: Arc<dyn RefreshTokenRepository>,
25        user_role_repo: Arc<dyn UserRoleRepository>,
26        jwt_secret: String,
27    ) -> Self {
28        Self {
29            user_repo,
30            refresh_token_repo,
31            user_role_repo,
32            jwt_secret,
33        }
34    }
35
36    pub async fn login(&self, request: LoginRequest) -> Result<LoginResponse, String> {
37        let user = self
38            .user_repo
39            .find_by_email(&request.email)
40            .await?
41            .ok_or_else(|| {
42                // Audit failed login attempt
43                tokio::spawn(async move {
44                    log_audit_event(
45                        AuditEventType::AuthenticationFailed,
46                        None,
47                        None,
48                        Some(format!("Failed login attempt for email: {}", request.email)),
49                        None,
50                    )
51                    .await;
52                });
53                "Invalid email or password".to_string()
54            })?;
55
56        if !user.is_active {
57            // Audit login attempt on deactivated account
58            let user_id = user.id;
59            tokio::spawn(async move {
60                log_audit_event(
61                    AuditEventType::AuthenticationFailed,
62                    Some(user_id),
63                    None,
64                    Some("Login attempt on deactivated account".to_string()),
65                    None,
66                )
67                .await;
68            });
69            return Err("User account is deactivated".to_string());
70        }
71
72        let is_valid = verify(&request.password, &user.password_hash)
73            .map_err(|e| format!("Password verification failed: {}", e))?;
74
75        if !is_valid {
76            // Audit failed password verification
77            let user_id = user.id;
78            tokio::spawn(async move {
79                log_audit_event(
80                    AuditEventType::AuthenticationFailed,
81                    Some(user_id),
82                    None,
83                    Some("Invalid password".to_string()),
84                    None,
85                )
86                .await;
87            });
88            return Err("Invalid email or password".to_string());
89        }
90
91        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
92        let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
93        let token = self.generate_token(&user_for_token, &active_role)?;
94
95        let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
96        let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
97        self.refresh_token_repo.create(&refresh_token).await?;
98
99        // Audit successful login
100        let user_id = user_for_token.id;
101        let organization_id = active_role.organization_id;
102        tokio::spawn(async move {
103            log_audit_event(
104                AuditEventType::UserLogin,
105                Some(user_id),
106                organization_id,
107                Some("Successful login".to_string()),
108                None,
109            )
110            .await;
111        });
112
113        Ok(LoginResponse {
114            token,
115            refresh_token: refresh_token_string,
116            user: self.build_user_response(&user_for_token, &roles, &active_role),
117        })
118    }
119
120    pub async fn register(&self, request: RegisterRequest) -> Result<LoginResponse, String> {
121        if (self.user_repo.find_by_email(&request.email).await?).is_some() {
122            return Err("Email already exists".to_string());
123        }
124
125        let role: UserRole = request
126            .role
127            .parse()
128            .map_err(|e| format!("Invalid role: {}", e))?;
129
130        let password_hash = hash(&request.password, DEFAULT_COST)
131            .map_err(|e| format!("Failed to hash password: {}", e))?;
132
133        let user = User::new(
134            request.email,
135            password_hash,
136            request.first_name,
137            request.last_name,
138            role.clone(),
139            request.organization_id,
140        )?;
141
142        let created_user = self.user_repo.create(&user).await?;
143
144        // Create primary role assignment
145        let primary_assignment = self
146            .user_role_repo
147            .create(&UserRoleAssignment::new(
148                created_user.id,
149                role,
150                created_user.organization_id,
151                true,
152            ))
153            .await?;
154        let roles = vec![primary_assignment.clone()];
155        let user_for_token = self
156            .apply_active_role_metadata(&created_user, &primary_assignment)
157            .await?;
158
159        let token = self.generate_token(&user_for_token, &primary_assignment)?;
160        let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
161        let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
162        self.refresh_token_repo.create(&refresh_token).await?;
163
164        // Audit successful registration
165        let user_id = created_user.id;
166        let organization_id = created_user.organization_id;
167        let email = created_user.email.clone();
168        tokio::spawn(async move {
169            log_audit_event(
170                AuditEventType::UserRegistration,
171                Some(user_id),
172                organization_id,
173                Some(format!("New user registered: {}", email)),
174                None,
175            )
176            .await;
177        });
178
179        Ok(LoginResponse {
180            token,
181            refresh_token: refresh_token_string,
182            user: self.build_user_response(&user_for_token, &roles, &primary_assignment),
183        })
184    }
185
186    pub async fn switch_active_role(
187        &self,
188        user_id: Uuid,
189        role_id: Uuid,
190    ) -> Result<LoginResponse, String> {
191        let user = self
192            .user_repo
193            .find_by_id(user_id)
194            .await?
195            .ok_or("User not found")?;
196
197        if !user.is_active {
198            return Err("User account is deactivated".to_string());
199        }
200
201        let target_role = self
202            .user_role_repo
203            .find_by_id(role_id)
204            .await?
205            .ok_or("Role assignment not found")?;
206
207        if target_role.user_id != user.id {
208            return Err("Role assignment does not belong to user".to_string());
209        }
210
211        let updated_primary = self
212            .user_role_repo
213            .set_primary_role(user.id, role_id)
214            .await?;
215
216        let roles = self.user_role_repo.list_for_user(user.id).await?;
217        let active_role = roles
218            .iter()
219            .find(|assignment| assignment.is_primary)
220            .cloned()
221            .unwrap_or(updated_primary.clone());
222
223        let updated_user = self.apply_active_role_metadata(&user, &active_role).await?;
224
225        let token = self.generate_token(&updated_user, &active_role)?;
226        let refresh_token_string = self.generate_refresh_token_string(&updated_user);
227        let refresh_token = RefreshToken::new(updated_user.id, refresh_token_string.clone());
228        self.refresh_token_repo.create(&refresh_token).await?;
229
230        Ok(LoginResponse {
231            token,
232            refresh_token: refresh_token_string,
233            user: self.build_user_response(&updated_user, &roles, &active_role),
234        })
235    }
236
237    pub async fn get_user_by_id(&self, user_id: uuid::Uuid) -> Result<UserResponse, String> {
238        let user = self
239            .user_repo
240            .find_by_id(user_id)
241            .await?
242            .ok_or("User not found")?;
243
244        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
245        Ok(self.build_user_response(&user, &roles, &active_role))
246    }
247
248    pub fn verify_token(&self, token: &str) -> Result<Claims, String> {
249        use jsonwebtoken::{decode, DecodingKey, Validation};
250
251        let token_data = decode::<Claims>(
252            token,
253            &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
254            &Validation::default(),
255        )
256        .map_err(|e| format!("Invalid token: {}", e))?;
257
258        Ok(token_data.claims)
259    }
260
261    pub async fn refresh_token(
262        &self,
263        request: RefreshTokenRequest,
264    ) -> Result<LoginResponse, String> {
265        let refresh_token = self
266            .refresh_token_repo
267            .find_by_token(&request.refresh_token)
268            .await?
269            .ok_or_else(|| {
270                // Audit invalid refresh token attempt
271                tokio::spawn(async {
272                    log_audit_event(
273                        AuditEventType::InvalidToken,
274                        None,
275                        None,
276                        Some("Invalid refresh token attempted".to_string()),
277                        None,
278                    )
279                    .await;
280                });
281                "Invalid refresh token".to_string()
282            })?;
283
284        if !refresh_token.is_valid() {
285            // Audit expired/revoked token attempt
286            let user_id = refresh_token.user_id;
287            let reason = if refresh_token.is_expired() {
288                "Expired refresh token"
289            } else {
290                "Revoked refresh token"
291            };
292            tokio::spawn(async move {
293                log_audit_event(
294                    AuditEventType::InvalidToken,
295                    Some(user_id),
296                    None,
297                    Some(format!("{} attempted", reason)),
298                    None,
299                )
300                .await;
301            });
302            return Err("Refresh token expired or revoked".to_string());
303        }
304
305        let user = self
306            .user_repo
307            .find_by_id(refresh_token.user_id)
308            .await?
309            .ok_or("User not found")?;
310
311        if !user.is_active {
312            // Audit refresh attempt on deactivated account
313            let user_id = user.id;
314            tokio::spawn(async move {
315                log_audit_event(
316                    AuditEventType::AuthenticationFailed,
317                    Some(user_id),
318                    None,
319                    Some("Refresh token attempt on deactivated account".to_string()),
320                    None,
321                )
322                .await;
323            });
324            return Err("User account is deactivated".to_string());
325        }
326
327        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
328        let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
329        let token = self.generate_token(&user_for_token, &active_role)?;
330
331        // Revoke old token (refresh token rotation)
332        self.refresh_token_repo
333            .revoke(&request.refresh_token)
334            .await?;
335
336        let new_refresh_token_string = self.generate_refresh_token_string(&user_for_token);
337        let new_refresh_token =
338            RefreshToken::new(user_for_token.id, new_refresh_token_string.clone());
339        self.refresh_token_repo.create(&new_refresh_token).await?;
340
341        // Audit successful token refresh
342        let user_id = user_for_token.id;
343        let organization_id = active_role.organization_id;
344        tokio::spawn(async move {
345            log_audit_event(
346                AuditEventType::TokenRefresh,
347                Some(user_id),
348                organization_id,
349                Some("Refresh token successfully exchanged".to_string()),
350                None,
351            )
352            .await;
353        });
354
355        Ok(LoginResponse {
356            token,
357            refresh_token: new_refresh_token_string,
358            user: self.build_user_response(&user_for_token, &roles, &active_role),
359        })
360    }
361
362    pub async fn revoke_all_refresh_tokens(&self, user_id: Uuid) -> Result<u64, String> {
363        self.refresh_token_repo.revoke_all_for_user(user_id).await
364    }
365
366    fn build_user_response(
367        &self,
368        user: &User,
369        roles: &[UserRoleAssignment],
370        active_role: &UserRoleAssignment,
371    ) -> UserResponse {
372        UserResponse {
373            id: user.id,
374            email: user.email.clone(),
375            first_name: user.first_name.clone(),
376            last_name: user.last_name.clone(),
377            role: active_role.role.to_string(),
378            organization_id: active_role.organization_id,
379            is_active: user.is_active,
380            roles: roles.iter().map(Self::summarize_role).collect(),
381            active_role: Some(Self::summarize_role(active_role)),
382        }
383    }
384
385    async fn ensure_role_assignments(
386        &self,
387        user: &User,
388    ) -> Result<(Vec<UserRoleAssignment>, UserRoleAssignment), String> {
389        let mut assignments = self.user_role_repo.list_for_user(user.id).await?;
390
391        if assignments.is_empty() {
392            let assignment = self
393                .user_role_repo
394                .create(&UserRoleAssignment::new(
395                    user.id,
396                    user.role.clone(),
397                    user.organization_id,
398                    true,
399                ))
400                .await?;
401            assignments.push(assignment.clone());
402        }
403
404        if !assignments.iter().any(|assignment| assignment.is_primary) {
405            let first = assignments[0].id;
406            self.user_role_repo.set_primary_role(user.id, first).await?;
407            assignments = self.user_role_repo.list_for_user(user.id).await?;
408        }
409
410        let active = assignments
411            .iter()
412            .find(|assignment| assignment.is_primary)
413            .cloned()
414            .unwrap_or_else(|| assignments[0].clone());
415
416        Ok((assignments, active))
417    }
418
419    async fn apply_active_role_metadata(
420        &self,
421        user: &User,
422        active_role: &UserRoleAssignment,
423    ) -> Result<User, String> {
424        let mut updated_user = user.clone();
425        let mut requires_update = false;
426
427        if updated_user.role != active_role.role {
428            updated_user.role = active_role.role.clone();
429            requires_update = true;
430        }
431
432        if updated_user.organization_id != active_role.organization_id {
433            updated_user.organization_id = active_role.organization_id;
434            requires_update = true;
435        }
436
437        if requires_update {
438            updated_user.updated_at = Utc::now();
439            return self.user_repo.update(&updated_user).await;
440        }
441
442        Ok(updated_user)
443    }
444
445    fn summarize_role(assignment: &UserRoleAssignment) -> UserRoleSummary {
446        UserRoleSummary {
447            id: assignment.id,
448            role: assignment.role.to_string(),
449            organization_id: assignment.organization_id,
450            is_primary: assignment.is_primary,
451        }
452    }
453
454    fn generate_token(
455        &self,
456        user: &User,
457        active_role: &UserRoleAssignment,
458    ) -> Result<String, String> {
459        let now = Utc::now().timestamp();
460        let expiration = now + (15 * 60);
461
462        let claims = Claims {
463            sub: user.id.to_string(),
464            email: user.email.clone(),
465            role: active_role.role.to_string(),
466            organization_id: active_role.organization_id,
467            role_id: Some(active_role.id),
468            exp: expiration,
469            iat: now,
470        };
471
472        encode(
473            &Header::default(),
474            &claims,
475            &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
476        )
477        .map_err(|e| format!("Failed to generate token: {}", e))
478    }
479
480    fn generate_refresh_token_string(&self, user: &User) -> String {
481        let now = Utc::now().timestamp();
482        format!("{}:{}:{}", user.id, now, uuid::Uuid::new_v4())
483    }
484}