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::{Duration, Utc}; 10use serde::Deserialize; 11use serde_json::json; 12use tracing::{error, info, warn}; 13fn generate_confirmation_code() -> String { 14 crate::util::generate_token_code() 15} 16#[derive(Deserialize)] 17#[serde(rename_all = "camelCase")] 18pub struct RequestEmailUpdateInput { 19 pub email: String, 20} 21pub async fn request_email_update( 22 State(state): State<AppState>, 23 headers: axum::http::HeaderMap, 24 Json(input): Json<RequestEmailUpdateInput>, 25) -> Response { 26 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 27 if !state.check_rate_limit(RateLimitKind::EmailUpdate, &client_ip).await { 28 warn!(ip = %client_ip, "Email update rate limit exceeded"); 29 return ( 30 StatusCode::TOO_MANY_REQUESTS, 31 Json(json!({ 32 "error": "RateLimitExceeded", 33 "message": "Too many requests. Please try again later." 34 })), 35 ).into_response(); 36 } 37 let token = match crate::auth::extract_bearer_token_from_header( 38 headers.get("Authorization").and_then(|h| h.to_str().ok()) 39 ) { 40 Some(t) => t, 41 None => { 42 return ( 43 StatusCode::UNAUTHORIZED, 44 Json(json!({"error": "AuthenticationRequired"})), 45 ) 46 .into_response(); 47 } 48 }; 49 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 50 let did = match auth_result { 51 Ok(user) => user.did, 52 Err(e) => return ApiError::from(e).into_response(), 53 }; 54 let user = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did) 55 .fetch_optional(&state.db) 56 .await 57 { 58 Ok(Some(row)) => row, 59 _ => { 60 return ( 61 StatusCode::INTERNAL_SERVER_ERROR, 62 Json(json!({"error": "InternalError"})), 63 ) 64 .into_response(); 65 } 66 }; 67 let user_id = user.id; 68 let handle = user.handle; 69 let email = input.email.trim().to_lowercase(); 70 if !crate::api::validation::is_valid_email(&email) { 71 return ( 72 StatusCode::BAD_REQUEST, 73 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 74 ) 75 .into_response(); 76 } 77 let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email) 78 .fetch_optional(&state.db) 79 .await; 80 if let Ok(Some(_)) = exists { 81 return ( 82 StatusCode::BAD_REQUEST, 83 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 84 ) 85 .into_response(); 86 } 87 let code = generate_confirmation_code(); 88 let expires_at = Utc::now() + Duration::minutes(10); 89 let update = sqlx::query!( 90 "UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4", 91 email, 92 code, 93 expires_at, 94 user_id 95 ) 96 .execute(&state.db) 97 .await; 98 if let Err(e) = update { 99 error!("DB error setting email update code: {:?}", e); 100 return ( 101 StatusCode::INTERNAL_SERVER_ERROR, 102 Json(json!({"error": "InternalError"})), 103 ) 104 .into_response(); 105 } 106 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 107 if let Err(e) = crate::notifications::enqueue_email_update( 108 &state.db, 109 user_id, 110 &email, 111 &handle, 112 &code, 113 &hostname, 114 ) 115 .await 116 { 117 warn!("Failed to enqueue email update notification: {:?}", e); 118 } 119 info!("Email update requested for user {}", user_id); 120 (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response() 121} 122#[derive(Deserialize)] 123#[serde(rename_all = "camelCase")] 124pub struct ConfirmEmailInput { 125 pub email: String, 126 pub token: String, 127} 128pub async fn confirm_email( 129 State(state): State<AppState>, 130 headers: axum::http::HeaderMap, 131 Json(input): Json<ConfirmEmailInput>, 132) -> Response { 133 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 134 if !state.check_rate_limit(RateLimitKind::AppPassword, &client_ip).await { 135 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 136 return ( 137 StatusCode::TOO_MANY_REQUESTS, 138 Json(json!({ 139 "error": "RateLimitExceeded", 140 "message": "Too many requests. Please try again later." 141 })), 142 ).into_response(); 143 } 144 let token = match crate::auth::extract_bearer_token_from_header( 145 headers.get("Authorization").and_then(|h| h.to_str().ok()) 146 ) { 147 Some(t) => t, 148 None => { 149 return ( 150 StatusCode::UNAUTHORIZED, 151 Json(json!({"error": "AuthenticationRequired"})), 152 ) 153 .into_response(); 154 } 155 }; 156 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 157 let did = match auth_result { 158 Ok(user) => user.did, 159 Err(e) => return ApiError::from(e).into_response(), 160 }; 161 let user = match sqlx::query!( 162 "SELECT id, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1", 163 did 164 ) 165 .fetch_optional(&state.db) 166 .await 167 { 168 Ok(Some(row)) => row, 169 _ => { 170 return ( 171 StatusCode::INTERNAL_SERVER_ERROR, 172 Json(json!({"error": "InternalError"})), 173 ) 174 .into_response(); 175 } 176 }; 177 let user_id = user.id; 178 let stored_code = user.email_confirmation_code; 179 let expires_at = user.email_confirmation_code_expires_at; 180 let email_pending_verification = user.email_pending_verification; 181 let email = input.email.trim().to_lowercase(); 182 let confirmation_code = input.token.trim(); 183 let (pending_email, saved_code, expiry) = match (email_pending_verification, stored_code, expires_at) { 184 (Some(p), Some(c), Some(e)) => (p, c, e), 185 _ => { 186 return ( 187 StatusCode::BAD_REQUEST, 188 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 189 ) 190 .into_response(); 191 } 192 }; 193 if pending_email != email { 194 return ( 195 StatusCode::BAD_REQUEST, 196 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 197 ) 198 .into_response(); 199 } 200 if saved_code != confirmation_code { 201 return ( 202 StatusCode::BAD_REQUEST, 203 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 204 ) 205 .into_response(); 206 } 207 if Utc::now() > expiry { 208 return ( 209 StatusCode::BAD_REQUEST, 210 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 211 ) 212 .into_response(); 213 } 214 let update = sqlx::query!( 215 "UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2", 216 pending_email, 217 user_id 218 ) 219 .execute(&state.db) 220 .await; 221 if let Err(e) = update { 222 error!("DB error finalizing email update: {:?}", e); 223 if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) { 224 return ( 225 StatusCode::BAD_REQUEST, 226 Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 227 ) 228 .into_response(); 229 } 230 return ( 231 StatusCode::INTERNAL_SERVER_ERROR, 232 Json(json!({"error": "InternalError"})), 233 ) 234 .into_response(); 235 } 236 info!("Email updated for user {}", user_id); 237 (StatusCode::OK, Json(json!({}))).into_response() 238} 239#[derive(Deserialize)] 240#[serde(rename_all = "camelCase")] 241pub struct UpdateEmailInput { 242 pub email: String, 243 #[serde(default)] 244 pub email_auth_factor: Option<bool>, 245 pub token: Option<String>, 246} 247pub async fn update_email( 248 State(state): State<AppState>, 249 headers: axum::http::HeaderMap, 250 Json(input): Json<UpdateEmailInput>, 251) -> Response { 252 let token = match crate::auth::extract_bearer_token_from_header( 253 headers.get("Authorization").and_then(|h| h.to_str().ok()) 254 ) { 255 Some(t) => t, 256 None => { 257 return ( 258 StatusCode::UNAUTHORIZED, 259 Json(json!({"error": "AuthenticationRequired"})), 260 ) 261 .into_response(); 262 } 263 }; 264 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await; 265 let did = match auth_result { 266 Ok(user) => user.did, 267 Err(e) => return ApiError::from(e).into_response(), 268 }; 269 let user = match sqlx::query!( 270 "SELECT id, email, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1", 271 did 272 ) 273 .fetch_optional(&state.db) 274 .await 275 { 276 Ok(Some(row)) => row, 277 _ => { 278 return ( 279 StatusCode::INTERNAL_SERVER_ERROR, 280 Json(json!({"error": "InternalError"})), 281 ) 282 .into_response(); 283 } 284 }; 285 let user_id = user.id; 286 let current_email = user.email; 287 let stored_code = user.email_confirmation_code; 288 let expires_at = user.email_confirmation_code_expires_at; 289 let email_pending_verification = user.email_pending_verification; 290 let new_email = input.email.trim().to_lowercase(); 291 if !crate::api::validation::is_valid_email(&new_email) { 292 return ( 293 StatusCode::BAD_REQUEST, 294 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 295 ) 296 .into_response(); 297 } 298 if let Some(ref current) = current_email { 299 if new_email == current.to_lowercase() { 300 return (StatusCode::OK, Json(json!({}))).into_response(); 301 } 302 } 303 let email_confirmed = stored_code.is_some() && email_pending_verification.is_some(); 304 if email_confirmed { 305 let confirmation_token = match &input.token { 306 Some(t) => t.trim(), 307 None => { 308 return ( 309 StatusCode::BAD_REQUEST, 310 Json(json!({"error": "TokenRequired", "message": "Token required for confirmed accounts. Call requestEmailUpdate first."})), 311 ) 312 .into_response(); 313 } 314 }; 315 let pending_email = match email_pending_verification { 316 Some(p) => p, 317 None => { 318 return ( 319 StatusCode::BAD_REQUEST, 320 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 321 ) 322 .into_response(); 323 } 324 }; 325 if pending_email.to_lowercase() != new_email { 326 return ( 327 StatusCode::BAD_REQUEST, 328 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 329 ) 330 .into_response(); 331 } 332 let saved_code = match stored_code { 333 Some(c) => c, 334 None => { 335 return ( 336 StatusCode::BAD_REQUEST, 337 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 338 ) 339 .into_response(); 340 } 341 }; 342 if saved_code != confirmation_token { 343 return ( 344 StatusCode::BAD_REQUEST, 345 Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 346 ) 347 .into_response(); 348 } 349 if let Some(exp) = expires_at { 350 if Utc::now() > exp { 351 return ( 352 StatusCode::BAD_REQUEST, 353 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 354 ) 355 .into_response(); 356 } 357 } 358 } 359 let exists = sqlx::query!( 360 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 361 new_email, 362 user_id 363 ) 364 .fetch_optional(&state.db) 365 .await; 366 if let Ok(Some(_)) = exists { 367 return ( 368 StatusCode::BAD_REQUEST, 369 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 370 ) 371 .into_response(); 372 } 373 let update = sqlx::query!( 374 r#" 375 UPDATE users 376 SET email = $1, 377 email_pending_verification = NULL, 378 email_confirmation_code = NULL, 379 email_confirmation_code_expires_at = NULL, 380 updated_at = NOW() 381 WHERE id = $2 382 "#, 383 new_email, 384 user_id 385 ) 386 .execute(&state.db) 387 .await; 388 match update { 389 Ok(_) => { 390 info!("Email updated for user {}", user_id); 391 (StatusCode::OK, Json(json!({}))).into_response() 392 } 393 Err(e) => { 394 error!("DB error finalizing email update: {:?}", e); 395 if e.as_database_error() 396 .map(|db_err| db_err.is_unique_violation()) 397 .unwrap_or(false) 398 { 399 return ( 400 StatusCode::BAD_REQUEST, 401 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})), 402 ) 403 .into_response(); 404 } 405 ( 406 StatusCode::INTERNAL_SERVER_ERROR, 407 Json(json!({"error": "InternalError"})), 408 ) 409 .into_response() 410 } 411 } 412}