users.rs (17081B)
1 //! User profile and address API handlers 2 //! 3 //! This module contains the HTTP request handlers for user profile and address endpoints. 4 //! Database operations are delegated to the repository layer in `db::users`. 5 6 use axum::{ 7 extract::{Path, State}, 8 http::StatusCode, 9 response::IntoResponse, 10 Json, 11 }; 12 use uuid::Uuid; 13 14 use crate::{ 15 auth, db, 16 models::{AddAddressRequest, AddressResponse, UpdateProfileRequest}, 17 AppState, 18 }; 19 20 use super::auth::ErrorResponse; 21 22 /// Get user profile 23 pub async fn get_profile( 24 State(state): State<AppState>, 25 headers: axum::http::HeaderMap, 26 ) -> impl IntoResponse { 27 let token = extract_token(&headers); 28 29 if let Some(token) = token { 30 match auth::validate_session(state.db.pool(), &token).await { 31 Ok(Some(user)) => { 32 // Delegate profile lookup to the repository layer 33 match db::users::get_profile(state.db.pool(), user.id).await { 34 Ok(Some(profile)) => ( 35 StatusCode::OK, 36 Json(serde_json::json!({ 37 "user_id": user.uuid, 38 "email": user.email, 39 "first_name": user.first_name, 40 "last_name": user.last_name, 41 "phone": user.phone, 42 "is_verified": user.is_verified, 43 "profile_eid": profile.eid, 44 "avatar_url": profile.avatar_url, 45 "date_of_birth": profile.date_of_birth, 46 "email_notifications": profile.email_notifications, 47 "marketing_emails": profile.marketing_emails, 48 })), 49 ) 50 .into_response(), 51 Ok(None) => { 52 // Return user data even if profile doesn't exist yet 53 ( 54 StatusCode::OK, 55 Json(serde_json::json!({ 56 "user_id": user.uuid, 57 "email": user.email, 58 "first_name": user.first_name, 59 "last_name": user.last_name, 60 "phone": user.phone, 61 "is_verified": user.is_verified, 62 "profile_eid": null, 63 "avatar_url": null, 64 "date_of_birth": null, 65 "email_notifications": true, 66 "marketing_emails": false, 67 })), 68 ) 69 .into_response() 70 } 71 Err(e) => ( 72 StatusCode::INTERNAL_SERVER_ERROR, 73 Json(ErrorResponse { 74 error: e.to_string(), 75 }), 76 ) 77 .into_response(), 78 } 79 } 80 Ok(None) => ( 81 StatusCode::UNAUTHORIZED, 82 Json(ErrorResponse { 83 error: "Invalid or expired session".to_string(), 84 }), 85 ) 86 .into_response(), 87 Err(e) => ( 88 StatusCode::INTERNAL_SERVER_ERROR, 89 Json(ErrorResponse { 90 error: e.to_string(), 91 }), 92 ) 93 .into_response(), 94 } 95 } else { 96 ( 97 StatusCode::UNAUTHORIZED, 98 Json(ErrorResponse { 99 error: "No session token provided".to_string(), 100 }), 101 ) 102 .into_response() 103 } 104 } 105 106 /// Update or create user profile 107 pub async fn update_profile( 108 State(state): State<AppState>, 109 headers: axum::http::HeaderMap, 110 Json(req): Json<UpdateProfileRequest>, 111 ) -> impl IntoResponse { 112 let token = extract_token(&headers); 113 114 if let Some(token) = token { 115 match auth::validate_session(state.db.pool(), &token).await { 116 Ok(Some(user)) => { 117 // Check if profile exists via repository 118 let exists = match db::users::profile_exists(state.db.pool(), user.id).await { 119 Ok(exists) => exists, 120 Err(e) => { 121 return ( 122 StatusCode::INTERNAL_SERVER_ERROR, 123 Json(ErrorResponse { 124 error: e.to_string(), 125 }), 126 ) 127 .into_response(); 128 } 129 }; 130 131 if exists { 132 // Update existing profile via repository 133 match db::users::update_profile( 134 state.db.pool(), 135 user.id, 136 req.avatar_url.as_deref(), 137 req.date_of_birth, 138 req.email_notifications, 139 req.marketing_emails, 140 ) 141 .await 142 { 143 Ok(profile) => ( 144 StatusCode::OK, 145 Json(serde_json::json!({ 146 "eid": profile.eid, 147 "avatar_url": profile.avatar_url, 148 "date_of_birth": profile.date_of_birth, 149 "email_notifications": profile.email_notifications, 150 "marketing_emails": profile.marketing_emails, 151 })), 152 ) 153 .into_response(), 154 Err(e) => ( 155 StatusCode::INTERNAL_SERVER_ERROR, 156 Json(ErrorResponse { 157 error: e.to_string(), 158 }), 159 ) 160 .into_response(), 161 } 162 } else { 163 // Create new profile via repository 164 match db::users::create_profile( 165 state.db.pool(), 166 user.id, 167 req.avatar_url.as_deref(), 168 req.date_of_birth, 169 req.email_notifications.unwrap_or(true), 170 req.marketing_emails.unwrap_or(false), 171 ) 172 .await 173 { 174 Ok(profile) => ( 175 StatusCode::CREATED, 176 Json(serde_json::json!({ 177 "eid": profile.eid, 178 "avatar_url": profile.avatar_url, 179 "date_of_birth": profile.date_of_birth, 180 "email_notifications": profile.email_notifications, 181 "marketing_emails": profile.marketing_emails, 182 })), 183 ) 184 .into_response(), 185 Err(e) => ( 186 StatusCode::INTERNAL_SERVER_ERROR, 187 Json(ErrorResponse { 188 error: e.to_string(), 189 }), 190 ) 191 .into_response(), 192 } 193 } 194 } 195 Ok(None) => ( 196 StatusCode::UNAUTHORIZED, 197 Json(ErrorResponse { 198 error: "Invalid or expired session".to_string(), 199 }), 200 ) 201 .into_response(), 202 Err(e) => ( 203 StatusCode::INTERNAL_SERVER_ERROR, 204 Json(ErrorResponse { 205 error: e.to_string(), 206 }), 207 ) 208 .into_response(), 209 } 210 } else { 211 ( 212 StatusCode::UNAUTHORIZED, 213 Json(ErrorResponse { 214 error: "No session token provided".to_string(), 215 }), 216 ) 217 .into_response() 218 } 219 } 220 221 /// Get all user addresses 222 pub async fn get_addresses( 223 State(state): State<AppState>, 224 headers: axum::http::HeaderMap, 225 ) -> impl IntoResponse { 226 let token = extract_token(&headers); 227 228 if let Some(token) = token { 229 match auth::validate_session(state.db.pool(), &token).await { 230 Ok(Some(user)) => { 231 // Delegate address listing to the repository layer 232 match db::users::list_addresses(state.db.pool(), user.id).await { 233 Ok(addresses) => { 234 let response: Vec<AddressResponse> = 235 addresses.into_iter().map(AddressResponse::from).collect(); 236 237 (StatusCode::OK, Json(response)).into_response() 238 } 239 Err(e) => ( 240 StatusCode::INTERNAL_SERVER_ERROR, 241 Json(ErrorResponse { 242 error: e.to_string(), 243 }), 244 ) 245 .into_response(), 246 } 247 } 248 Ok(None) => ( 249 StatusCode::UNAUTHORIZED, 250 Json(ErrorResponse { 251 error: "Invalid or expired session".to_string(), 252 }), 253 ) 254 .into_response(), 255 Err(e) => ( 256 StatusCode::INTERNAL_SERVER_ERROR, 257 Json(ErrorResponse { 258 error: e.to_string(), 259 }), 260 ) 261 .into_response(), 262 } 263 } else { 264 ( 265 StatusCode::UNAUTHORIZED, 266 Json(ErrorResponse { 267 error: "No session token provided".to_string(), 268 }), 269 ) 270 .into_response() 271 } 272 } 273 274 /// Add a new user address 275 pub async fn add_address( 276 State(state): State<AppState>, 277 headers: axum::http::HeaderMap, 278 Json(req): Json<AddAddressRequest>, 279 ) -> impl IntoResponse { 280 let token = extract_token(&headers); 281 282 if let Some(token) = token { 283 match auth::validate_session(state.db.pool(), &token).await { 284 Ok(Some(user)) => { 285 // Delegate address creation to the repository layer 286 match db::users::add_address(state.db.pool(), user.id, &req).await { 287 Ok(address) => { 288 let response = AddressResponse::from(address); 289 (StatusCode::CREATED, Json(response)).into_response() 290 } 291 Err(e) => ( 292 StatusCode::BAD_REQUEST, 293 Json(ErrorResponse { 294 error: e.to_string(), 295 }), 296 ) 297 .into_response(), 298 } 299 } 300 Ok(None) => ( 301 StatusCode::UNAUTHORIZED, 302 Json(ErrorResponse { 303 error: "Invalid or expired session".to_string(), 304 }), 305 ) 306 .into_response(), 307 Err(e) => ( 308 StatusCode::INTERNAL_SERVER_ERROR, 309 Json(ErrorResponse { 310 error: e.to_string(), 311 }), 312 ) 313 .into_response(), 314 } 315 } else { 316 ( 317 StatusCode::UNAUTHORIZED, 318 Json(ErrorResponse { 319 error: "No session token provided".to_string(), 320 }), 321 ) 322 .into_response() 323 } 324 } 325 326 /// Update an existing user address 327 pub async fn update_address( 328 State(state): State<AppState>, 329 headers: axum::http::HeaderMap, 330 Path(id): Path<Uuid>, 331 Json(req): Json<AddAddressRequest>, 332 ) -> impl IntoResponse { 333 let token = extract_token(&headers); 334 335 if let Some(token) = token { 336 match auth::validate_session(state.db.pool(), &token).await { 337 Ok(Some(user)) => { 338 // Delegate address update to the repository layer 339 match db::users::update_address(state.db.pool(), user.id, id, &req).await { 340 Ok(Some(address)) => { 341 let response = AddressResponse::from(address); 342 (StatusCode::OK, Json(response)).into_response() 343 } 344 Ok(None) => ( 345 StatusCode::NOT_FOUND, 346 Json(ErrorResponse { 347 error: "Address not found or access denied".to_string(), 348 }), 349 ) 350 .into_response(), 351 Err(e) => ( 352 StatusCode::BAD_REQUEST, 353 Json(ErrorResponse { 354 error: e.to_string(), 355 }), 356 ) 357 .into_response(), 358 } 359 } 360 Ok(None) => ( 361 StatusCode::UNAUTHORIZED, 362 Json(ErrorResponse { 363 error: "Invalid or expired session".to_string(), 364 }), 365 ) 366 .into_response(), 367 Err(e) => ( 368 StatusCode::INTERNAL_SERVER_ERROR, 369 Json(ErrorResponse { 370 error: e.to_string(), 371 }), 372 ) 373 .into_response(), 374 } 375 } else { 376 ( 377 StatusCode::UNAUTHORIZED, 378 Json(ErrorResponse { 379 error: "No session token provided".to_string(), 380 }), 381 ) 382 .into_response() 383 } 384 } 385 386 /// Soft delete a user address 387 pub async fn delete_address( 388 State(state): State<AppState>, 389 headers: axum::http::HeaderMap, 390 Path(id): Path<Uuid>, 391 ) -> impl IntoResponse { 392 let token = extract_token(&headers); 393 394 if let Some(token) = token { 395 match auth::validate_session(state.db.pool(), &token).await { 396 Ok(Some(user)) => { 397 // Delegate soft-delete to the repository layer 398 match db::users::delete_address(state.db.pool(), user.id, id).await { 399 Ok(rows) => { 400 if rows > 0 { 401 ( 402 StatusCode::OK, 403 Json(serde_json::json!({ 404 "message": "Address deleted successfully" 405 })), 406 ) 407 .into_response() 408 } else { 409 ( 410 StatusCode::NOT_FOUND, 411 Json(ErrorResponse { 412 error: "Address not found or access denied".to_string(), 413 }), 414 ) 415 .into_response() 416 } 417 } 418 Err(e) => ( 419 StatusCode::INTERNAL_SERVER_ERROR, 420 Json(ErrorResponse { 421 error: e.to_string(), 422 }), 423 ) 424 .into_response(), 425 } 426 } 427 Ok(None) => ( 428 StatusCode::UNAUTHORIZED, 429 Json(ErrorResponse { 430 error: "Invalid or expired session".to_string(), 431 }), 432 ) 433 .into_response(), 434 Err(e) => ( 435 StatusCode::INTERNAL_SERVER_ERROR, 436 Json(ErrorResponse { 437 error: e.to_string(), 438 }), 439 ) 440 .into_response(), 441 } 442 } else { 443 ( 444 StatusCode::UNAUTHORIZED, 445 Json(ErrorResponse { 446 error: "No session token provided".to_string(), 447 }), 448 ) 449 .into_response() 450 } 451 } 452 453 fn extract_token(headers: &axum::http::HeaderMap) -> Option<String> { 454 use axum::http::header; 455 456 // Try to get from Authorization header 457 if let Some(auth_header) = headers.get(header::AUTHORIZATION) { 458 if let Ok(auth_str) = auth_header.to_str() { 459 if auth_str.starts_with("Bearer ") { 460 return Some(auth_str[7..].to_string()); 461 } 462 } 463 } 464 465 // Try to get from Cookie header 466 if let Some(cookie_header) = headers.get(header::COOKIE) { 467 if let Ok(cookie_str) = cookie_header.to_str() { 468 for cookie in cookie_str.split(';') { 469 let cookie = cookie.trim(); 470 if cookie.starts_with("session_token=") { 471 return Some(cookie[14..].to_string()); 472 } 473 } 474 } 475 } 476 477 None 478 }