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::Utc; 10use serde::Deserialize; 11use serde_json::json; 12use tracing::{error, info, warn}; 13 14#[derive(Deserialize)] 15#[serde(rename_all = "camelCase")] 16pub struct RequestEmailUpdateInput { 17 pub email: String, 18} 19 20pub async fn request_email_update( 21 State(state): State<AppState>, 22 headers: axum::http::HeaderMap, 23 Json(input): Json<RequestEmailUpdateInput>, 24) -> Response { 25 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 26 if !state 27 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 28 .await 29 { 30 warn!(ip = %client_ip, "Email update rate limit exceeded"); 31 return ( 32 StatusCode::TOO_MANY_REQUESTS, 33 Json(json!({ 34 "error": "RateLimitExceeded", 35 "message": "Too many requests. Please try again later." 36 })), 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 auth_user = match auth_result { 56 Ok(user) => user, 57 Err(e) => return ApiError::from(e).into_response(), 58 }; 59 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 auth_user.is_oauth, 62 auth_user.scope.as_deref(), 63 crate::oauth::scopes::AccountAttr::Email, 64 crate::oauth::scopes::AccountAction::Manage, 65 ) { 66 return e; 67 } 68 69 let did = auth_user.did; 70 let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did) 71 .fetch_optional(&state.db) 72 .await 73 { 74 Ok(Some(row)) => row, 75 _ => { 76 return ( 77 StatusCode::INTERNAL_SERVER_ERROR, 78 Json(json!({"error": "InternalError"})), 79 ) 80 .into_response(); 81 } 82 }; 83 84 let user_id = user.id; 85 let handle = user.handle; 86 let current_email = user.email; 87 let email = input.email.trim().to_lowercase(); 88 89 if !crate::api::validation::is_valid_email(&email) { 90 return ( 91 StatusCode::BAD_REQUEST, 92 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 93 ) 94 .into_response(); 95 } 96 97 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) { 98 return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response(); 99 } 100 101 let exists = sqlx::query!( 102 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 103 email, 104 user_id 105 ) 106 .fetch_optional(&state.db) 107 .await; 108 109 if let Ok(Some(_)) = exists { 110 return ( 111 StatusCode::BAD_REQUEST, 112 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 113 ) 114 .into_response(); 115 } 116 117 if let Err(e) = crate::api::notification_prefs::request_channel_verification( 118 &state.db, 119 user_id, 120 "email", 121 &email, 122 Some(&handle), 123 ) 124 .await 125 { 126 error!("Failed to request email verification: {}", e); 127 return ( 128 StatusCode::INTERNAL_SERVER_ERROR, 129 Json(json!({"error": "InternalError"})), 130 ) 131 .into_response(); 132 } 133 134 info!("Email update requested for user {}", user_id); 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 152 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 153 .await 154 { 155 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 156 return ( 157 StatusCode::TOO_MANY_REQUESTS, 158 Json(json!({ 159 "error": "RateLimitExceeded", 160 "message": "Too many requests. Please try again later." 161 })), 162 ) 163 .into_response(); 164 } 165 166 let token = match crate::auth::extract_bearer_token_from_header( 167 headers.get("Authorization").and_then(|h| h.to_str().ok()), 168 ) { 169 Some(t) => t, 170 None => { 171 return ( 172 StatusCode::UNAUTHORIZED, 173 Json(json!({"error": "AuthenticationRequired"})), 174 ) 175 .into_response(); 176 } 177 }; 178 179 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 180 let auth_user = match auth_result { 181 Ok(user) => user, 182 Err(e) => return ApiError::from(e).into_response(), 183 }; 184 185 if let Err(e) = crate::auth::scope_check::check_account_scope( 186 auth_user.is_oauth, 187 auth_user.scope.as_deref(), 188 crate::oauth::scopes::AccountAttr::Email, 189 crate::oauth::scopes::AccountAction::Manage, 190 ) { 191 return e; 192 } 193 194 let did = auth_user.did; 195 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 196 .fetch_one(&state.db) 197 .await 198 { 199 Ok(id) => id, 200 Err(_) => { 201 return ( 202 StatusCode::INTERNAL_SERVER_ERROR, 203 Json(json!({"error": "InternalError"})), 204 ) 205 .into_response(); 206 } 207 }; 208 209 let verification = match sqlx::query!( 210 "SELECT code, pending_identifier, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 211 user_id 212 ) 213 .fetch_optional(&state.db) 214 .await 215 { 216 Ok(Some(row)) => row, 217 _ => { 218 return ( 219 StatusCode::BAD_REQUEST, 220 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 221 ) 222 .into_response(); 223 } 224 }; 225 226 let pending_email = verification.pending_identifier.unwrap_or_default(); 227 let email = input.email.trim().to_lowercase(); 228 let confirmation_code = input.token.trim(); 229 230 if pending_email != email { 231 return ( 232 StatusCode::BAD_REQUEST, 233 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 234 ) 235 .into_response(); 236 } 237 238 if verification.code != confirmation_code { 239 return ( 240 StatusCode::BAD_REQUEST, 241 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 242 ) 243 .into_response(); 244 } 245 246 if Utc::now() > verification.expires_at { 247 return ( 248 StatusCode::BAD_REQUEST, 249 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 250 ) 251 .into_response(); 252 } 253 254 let mut tx = match state.db.begin().await { 255 Ok(tx) => tx, 256 Err(_) => return ApiError::InternalError.into_response(), 257 }; 258 259 let update = sqlx::query!( 260 "UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2", 261 pending_email, 262 user_id 263 ) 264 .execute(&mut *tx) 265 .await; 266 267 if let Err(e) = update { 268 error!("DB error finalizing email update: {:?}", e); 269 if e.as_database_error() 270 .map(|db_err| db_err.is_unique_violation()) 271 .unwrap_or(false) 272 { 273 return ( 274 StatusCode::BAD_REQUEST, 275 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 276 ) 277 .into_response(); 278 } 279 return ( 280 StatusCode::INTERNAL_SERVER_ERROR, 281 Json(json!({"error": "InternalError"})), 282 ) 283 .into_response(); 284 } 285 286 if let Err(e) = sqlx::query!( 287 "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 288 user_id 289 ) 290 .execute(&mut *tx) 291 .await 292 { 293 error!("Failed to delete verification record: {:?}", e); 294 return ApiError::InternalError.into_response(); 295 } 296 297 if tx.commit().await.is_err() { 298 return ApiError::InternalError.into_response(); 299 } 300 301 info!("Email updated for user {}", user_id); 302 (StatusCode::OK, Json(json!({}))).into_response() 303} 304 305#[derive(Deserialize)] 306#[serde(rename_all = "camelCase")] 307pub struct UpdateEmailInput { 308 pub email: String, 309 #[serde(default)] 310 pub email_auth_factor: Option<bool>, 311 pub token: Option<String>, 312} 313 314pub async fn update_email( 315 State(state): State<AppState>, 316 headers: axum::http::HeaderMap, 317 Json(input): Json<UpdateEmailInput>, 318) -> Response { 319 let token = match crate::auth::extract_bearer_token_from_header( 320 headers.get("Authorization").and_then(|h| h.to_str().ok()), 321 ) { 322 Some(t) => t, 323 None => { 324 return ( 325 StatusCode::UNAUTHORIZED, 326 Json(json!({"error": "AuthenticationRequired"})), 327 ) 328 .into_response(); 329 } 330 }; 331 332 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 333 let auth_user = match auth_result { 334 Ok(user) => user, 335 Err(e) => return ApiError::from(e).into_response(), 336 }; 337 338 if let Err(e) = crate::auth::scope_check::check_account_scope( 339 auth_user.is_oauth, 340 auth_user.scope.as_deref(), 341 crate::oauth::scopes::AccountAttr::Email, 342 crate::oauth::scopes::AccountAction::Manage, 343 ) { 344 return e; 345 } 346 347 let did = auth_user.did; 348 let user = match sqlx::query!("SELECT id, email FROM users WHERE did = $1", did) 349 .fetch_optional(&state.db) 350 .await 351 { 352 Ok(Some(row)) => row, 353 _ => { 354 return ( 355 StatusCode::INTERNAL_SERVER_ERROR, 356 Json(json!({"error": "InternalError"})), 357 ) 358 .into_response(); 359 } 360 }; 361 362 let user_id = user.id; 363 let current_email = user.email; 364 let new_email = input.email.trim().to_lowercase(); 365 366 if !crate::api::validation::is_valid_email(&new_email) { 367 return ( 368 StatusCode::BAD_REQUEST, 369 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 370 ) 371 .into_response(); 372 } 373 374 if let Some(ref current) = current_email 375 && new_email == current.to_lowercase() 376 { 377 return (StatusCode::OK, Json(json!({}))).into_response(); 378 } 379 380 let verification = sqlx::query!( 381 "SELECT code, pending_identifier, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 382 user_id 383 ) 384 .fetch_optional(&state.db) 385 .await 386 .unwrap_or(None); 387 388 if let Some(ver) = verification { 389 let confirmation_token = match &input.token { 390 Some(t) => t.trim(), 391 None => { 392 return ( 393 StatusCode::BAD_REQUEST, 394 Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})), 395 ) 396 .into_response(); 397 } 398 }; 399 400 let pending_email = ver.pending_identifier.unwrap_or_default(); 401 if pending_email.to_lowercase() != new_email { 402 return ( 403 StatusCode::BAD_REQUEST, 404 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 405 ) 406 .into_response(); 407 } 408 409 if ver.code != confirmation_token { 410 return ( 411 StatusCode::BAD_REQUEST, 412 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 413 ) 414 .into_response(); 415 } 416 417 if Utc::now() > ver.expires_at { 418 return ( 419 StatusCode::BAD_REQUEST, 420 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 421 ) 422 .into_response(); 423 } 424 } 425 426 let exists = sqlx::query!( 427 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 428 new_email, 429 user_id 430 ) 431 .fetch_optional(&state.db) 432 .await; 433 434 if let Ok(Some(_)) = exists { 435 return ( 436 StatusCode::BAD_REQUEST, 437 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 438 ) 439 .into_response(); 440 } 441 442 let mut tx = match state.db.begin().await { 443 Ok(tx) => tx, 444 Err(_) => return ApiError::InternalError.into_response(), 445 }; 446 447 let update = sqlx::query!( 448 "UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2", 449 new_email, 450 user_id 451 ) 452 .execute(&mut *tx) 453 .await; 454 455 if let Err(e) = update { 456 error!("DB error finalizing email update: {:?}", e); 457 if e.as_database_error() 458 .map(|db_err| db_err.is_unique_violation()) 459 .unwrap_or(false) 460 { 461 return ( 462 StatusCode::BAD_REQUEST, 463 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 464 ) 465 .into_response(); 466 } 467 return ( 468 StatusCode::INTERNAL_SERVER_ERROR, 469 Json(json!({"error": "InternalError"})), 470 ) 471 .into_response(); 472 } 473 474 let _ = sqlx::query!( 475 "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 476 user_id 477 ) 478 .execute(&mut *tx) 479 .await; 480 481 if tx.commit().await.is_err() { 482 return ApiError::InternalError.into_response(); 483 } 484 485 match sqlx::query!( 486 "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", 487 user_id, 488 json!(input.email_auth_factor.unwrap_or(false)) 489 ) 490 .execute(&state.db) 491 .await 492 { 493 Ok(_) => {} 494 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e), 495 } 496 497 info!("Email updated for user {}", user_id); 498 (StatusCode::OK, Json(json!({}))).into_response() 499}