use argon2::{ Argon2, PasswordHasher, password_hash::{PasswordHash, PasswordVerifier, SaltString}, }; use chrono::Utc; use serde::{Deserialize, Serialize}; use sqlx::Row; use crate::error::AppError; use crate::service::AuthService; use crate::session::Session; #[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] pub struct ChangePasswordParams { pub current_password: String, pub new_password: String, } impl AuthService { pub async fn auth_change_password( &self, session: &Session, params: ChangePasswordParams, ) -> Result<(), AppError> { let user_uid = session.user().ok_or(AppError::Unauthorized)?; let current_password = self .auth_rsa_decode(session, params.current_password) .await?; let row = sqlx::query("SELECT password_hash FROM user_password WHERE user_id = $1") .bind(user_uid) .fetch_optional(self.ctx.db.writer()) .await .map_err(AppError::Database)? .ok_or(AppError::UserNotFound)?; let hash: String = row.try_get("password_hash").map_err(AppError::Database)?; let password_hash = PasswordHash::new(&hash).map_err(|_| AppError::InvalidPassword)?; if Argon2::default() .verify_password(current_password.as_bytes(), &password_hash) .is_err() { return Err(AppError::InvalidPassword); } let new_password = self.auth_rsa_decode(session, params.new_password).await?; crate::service::util::validate_password_strength(&new_password)?; let salt = SaltString::generate(&mut rand::thread_rng()); let new_hash = Argon2::default() .hash_password(new_password.as_bytes(), &salt) .map_err(|e| AppError::PasswordHashError(e.to_string()))? .to_string(); let now = Utc::now(); let result = sqlx::query( "UPDATE user_password SET password_hash = $1, password_updated_at = $2, updated_at = $2 \ WHERE user_id = $3", ) .bind(&new_hash) .bind(now) .bind(user_uid) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; if result.rows_affected() == 0 { return Err(AppError::UserNotFound); } session.remove(Self::RSA_PRIVATE_KEY); session.remove(Self::RSA_PUBLIC_KEY); tracing::info!(user_uid = %user_uid, "Password changed successfully"); Ok(()) } }