1use 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#[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#[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#[derive(Debug, Deserialize)]
41pub struct ChangePasswordRequest {
42 pub current_password: String,
43 pub new_password: String,
44}
45
46pub 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 match state
58 .auth_framework
59 .get_user_profile(&auth_token.user_id)
60 .await
61 {
62 Ok(user_profile) => {
63 let mfa_enabled =
65 check_user_mfa_status(&state.auth_framework, &auth_token.user_id)
66 .await;
67
68 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(), 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 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
133pub 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 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 tracing::info!(
164 "Updating profile for user: {} with data: {:?}",
165 auth_token.user_id,
166 updated_profile_data
167 );
168
169 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(), 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
198pub 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 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
233pub 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 if !auth_token.roles.contains(&"admin".to_string()) {
246 return ApiResponse::<UserProfile>::forbidden_typed();
247 }
248
249 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
272pub 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 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#[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
325pub 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 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
347async fn check_user_mfa_status(
349 auth_framework: &std::sync::Arc<crate::AuthFramework>,
350 user_id: &str,
351) -> bool {
352 match auth_framework.get_user_profile(user_id).await {
355 Ok(profile) => {
356 profile
358 .additional_data
359 .get("mfa_enabled")
360 .and_then(|v| v.as_bool())
361 .unwrap_or(false)
362 }
363 Err(_) => false, }
365}