use axum::{extract::State, http::HeaderMap, Json};
use serde::Deserialize;
use std::sync::Arc;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::callback::AuthCallback;
use crate::errors::AppError;
use crate::models::MessageResponse;
use crate::repositories::{AuditEventType, RotateUserSecret, ShareAAuthMethod};
use crate::services::EmailService;
use crate::utils::authenticate;
use crate::AppState;
#[derive(Debug, Deserialize, Zeroize, ZeroizeOnDrop)]
#[serde(rename_all = "camelCase")]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
}
pub async fn change_password<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Json(req): Json<ChangePasswordRequest>,
) -> Result<Json<MessageResponse>, AppError> {
let auth = authenticate(&state, &headers).await?;
let user_id = auth.user_id;
let user = state
.user_repo
.find_by_id(user_id)
.await?
.ok_or(AppError::NotFound("User not found".into()))?;
let password_hash = user
.password_hash
.as_ref()
.ok_or_else(|| AppError::Validation("User has no password set".into()))?;
if !state
.password_service
.verify(req.current_password.clone(), password_hash.clone())
.await?
{
return Err(AppError::InvalidCredentials);
}
state.password_service.validate(&req.new_password)?;
let new_password_hash = state
.password_service
.hash(req.new_password.clone())
.await?;
let wallet_material = state.wallet_material_repo.find_by_user(user_id).await?;
let needs_wallet_reencrypt = wallet_material
.as_ref()
.map(|m| m.share_a_auth_method == ShareAAuthMethod::Password)
.unwrap_or(false);
if needs_wallet_reencrypt {
let material = wallet_material.as_ref().unwrap();
let reencrypted = state
.wallet_signing_service
.reencrypt_share_a(material, &req.current_password, &req.new_password)
.await?;
state
.wallet_material_repo
.rotate_user_secret(
user_id,
RotateUserSecret {
new_auth_method: ShareAAuthMethod::Password,
share_a_ciphertext: reencrypted.ciphertext,
share_a_nonce: reencrypted.nonce,
share_a_kdf_salt: Some(reencrypted.salt),
share_a_kdf_params: material.share_a_kdf_params.clone(),
prf_salt: None,
share_a_pin_hash: None,
},
)
.await?;
tracing::info!(user_id = %user_id, "Re-encrypted wallet Share A for password change");
}
state
.user_repo
.update_password(user_id, &new_password_hash)
.await?;
if let Some(session_id) = auth.session_id {
state
.session_repo
.revoke_all_except(user_id, session_id)
.await?;
} else {
state
.session_repo
.revoke_all_for_user_with_reason(user_id, "password_change")
.await?;
}
let _ = state
.audit_service
.log_password_event(AuditEventType::UserPasswordChanged, user_id, Some(&headers))
.await;
Ok(Json(MessageResponse {
message: "Password changed successfully".into(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_change_password_request_deserialize() {
let json = r#"{"currentPassword": "old123", "newPassword": "new456"}"#;
let req: ChangePasswordRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.current_password, "old123");
assert_eq!(req.new_password, "new456");
}
}