this repo has no description
1use crate::api::ApiError; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 extract::State, 6 http::StatusCode, 7 response::{IntoResponse, Response}, 8}; 9use chrono::{Duration, Utc}; 10use serde::Deserialize; 11use serde_json::json; 12use tracing::{error, info, warn}; 13 14fn generate_confirmation_code() -> String { 15 crate::util::generate_token_code() 16} 17 18#[derive(Deserialize)] 19#[serde(rename_all = "camelCase")] 20pub struct RequestEmailUpdateInput { 21 pub email: String, 22} 23 24pub async fn request_email_update( 25 State(state): State<AppState>, 26 headers: axum::http::HeaderMap, 27 Json(input): Json<RequestEmailUpdateInput>, 28) -> Response { 29 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 30 if !state.distributed_rate_limiter.check_rate_limit( 31 &format!("email_update:{}", client_ip), 32 5, 33 3_600_000, 34 ).await { 35 if state.rate_limiters.email_update.check_key(&client_ip).is_err() { 36 warn!(ip = %client_ip, "Email update rate limit exceeded"); 37 return ( 38 StatusCode::TOO_MANY_REQUESTS, 39 Json(json!({ 40 "error": "RateLimitExceeded", 41 "message": "Too many requests. Please try again later." 42 })), 43 ).into_response(); 44 } 45 } 46 47 let token = match crate::auth::extract_bearer_token_from_header( 48 headers.get("Authorization").and_then(|h| h.to_str().ok()) 49 ) { 50 Some(t) => t, 51 None => { 52 return ( 53 StatusCode::UNAUTHORIZED, 54 Json(json!({"error": "AuthenticationRequired"})), 55 ) 56 .into_response(); 57 } 58 }; 59 60 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 61 let did = match auth_result { 62 Ok(user) => user.did, 63 Err(e) => return ApiError::from(e).into_response(), 64 }; 65 66 let user = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did) 67 .fetch_optional(&state.db) 68 .await 69 { 70 Ok(Some(row)) => row, 71 _ => { 72 return ( 73 StatusCode::INTERNAL_SERVER_ERROR, 74 Json(json!({"error": "InternalError"})), 75 ) 76 .into_response(); 77 } 78 }; 79 let user_id = user.id; 80 let handle = user.handle; 81 82 let email = input.email.trim().to_lowercase(); 83 if !crate::api::validation::is_valid_email(&email) { 84 return ( 85 StatusCode::BAD_REQUEST, 86 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 87 ) 88 .into_response(); 89 } 90 91 let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email) 92 .fetch_optional(&state.db) 93 .await; 94 95 if let Ok(Some(_)) = exists { 96 return ( 97 StatusCode::BAD_REQUEST, 98 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 99 ) 100 .into_response(); 101 } 102 103 let code = generate_confirmation_code(); 104 let expires_at = Utc::now() + Duration::minutes(10); 105 106 let update = sqlx::query!( 107 "UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4", 108 email, 109 code, 110 expires_at, 111 user_id 112 ) 113 .execute(&state.db) 114 .await; 115 116 if let Err(e) = update { 117 error!("DB error setting email update code: {:?}", e); 118 return ( 119 StatusCode::INTERNAL_SERVER_ERROR, 120 Json(json!({"error": "InternalError"})), 121 ) 122 .into_response(); 123 } 124 125 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 126 if let Err(e) = crate::notifications::enqueue_email_update( 127 &state.db, 128 user_id, 129 &email, 130 &handle, 131 &code, 132 &hostname, 133 ) 134 .await 135 { 136 warn!("Failed to enqueue email update notification: {:?}", e); 137 } 138 139 info!("Email update requested for user {}", user_id); 140 141 (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response() 142} 143 144#[derive(Deserialize)] 145#[serde(rename_all = "camelCase")] 146pub struct ConfirmEmailInput { 147 pub email: String, 148 pub token: String, 149} 150 151pub async fn confirm_email( 152 State(state): State<AppState>, 153 headers: axum::http::HeaderMap, 154 Json(input): Json<ConfirmEmailInput>, 155) -> Response { 156 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 157 if !state.distributed_rate_limiter.check_rate_limit( 158 &format!("confirm_email:{}", client_ip), 159 10, 160 60_000, 161 ).await { 162 if state.rate_limiters.app_password.check_key(&client_ip).is_err() { 163 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 164 return ( 165 StatusCode::TOO_MANY_REQUESTS, 166 Json(json!({ 167 "error": "RateLimitExceeded", 168 "message": "Too many requests. Please try again later." 169 })), 170 ).into_response(); 171 } 172 } 173 174 let token = match crate::auth::extract_bearer_token_from_header( 175 headers.get("Authorization").and_then(|h| h.to_str().ok()) 176 ) { 177 Some(t) => t, 178 None => { 179 return ( 180 StatusCode::UNAUTHORIZED, 181 Json(json!({"error": "AuthenticationRequired"})), 182 ) 183 .into_response(); 184 } 185 }; 186 187 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 188 let did = match auth_result { 189 Ok(user) => user.did, 190 Err(e) => return ApiError::from(e).into_response(), 191 }; 192 193 let user = match sqlx::query!( 194 "SELECT id, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1", 195 did 196 ) 197 .fetch_optional(&state.db) 198 .await 199 { 200 Ok(Some(row)) => row, 201 _ => { 202 return ( 203 StatusCode::INTERNAL_SERVER_ERROR, 204 Json(json!({"error": "InternalError"})), 205 ) 206 .into_response(); 207 } 208 }; 209 let user_id = user.id; 210 let stored_code = user.email_confirmation_code; 211 let expires_at = user.email_confirmation_code_expires_at; 212 let email_pending_verification = user.email_pending_verification; 213 214 let email = input.email.trim().to_lowercase(); 215 let confirmation_code = input.token.trim(); 216 217 let (pending_email, saved_code, expiry) = match (email_pending_verification, stored_code, expires_at) { 218 (Some(p), Some(c), Some(e)) => (p, c, e), 219 _ => { 220 return ( 221 StatusCode::BAD_REQUEST, 222 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 223 ) 224 .into_response(); 225 } 226 }; 227 228 if pending_email != email { 229 return ( 230 StatusCode::BAD_REQUEST, 231 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 232 ) 233 .into_response(); 234 } 235 236 if saved_code != confirmation_code { 237 return ( 238 StatusCode::BAD_REQUEST, 239 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 240 ) 241 .into_response(); 242 } 243 244 if Utc::now() > expiry { 245 return ( 246 StatusCode::BAD_REQUEST, 247 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 248 ) 249 .into_response(); 250 } 251 252 let update = sqlx::query!( 253 "UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2", 254 pending_email, 255 user_id 256 ) 257 .execute(&state.db) 258 .await; 259 260 if let Err(e) = update { 261 error!("DB error finalizing email update: {:?}", e); 262 if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) { 263 return ( 264 StatusCode::BAD_REQUEST, 265 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 266 ) 267 .into_response(); 268 } 269 270 return ( 271 StatusCode::INTERNAL_SERVER_ERROR, 272 Json(json!({"error": "InternalError"})), 273 ) 274 .into_response(); 275 } 276 277 info!("Email updated for user {}", user_id); 278 279 (StatusCode::OK, Json(json!({}))).into_response() 280} 281 282#[derive(Deserialize)] 283#[serde(rename_all = "camelCase")] 284pub struct UpdateEmailInput { 285 pub email: String, 286 #[serde(default)] 287 pub email_auth_factor: Option<bool>, 288 pub token: Option<String>, 289} 290 291pub async fn update_email( 292 State(state): State<AppState>, 293 headers: axum::http::HeaderMap, 294 Json(input): Json<UpdateEmailInput>, 295) -> Response { 296 let token = match crate::auth::extract_bearer_token_from_header( 297 headers.get("Authorization").and_then(|h| h.to_str().ok()) 298 ) { 299 Some(t) => t, 300 None => { 301 return ( 302 StatusCode::UNAUTHORIZED, 303 Json(json!({"error": "AuthenticationRequired"})), 304 ) 305 .into_response(); 306 } 307 }; 308 309 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 310 let did = match auth_result { 311 Ok(user) => user.did, 312 Err(e) => return ApiError::from(e).into_response(), 313 }; 314 315 let user = match sqlx::query!( 316 "SELECT id, email, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1", 317 did 318 ) 319 .fetch_optional(&state.db) 320 .await 321 { 322 Ok(Some(row)) => row, 323 _ => { 324 return ( 325 StatusCode::INTERNAL_SERVER_ERROR, 326 Json(json!({"error": "InternalError"})), 327 ) 328 .into_response(); 329 } 330 }; 331 let user_id = user.id; 332 let current_email = user.email; 333 let stored_code = user.email_confirmation_code; 334 let expires_at = user.email_confirmation_code_expires_at; 335 let email_pending_verification = user.email_pending_verification; 336 337 let new_email = input.email.trim().to_lowercase(); 338 if !crate::api::validation::is_valid_email(&new_email) { 339 return ( 340 StatusCode::BAD_REQUEST, 341 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 342 ) 343 .into_response(); 344 } 345 346 if new_email == current_email.to_lowercase() { 347 return (StatusCode::OK, Json(json!({}))).into_response(); 348 } 349 350 let email_confirmed = stored_code.is_some() && email_pending_verification.is_some(); 351 352 if email_confirmed { 353 let confirmation_token = match &input.token { 354 Some(t) => t.trim(), 355 None => { 356 return ( 357 StatusCode::BAD_REQUEST, 358 Json(json!({"error": "TokenRequired", "message": "Token required for confirmed accounts. Call requestEmailUpdate first."})), 359 ) 360 .into_response(); 361 } 362 }; 363 364 let pending_email = match email_pending_verification { 365 Some(p) => p, 366 None => { 367 return ( 368 StatusCode::BAD_REQUEST, 369 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 370 ) 371 .into_response(); 372 } 373 }; 374 375 if pending_email.to_lowercase() != new_email { 376 return ( 377 StatusCode::BAD_REQUEST, 378 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 379 ) 380 .into_response(); 381 } 382 383 let saved_code = match stored_code { 384 Some(c) => c, 385 None => { 386 return ( 387 StatusCode::BAD_REQUEST, 388 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 389 ) 390 .into_response(); 391 } 392 }; 393 394 if saved_code != confirmation_token { 395 return ( 396 StatusCode::BAD_REQUEST, 397 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 398 ) 399 .into_response(); 400 } 401 402 if let Some(exp) = expires_at { 403 if Utc::now() > exp { 404 return ( 405 StatusCode::BAD_REQUEST, 406 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 407 ) 408 .into_response(); 409 } 410 } 411 } 412 413 let exists = sqlx::query!( 414 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 415 new_email, 416 user_id 417 ) 418 .fetch_optional(&state.db) 419 .await; 420 421 if let Ok(Some(_)) = exists { 422 return ( 423 StatusCode::BAD_REQUEST, 424 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 425 ) 426 .into_response(); 427 } 428 429 let update = sqlx::query!( 430 r#" 431 UPDATE users 432 SET email = $1, 433 email_pending_verification = NULL, 434 email_confirmation_code = NULL, 435 email_confirmation_code_expires_at = NULL, 436 updated_at = NOW() 437 WHERE id = $2 438 "#, 439 new_email, 440 user_id 441 ) 442 .execute(&state.db) 443 .await; 444 445 match update { 446 Ok(_) => { 447 info!("Email updated for user {}", user_id); 448 (StatusCode::OK, Json(json!({}))).into_response() 449 } 450 Err(e) => { 451 error!("DB error finalizing email update: {:?}", e); 452 if e.as_database_error() 453 .map(|db_err| db_err.is_unique_violation()) 454 .unwrap_or(false) 455 { 456 return ( 457 StatusCode::BAD_REQUEST, 458 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 459 ) 460 .into_response(); 461 } 462 463 ( 464 StatusCode::INTERNAL_SERVER_ERROR, 465 Json(json!({"error": "InternalError"})), 466 ) 467 .into_response() 468 } 469 } 470}