this repo has no description
1use crate::state::{AppState, RateLimitKind}; 2use axum::{ 3 Json, 4 extract::State, 5 http::{HeaderMap, StatusCode}, 6 response::{IntoResponse, Response}, 7}; 8use bcrypt::{hash, DEFAULT_COST}; 9use chrono::{Duration, Utc}; 10use serde::Deserialize; 11use serde_json::json; 12use tracing::{error, info, warn}; 13 14fn generate_reset_code() -> String { 15 crate::util::generate_token_code() 16} 17 18fn extract_client_ip(headers: &HeaderMap) -> String { 19 if let Some(forwarded) = headers.get("x-forwarded-for") { 20 if let Ok(value) = forwarded.to_str() { 21 if let Some(first_ip) = value.split(',').next() { 22 return first_ip.trim().to_string(); 23 } 24 } 25 } 26 if let Some(real_ip) = headers.get("x-real-ip") { 27 if let Ok(value) = real_ip.to_str() { 28 return value.trim().to_string(); 29 } 30 } 31 "unknown".to_string() 32} 33 34#[derive(Deserialize)] 35pub struct RequestPasswordResetInput { 36 pub email: String, 37} 38 39pub async fn request_password_reset( 40 State(state): State<AppState>, 41 headers: HeaderMap, 42 Json(input): Json<RequestPasswordResetInput>, 43) -> Response { 44 let client_ip = extract_client_ip(&headers); 45 if !state.check_rate_limit(RateLimitKind::PasswordReset, &client_ip).await { 46 warn!(ip = %client_ip, "Password reset rate limit exceeded"); 47 return ( 48 StatusCode::TOO_MANY_REQUESTS, 49 Json(json!({ 50 "error": "RateLimitExceeded", 51 "message": "Too many password reset requests. Please try again later." 52 })), 53 ) 54 .into_response(); 55 } 56 57 let email = input.email.trim().to_lowercase(); 58 if email.is_empty() { 59 return ( 60 StatusCode::BAD_REQUEST, 61 Json(json!({"error": "InvalidRequest", "message": "email is required"})), 62 ) 63 .into_response(); 64 } 65 66 let user = sqlx::query!("SELECT id FROM users WHERE LOWER(email) = $1", email) 67 .fetch_optional(&state.db) 68 .await; 69 70 let user_id = match user { 71 Ok(Some(row)) => row.id, 72 Ok(None) => { 73 info!("Password reset requested for unknown email"); 74 return (StatusCode::OK, Json(json!({}))).into_response(); 75 } 76 Err(e) => { 77 error!("DB error in request_password_reset: {:?}", e); 78 return ( 79 StatusCode::INTERNAL_SERVER_ERROR, 80 Json(json!({"error": "InternalError"})), 81 ) 82 .into_response(); 83 } 84 }; 85 86 let code = generate_reset_code(); 87 let expires_at = Utc::now() + Duration::minutes(10); 88 89 let update = sqlx::query!( 90 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 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 reset 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) = 109 crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await 110 { 111 warn!("Failed to enqueue password reset notification: {:?}", e); 112 } 113 114 info!("Password reset requested for user {}", user_id); 115 116 (StatusCode::OK, Json(json!({}))).into_response() 117} 118 119#[derive(Deserialize)] 120pub struct ResetPasswordInput { 121 pub token: String, 122 pub password: String, 123} 124 125pub async fn reset_password( 126 State(state): State<AppState>, 127 headers: HeaderMap, 128 Json(input): Json<ResetPasswordInput>, 129) -> Response { 130 let client_ip = extract_client_ip(&headers); 131 if !state.check_rate_limit(RateLimitKind::ResetPassword, &client_ip).await { 132 warn!(ip = %client_ip, "Reset password rate limit exceeded"); 133 return ( 134 StatusCode::TOO_MANY_REQUESTS, 135 Json(json!({ 136 "error": "RateLimitExceeded", 137 "message": "Too many requests. Please try again later." 138 })), 139 ).into_response(); 140 } 141 142 let token = input.token.trim(); 143 let password = &input.password; 144 145 if token.is_empty() { 146 return ( 147 StatusCode::BAD_REQUEST, 148 Json(json!({"error": "InvalidToken", "message": "token is required"})), 149 ) 150 .into_response(); 151 } 152 153 if password.is_empty() { 154 return ( 155 StatusCode::BAD_REQUEST, 156 Json(json!({"error": "InvalidRequest", "message": "password is required"})), 157 ) 158 .into_response(); 159 } 160 161 let user = sqlx::query!( 162 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 163 token 164 ) 165 .fetch_optional(&state.db) 166 .await; 167 168 let (user_id, expires_at) = match user { 169 Ok(Some(row)) => { 170 let expires = row.password_reset_code_expires_at; 171 (row.id, expires) 172 } 173 Ok(None) => { 174 return ( 175 StatusCode::BAD_REQUEST, 176 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 177 ) 178 .into_response(); 179 } 180 Err(e) => { 181 error!("DB error in reset_password: {:?}", e); 182 return ( 183 StatusCode::INTERNAL_SERVER_ERROR, 184 Json(json!({"error": "InternalError"})), 185 ) 186 .into_response(); 187 } 188 }; 189 190 if let Some(exp) = expires_at { 191 if Utc::now() > exp { 192 if let Err(e) = sqlx::query!( 193 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 194 user_id 195 ) 196 .execute(&state.db) 197 .await 198 { 199 error!("Failed to clear expired reset code: {:?}", e); 200 } 201 202 return ( 203 StatusCode::BAD_REQUEST, 204 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 205 ) 206 .into_response(); 207 } 208 } else { 209 return ( 210 StatusCode::BAD_REQUEST, 211 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 212 ) 213 .into_response(); 214 } 215 216 let password_hash = match hash(password, DEFAULT_COST) { 217 Ok(h) => h, 218 Err(e) => { 219 error!("Failed to hash password: {:?}", e); 220 return ( 221 StatusCode::INTERNAL_SERVER_ERROR, 222 Json(json!({"error": "InternalError"})), 223 ) 224 .into_response(); 225 } 226 }; 227 228 let mut tx = match state.db.begin().await { 229 Ok(tx) => tx, 230 Err(e) => { 231 error!("Failed to begin transaction: {:?}", e); 232 return ( 233 StatusCode::INTERNAL_SERVER_ERROR, 234 Json(json!({"error": "InternalError"})), 235 ) 236 .into_response(); 237 } 238 }; 239 240 if let Err(e) = sqlx::query!( 241 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", 242 password_hash, 243 user_id 244 ) 245 .execute(&mut *tx) 246 .await 247 { 248 error!("DB error updating password: {:?}", e); 249 return ( 250 StatusCode::INTERNAL_SERVER_ERROR, 251 Json(json!({"error": "InternalError"})), 252 ) 253 .into_response(); 254 } 255 256 let user_did = match sqlx::query_scalar!( 257 "SELECT did FROM users WHERE id = $1", 258 user_id 259 ) 260 .fetch_one(&mut *tx) 261 .await 262 { 263 Ok(did) => did, 264 Err(e) => { 265 error!("Failed to get DID for user {}: {:?}", user_id, e); 266 return ( 267 StatusCode::INTERNAL_SERVER_ERROR, 268 Json(json!({"error": "InternalError"})), 269 ) 270 .into_response(); 271 } 272 }; 273 274 let session_jtis: Vec<String> = match sqlx::query_scalar!( 275 "SELECT access_jti FROM session_tokens WHERE did = $1", 276 user_did 277 ) 278 .fetch_all(&mut *tx) 279 .await 280 { 281 Ok(jtis) => jtis, 282 Err(e) => { 283 error!("Failed to fetch session JTIs: {:?}", e); 284 vec![] 285 } 286 }; 287 288 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did) 289 .execute(&mut *tx) 290 .await 291 { 292 error!("Failed to invalidate sessions after password reset: {:?}", e); 293 return ( 294 StatusCode::INTERNAL_SERVER_ERROR, 295 Json(json!({"error": "InternalError"})), 296 ) 297 .into_response(); 298 } 299 300 if let Err(e) = tx.commit().await { 301 error!("Failed to commit password reset transaction: {:?}", e); 302 return ( 303 StatusCode::INTERNAL_SERVER_ERROR, 304 Json(json!({"error": "InternalError"})), 305 ) 306 .into_response(); 307 } 308 309 for jti in session_jtis { 310 let cache_key = format!("auth:session:{}:{}", user_did, jti); 311 if let Err(e) = state.cache.delete(&cache_key).await { 312 warn!("Failed to invalidate session cache for {}: {:?}", cache_key, e); 313 } 314 } 315 316 info!("Password reset completed for user {}", user_id); 317 318 (StatusCode::OK, Json(json!({}))).into_response() 319}