PDS software with bells & whistles you didn’t even know you needed. will move this to its own account when ready.
at main 11 kB view raw
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 auth: BearerAuth, 197 Json(input): Json<UpdateEmailInput>, 198) -> Response { 199 let auth_user = auth.0; 200 201 if let Err(e) = crate::auth::scope_check::check_account_scope( 202 auth_user.is_oauth, 203 auth_user.scope.as_deref(), 204 crate::oauth::scopes::AccountAttr::Email, 205 crate::oauth::scopes::AccountAction::Manage, 206 ) { 207 return e; 208 } 209 210 let did = auth_user.did.to_string(); 211 let user = match sqlx::query!( 212 "SELECT id, email, email_verified FROM users WHERE did = $1", 213 did 214 ) 215 .fetch_optional(&state.db) 216 .await 217 { 218 Ok(Some(row)) => row, 219 Ok(None) => { 220 return ApiError::AccountNotFound.into_response(); 221 } 222 Err(e) => { 223 error!("DB error: {:?}", e); 224 return ApiError::InternalError(None).into_response(); 225 } 226 }; 227 228 let user_id = user.id; 229 let current_email = user.email.clone(); 230 let email_verified = user.email_verified; 231 let new_email = input.email.trim().to_lowercase(); 232 233 if !crate::api::validation::is_valid_email(&new_email) { 234 return ApiError::InvalidRequest( 235 "This email address is not supported, please use a different email.".into(), 236 ) 237 .into_response(); 238 } 239 240 if let Some(ref current) = current_email 241 && new_email == current.to_lowercase() 242 { 243 return EmptyResponse::ok().into_response(); 244 } 245 246 if email_verified { 247 let Some(ref t) = input.token else { 248 return ApiError::TokenRequired.into_response(); 249 }; 250 let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim()); 251 252 let current_email_lower = current_email 253 .as_ref() 254 .map(|e| e.to_lowercase()) 255 .unwrap_or_default(); 256 257 let verified = crate::auth::verification_token::verify_channel_update_token( 258 &confirmation_token, 259 "email_update", 260 &current_email_lower, 261 ); 262 263 match verified { 264 Ok(token_data) => { 265 if token_data.did != did { 266 return ApiError::InvalidToken(None).into_response(); 267 } 268 } 269 Err(crate::auth::verification_token::VerifyError::Expired) => { 270 return ApiError::ExpiredToken(None).into_response(); 271 } 272 Err(_) => { 273 return ApiError::InvalidToken(None).into_response(); 274 } 275 } 276 } 277 278 let exists = sqlx::query!( 279 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2", 280 new_email, 281 user_id 282 ) 283 .fetch_optional(&state.db) 284 .await; 285 286 if let Ok(Some(_)) = exists { 287 return ApiError::InvalidRequest("Email is already in use".into()).into_response(); 288 } 289 290 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!( 291 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2", 292 new_email, 293 user_id 294 ) 295 .execute(&state.db) 296 .await; 297 298 if let Err(e) = update { 299 error!("DB error updating email: {:?}", e); 300 if e.as_database_error() 301 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation()) 302 .unwrap_or(false) 303 { 304 return ApiError::EmailTaken.into_response(); 305 } 306 return ApiError::InternalError(None).into_response(); 307 } 308 309 let verification_token = 310 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email); 311 let formatted_token = 312 crate::auth::verification_token::format_token_for_display(&verification_token); 313 if let Err(e) = crate::comms::enqueue_signup_verification( 314 &state.db, 315 user_id, 316 "email", 317 &new_email, 318 &formatted_token, 319 None, 320 ) 321 .await 322 { 323 warn!("Failed to send verification email to new address: {:?}", e); 324 } 325 326 match sqlx::query!( 327 "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", 328 user_id, 329 json!(input.email_auth_factor.unwrap_or(false)) 330 ) 331 .execute(&state.db) 332 .await 333 { 334 Ok(_) => {} 335 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e), 336 } 337 338 info!("Email updated for user {}", user_id); 339 EmptyResponse::ok().into_response() 340} 341 342#[derive(Deserialize)] 343pub struct CheckEmailVerifiedInput { 344 pub identifier: String, 345} 346 347pub async fn check_email_verified( 348 State(state): State<AppState>, 349 headers: axum::http::HeaderMap, 350 Json(input): Json<CheckEmailVerifiedInput>, 351) -> Response { 352 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 353 if !state 354 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 355 .await 356 { 357 return ApiError::RateLimitExceeded(None).into_response(); 358 } 359 360 let user = sqlx::query!( 361 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1", 362 input.identifier 363 ) 364 .fetch_optional(&state.db) 365 .await; 366 367 match user { 368 Ok(Some(row)) => VerifiedResponse::response(row.email_verified).into_response(), 369 Ok(None) => ApiError::AccountNotFound.into_response(), 370 Err(e) => { 371 error!("DB error checking email verified: {:?}", e); 372 ApiError::InternalError(None).into_response() 373 } 374 } 375}