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