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