Files
hms/crates/erp-auth/src/dto.rs
iven 7e8fabb095 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.
2026-04-15 01:32:18 +08:00

437 lines
11 KiB
Rust

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use validator::Validate;
// --- Auth DTOs ---
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct LoginReq {
#[validate(length(min = 1, message = "用户名不能为空"))]
pub username: String,
#[validate(length(min = 1, message = "密码不能为空"))]
pub password: String,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct LoginResp {
pub access_token: String,
pub refresh_token: String,
pub expires_in: u64,
pub user: UserResp,
}
#[derive(Debug, Deserialize, ToSchema)]
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)]
pub struct UserResp {
pub id: Uuid,
pub username: String,
pub email: Option<String>,
pub phone: Option<String>,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub status: String,
pub roles: Vec<RoleResp>,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateUserReq {
#[validate(length(min = 1, max = 50))]
pub username: String,
#[validate(length(min = 6, max = 128))]
pub password: String,
#[validate(email)]
pub email: Option<String>,
pub phone: Option<String>,
pub display_name: Option<String>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateUserReq {
pub email: Option<String>,
pub phone: Option<String>,
pub display_name: Option<String>,
pub status: Option<String>,
pub version: i32,
}
// --- Role DTOs ---
#[derive(Debug, Serialize, ToSchema)]
pub struct RoleResp {
pub id: Uuid,
pub name: String,
pub code: String,
pub description: Option<String>,
pub is_system: bool,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateRoleReq {
#[validate(length(min = 1, max = 50))]
pub name: String,
#[validate(length(min = 1, max = 50))]
pub code: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateRoleReq {
pub name: Option<String>,
pub description: Option<String>,
pub version: i32,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct AssignRolesReq {
pub role_ids: Vec<Uuid>,
}
// --- Permission DTOs ---
#[derive(Debug, Serialize, ToSchema)]
pub struct PermissionResp {
pub id: Uuid,
pub code: String,
pub name: String,
pub resource: String,
pub action: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct AssignPermissionsReq {
pub permission_ids: Vec<Uuid>,
}
// --- Organization DTOs ---
#[derive(Debug, Serialize, ToSchema)]
pub struct OrganizationResp {
pub id: Uuid,
pub name: String,
pub code: Option<String>,
pub parent_id: Option<Uuid>,
pub path: Option<String>,
pub level: i32,
pub sort_order: i32,
pub children: Vec<OrganizationResp>,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateOrganizationReq {
#[validate(length(min = 1))]
pub name: String,
pub code: Option<String>,
pub parent_id: Option<Uuid>,
pub sort_order: Option<i32>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateOrganizationReq {
pub name: Option<String>,
pub code: Option<String>,
pub sort_order: Option<i32>,
pub version: i32,
}
// --- Department DTOs ---
#[derive(Debug, Serialize, ToSchema)]
pub struct DepartmentResp {
pub id: Uuid,
pub org_id: Uuid,
pub name: String,
pub code: Option<String>,
pub parent_id: Option<Uuid>,
pub manager_id: Option<Uuid>,
pub path: Option<String>,
pub sort_order: i32,
pub children: Vec<DepartmentResp>,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateDepartmentReq {
#[validate(length(min = 1))]
pub name: String,
pub code: Option<String>,
pub parent_id: Option<Uuid>,
pub manager_id: Option<Uuid>,
pub sort_order: Option<i32>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateDepartmentReq {
pub name: Option<String>,
pub code: Option<String>,
pub manager_id: Option<Uuid>,
pub sort_order: Option<i32>,
pub version: i32,
}
// --- Position DTOs ---
#[derive(Debug, Serialize, ToSchema)]
pub struct PositionResp {
pub id: Uuid,
pub dept_id: Uuid,
pub name: String,
pub code: Option<String>,
pub level: i32,
pub sort_order: i32,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreatePositionReq {
#[validate(length(min = 1))]
pub name: String,
pub code: Option<String>,
pub level: Option<i32>,
pub sort_order: Option<i32>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdatePositionReq {
pub name: Option<String>,
pub code: Option<String>,
pub level: Option<i32>,
pub sort_order: Option<i32>,
pub version: i32,
}
#[cfg(test)]
mod tests {
use super::*;
use validator::Validate;
#[test]
fn login_req_valid() {
let req = LoginReq {
username: "admin".to_string(),
password: "password123".to_string(),
};
assert!(req.validate().is_ok());
}
#[test]
fn login_req_empty_username_fails() {
let req = LoginReq {
username: "".to_string(),
password: "password123".to_string(),
};
let result = req.validate();
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 {
username: "admin".to_string(),
password: "".to_string(),
};
assert!(req.validate().is_err());
}
#[test]
fn create_user_req_valid() {
let req = CreateUserReq {
username: "alice".to_string(),
password: "secret123".to_string(),
email: Some("alice@example.com".to_string()),
phone: None,
display_name: Some("Alice".to_string()),
};
assert!(req.validate().is_ok());
}
#[test]
fn create_user_req_short_password_fails() {
let req = CreateUserReq {
username: "bob".to_string(),
password: "12345".to_string(), // min 6
email: None,
phone: None,
display_name: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_user_req_empty_username_fails() {
let req = CreateUserReq {
username: "".to_string(),
password: "secret123".to_string(),
email: None,
phone: None,
display_name: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_user_req_invalid_email_fails() {
let req = CreateUserReq {
username: "charlie".to_string(),
password: "secret123".to_string(),
email: Some("not-an-email".to_string()),
phone: None,
display_name: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_user_req_long_username_fails() {
let req = CreateUserReq {
username: "a".repeat(51), // max 50
password: "secret123".to_string(),
email: None,
phone: None,
display_name: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_role_req_valid() {
let req = CreateRoleReq {
name: "管理员".to_string(),
code: "admin".to_string(),
description: Some("系统管理员".to_string()),
};
assert!(req.validate().is_ok());
}
#[test]
fn create_role_req_empty_name_fails() {
let req = CreateRoleReq {
name: "".to_string(),
code: "admin".to_string(),
description: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_role_req_empty_code_fails() {
let req = CreateRoleReq {
name: "管理员".to_string(),
code: "".to_string(),
description: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_org_req_valid() {
let req = CreateOrganizationReq {
name: "总部".to_string(),
code: Some("HQ".to_string()),
parent_id: None,
sort_order: Some(0),
};
assert!(req.validate().is_ok());
}
#[test]
fn create_org_req_empty_name_fails() {
let req = CreateOrganizationReq {
name: "".to_string(),
code: None,
parent_id: None,
sort_order: None,
};
assert!(req.validate().is_err());
}
#[test]
fn create_dept_req_valid() {
let req = CreateDepartmentReq {
name: "技术部".to_string(),
code: Some("TECH".to_string()),
parent_id: None,
manager_id: None,
sort_order: Some(1),
};
assert!(req.validate().is_ok());
}
#[test]
fn create_position_req_valid() {
let req = CreatePositionReq {
name: "高级工程师".to_string(),
code: Some("SENIOR".to_string()),
level: Some(3),
sort_order: None,
};
assert!(req.validate().is_ok());
}
#[test]
fn create_position_req_empty_name_fails() {
let req = CreatePositionReq {
name: "".to_string(),
code: None,
level: None,
sort_order: None,
};
assert!(req.validate().is_err());
}
}