feat(auth): add change password API and frontend page

Backend:
- Add ChangePasswordReq DTO with validation (current + new password)
- Add AuthService::change_password() method with credential verification,
  password rehash, and token revocation
- Add POST /api/v1/auth/change-password endpoint with utoipa annotation

Frontend:
- Add changePassword() API function in auth.ts
- Add ChangePassword.tsx page with form validation and confirmation
- Add "修改密码" tab in Settings page

After password change, all refresh tokens are revoked and the user
is redirected to the login page.
This commit is contained in:
iven
2026-04-15 01:32:18 +08:00
parent d8a0ac7519
commit 7e8fabb095
8 changed files with 267 additions and 1 deletions

View File

@@ -210,6 +210,60 @@ impl AuthService {
TokenService::revoke_all_user_tokens(user_id, tenant_id, db).await
}
/// Change password for the authenticated user.
///
/// Steps:
/// 1. Verify current password
/// 2. Hash the new password
/// 3. Update the credential record
/// 4. Revoke all existing refresh tokens (force re-login)
pub async fn change_password(
user_id: Uuid,
tenant_id: Uuid,
current_password: &str,
new_password: &str,
db: &sea_orm::DatabaseConnection,
) -> AuthResult<()> {
// 1. Find the user's password credential
let cred = user_credential::Entity::find()
.filter(user_credential::Column::UserId.eq(user_id))
.filter(user_credential::Column::TenantId.eq(tenant_id))
.filter(user_credential::Column::CredentialType.eq("password"))
.filter(user_credential::Column::DeletedAt.is_null())
.one(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?
.ok_or_else(|| AuthError::Validation("用户凭证不存在".to_string()))?;
// 2. Verify current password
let stored_hash = cred
.credential_data
.as_ref()
.and_then(|v| v.get("hash").and_then(|h| h.as_str()))
.ok_or_else(|| AuthError::Validation("用户凭证异常".to_string()))?;
if !password::verify_password(current_password, stored_hash)? {
return Err(AuthError::Validation("当前密码不正确".to_string()));
}
// 3. Hash new password and update credential
let new_hash = password::hash_password(new_password)?;
let mut cred_active: user_credential::ActiveModel = cred.into();
cred_active.credential_data = Set(Some(serde_json::json!({ "hash": new_hash })));
cred_active.updated_at = Set(Utc::now());
cred_active.version = Set(2);
cred_active
.update(db)
.await
.map_err(|e| AuthError::Validation(e.to_string()))?;
// 4. Revoke all refresh tokens — force re-login on all devices
TokenService::revoke_all_user_tokens(user_id, tenant_id, db).await?;
tracing::info!(user_id = %user_id, "Password changed successfully");
Ok(())
}
/// Fetch role details for a user, returning RoleResp DTOs.
async fn get_user_role_resps(
user_id: Uuid,