exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }