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