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 = crate::auth::verification_token::format_token_for_display(&code); 90 91 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 92 if let Err(e) = 93 crate::comms::enqueue_email_update_token(&state.db, user.id, &formatted_code, &hostname) 94 .await 95 { 96 warn!("Failed to enqueue email update notification: {:?}", e); 97 } 98 } 99 100 info!("Email update requested for user {}", user.id); 101 ( 102 StatusCode::OK, 103 Json(json!({ "tokenRequired": token_required })), 104 ) 105 .into_response() 106} 107 108#[derive(Deserialize)] 109#[serde(rename_all = "camelCase")] 110pub struct ConfirmEmailInput { 111 pub email: String, 112 pub token: String, 113} 114 115pub async fn confirm_email( 116 State(state): State<AppState>, 117 headers: axum::http::HeaderMap, 118 auth: BearerAuth, 119 Json(input): Json<ConfirmEmailInput>, 120) -> Response { 121 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 122 if !state 123 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 124 .await 125 { 126 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 127 return ( 128 StatusCode::TOO_MANY_REQUESTS, 129 Json(json!({ 130 "error": "RateLimitExceeded", 131 "message": "Too many requests. Please try again later." 132 })), 133 ) 134 .into_response(); 135 } 136 137 if let Err(e) = crate::auth::scope_check::check_account_scope( 138 auth.0.is_oauth, 139 auth.0.scope.as_deref(), 140 crate::oauth::scopes::AccountAttr::Email, 141 crate::oauth::scopes::AccountAction::Manage, 142 ) { 143 return e; 144 } 145 146 let did = auth.0.did; 147 let user = match sqlx::query!( 148 "SELECT id, email, email_verified FROM users WHERE did = $1", 149 did 150 ) 151 .fetch_optional(&state.db) 152 .await 153 { 154 Ok(Some(row)) => row, 155 Ok(None) => { 156 return ( 157 StatusCode::BAD_REQUEST, 158 Json(json!({"error": "AccountNotFound", "message": "user not found"})), 159 ) 160 .into_response(); 161 } 162 Err(e) => { 163 error!("DB error: {:?}", e); 164 return ( 165 StatusCode::INTERNAL_SERVER_ERROR, 166 Json(json!({"error": "InternalError"})), 167 ) 168 .into_response(); 169 } 170 }; 171 172 let current_email = match &user.email { 173 Some(e) => e.to_lowercase(), 174 None => { 175 return ( 176 StatusCode::BAD_REQUEST, 177 Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})), 178 ) 179 .into_response(); 180 } 181 }; 182 183 let provided_email = input.email.trim().to_lowercase(); 184 if provided_email != current_email { 185 return ( 186 StatusCode::BAD_REQUEST, 187 Json(json!({"error": "InvalidEmail", "message": "invalid email"})), 188 ) 189 .into_response(); 190 } 191 192 if user.email_verified { 193 return (StatusCode::OK, Json(json!({}))).into_response(); 194 } 195 196 let confirmation_code = 197 crate::auth::verification_token::normalize_token_input(input.token.trim()); 198 199 let verified = crate::auth::verification_token::verify_signup_token( 200 &confirmation_code, 201 "email", 202 &provided_email, 203 ); 204 205 match verified { 206 Ok(token_data) => { 207 if token_data.did != did { 208 return ( 209 StatusCode::BAD_REQUEST, 210 Json( 211 json!({"error": "InvalidToken", "message": "Token does not match account"}), 212 ), 213 ) 214 .into_response(); 215 } 216 } 217 Err(crate::auth::verification_token::VerifyError::Expired) => { 218 return ( 219 StatusCode::BAD_REQUEST, 220 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 221 ) 222 .into_response(); 223 } 224 Err(_) => { 225 return ( 226 StatusCode::BAD_REQUEST, 227 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 228 ) 229 .into_response(); 230 } 231 } 232 233 let update = sqlx::query!( 234 "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1", 235 user.id 236 ) 237 .execute(&state.db) 238 .await; 239 240 if let Err(e) = update { 241 error!("DB error confirming email: {:?}", e); 242 return ( 243 StatusCode::INTERNAL_SERVER_ERROR, 244 Json(json!({"error": "InternalError"})), 245 ) 246 .into_response(); 247 } 248 249 info!("Email confirmed for user {}", user.id); 250 (StatusCode::OK, Json(json!({}))).into_response() 251} 252 253#[derive(Deserialize)] 254#[serde(rename_all = "camelCase")] 255pub struct UpdateEmailInput { 256 pub email: String, 257 #[serde(default)] 258 pub email_auth_factor: Option<bool>, 259 pub token: Option<String>, 260} 261 262pub async fn update_email( 263 State(state): State<AppState>, 264 headers: axum::http::HeaderMap, 265 Json(input): Json<UpdateEmailInput>, 266) -> Response { 267 let bearer_token = match crate::auth::extract_bearer_token_from_header( 268 headers.get("Authorization").and_then(|h| h.to_str().ok()), 269 ) { 270 Some(t) => t, 271 None => { 272 return ( 273 StatusCode::UNAUTHORIZED, 274 Json(json!({"error": "AuthenticationRequired"})), 275 ) 276 .into_response(); 277 } 278 }; 279 280 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await; 281 let auth_user = match auth_result { 282 Ok(user) => user, 283 Err(e) => return ApiError::from(e).into_response(), 284 }; 285 286 if let Err(e) = crate::auth::scope_check::check_account_scope( 287 auth_user.is_oauth, 288 auth_user.scope.as_deref(), 289 crate::oauth::scopes::AccountAttr::Email, 290 crate::oauth::scopes::AccountAction::Manage, 291 ) { 292 return e; 293 } 294 295 let did = auth_user.did; 296 let user = match sqlx::query!( 297 "SELECT id, email, email_verified FROM users WHERE did = $1", 298 did 299 ) 300 .fetch_optional(&state.db) 301 .await 302 { 303 Ok(Some(row)) => row, 304 Ok(None) => { 305 return ( 306 StatusCode::BAD_REQUEST, 307 Json(json!({"error": "InvalidRequest", "message": "account not found"})), 308 ) 309 .into_response(); 310 } 311 Err(e) => { 312 error!("DB error: {:?}", e); 313 return ( 314 StatusCode::INTERNAL_SERVER_ERROR, 315 Json(json!({"error": "InternalError"})), 316 ) 317 .into_response(); 318 } 319 }; 320 321 let user_id = user.id; 322 let current_email = user.email.clone(); 323 let email_verified = user.email_verified; 324 let new_email = input.email.trim().to_lowercase(); 325 326 if !crate::api::validation::is_valid_email(&new_email) { 327 return ( 328 StatusCode::BAD_REQUEST, 329 Json(json!({ 330 "error": "InvalidRequest", 331 "message": "This email address is not supported, please use a different email." 332 })), 333 ) 334 .into_response(); 335 } 336 337 if let Some(ref current) = current_email 338 && new_email == current.to_lowercase() 339 { 340 return (StatusCode::OK, Json(json!({}))).into_response(); 341 } 342 343 if email_verified { 344 let confirmation_token = match &input.token { 345 Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()), 346 None => { 347 return ( 348 StatusCode::BAD_REQUEST, 349 Json(json!({ 350 "error": "TokenRequired", 351 "message": "confirmation token required" 352 })), 353 ) 354 .into_response(); 355 } 356 }; 357 358 let current_email_lower = current_email 359 .as_ref() 360 .map(|e| e.to_lowercase()) 361 .unwrap_or_default(); 362 363 let verified = crate::auth::verification_token::verify_channel_update_token( 364 &confirmation_token, 365 "email_update", 366 &current_email_lower, 367 ); 368 369 match verified { 370 Ok(token_data) => { 371 if token_data.did != did { 372 return ( 373 StatusCode::BAD_REQUEST, 374 Json( 375 json!({"error": "InvalidToken", "message": "Token does not match account"}), 376 ), 377 ) 378 .into_response(); 379 } 380 } 381 Err(crate::auth::verification_token::VerifyError::Expired) => { 382 return ( 383 StatusCode::BAD_REQUEST, 384 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 385 ) 386 .into_response(); 387 } 388 Err(_) => { 389 return ( 390 StatusCode::BAD_REQUEST, 391 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 392 ) 393 .into_response(); 394 } 395 } 396 } 397 398 let exists = sqlx::query!( 399 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 400 new_email, 401 user_id 402 ) 403 .fetch_optional(&state.db) 404 .await; 405 406 if let Ok(Some(_)) = exists { 407 return ( 408 StatusCode::BAD_REQUEST, 409 Json(json!({ 410 "error": "InvalidRequest", 411 "message": "This email address is already in use, please use a different email." 412 })), 413 ) 414 .into_response(); 415 } 416 417 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!( 418 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2", 419 new_email, 420 user_id 421 ) 422 .execute(&state.db) 423 .await; 424 425 if let Err(e) = update { 426 error!("DB error updating email: {:?}", e); 427 if e.as_database_error() 428 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation()) 429 .unwrap_or(false) 430 { 431 return ( 432 StatusCode::BAD_REQUEST, 433 Json(json!({ 434 "error": "InvalidRequest", 435 "message": "This email address is already in use, please use a different email." 436 })), 437 ) 438 .into_response(); 439 } 440 return ( 441 StatusCode::INTERNAL_SERVER_ERROR, 442 Json(json!({"error": "InternalError"})), 443 ) 444 .into_response(); 445 } 446 447 let verification_token = 448 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email); 449 let formatted_token = 450 crate::auth::verification_token::format_token_for_display(&verification_token); 451 if let Err(e) = crate::comms::enqueue_signup_verification( 452 &state.db, 453 user_id, 454 "email", 455 &new_email, 456 &formatted_token, 457 None, 458 ) 459 .await 460 { 461 warn!("Failed to send verification email to new address: {:?}", e); 462 } 463 464 match sqlx::query!( 465 "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", 466 user_id, 467 json!(input.email_auth_factor.unwrap_or(false)) 468 ) 469 .execute(&state.db) 470 .await 471 { 472 Ok(_) => {} 473 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e), 474 } 475 476 info!("Email updated for user {}", user_id); 477 (StatusCode::OK, Json(json!({}))).into_response() 478} 479 480#[derive(Deserialize)] 481pub struct CheckEmailVerifiedInput { 482 pub identifier: String, 483} 484 485pub async fn check_email_verified( 486 State(state): State<AppState>, 487 headers: axum::http::HeaderMap, 488 Json(input): Json<CheckEmailVerifiedInput>, 489) -> Response { 490 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 491 if !state 492 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 493 .await 494 { 495 return ( 496 StatusCode::TOO_MANY_REQUESTS, 497 Json(json!({ 498 "error": "RateLimitExceeded", 499 "message": "Too many requests. Please try again later." 500 })), 501 ) 502 .into_response(); 503 } 504 505 let user = sqlx::query!( 506 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1", 507 input.identifier 508 ) 509 .fetch_optional(&state.db) 510 .await; 511 512 match user { 513 Ok(Some(row)) => ( 514 StatusCode::OK, 515 Json(json!({ "verified": row.email_verified })), 516 ) 517 .into_response(), 518 Ok(None) => ( 519 StatusCode::NOT_FOUND, 520 Json(json!({ "error": "AccountNotFound", "message": "Account not found" })), 521 ) 522 .into_response(), 523 Err(e) => { 524 error!("DB error checking email verified: {:?}", e); 525 ( 526 StatusCode::INTERNAL_SERVER_ERROR, 527 Json(json!({ "error": "InternalError" })), 528 ) 529 .into_response() 530 } 531 } 532}