this repo has no description
1use crate::api::error::ApiError; 2use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3use crate::auth::BearerAuth; 4use crate::state::{AppState, RateLimitKind}; 5use crate::types::PlainPassword; 6use crate::validation::validate_password; 7use axum::{ 8 Json, 9 extract::State, 10 http::HeaderMap, 11 response::{IntoResponse, Response}, 12}; 13use bcrypt::{DEFAULT_COST, hash, verify}; 14use chrono::{Duration, Utc}; 15use serde::Deserialize; 16use tracing::{error, info, warn}; 17use uuid::Uuid; 18 19fn generate_reset_code() -> String { 20 crate::util::generate_token_code() 21} 22fn extract_client_ip(headers: &HeaderMap) -> String { 23 if let Some(forwarded) = headers.get("x-forwarded-for") 24 && let Ok(value) = forwarded.to_str() 25 && let Some(first_ip) = value.split(',').next() 26 { 27 return first_ip.trim().to_string(); 28 } 29 if let Some(real_ip) = headers.get("x-real-ip") 30 && let Ok(value) = real_ip.to_str() 31 { 32 return value.trim().to_string(); 33 } 34 "unknown".to_string() 35} 36 37#[derive(Deserialize)] 38pub struct RequestPasswordResetInput { 39 #[serde(alias = "identifier")] 40 pub email: String, 41} 42 43pub async fn request_password_reset( 44 State(state): State<AppState>, 45 headers: HeaderMap, 46 Json(input): Json<RequestPasswordResetInput>, 47) -> Response { 48 let client_ip = extract_client_ip(&headers); 49 if !state 50 .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 51 .await 52 { 53 warn!(ip = %client_ip, "Password reset rate limit exceeded"); 54 return ApiError::RateLimitExceeded(None).into_response(); 55 } 56 let identifier = input.email.trim(); 57 if identifier.is_empty() { 58 return ApiError::InvalidRequest("email or handle is required".into()).into_response(); 59 } 60 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 61 let normalized = identifier.to_lowercase(); 62 let normalized = normalized.strip_prefix('@').unwrap_or(&normalized); 63 let normalized_handle = if normalized.contains('@') || normalized.contains('.') { 64 normalized.to_string() 65 } else { 66 format!("{}.{}", normalized, pds_hostname) 67 }; 68 let user = sqlx::query!( 69 "SELECT id FROM users WHERE LOWER(email) = $1 OR handle = $2", 70 normalized, 71 normalized_handle 72 ) 73 .fetch_optional(&state.db) 74 .await; 75 let user_id = match user { 76 Ok(Some(row)) => row.id, 77 Ok(None) => { 78 info!("Password reset requested for unknown identifier"); 79 return EmptyResponse::ok().into_response(); 80 } 81 Err(e) => { 82 error!("DB error in request_password_reset: {:?}", e); 83 return ApiError::InternalError(None).into_response(); 84 } 85 }; 86 let code = generate_reset_code(); 87 let expires_at = Utc::now() + Duration::minutes(10); 88 let update = sqlx::query!( 89 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 90 code, 91 expires_at, 92 user_id 93 ) 94 .execute(&state.db) 95 .await; 96 if let Err(e) = update { 97 error!("DB error setting reset code: {:?}", e); 98 return ApiError::InternalError(None).into_response(); 99 } 100 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 101 if let Err(e) = crate::comms::enqueue_password_reset(&state.db, user_id, &code, &hostname).await 102 { 103 warn!("Failed to enqueue password reset notification: {:?}", e); 104 } 105 info!("Password reset requested for user {}", user_id); 106 EmptyResponse::ok().into_response() 107} 108 109#[derive(Deserialize)] 110pub struct ResetPasswordInput { 111 pub token: String, 112 pub password: PlainPassword, 113} 114 115pub async fn reset_password( 116 State(state): State<AppState>, 117 headers: HeaderMap, 118 Json(input): Json<ResetPasswordInput>, 119) -> Response { 120 let client_ip = extract_client_ip(&headers); 121 if !state 122 .check_rate_limit(RateLimitKind::ResetPassword, &client_ip) 123 .await 124 { 125 warn!(ip = %client_ip, "Reset password rate limit exceeded"); 126 return ApiError::RateLimitExceeded(None).into_response(); 127 } 128 let token = input.token.trim(); 129 let password = &input.password; 130 if token.is_empty() { 131 return ApiError::InvalidToken(None).into_response(); 132 } 133 if password.is_empty() { 134 return ApiError::InvalidRequest("password is required".into()).into_response(); 135 } 136 if let Err(e) = validate_password(password) { 137 return ApiError::InvalidRequest(e.to_string()).into_response(); 138 } 139 let user = sqlx::query!( 140 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 141 token 142 ) 143 .fetch_optional(&state.db) 144 .await; 145 let (user_id, expires_at) = match user { 146 Ok(Some(row)) => { 147 let expires = row.password_reset_code_expires_at; 148 (row.id, expires) 149 } 150 Ok(None) => { 151 return ApiError::InvalidToken(None).into_response(); 152 } 153 Err(e) => { 154 error!("DB error in reset_password: {:?}", e); 155 return ApiError::InternalError(None).into_response(); 156 } 157 }; 158 if let Some(exp) = expires_at { 159 if Utc::now() > exp { 160 if let Err(e) = sqlx::query!( 161 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 162 user_id 163 ) 164 .execute(&state.db) 165 .await 166 { 167 error!("Failed to clear expired reset code: {:?}", e); 168 } 169 return ApiError::ExpiredToken(None).into_response(); 170 } 171 } else { 172 return ApiError::InvalidToken(None).into_response(); 173 } 174 let password_clone = password.to_string(); 175 let password_hash = 176 match tokio::task::spawn_blocking(move || hash(password_clone, DEFAULT_COST)).await { 177 Ok(Ok(h)) => h, 178 Ok(Err(e)) => { 179 error!("Failed to hash password: {:?}", e); 180 return ApiError::InternalError(None).into_response(); 181 } 182 Err(e) => { 183 error!("Failed to spawn blocking task: {:?}", e); 184 return ApiError::InternalError(None).into_response(); 185 } 186 }; 187 let mut tx = match state.db.begin().await { 188 Ok(tx) => tx, 189 Err(e) => { 190 error!("Failed to begin transaction: {:?}", e); 191 return ApiError::InternalError(None).into_response(); 192 } 193 }; 194 if let Err(e) = sqlx::query!( 195 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL, password_required = TRUE WHERE id = $2", 196 password_hash, 197 user_id 198 ) 199 .execute(&mut *tx) 200 .await 201 { 202 error!("DB error updating password: {:?}", e); 203 return ApiError::InternalError(None).into_response(); 204 } 205 let user_did = match sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", user_id) 206 .fetch_one(&mut *tx) 207 .await 208 { 209 Ok(did) => did, 210 Err(e) => { 211 error!("Failed to get DID for user {}: {:?}", user_id, e); 212 return ApiError::InternalError(None).into_response(); 213 } 214 }; 215 let session_jtis: Vec<String> = match sqlx::query_scalar!( 216 "SELECT access_jti FROM session_tokens WHERE did = $1", 217 user_did 218 ) 219 .fetch_all(&mut *tx) 220 .await 221 { 222 Ok(jtis) => jtis, 223 Err(e) => { 224 error!("Failed to fetch session JTIs: {:?}", e); 225 vec![] 226 } 227 }; 228 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did) 229 .execute(&mut *tx) 230 .await 231 { 232 error!( 233 "Failed to invalidate sessions after password reset: {:?}", 234 e 235 ); 236 return ApiError::InternalError(None).into_response(); 237 } 238 if let Err(e) = tx.commit().await { 239 error!("Failed to commit password reset transaction: {:?}", e); 240 return ApiError::InternalError(None).into_response(); 241 } 242 for jti in session_jtis { 243 let cache_key = format!("auth:session:{}:{}", user_did, jti); 244 if let Err(e) = state.cache.delete(&cache_key).await { 245 warn!( 246 "Failed to invalidate session cache for {}: {:?}", 247 cache_key, e 248 ); 249 } 250 } 251 info!("Password reset completed for user {}", user_id); 252 EmptyResponse::ok().into_response() 253} 254 255#[derive(Deserialize)] 256#[serde(rename_all = "camelCase")] 257pub struct ChangePasswordInput { 258 pub current_password: PlainPassword, 259 pub new_password: PlainPassword, 260} 261 262pub async fn change_password( 263 State(state): State<AppState>, 264 auth: BearerAuth, 265 Json(input): Json<ChangePasswordInput>, 266) -> Response { 267 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 268 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 269 .await; 270 } 271 272 let current_password = &input.current_password; 273 let new_password = &input.new_password; 274 if current_password.is_empty() { 275 return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 276 } 277 if new_password.is_empty() { 278 return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 279 } 280 if let Err(e) = validate_password(new_password) { 281 return ApiError::InvalidRequest(e.to_string()).into_response(); 282 } 283 let user = 284 sqlx::query_as::<_, (Uuid, String)>("SELECT id, password_hash FROM users WHERE did = $1") 285 .bind(&auth.0.did) 286 .fetch_optional(&state.db) 287 .await; 288 let (user_id, password_hash) = match user { 289 Ok(Some(row)) => row, 290 Ok(None) => { 291 return ApiError::AccountNotFound.into_response(); 292 } 293 Err(e) => { 294 error!("DB error in change_password: {:?}", e); 295 return ApiError::InternalError(None).into_response(); 296 } 297 }; 298 let valid = match verify(current_password, &password_hash) { 299 Ok(v) => v, 300 Err(e) => { 301 error!("Password verification error: {:?}", e); 302 return ApiError::InternalError(None).into_response(); 303 } 304 }; 305 if !valid { 306 return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 307 } 308 let new_password_clone = new_password.to_string(); 309 let new_hash = 310 match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 311 Ok(Ok(h)) => h, 312 Ok(Err(e)) => { 313 error!("Failed to hash password: {:?}", e); 314 return ApiError::InternalError(None).into_response(); 315 } 316 Err(e) => { 317 error!("Failed to spawn blocking task: {:?}", e); 318 return ApiError::InternalError(None).into_response(); 319 } 320 }; 321 if let Err(e) = sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2") 322 .bind(&new_hash) 323 .bind(user_id) 324 .execute(&state.db) 325 .await 326 { 327 error!("DB error updating password: {:?}", e); 328 return ApiError::InternalError(None).into_response(); 329 } 330 info!(did = %&auth.0.did, "Password changed successfully"); 331 EmptyResponse::ok().into_response() 332} 333 334pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 335 let user = sqlx::query!( 336 "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", 337 &auth.0.did 338 ) 339 .fetch_optional(&state.db) 340 .await; 341 342 match user { 343 Ok(Some(row)) => { 344 HasPasswordResponse::response(row.has_password.unwrap_or(false)).into_response() 345 } 346 Ok(None) => ApiError::AccountNotFound.into_response(), 347 Err(e) => { 348 error!("DB error: {:?}", e); 349 ApiError::InternalError(None).into_response() 350 } 351 } 352} 353 354pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 355 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 356 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 357 .await; 358 } 359 360 if crate::api::server::reauth::check_reauth_required_cached( 361 &state.db, 362 &state.cache, 363 &auth.0.did, 364 ) 365 .await 366 { 367 return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await; 368 } 369 370 let has_passkeys = 371 crate::api::server::passkeys::has_passkeys_for_user_db(&state.db, &auth.0.did).await; 372 if !has_passkeys { 373 return ApiError::InvalidRequest( 374 "You must have at least one passkey registered before removing your password".into(), 375 ) 376 .into_response(); 377 } 378 379 let user = sqlx::query!( 380 "SELECT id, password_hash FROM users WHERE did = $1", 381 &auth.0.did 382 ) 383 .fetch_optional(&state.db) 384 .await; 385 386 let user = match user { 387 Ok(Some(u)) => u, 388 Ok(None) => { 389 return ApiError::AccountNotFound.into_response(); 390 } 391 Err(e) => { 392 error!("DB error: {:?}", e); 393 return ApiError::InternalError(None).into_response(); 394 } 395 }; 396 397 if user.password_hash.is_none() { 398 return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 399 } 400 401 if let Err(e) = sqlx::query!( 402 "UPDATE users SET password_hash = NULL, password_required = FALSE WHERE id = $1", 403 user.id 404 ) 405 .execute(&state.db) 406 .await 407 { 408 error!("DB error removing password: {:?}", e); 409 return ApiError::InternalError(None).into_response(); 410 } 411 412 info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 413 SuccessResponse::ok().into_response() 414}