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