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

@@ -26,6 +26,15 @@ pub struct RefreshReq {
pub refresh_token: String,
}
/// 修改密码请求
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct ChangePasswordReq {
#[validate(length(min = 1, message = "当前密码不能为空"))]
pub current_password: String,
#[validate(length(min = 6, max = 128, message = "新密码长度需在6-128之间"))]
pub new_password: String,
}
// --- User DTOs ---
#[derive(Debug, Serialize, ToSchema)]
@@ -234,6 +243,42 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn change_password_req_valid() {
let req = ChangePasswordReq {
current_password: "oldPassword123".to_string(),
new_password: "newPassword456".to_string(),
};
assert!(req.validate().is_ok());
}
#[test]
fn change_password_req_empty_current_fails() {
let req = ChangePasswordReq {
current_password: "".to_string(),
new_password: "newPassword456".to_string(),
};
assert!(req.validate().is_err());
}
#[test]
fn change_password_req_short_new_fails() {
let req = ChangePasswordReq {
current_password: "oldPassword123".to_string(),
new_password: "12345".to_string(), // min 6
};
assert!(req.validate().is_err());
}
#[test]
fn change_password_req_long_new_fails() {
let req = ChangePasswordReq {
current_password: "oldPassword123".to_string(),
new_password: "a".repeat(129), // max 128
};
assert!(req.validate().is_err());
}
#[test]
fn login_req_empty_password_fails() {
let req = LoginReq {