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