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