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