this repo has no description
1use crate::api::error::ApiError; 2use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3use crate::auth::BearerAuth; 4use crate::state::{AppState, RateLimitKind}; 5use axum::{ 6 Json, 7 extract::State, 8 response::{IntoResponse, Response}, 9}; 10use serde::Deserialize; 11use serde_json::json; 12use tracing::{error, info, warn}; 13 14pub async fn request_email_update( 15 State(state): State<AppState>, 16 headers: axum::http::HeaderMap, 17 auth: BearerAuth, 18) -> Response { 19 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 20 if !state 21 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 22 .await 23 { 24 warn!(ip = %client_ip, "Email update rate limit exceeded"); 25 return ApiError::RateLimitExceeded(None).into_response(); 26 } 27 28 if let Err(e) = crate::auth::scope_check::check_account_scope( 29 auth.0.is_oauth, 30 auth.0.scope.as_deref(), 31 crate::oauth::scopes::AccountAttr::Email, 32 crate::oauth::scopes::AccountAction::Manage, 33 ) { 34 return e; 35 } 36 37 let did = auth.0.did.to_string(); 38 let user = match sqlx::query!( 39 "SELECT id, handle, email, email_verified FROM users WHERE did = $1", 40 did 41 ) 42 .fetch_optional(&state.db) 43 .await 44 { 45 Ok(Some(row)) => row, 46 Ok(None) => { 47 return ApiError::AccountNotFound.into_response(); 48 } 49 Err(e) => { 50 error!("DB error: {:?}", e); 51 return ApiError::InternalError(None).into_response(); 52 } 53 }; 54 55 let Some(current_email) = user.email else { 56 return ApiError::InvalidRequest("account does not have an email address".into()) 57 .into_response(); 58 }; 59 60 let token_required = user.email_verified; 61 62 if token_required { 63 let code = crate::auth::verification_token::generate_channel_update_token( 64 &did, 65 "email_update", 66 &current_email.to_lowercase(), 67 ); 68 let formatted_code = crate::auth::verification_token::format_token_for_display(&code); 69 70 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 71 if let Err(e) = 72 crate::comms::enqueue_email_update_token(&state.db, user.id, &formatted_code, &hostname) 73 .await 74 { 75 warn!("Failed to enqueue email update notification: {:?}", e); 76 } 77 } 78 79 info!("Email update requested for user {}", user.id); 80 TokenRequiredResponse::response(token_required).into_response() 81} 82 83#[derive(Deserialize)] 84#[serde(rename_all = "camelCase")] 85pub struct ConfirmEmailInput { 86 pub email: String, 87 pub token: String, 88} 89 90pub async fn confirm_email( 91 State(state): State<AppState>, 92 headers: axum::http::HeaderMap, 93 auth: BearerAuth, 94 Json(input): Json<ConfirmEmailInput>, 95) -> Response { 96 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 97 if !state 98 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 99 .await 100 { 101 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 102 return ApiError::RateLimitExceeded(None).into_response(); 103 } 104 105 if let Err(e) = crate::auth::scope_check::check_account_scope( 106 auth.0.is_oauth, 107 auth.0.scope.as_deref(), 108 crate::oauth::scopes::AccountAttr::Email, 109 crate::oauth::scopes::AccountAction::Manage, 110 ) { 111 return e; 112 } 113 114 let did = auth.0.did.to_string(); 115 let user = match sqlx::query!( 116 "SELECT id, email, email_verified FROM users WHERE did = $1", 117 did 118 ) 119 .fetch_optional(&state.db) 120 .await 121 { 122 Ok(Some(row)) => row, 123 Ok(None) => { 124 return ApiError::AccountNotFound.into_response(); 125 } 126 Err(e) => { 127 error!("DB error: {:?}", e); 128 return ApiError::InternalError(None).into_response(); 129 } 130 }; 131 132 let Some(ref email) = user.email else { 133 return ApiError::InvalidEmail.into_response(); 134 }; 135 let current_email = email.to_lowercase(); 136 137 let provided_email = input.email.trim().to_lowercase(); 138 if provided_email != current_email { 139 return ApiError::InvalidEmail.into_response(); 140 } 141 142 if user.email_verified { 143 return EmptyResponse::ok().into_response(); 144 } 145 146 let confirmation_code = 147 crate::auth::verification_token::normalize_token_input(input.token.trim()); 148 149 let verified = crate::auth::verification_token::verify_signup_token( 150 &confirmation_code, 151 "email", 152 &provided_email, 153 ); 154 155 match verified { 156 Ok(token_data) => { 157 if token_data.did != did { 158 return ApiError::InvalidToken(None).into_response(); 159 } 160 } 161 Err(crate::auth::verification_token::VerifyError::Expired) => { 162 return ApiError::ExpiredToken(None).into_response(); 163 } 164 Err(_) => { 165 return ApiError::InvalidToken(None).into_response(); 166 } 167 } 168 169 let update = sqlx::query!( 170 "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1", 171 user.id 172 ) 173 .execute(&state.db) 174 .await; 175 176 if let Err(e) = update { 177 error!("DB error confirming email: {:?}", e); 178 return ApiError::InternalError(None).into_response(); 179 } 180 181 info!("Email confirmed for user {}", user.id); 182 EmptyResponse::ok().into_response() 183} 184 185#[derive(Deserialize)] 186#[serde(rename_all = "camelCase")] 187pub struct UpdateEmailInput { 188 pub email: String, 189 #[serde(default)] 190 pub email_auth_factor: Option<bool>, 191 pub token: Option<String>, 192} 193 194pub async fn update_email( 195 State(state): State<AppState>, 196 headers: axum::http::HeaderMap, 197 Json(input): Json<UpdateEmailInput>, 198) -> Response { 199 let Some(bearer_token) = crate::auth::extract_bearer_token_from_header( 200 headers.get("Authorization").and_then(|h| h.to_str().ok()), 201 ) else { 202 return ApiError::AuthenticationRequired.into_response(); 203 }; 204 205 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await; 206 let auth_user = match auth_result { 207 Ok(user) => user, 208 Err(e) => return ApiError::from(e).into_response(), 209 }; 210 211 if let Err(e) = crate::auth::scope_check::check_account_scope( 212 auth_user.is_oauth, 213 auth_user.scope.as_deref(), 214 crate::oauth::scopes::AccountAttr::Email, 215 crate::oauth::scopes::AccountAction::Manage, 216 ) { 217 return e; 218 } 219 220 let did = auth_user.did.to_string(); 221 let user = match sqlx::query!( 222 "SELECT id, email, email_verified FROM users WHERE did = $1", 223 did 224 ) 225 .fetch_optional(&state.db) 226 .await 227 { 228 Ok(Some(row)) => row, 229 Ok(None) => { 230 return ApiError::AccountNotFound.into_response(); 231 } 232 Err(e) => { 233 error!("DB error: {:?}", e); 234 return ApiError::InternalError(None).into_response(); 235 } 236 }; 237 238 let user_id = user.id; 239 let current_email = user.email.clone(); 240 let email_verified = user.email_verified; 241 let new_email = input.email.trim().to_lowercase(); 242 243 if !crate::api::validation::is_valid_email(&new_email) { 244 return ApiError::InvalidRequest( 245 "This email address is not supported, please use a different email.".into(), 246 ) 247 .into_response(); 248 } 249 250 if let Some(ref current) = current_email 251 && new_email == current.to_lowercase() 252 { 253 return EmptyResponse::ok().into_response(); 254 } 255 256 if email_verified { 257 let Some(ref t) = input.token else { 258 return ApiError::TokenRequired.into_response(); 259 }; 260 let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim()); 261 262 let current_email_lower = current_email 263 .as_ref() 264 .map(|e| e.to_lowercase()) 265 .unwrap_or_default(); 266 267 let verified = crate::auth::verification_token::verify_channel_update_token( 268 &confirmation_token, 269 "email_update", 270 &current_email_lower, 271 ); 272 273 match verified { 274 Ok(token_data) => { 275 if token_data.did != did { 276 return ApiError::InvalidToken(None).into_response(); 277 } 278 } 279 Err(crate::auth::verification_token::VerifyError::Expired) => { 280 return ApiError::ExpiredToken(None).into_response(); 281 } 282 Err(_) => { 283 return ApiError::InvalidToken(None).into_response(); 284 } 285 } 286 } 287 288 let exists = sqlx::query!( 289 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 290 new_email, 291 user_id 292 ) 293 .fetch_optional(&state.db) 294 .await; 295 296 if let Ok(Some(_)) = exists { 297 return ApiError::InvalidRequest("Email is already in use".into()).into_response(); 298 } 299 300 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!( 301 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2", 302 new_email, 303 user_id 304 ) 305 .execute(&state.db) 306 .await; 307 308 if let Err(e) = update { 309 error!("DB error updating email: {:?}", e); 310 if e.as_database_error() 311 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation()) 312 .unwrap_or(false) 313 { 314 return ApiError::EmailTaken.into_response(); 315 } 316 return ApiError::InternalError(None).into_response(); 317 } 318 319 let verification_token = 320 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email); 321 let formatted_token = 322 crate::auth::verification_token::format_token_for_display(&verification_token); 323 if let Err(e) = crate::comms::enqueue_signup_verification( 324 &state.db, 325 user_id, 326 "email", 327 &new_email, 328 &formatted_token, 329 None, 330 ) 331 .await 332 { 333 warn!("Failed to send verification email to new address: {:?}", e); 334 } 335 336 match sqlx::query!( 337 "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", 338 user_id, 339 json!(input.email_auth_factor.unwrap_or(false)) 340 ) 341 .execute(&state.db) 342 .await 343 { 344 Ok(_) => {} 345 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e), 346 } 347 348 info!("Email updated for user {}", user_id); 349 EmptyResponse::ok().into_response() 350} 351 352#[derive(Deserialize)] 353pub struct CheckEmailVerifiedInput { 354 pub identifier: String, 355} 356 357pub async fn check_email_verified( 358 State(state): State<AppState>, 359 headers: axum::http::HeaderMap, 360 Json(input): Json<CheckEmailVerifiedInput>, 361) -> Response { 362 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 363 if !state 364 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 365 .await 366 { 367 return ApiError::RateLimitExceeded(None).into_response(); 368 } 369 370 let user = sqlx::query!( 371 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1", 372 input.identifier 373 ) 374 .fetch_optional(&state.db) 375 .await; 376 377 match user { 378 Ok(Some(row)) => VerifiedResponse::response(row.email_verified).into_response(), 379 Ok(None) => ApiError::AccountNotFound.into_response(), 380 Err(e) => { 381 error!("DB error checking email verified: {:?}", e); 382 ApiError::InternalError(None).into_response() 383 } 384 } 385}