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