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