From 411a07caa1510bcd21ae4004de8ec0c37f612570 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 02:53:41 +0800 Subject: [PATCH] feat(auth): add SeaORM entities and DTOs for auth module - 11 entity files mapping to all auth migration tables - DTOs with validation: LoginReq, CreateUserReq, CreateRoleReq, etc. - Response DTOs: UserResp, RoleResp, PermissionResp, OrganizationResp, etc. - Added workspace dependencies: jsonwebtoken, argon2, validator, thiserror, utoipa --- crates/erp-auth/Cargo.toml | 6 + crates/erp-auth/src/dto.rs | 185 ++++++++++++++++++ crates/erp-auth/src/entity/department.rs | 68 +++++++ crates/erp-auth/src/entity/mod.rs | 10 + crates/erp-auth/src/entity/organization.rs | 40 ++++ crates/erp-auth/src/entity/permission.rs | 37 ++++ crates/erp-auth/src/entity/position.rs | 42 ++++ crates/erp-auth/src/entity/role.rs | 44 +++++ crates/erp-auth/src/entity/role_permission.rs | 51 +++++ crates/erp-auth/src/entity/user.rs | 59 ++++++ crates/erp-auth/src/entity/user_credential.rs | 41 ++++ crates/erp-auth/src/entity/user_role.rs | 51 +++++ crates/erp-auth/src/entity/user_token.rs | 44 +++++ crates/erp-auth/src/lib.rs | 3 +- 14 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 crates/erp-auth/src/dto.rs create mode 100644 crates/erp-auth/src/entity/department.rs create mode 100644 crates/erp-auth/src/entity/mod.rs create mode 100644 crates/erp-auth/src/entity/organization.rs create mode 100644 crates/erp-auth/src/entity/permission.rs create mode 100644 crates/erp-auth/src/entity/position.rs create mode 100644 crates/erp-auth/src/entity/role.rs create mode 100644 crates/erp-auth/src/entity/role_permission.rs create mode 100644 crates/erp-auth/src/entity/user.rs create mode 100644 crates/erp-auth/src/entity/user_credential.rs create mode 100644 crates/erp-auth/src/entity/user_role.rs create mode 100644 crates/erp-auth/src/entity/user_token.rs diff --git a/crates/erp-auth/Cargo.toml b/crates/erp-auth/Cargo.toml index 34e3fc9..c61ba49 100644 --- a/crates/erp-auth/Cargo.toml +++ b/crates/erp-auth/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] erp-core.workspace = true +erp-common.workspace = true tokio.workspace = true serde.workspace = true serde_json.workspace = true @@ -14,3 +15,8 @@ axum.workspace = true sea-orm.workspace = true tracing.workspace = true anyhow.workspace = true +thiserror.workspace = true +jsonwebtoken.workspace = true +argon2.workspace = true +validator.workspace = true +utoipa.workspace = true diff --git a/crates/erp-auth/src/dto.rs b/crates/erp-auth/src/dto.rs new file mode 100644 index 0000000..7415445 --- /dev/null +++ b/crates/erp-auth/src/dto.rs @@ -0,0 +1,185 @@ +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, +} + +// --- User DTOs --- + +#[derive(Debug, Serialize, ToSchema)] +pub struct UserResp { + pub id: Uuid, + pub username: String, + pub email: Option, + pub phone: Option, + pub display_name: Option, + pub avatar_url: Option, + pub status: String, + pub roles: Vec, +} + +#[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, + pub phone: Option, + pub display_name: Option, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct UpdateUserReq { + pub email: Option, + pub phone: Option, + pub display_name: Option, + pub status: Option, +} + +// --- Role DTOs --- + +#[derive(Debug, Serialize, ToSchema)] +pub struct RoleResp { + pub id: Uuid, + pub name: String, + pub code: String, + pub description: Option, + pub is_system: bool, +} + +#[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, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct UpdateRoleReq { + pub name: Option, + pub description: Option, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct AssignRolesReq { + pub role_ids: Vec, +} + +// --- 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, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct AssignPermissionsReq { + pub permission_ids: Vec, +} + +// --- Organization DTOs --- + +#[derive(Debug, Serialize, ToSchema)] +pub struct OrganizationResp { + pub id: Uuid, + pub name: String, + pub code: Option, + pub parent_id: Option, + pub path: Option, + pub level: i32, + pub sort_order: i32, + pub children: Vec, +} + +#[derive(Debug, Deserialize, Validate, ToSchema)] +pub struct CreateOrganizationReq { + #[validate(length(min = 1))] + pub name: String, + pub code: Option, + pub parent_id: Option, + pub sort_order: Option, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct UpdateOrganizationReq { + pub name: Option, + pub code: Option, + pub sort_order: Option, +} + +// --- Department DTOs --- + +#[derive(Debug, Serialize, ToSchema)] +pub struct DepartmentResp { + pub id: Uuid, + pub org_id: Uuid, + pub name: String, + pub code: Option, + pub parent_id: Option, + pub manager_id: Option, + pub path: Option, + pub sort_order: i32, + pub children: Vec, +} + +#[derive(Debug, Deserialize, Validate, ToSchema)] +pub struct CreateDepartmentReq { + #[validate(length(min = 1))] + pub name: String, + pub code: Option, + pub parent_id: Option, + pub manager_id: Option, + pub sort_order: Option, +} + +// --- Position DTOs --- + +#[derive(Debug, Serialize, ToSchema)] +pub struct PositionResp { + pub id: Uuid, + pub dept_id: Uuid, + pub name: String, + pub code: Option, + pub level: i32, + pub sort_order: i32, +} + +#[derive(Debug, Deserialize, Validate, ToSchema)] +pub struct CreatePositionReq { + #[validate(length(min = 1))] + pub name: String, + pub code: Option, + pub level: Option, + pub sort_order: Option, +} diff --git a/crates/erp-auth/src/entity/department.rs b/crates/erp-auth/src/entity/department.rs new file mode 100644 index 0000000..41c9686 --- /dev/null +++ b/crates/erp-auth/src/entity/department.rs @@ -0,0 +1,68 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "departments")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub org_id: Uuid, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manager_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + pub sort_order: i32, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::organization::Entity", + from = "Column::OrgId", + to = "super::organization::Column::Id", + on_delete = "Restrict" + )] + Organization, + #[sea_orm(has_many = "super::position::Entity")] + Position, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::ManagerId", + to = "super::user::Column::Id", + on_delete = "SetNull" + )] + Manager, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Organization.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Position.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Manager.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/mod.rs b/crates/erp-auth/src/entity/mod.rs new file mode 100644 index 0000000..0d84550 --- /dev/null +++ b/crates/erp-auth/src/entity/mod.rs @@ -0,0 +1,10 @@ +pub mod user; +pub mod user_credential; +pub mod user_token; +pub mod role; +pub mod permission; +pub mod role_permission; +pub mod user_role; +pub mod organization; +pub mod department; +pub mod position; diff --git a/crates/erp-auth/src/entity/organization.rs b/crates/erp-auth/src/entity/organization.rs new file mode 100644 index 0000000..bf8369f --- /dev/null +++ b/crates/erp-auth/src/entity/organization.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "organizations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + pub level: i32, + pub sort_order: i32, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::department::Entity")] + Department, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Department.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/permission.rs b/crates/erp-auth/src/entity/permission.rs new file mode 100644 index 0000000..6045eea --- /dev/null +++ b/crates/erp-auth/src/entity/permission.rs @@ -0,0 +1,37 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "permissions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub code: String, + pub name: String, + pub resource: String, + pub action: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::role_permission::Entity")] + RolePermission, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RolePermission.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/position.rs b/crates/erp-auth/src/entity/position.rs new file mode 100644 index 0000000..cea648a --- /dev/null +++ b/crates/erp-auth/src/entity/position.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "positions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub dept_id: Uuid, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + pub level: i32, + pub sort_order: i32, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::department::Entity", + from = "Column::DeptId", + to = "super::department::Column::Id", + on_delete = "Restrict" + )] + Department, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Department.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/role.rs b/crates/erp-auth/src/entity/role.rs new file mode 100644 index 0000000..a54e140 --- /dev/null +++ b/crates/erp-auth/src/entity/role.rs @@ -0,0 +1,44 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "roles")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub name: String, + pub code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub is_system: bool, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::role_permission::Entity")] + RolePermission, + #[sea_orm(has_many = "super::user_role::Entity")] + UserRole, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RolePermission.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserRole.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/role_permission.rs b/crates/erp-auth/src/entity/role_permission.rs new file mode 100644 index 0000000..6e85642 --- /dev/null +++ b/crates/erp-auth/src/entity/role_permission.rs @@ -0,0 +1,51 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "role_permissions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub role_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub permission_id: Uuid, + pub tenant_id: Uuid, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::role::Entity", + from = "Column::RoleId", + to = "super::role::Column::Id", + on_delete = "Cascade" + )] + Role, + #[sea_orm( + belongs_to = "super::permission::Entity", + from = "Column::PermissionId", + to = "super::permission::Column::Id", + on_delete = "Cascade" + )] + Permission, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Role.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Permission.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/user.rs b/crates/erp-auth/src/entity/user.rs new file mode 100644 index 0000000..fc90ef1 --- /dev/null +++ b/crates/erp-auth/src/entity/user.rs @@ -0,0 +1,59 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_login_at: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_credential::Entity")] + UserCredential, + #[sea_orm(has_many = "super::user_token::Entity")] + UserToken, + #[sea_orm(has_many = "super::user_role::Entity")] + UserRole, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserCredential.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserToken.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserRole.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/user_credential.rs b/crates/erp-auth/src/entity/user_credential.rs new file mode 100644 index 0000000..ce99e23 --- /dev/null +++ b/crates/erp-auth/src/entity/user_credential.rs @@ -0,0 +1,41 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "user_credentials")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub user_id: Uuid, + pub credential_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub credential_data: Option, + pub verified: bool, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/user_role.rs b/crates/erp-auth/src/entity/user_role.rs new file mode 100644 index 0000000..915ef8e --- /dev/null +++ b/crates/erp-auth/src/entity/user_role.rs @@ -0,0 +1,51 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "user_roles")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub role_id: Uuid, + pub tenant_id: Uuid, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_delete = "Cascade" + )] + User, + #[sea_orm( + belongs_to = "super::role::Entity", + from = "Column::RoleId", + to = "super::role::Column::Id", + on_delete = "Cascade" + )] + Role, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Role.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/entity/user_token.rs b/crates/erp-auth/src/entity/user_token.rs new file mode 100644 index 0000000..63f2eaf --- /dev/null +++ b/crates/erp-auth/src/entity/user_token.rs @@ -0,0 +1,44 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "user_tokens")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub tenant_id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub token_type: String, + pub expires_at: DateTimeUtc, + #[serde(skip_serializing_if = "Option::is_none")] + pub revoked_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub device_info: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/erp-auth/src/lib.rs b/crates/erp-auth/src/lib.rs index 05c94e7..127022d 100644 --- a/crates/erp-auth/src/lib.rs +++ b/crates/erp-auth/src/lib.rs @@ -1 +1,2 @@ -// erp-auth: 身份与权限模块 (Phase 2) +pub mod dto; +pub mod entity;