use crate::state::AppState; use axum::{ Json, extract::State, http::StatusCode, response::{IntoResponse, Response}, }; use bcrypt::{hash, DEFAULT_COST}; use chrono::{Duration, Utc}; use rand::Rng; use serde::Deserialize; use serde_json::json; use tracing::{error, info, warn}; fn generate_reset_code() -> String { let mut rng = rand::thread_rng(); let chars: Vec = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); format!("{}-{}", part1, part2) } #[derive(Deserialize)] pub struct RequestPasswordResetInput { pub email: String, } pub async fn request_password_reset( State(state): State, Json(input): Json, ) -> Response { let email = input.email.trim().to_lowercase(); if email.is_empty() { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRequest", "message": "email is required"})), ) .into_response(); } let user = sqlx::query!( "SELECT id, handle FROM users WHERE LOWER(email) = $1", email ) .fetch_optional(&state.db) .await; let (user_id, handle) = match user { Ok(Some(row)) => (row.id, row.handle), Ok(None) => { info!("Password reset requested for unknown email: {}", email); return (StatusCode::OK, Json(json!({}))).into_response(); } Err(e) => { error!("DB error in request_password_reset: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } }; let code = generate_reset_code(); let expires_at = Utc::now() + Duration::minutes(10); let update = sqlx::query!( "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", code, expires_at, user_id ) .execute(&state.db) .await; if let Err(e) = update { error!("DB error setting reset code: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); if let Err(e) = crate::notifications::enqueue_password_reset( &state.db, user_id, &email, &handle, &code, &hostname, ) .await { warn!("Failed to enqueue password reset notification: {:?}", e); } info!("Password reset requested for user {}", user_id); (StatusCode::OK, Json(json!({}))).into_response() } #[derive(Deserialize)] pub struct ResetPasswordInput { pub token: String, pub password: String, } pub async fn reset_password( State(state): State, Json(input): Json, ) -> Response { let token = input.token.trim(); let password = &input.password; if token.is_empty() { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidToken", "message": "token is required"})), ) .into_response(); } if password.is_empty() { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRequest", "message": "password is required"})), ) .into_response(); } let user = sqlx::query!( "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", token ) .fetch_optional(&state.db) .await; let (user_id, expires_at) = match user { Ok(Some(row)) => { let expires = row.password_reset_code_expires_at; (row.id, expires) } Ok(None) => { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), ) .into_response(); } Err(e) => { error!("DB error in reset_password: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } }; if let Some(exp) = expires_at { if Utc::now() > exp { let _ = sqlx::query!( "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", user_id ) .execute(&state.db) .await; return ( StatusCode::BAD_REQUEST, Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), ) .into_response(); } } else { return ( StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), ) .into_response(); } let password_hash = match hash(password, DEFAULT_COST) { Ok(h) => h, Err(e) => { error!("Failed to hash password: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } }; let update = sqlx::query!( "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", password_hash, user_id ) .execute(&state.db) .await; if let Err(e) = update { error!("DB error updating password: {:?}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"})), ) .into_response(); } let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id) .execute(&state.db) .await; info!("Password reset completed for user {}", user_id); (StatusCode::OK, Json(json!({}))).into_response() }