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 serde::Deserialize; 10use serde_json::json; 11use tracing::{error, info, warn}; 12 13#[derive(Deserialize)] 14#[serde(rename_all = "camelCase")] 15pub struct RequestEmailUpdateInput { 16 pub email: String, 17} 18 19pub async fn request_email_update( 20 State(state): State<AppState>, 21 headers: axum::http::HeaderMap, 22 Json(input): Json<RequestEmailUpdateInput>, 23) -> Response { 24 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 25 if !state 26 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 27 .await 28 { 29 warn!(ip = %client_ip, "Email update rate limit exceeded"); 30 return ( 31 StatusCode::TOO_MANY_REQUESTS, 32 Json(json!({ 33 "error": "RateLimitExceeded", 34 "message": "Too many requests. Please try again later." 35 })), 36 ) 37 .into_response(); 38 } 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 53 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 54 let auth_user = match auth_result { 55 Ok(user) => user, 56 Err(e) => return ApiError::from(e).into_response(), 57 }; 58 59 if let Err(e) = crate::auth::scope_check::check_account_scope( 60 auth_user.is_oauth, 61 auth_user.scope.as_deref(), 62 crate::oauth::scopes::AccountAttr::Email, 63 crate::oauth::scopes::AccountAction::Manage, 64 ) { 65 return e; 66 } 67 68 let did = auth_user.did.clone(); 69 let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did) 70 .fetch_optional(&state.db) 71 .await 72 { 73 Ok(Some(row)) => row, 74 _ => { 75 return ( 76 StatusCode::INTERNAL_SERVER_ERROR, 77 Json(json!({"error": "InternalError"})), 78 ) 79 .into_response(); 80 } 81 }; 82 83 let user_id = user.id; 84 let handle = user.handle; 85 let current_email = user.email; 86 let email = input.email.trim().to_lowercase(); 87 88 if !crate::api::validation::is_valid_email(&email) { 89 return ( 90 StatusCode::BAD_REQUEST, 91 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 92 ) 93 .into_response(); 94 } 95 96 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) { 97 return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response(); 98 } 99 100 let exists = sqlx::query!( 101 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 102 email, 103 user_id 104 ) 105 .fetch_optional(&state.db) 106 .await; 107 108 if let Ok(Some(_)) = exists { 109 return ( 110 StatusCode::BAD_REQUEST, 111 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 112 ) 113 .into_response(); 114 } 115 116 if let Err(e) = crate::api::notification_prefs::request_channel_verification( 117 &state.db, 118 user_id, 119 &did, 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 email = input.email.trim().to_lowercase(); 210 let confirmation_code = 211 crate::auth::verification_token::normalize_token_input(input.token.trim()); 212 213 let verified = crate::auth::verification_token::verify_channel_update_token( 214 &confirmation_code, 215 "email", 216 &email, 217 ); 218 219 match verified { 220 Ok(token_data) => { 221 if token_data.did != did { 222 return ( 223 StatusCode::BAD_REQUEST, 224 Json( 225 json!({"error": "InvalidToken", "message": "Token does not match account"}), 226 ), 227 ) 228 .into_response(); 229 } 230 } 231 Err(crate::auth::verification_token::VerifyError::Expired) => { 232 return ( 233 StatusCode::BAD_REQUEST, 234 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 235 ) 236 .into_response(); 237 } 238 Err(_) => { 239 return ( 240 StatusCode::BAD_REQUEST, 241 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 242 ) 243 .into_response(); 244 } 245 } 246 247 let update = sqlx::query!( 248 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2", 249 email, 250 user_id 251 ) 252 .execute(&state.db) 253 .await; 254 255 if let Err(e) = update { 256 error!("DB error finalizing email update: {:?}", e); 257 if e.as_database_error() 258 .map(|db_err| db_err.is_unique_violation()) 259 .unwrap_or(false) 260 { 261 return ( 262 StatusCode::BAD_REQUEST, 263 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 264 ) 265 .into_response(); 266 } 267 return ( 268 StatusCode::INTERNAL_SERVER_ERROR, 269 Json(json!({"error": "InternalError"})), 270 ) 271 .into_response(); 272 } 273 274 info!("Email updated for user {}", user_id); 275 (StatusCode::OK, Json(json!({}))).into_response() 276} 277 278#[derive(Deserialize)] 279#[serde(rename_all = "camelCase")] 280pub struct UpdateEmailInput { 281 pub email: String, 282 #[serde(default)] 283 pub email_auth_factor: Option<bool>, 284 pub token: Option<String>, 285} 286 287pub async fn update_email( 288 State(state): State<AppState>, 289 headers: axum::http::HeaderMap, 290 Json(input): Json<UpdateEmailInput>, 291) -> Response { 292 let token = match crate::auth::extract_bearer_token_from_header( 293 headers.get("Authorization").and_then(|h| h.to_str().ok()), 294 ) { 295 Some(t) => t, 296 None => { 297 return ( 298 StatusCode::UNAUTHORIZED, 299 Json(json!({"error": "AuthenticationRequired"})), 300 ) 301 .into_response(); 302 } 303 }; 304 305 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 306 let auth_user = match auth_result { 307 Ok(user) => user, 308 Err(e) => return ApiError::from(e).into_response(), 309 }; 310 311 if let Err(e) = crate::auth::scope_check::check_account_scope( 312 auth_user.is_oauth, 313 auth_user.scope.as_deref(), 314 crate::oauth::scopes::AccountAttr::Email, 315 crate::oauth::scopes::AccountAction::Manage, 316 ) { 317 return e; 318 } 319 320 let did = auth_user.did; 321 let user = match sqlx::query!("SELECT id, email FROM users WHERE did = $1", did) 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 confirmation_token = match &input.token { 354 Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()), 355 None => { 356 return ( 357 StatusCode::BAD_REQUEST, 358 Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})), 359 ) 360 .into_response(); 361 } 362 }; 363 364 let verified = crate::auth::verification_token::verify_channel_update_token( 365 &confirmation_token, 366 "email", 367 &new_email, 368 ); 369 370 match verified { 371 Ok(token_data) => { 372 if token_data.did != did { 373 return ( 374 StatusCode::BAD_REQUEST, 375 Json( 376 json!({"error": "InvalidToken", "message": "Token does not match account"}), 377 ), 378 ) 379 .into_response(); 380 } 381 } 382 Err(crate::auth::verification_token::VerifyError::Expired) => { 383 return ( 384 StatusCode::BAD_REQUEST, 385 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 386 ) 387 .into_response(); 388 } 389 Err(_) => { 390 return ( 391 StatusCode::BAD_REQUEST, 392 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 393 ) 394 .into_response(); 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!({"error": "InvalidRequest", "message": "Email already in use"})), 410 ) 411 .into_response(); 412 } 413 414 let update = sqlx::query!( 415 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2", 416 new_email, 417 user_id 418 ) 419 .execute(&state.db) 420 .await; 421 422 if let Err(e) = update { 423 error!("DB error finalizing email update: {:?}", e); 424 if e.as_database_error() 425 .map(|db_err| db_err.is_unique_violation()) 426 .unwrap_or(false) 427 { 428 return ( 429 StatusCode::BAD_REQUEST, 430 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 431 ) 432 .into_response(); 433 } 434 return ( 435 StatusCode::INTERNAL_SERVER_ERROR, 436 Json(json!({"error": "InternalError"})), 437 ) 438 .into_response(); 439 } 440 441 match sqlx::query!( 442 "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", 443 user_id, 444 json!(input.email_auth_factor.unwrap_or(false)) 445 ) 446 .execute(&state.db) 447 .await 448 { 449 Ok(_) => {} 450 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e), 451 } 452 453 info!("Email updated for user {}", user_id); 454 (StatusCode::OK, Json(json!({}))).into_response() 455}