auth_framework/api/
users.rs

1//! User Management API Endpoints
2//!
3//! Handles user profile, password changes, and related user operations
4
5use crate::api::{ApiResponse, ApiState, extract_bearer_token, validate_api_token};
6use axum::{
7    Json,
8    extract::{Path, State},
9    http::HeaderMap,
10};
11use serde::{Deserialize, Serialize};
12
13/// User profile information
14#[derive(Debug, Serialize)]
15pub struct UserProfile {
16    pub id: String,
17    pub username: String,
18    pub email: String,
19    pub first_name: Option<String>,
20    pub last_name: Option<String>,
21    pub roles: Vec<String>,
22    pub permissions: Vec<String>,
23    pub mfa_enabled: bool,
24    pub created_at: String,
25    pub updated_at: String,
26}
27
28/// Update profile request
29#[derive(Debug, Deserialize)]
30pub struct UpdateProfileRequest {
31    #[serde(default)]
32    pub first_name: Option<String>,
33    #[serde(default)]
34    pub last_name: Option<String>,
35    #[serde(default)]
36    pub email: Option<String>,
37}
38
39/// Change password request
40#[derive(Debug, Deserialize)]
41pub struct ChangePasswordRequest {
42    pub current_password: String,
43    pub new_password: String,
44}
45
46/// GET /users/profile
47/// Get current user profile
48pub async fn get_profile(
49    State(state): State<ApiState>,
50    headers: HeaderMap,
51) -> ApiResponse<UserProfile> {
52    match extract_bearer_token(&headers) {
53        Some(token) => {
54            match validate_api_token(&state.auth_framework, &token).await {
55                Ok(auth_token) => {
56                    // Fetch actual user profile from storage
57                    match state
58                        .auth_framework
59                        .get_user_profile(&auth_token.user_id)
60                        .await
61                    {
62                        Ok(user_profile) => {
63                            // Check MFA status from AuthFramework
64                            let mfa_enabled =
65                                check_user_mfa_status(&state.auth_framework, &auth_token.user_id)
66                                    .await;
67
68                            // Extract first_name and last_name from the name field if available
69                            let (first_name, last_name) = if let Some(name) = &user_profile.name {
70                                let parts: Vec<&str> = name.split_whitespace().collect();
71                                if parts.len() >= 2 {
72                                    (Some(parts[0].to_string()), Some(parts[1..].join(" ")))
73                                } else if parts.len() == 1 {
74                                    (Some(parts[0].to_string()), None)
75                                } else {
76                                    (None, None)
77                                }
78                            } else {
79                                (None, None)
80                            };
81
82                            let profile = UserProfile {
83                                id: auth_token.user_id.clone(),
84                                username: user_profile.username.unwrap_or_else(|| auth_token.user_id.clone()),
85                                email: user_profile.email.unwrap_or_default(),
86                                first_name,
87                                last_name,
88                                roles: auth_token.roles,
89                                permissions: auth_token.permissions,
90                                mfa_enabled,
91                                created_at: chrono::Utc::now().to_rfc3339(), // Default to current time
92                                updated_at: chrono::Utc::now().to_rfc3339(),
93                            };
94
95                            ApiResponse::success(profile)
96                        }
97                        Err(e) => {
98                            tracing::warn!(
99                                "Failed to fetch user profile for user {}: {}",
100                                auth_token.user_id,
101                                e
102                            );
103
104                            // Fallback profile if storage fetch fails
105                            let profile = UserProfile {
106                                id: auth_token.user_id.clone(),
107                                username: format!("user_{}", auth_token.user_id),
108                                email: format!("{}@example.com", auth_token.user_id),
109                                first_name: Some("Unknown".to_string()),
110                                last_name: Some("User".to_string()),
111                                roles: auth_token.roles,
112                                permissions: auth_token.permissions,
113                                mfa_enabled: false,
114                                created_at: "2024-01-01T00:00:00Z".to_string(),
115                                updated_at: chrono::Utc::now().to_rfc3339(),
116                            };
117
118                            ApiResponse::success(profile)
119                        }
120                    }
121                }
122                Err(_e) => ApiResponse::error_typed("USER_ERROR", "User operation failed"),
123            }
124        }
125        None => ApiResponse::<UserProfile>::unauthorized_typed(
126            "UNAUTHORIZED",
127            "Authentication required",
128        ),
129    }
130}
131
132/// PUT /users/profile
133/// Update user profile
134pub async fn update_profile(
135    State(state): State<ApiState>,
136    headers: HeaderMap,
137    Json(req): Json<UpdateProfileRequest>,
138) -> ApiResponse<UserProfile> {
139    match extract_bearer_token(&headers) {
140        Some(token) => {
141            match validate_api_token(&state.auth_framework, &token).await {
142                Ok(auth_token) => {
143                    // Update user profile in storage
144                    let updated_profile_data = crate::providers::UserProfile {
145                        id: Some(auth_token.user_id.clone()),
146                        provider: Some("local".to_string()),
147                        username: Some(format!("user_{}", auth_token.user_id)),
148                        name: match (&req.first_name, &req.last_name) {
149                            (Some(first), Some(last)) => Some(format!("{} {}", first, last)),
150                            (Some(first), None) => Some(first.clone()),
151                            (None, Some(last)) => Some(last.clone()),
152                            (None, None) => None,
153                        },
154                        email: req.email.clone(),
155                        email_verified: Some(false),
156                        picture: None,
157                        locale: None,
158                        additional_data: std::collections::HashMap::new(),
159                    };
160
161                    // Store updated profile (in a real implementation, update storage)
162                    tracing::info!(
163                        "Updating profile for user: {} with data: {:?}",
164                        auth_token.user_id,
165                        updated_profile_data
166                    );
167
168                    // Return updated profile response
169                    let updated_profile = UserProfile {
170                        id: auth_token.user_id.clone(),
171                        username: format!("user_{}", auth_token.user_id),
172                        email: req
173                            .email
174                            .unwrap_or_else(|| format!("{}@example.com", auth_token.user_id)),
175                        first_name: req.first_name,
176                        last_name: req.last_name,
177                        roles: auth_token.roles,
178                        permissions: auth_token.permissions,
179                        mfa_enabled: check_user_mfa_status(
180                            &state.auth_framework,
181                            &auth_token.user_id,
182                        )
183                        .await,
184                        created_at: chrono::Utc::now().to_rfc3339(), // Default to current time
185                        updated_at: chrono::Utc::now().to_rfc3339(),
186                    };
187
188                    ApiResponse::success(updated_profile)
189                }
190                Err(_e) => ApiResponse::error_typed("USER_ERROR", "User operation failed"),
191            }
192        }
193        None => ApiResponse::<UserProfile>::unauthorized_typed(
194            "UNAUTHORIZED",
195            "Authentication required",
196        ),
197    }
198}
199
200/// POST /users/change-password
201/// Change user password
202pub async fn change_password(
203    State(state): State<ApiState>,
204    headers: HeaderMap,
205    Json(req): Json<ChangePasswordRequest>,
206) -> ApiResponse<()> {
207    if req.current_password.is_empty() || req.new_password.is_empty() {
208        return ApiResponse::validation_error("Current password and new password are required");
209    }
210
211    if req.new_password.len() < 8 {
212        return ApiResponse::validation_error("New password must be at least 8 characters long");
213    }
214
215    match extract_bearer_token(&headers) {
216        Some(token) => {
217            match validate_api_token(&state.auth_framework, &token).await {
218                Ok(auth_token) => {
219                    // In a real implementation:
220                    // 1. Verify current password
221                    // 2. Hash new password
222                    // 3. Update password in storage
223                    // 4. Optionally invalidate all existing sessions
224
225                    tracing::info!("Password changed for user: {}", auth_token.user_id);
226                    ApiResponse::<()>::ok_with_message("Password changed successfully")
227                }
228                Err(e) => ApiResponse::<()>::from(e),
229            }
230        }
231        None => ApiResponse::<()>::unauthorized(),
232    }
233}
234
235/// GET /users/{user_id}/profile
236/// Get specific user profile (admin only)
237pub async fn get_user_profile(
238    State(state): State<ApiState>,
239    headers: HeaderMap,
240    Path(user_id): Path<String>,
241) -> ApiResponse<UserProfile> {
242    match extract_bearer_token(&headers) {
243        Some(token) => {
244            match validate_api_token(&state.auth_framework, &token).await {
245                Ok(auth_token) => {
246                    // Check if user has admin permissions
247                    if !auth_token.roles.contains(&"admin".to_string()) {
248                        return ApiResponse::<UserProfile>::forbidden_typed();
249                    }
250
251                    // In a real implementation, fetch user profile from storage
252                    let profile = UserProfile {
253                        id: user_id.clone(),
254                        username: format!("user_{}", user_id),
255                        email: format!("{}@example.com", user_id),
256                        first_name: Some("User".to_string()),
257                        last_name: Some("Name".to_string()),
258                        roles: vec!["user".to_string()],
259                        permissions: vec!["read:profile".to_string()],
260                        mfa_enabled: false,
261                        created_at: "2024-01-01T00:00:00Z".to_string(),
262                        updated_at: "2024-01-01T00:00:00Z".to_string(),
263                    };
264
265                    ApiResponse::success(profile)
266                }
267                Err(_e) => ApiResponse::error_typed("USER_ERROR", "User operation failed"),
268            }
269        }
270        None => ApiResponse::<UserProfile>::unauthorized_typed(
271            "UNAUTHORIZED",
272            "Authentication required",
273        ),
274    }
275}
276
277/// GET /users/sessions
278/// Get user's active sessions
279pub async fn get_sessions(
280    State(state): State<ApiState>,
281    headers: HeaderMap,
282) -> ApiResponse<Vec<SessionInfo>> {
283    match extract_bearer_token(&headers) {
284        Some(token) => {
285            match validate_api_token(&state.auth_framework, &token).await {
286                Ok(_auth_token) => {
287                    // In a real implementation, fetch sessions from storage
288                    let sessions = vec![
289                        SessionInfo {
290                            id: "session_1".to_string(),
291                            device: "Chrome on Windows".to_string(),
292                            location: "New York, NY".to_string(),
293                            ip_address: "192.168.1.1".to_string(),
294                            created_at: "2024-01-01T10:00:00Z".to_string(),
295                            last_active: "2024-01-01T12:00:00Z".to_string(),
296                            is_current: true,
297                        },
298                        SessionInfo {
299                            id: "session_2".to_string(),
300                            device: "Safari on iPhone".to_string(),
301                            location: "San Francisco, CA".to_string(),
302                            ip_address: "10.0.0.1".to_string(),
303                            created_at: "2023-12-30T08:00:00Z".to_string(),
304                            last_active: "2023-12-31T09:30:00Z".to_string(),
305                            is_current: false,
306                        },
307                    ];
308
309                    ApiResponse::success(sessions)
310                }
311                Err(_e) => ApiResponse::error_typed("USER_ERROR", "Session operation failed"),
312            }
313        }
314        None => ApiResponse::<Vec<SessionInfo>>::unauthorized_typed(
315            "UNAUTHORIZED",
316            "Authentication required",
317        ),
318    }
319}
320
321/// Session information
322#[derive(Debug, Serialize)]
323pub struct SessionInfo {
324    pub id: String,
325    pub device: String,
326    pub location: String,
327    pub ip_address: String,
328    pub created_at: String,
329    pub last_active: String,
330    pub is_current: bool,
331}
332
333/// DELETE /users/sessions/{session_id}
334/// Revoke a specific session
335pub async fn revoke_session(
336    State(state): State<ApiState>,
337    headers: HeaderMap,
338    Path(session_id): Path<String>,
339) -> ApiResponse<()> {
340    match extract_bearer_token(&headers) {
341        Some(token) => {
342            match validate_api_token(&state.auth_framework, &token).await {
343                Ok(_auth_token) => {
344                    // In a real implementation, remove session from storage
345                    tracing::info!("Revoking session: {}", session_id);
346                    ApiResponse::<()>::ok_with_message("Session revoked successfully")
347                }
348                Err(e) => ApiResponse::<()>::from(e),
349            }
350        }
351        None => ApiResponse::<()>::unauthorized(),
352    }
353}
354
355/// Helper function for MFA status integration
356async fn check_user_mfa_status(
357    auth_framework: &std::sync::Arc<crate::AuthFramework>,
358    user_id: &str,
359) -> bool {
360    // Check if user has MFA enabled in storage
361    // This is a simplified check - in a real implementation, you would query the MFA service
362    match auth_framework.get_user_profile(user_id).await {
363        Ok(profile) => {
364            // Check for MFA-related attributes in user profile
365            profile
366                .additional_data
367                .get("mfa_enabled")
368                .and_then(|v| v.as_bool())
369                .unwrap_or(false)
370        }
371        Err(_) => false, // Default to false if profile fetch fails
372    }
373}