this repo has no description
1use crate::state::AppState; 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.rate_limiters.password_reset.check_key(&client_ip).is_err() { 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.distributed_rate_limiter.check_rate_limit( 132 &format!("reset_password:{}", client_ip), 133 10, 134 60_000, 135 ).await { 136 if state.rate_limiters.reset_password.check_key(&client_ip).is_err() { 137 warn!(ip = %client_ip, "Reset password rate limit exceeded"); 138 return ( 139 StatusCode::TOO_MANY_REQUESTS, 140 Json(json!({ 141 "error": "RateLimitExceeded", 142 "message": "Too many requests. Please try again later." 143 })), 144 ).into_response(); 145 } 146 } 147 148 let token = input.token.trim(); 149 let password = &input.password; 150 151 if token.is_empty() { 152 return ( 153 StatusCode::BAD_REQUEST, 154 Json(json!({"error": "InvalidToken", "message": "token is required"})), 155 ) 156 .into_response(); 157 } 158 159 if password.is_empty() { 160 return ( 161 StatusCode::BAD_REQUEST, 162 Json(json!({"error": "InvalidRequest", "message": "password is required"})), 163 ) 164 .into_response(); 165 } 166 167 let user = sqlx::query!( 168 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 169 token 170 ) 171 .fetch_optional(&state.db) 172 .await; 173 174 let (user_id, expires_at) = match user { 175 Ok(Some(row)) => { 176 let expires = row.password_reset_code_expires_at; 177 (row.id, expires) 178 } 179 Ok(None) => { 180 return ( 181 StatusCode::BAD_REQUEST, 182 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 183 ) 184 .into_response(); 185 } 186 Err(e) => { 187 error!("DB error in reset_password: {:?}", e); 188 return ( 189 StatusCode::INTERNAL_SERVER_ERROR, 190 Json(json!({"error": "InternalError"})), 191 ) 192 .into_response(); 193 } 194 }; 195 196 if let Some(exp) = expires_at { 197 if Utc::now() > exp { 198 if let Err(e) = sqlx::query!( 199 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 200 user_id 201 ) 202 .execute(&state.db) 203 .await 204 { 205 error!("Failed to clear expired reset code: {:?}", e); 206 } 207 208 return ( 209 StatusCode::BAD_REQUEST, 210 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 211 ) 212 .into_response(); 213 } 214 } else { 215 return ( 216 StatusCode::BAD_REQUEST, 217 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 218 ) 219 .into_response(); 220 } 221 222 let password_hash = match hash(password, DEFAULT_COST) { 223 Ok(h) => h, 224 Err(e) => { 225 error!("Failed to hash password: {:?}", e); 226 return ( 227 StatusCode::INTERNAL_SERVER_ERROR, 228 Json(json!({"error": "InternalError"})), 229 ) 230 .into_response(); 231 } 232 }; 233 234 let mut tx = match state.db.begin().await { 235 Ok(tx) => tx, 236 Err(e) => { 237 error!("Failed to begin transaction: {:?}", e); 238 return ( 239 StatusCode::INTERNAL_SERVER_ERROR, 240 Json(json!({"error": "InternalError"})), 241 ) 242 .into_response(); 243 } 244 }; 245 246 if let Err(e) = sqlx::query!( 247 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", 248 password_hash, 249 user_id 250 ) 251 .execute(&mut *tx) 252 .await 253 { 254 error!("DB error updating password: {:?}", e); 255 return ( 256 StatusCode::INTERNAL_SERVER_ERROR, 257 Json(json!({"error": "InternalError"})), 258 ) 259 .into_response(); 260 } 261 262 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id) 263 .execute(&mut *tx) 264 .await 265 { 266 error!("Failed to invalidate sessions after password reset: {:?}", e); 267 return ( 268 StatusCode::INTERNAL_SERVER_ERROR, 269 Json(json!({"error": "InternalError"})), 270 ) 271 .into_response(); 272 } 273 274 if let Err(e) = tx.commit().await { 275 error!("Failed to commit password reset transaction: {:?}", e); 276 return ( 277 StatusCode::INTERNAL_SERVER_ERROR, 278 Json(json!({"error": "InternalError"})), 279 ) 280 .into_response(); 281 } 282 283 info!("Password reset completed for user {}", user_id); 284 285 (StatusCode::OK, Json(json!({}))).into_response() 286}