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

@@ -7,7 +7,7 @@ use erp_core::error::AppError;
use erp_core::types::{ApiResponse, TenantContext};
use crate::auth_state::AuthState;
use crate::dto::{LoginReq, LoginResp, RefreshReq};
use crate::dto::{ChangePasswordReq, LoginReq, LoginResp, RefreshReq};
use crate::service::auth_service::{AuthService, JwtConfig};
#[utoipa::path(
@@ -122,3 +122,47 @@ where
message: Some("已成功登出".to_string()),
}))
}
#[utoipa::path(
post,
path = "/api/v1/auth/change-password",
request_body = ChangePasswordReq,
responses(
(status = 200, description = "密码修改成功,需重新登录"),
(status = 400, description = "当前密码不正确"),
(status = 401, description = "未授权"),
),
security(("bearer_auth" = [])),
tag = "认证"
)]
/// POST /api/v1/auth/change-password
///
/// 修改当前登录用户的密码。修改成功后所有已签发的 refresh token 将被吊销,
/// 用户需要在所有设备上重新登录。
pub async fn change_password<S>(
State(state): State<AuthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<ChangePasswordReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
AuthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
AuthService::change_password(
ctx.user_id,
ctx.tenant_id,
&req.current_password,
&req.new_password,
&state.db,
)
.await?;
Ok(Json(ApiResponse {
success: true,
data: None,
message: Some("密码修改成功,请重新登录".to_string()),
}))
}