this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::State, 5 http::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 18#[derive(Deserialize)] 19pub struct RequestPasswordResetInput { 20 pub email: String, 21} 22 23pub async fn request_password_reset( 24 State(state): State<AppState>, 25 Json(input): Json<RequestPasswordResetInput>, 26) -> Response { 27 let email = input.email.trim().to_lowercase(); 28 if email.is_empty() { 29 return ( 30 StatusCode::BAD_REQUEST, 31 Json(json!({"error": "InvalidRequest", "message": "email is required"})), 32 ) 33 .into_response(); 34 } 35 36 let user = sqlx::query!("SELECT id FROM users WHERE LOWER(email) = $1", email) 37 .fetch_optional(&state.db) 38 .await; 39 40 let user_id = match user { 41 Ok(Some(row)) => row.id, 42 Ok(None) => { 43 info!("Password reset requested for unknown email"); 44 return (StatusCode::OK, Json(json!({}))).into_response(); 45 } 46 Err(e) => { 47 error!("DB error in request_password_reset: {:?}", e); 48 return ( 49 StatusCode::INTERNAL_SERVER_ERROR, 50 Json(json!({"error": "InternalError"})), 51 ) 52 .into_response(); 53 } 54 }; 55 56 let code = generate_reset_code(); 57 let expires_at = Utc::now() + Duration::minutes(10); 58 59 let update = sqlx::query!( 60 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 61 code, 62 expires_at, 63 user_id 64 ) 65 .execute(&state.db) 66 .await; 67 68 if let Err(e) = update { 69 error!("DB error setting reset code: {:?}", e); 70 return ( 71 StatusCode::INTERNAL_SERVER_ERROR, 72 Json(json!({"error": "InternalError"})), 73 ) 74 .into_response(); 75 } 76 77 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 78 if let Err(e) = 79 crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await 80 { 81 warn!("Failed to enqueue password reset notification: {:?}", e); 82 } 83 84 info!("Password reset requested for user {}", user_id); 85 86 (StatusCode::OK, Json(json!({}))).into_response() 87} 88 89#[derive(Deserialize)] 90pub struct ResetPasswordInput { 91 pub token: String, 92 pub password: String, 93} 94 95pub async fn reset_password( 96 State(state): State<AppState>, 97 Json(input): Json<ResetPasswordInput>, 98) -> Response { 99 let token = input.token.trim(); 100 let password = &input.password; 101 102 if token.is_empty() { 103 return ( 104 StatusCode::BAD_REQUEST, 105 Json(json!({"error": "InvalidToken", "message": "token is required"})), 106 ) 107 .into_response(); 108 } 109 110 if password.is_empty() { 111 return ( 112 StatusCode::BAD_REQUEST, 113 Json(json!({"error": "InvalidRequest", "message": "password is required"})), 114 ) 115 .into_response(); 116 } 117 118 let user = sqlx::query!( 119 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 120 token 121 ) 122 .fetch_optional(&state.db) 123 .await; 124 125 let (user_id, expires_at) = match user { 126 Ok(Some(row)) => { 127 let expires = row.password_reset_code_expires_at; 128 (row.id, expires) 129 } 130 Ok(None) => { 131 return ( 132 StatusCode::BAD_REQUEST, 133 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 134 ) 135 .into_response(); 136 } 137 Err(e) => { 138 error!("DB error in reset_password: {:?}", e); 139 return ( 140 StatusCode::INTERNAL_SERVER_ERROR, 141 Json(json!({"error": "InternalError"})), 142 ) 143 .into_response(); 144 } 145 }; 146 147 if let Some(exp) = expires_at { 148 if Utc::now() > exp { 149 if let Err(e) = sqlx::query!( 150 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 151 user_id 152 ) 153 .execute(&state.db) 154 .await 155 { 156 error!("Failed to clear expired reset code: {:?}", e); 157 } 158 159 return ( 160 StatusCode::BAD_REQUEST, 161 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 162 ) 163 .into_response(); 164 } 165 } else { 166 return ( 167 StatusCode::BAD_REQUEST, 168 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 169 ) 170 .into_response(); 171 } 172 173 let password_hash = match hash(password, DEFAULT_COST) { 174 Ok(h) => h, 175 Err(e) => { 176 error!("Failed to hash password: {:?}", e); 177 return ( 178 StatusCode::INTERNAL_SERVER_ERROR, 179 Json(json!({"error": "InternalError"})), 180 ) 181 .into_response(); 182 } 183 }; 184 185 let mut tx = match state.db.begin().await { 186 Ok(tx) => tx, 187 Err(e) => { 188 error!("Failed to begin transaction: {:?}", e); 189 return ( 190 StatusCode::INTERNAL_SERVER_ERROR, 191 Json(json!({"error": "InternalError"})), 192 ) 193 .into_response(); 194 } 195 }; 196 197 if let Err(e) = sqlx::query!( 198 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", 199 password_hash, 200 user_id 201 ) 202 .execute(&mut *tx) 203 .await 204 { 205 error!("DB error updating password: {:?}", e); 206 return ( 207 StatusCode::INTERNAL_SERVER_ERROR, 208 Json(json!({"error": "InternalError"})), 209 ) 210 .into_response(); 211 } 212 213 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id) 214 .execute(&mut *tx) 215 .await 216 { 217 error!("Failed to invalidate sessions after password reset: {:?}", e); 218 return ( 219 StatusCode::INTERNAL_SERVER_ERROR, 220 Json(json!({"error": "InternalError"})), 221 ) 222 .into_response(); 223 } 224 225 if let Err(e) = tx.commit().await { 226 error!("Failed to commit password reset transaction: {:?}", e); 227 return ( 228 StatusCode::INTERNAL_SERVER_ERROR, 229 Json(json!({"error": "InternalError"})), 230 ) 231 .into_response(); 232 } 233 234 info!("Password reset completed for user {}", user_id); 235 236 (StatusCode::OK, Json(json!({}))).into_response() 237}