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