feat(auth,plugin): Q3 行级数据权限 — user_departments 表 + JWT 注入 department_ids + data_scope 接线
- 新增 user_departments 关联表(migration + entity) - JWT 中间件查询用户部门并注入 TenantContext.department_ids - role_permission entity 添加 data_scope 字段 - data_handler 接线行级数据权限过滤(list/count/aggregate) - DataScopeParams + build_scope_sql + merge_scope_condition 实现全链路
This commit is contained in:
@@ -6,5 +6,6 @@ pub mod role;
|
|||||||
pub mod role_permission;
|
pub mod role_permission;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_credential;
|
pub mod user_credential;
|
||||||
|
pub mod user_department;
|
||||||
pub mod user_role;
|
pub mod user_role;
|
||||||
pub mod user_token;
|
pub mod user_token;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key, auto_increment = false)]
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
pub permission_id: Uuid,
|
pub permission_id: Uuid,
|
||||||
pub tenant_id: Uuid,
|
pub tenant_id: Uuid,
|
||||||
|
/// 行级数据权限范围: all, self, department, department_tree
|
||||||
|
pub data_scope: String,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
pub created_by: Uuid,
|
pub created_by: Uuid,
|
||||||
|
|||||||
54
crates/erp-auth/src/entity/user_department.rs
Normal file
54
crates/erp-auth/src/entity/user_department.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "user_departments")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub user_id: Uuid,
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub department_id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
pub is_primary: bool,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated_by: Option<Uuid>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
|
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::department::Entity",
|
||||||
|
from = "Column::DepartmentId",
|
||||||
|
to = "super::department::Column::Id",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Department,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::department::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Department.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -17,6 +17,10 @@ use crate::service::token_service::TokenService;
|
|||||||
/// middleware construction time, avoiding any circular dependency between
|
/// middleware construction time, avoiding any circular dependency between
|
||||||
/// erp-auth and erp-server.
|
/// erp-auth and erp-server.
|
||||||
///
|
///
|
||||||
|
/// When `db` is provided, the middleware queries `user_departments` to populate
|
||||||
|
/// `department_ids` in the `TenantContext`. If `db` is `None` or the query fails,
|
||||||
|
/// `department_ids` defaults to an empty list (equivalent to "all" data scope).
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `AppError::Unauthorized` if:
|
/// Returns `AppError::Unauthorized` if:
|
||||||
@@ -26,6 +30,7 @@ use crate::service::token_service::TokenService;
|
|||||||
/// - The token type is not "access"
|
/// - The token type is not "access"
|
||||||
pub async fn jwt_auth_middleware_fn(
|
pub async fn jwt_auth_middleware_fn(
|
||||||
jwt_secret: String,
|
jwt_secret: String,
|
||||||
|
db: Option<sea_orm::DatabaseConnection>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
@@ -47,14 +52,18 @@ pub async fn jwt_auth_middleware_fn(
|
|||||||
return Err(AppError::Unauthorized);
|
return Err(AppError::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 待 user_positions 关联表建立后,从数据库查询用户所属部门 ID 列表
|
// 查询用户所属部门 ID 列表
|
||||||
// 当前阶段 department_ids 为空列表,行级数据权限默认为 all
|
let department_ids = match &db {
|
||||||
|
Some(conn) => fetch_user_department_ids(claims.sub, claims.tid, conn).await,
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
let ctx = TenantContext {
|
let ctx = TenantContext {
|
||||||
tenant_id: claims.tid,
|
tenant_id: claims.tid,
|
||||||
user_id: claims.sub,
|
user_id: claims.sub,
|
||||||
roles: claims.roles,
|
roles: claims.roles,
|
||||||
permissions: claims.permissions,
|
permissions: claims.permissions,
|
||||||
department_ids: vec![],
|
department_ids,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reconstruct the request with the TenantContext injected into extensions.
|
// Reconstruct the request with the TenantContext injected into extensions.
|
||||||
@@ -65,3 +74,25 @@ pub async fn jwt_auth_middleware_fn(
|
|||||||
|
|
||||||
Ok(next.run(req).await)
|
Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查询用户所属的所有部门 ID(通过 user_departments 关联表)
|
||||||
|
async fn fetch_user_department_ids(
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
tenant_id: uuid::Uuid,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> Vec<uuid::Uuid> {
|
||||||
|
use crate::entity::user_department;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
user_department::Entity::find()
|
||||||
|
.filter(user_department::Column::UserId.eq(user_id))
|
||||||
|
.filter(user_department::Column::TenantId.eq(tenant_id))
|
||||||
|
.filter(user_department::Column::DeletedAt.is_null())
|
||||||
|
.all(db)
|
||||||
|
.await
|
||||||
|
.map(|rows| rows.into_iter().map(|r| r.department_id).collect())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
tracing::warn!(error = %e, "查询用户部门列表失败,默认为空");
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ impl RoleService {
|
|||||||
role_id: Set(role_id),
|
role_id: Set(role_id),
|
||||||
permission_id: Set(*perm_id),
|
permission_id: Set(*perm_id),
|
||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
|
data_scope: Set("all".to_string()),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(operator_id),
|
created_by: Set(operator_id),
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ pub async fn seed_tenant_auth(
|
|||||||
role_id: Set(admin_role_id),
|
role_id: Set(admin_role_id),
|
||||||
permission_id: Set(*perm_id),
|
permission_id: Set(*perm_id),
|
||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
|
data_scope: Set("all".to_string()),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(system_user_id),
|
created_by: Set(system_user_id),
|
||||||
@@ -450,6 +451,7 @@ pub async fn seed_tenant_auth(
|
|||||||
role_id: Set(viewer_role_id),
|
role_id: Set(viewer_role_id),
|
||||||
permission_id: Set(perm_ids[*idx]),
|
permission_id: Set(perm_ids[*idx]),
|
||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
|
data_scope: Set("all".to_string()),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
created_by: Set(system_user_id),
|
created_by: Set(system_user_id),
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ use crate::error::PluginError;
|
|||||||
use crate::manifest::PluginField;
|
use crate::manifest::PluginField;
|
||||||
use crate::state::EntityInfo;
|
use crate::state::EntityInfo;
|
||||||
|
|
||||||
|
/// 行级数据权限参数 — 传递到 service 层注入 SQL 条件
|
||||||
|
pub struct DataScopeParams {
|
||||||
|
pub scope_level: String,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub dept_member_ids: Vec<Uuid>,
|
||||||
|
pub owner_field: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PluginDataService;
|
pub struct PluginDataService;
|
||||||
|
|
||||||
impl PluginDataService {
|
impl PluginDataService {
|
||||||
@@ -69,7 +77,7 @@ impl PluginDataService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 列表查询(支持过滤/搜索/排序/Generated Column 路由)
|
/// 列表查询(支持过滤/搜索/排序/Generated Column 路由/数据权限)
|
||||||
pub async fn list(
|
pub async fn list(
|
||||||
plugin_id: Uuid,
|
plugin_id: Uuid,
|
||||||
entity_name: &str,
|
entity_name: &str,
|
||||||
@@ -82,6 +90,7 @@ impl PluginDataService {
|
|||||||
sort_by: Option<String>,
|
sort_by: Option<String>,
|
||||||
sort_order: Option<String>,
|
sort_order: Option<String>,
|
||||||
cache: &moka::sync::Cache<String, EntityInfo>,
|
cache: &moka::sync::Cache<String, EntityInfo>,
|
||||||
|
scope: Option<DataScopeParams>,
|
||||||
) -> AppResult<(Vec<PluginDataResp>, u64)> {
|
) -> AppResult<(Vec<PluginDataResp>, u64)> {
|
||||||
let info =
|
let info =
|
||||||
resolve_entity_info_cached(plugin_id, entity_name, tenant_id, db, cache).await?;
|
resolve_entity_info_cached(plugin_id, entity_name, tenant_id, db, cache).await?;
|
||||||
@@ -100,9 +109,15 @@ impl PluginDataService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 构建数据权限条件
|
||||||
|
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
let (count_sql, count_values) =
|
let (count_sql, mut count_values) =
|
||||||
DynamicTableManager::build_count_sql(&info.table_name, tenant_id);
|
DynamicTableManager::build_count_sql(&info.table_name, tenant_id);
|
||||||
|
let count_sql = merge_scope_condition(count_sql, &scope_condition);
|
||||||
|
count_values.extend(scope_condition.1.clone());
|
||||||
|
|
||||||
#[derive(FromQueryResult)]
|
#[derive(FromQueryResult)]
|
||||||
struct CountResult {
|
struct CountResult {
|
||||||
count: i64,
|
count: i64,
|
||||||
@@ -119,7 +134,7 @@ impl PluginDataService {
|
|||||||
|
|
||||||
// Query — 使用 Generated Column 路由
|
// Query — 使用 Generated Column 路由
|
||||||
let offset = page.saturating_sub(1) * page_size;
|
let offset = page.saturating_sub(1) * page_size;
|
||||||
let (sql, values) = DynamicTableManager::build_filtered_query_sql_ex(
|
let (sql, mut values) = DynamicTableManager::build_filtered_query_sql_ex(
|
||||||
&info.table_name,
|
&info.table_name,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
page_size,
|
page_size,
|
||||||
@@ -132,6 +147,10 @@ impl PluginDataService {
|
|||||||
)
|
)
|
||||||
.map_err(|e| AppError::Validation(e))?;
|
.map_err(|e| AppError::Validation(e))?;
|
||||||
|
|
||||||
|
// 注入数据权限条件
|
||||||
|
let sql = merge_scope_condition(sql, &scope_condition);
|
||||||
|
values.extend(scope_condition.1);
|
||||||
|
|
||||||
#[derive(FromQueryResult)]
|
#[derive(FromQueryResult)]
|
||||||
struct DataRow {
|
struct DataRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@@ -911,6 +930,51 @@ async fn check_no_cycle(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从 DataScopeParams 构建 SQL 条件片段和参数
|
||||||
|
fn build_scope_sql(
|
||||||
|
scope: &Option<DataScopeParams>,
|
||||||
|
generated_fields: &[String],
|
||||||
|
) -> (String, Vec<sea_orm::Value>) {
|
||||||
|
match scope {
|
||||||
|
Some(s) => DynamicTableManager::build_data_scope_condition_with_params(
|
||||||
|
&s.scope_level,
|
||||||
|
&s.user_id,
|
||||||
|
&s.owner_field,
|
||||||
|
&s.dept_member_ids,
|
||||||
|
100, // 起始参数索引(远大于实际参数数量,避免冲突;后续重新编号)
|
||||||
|
generated_fields,
|
||||||
|
),
|
||||||
|
None => (String::new(), vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将数据权限条件合并到现有 SQL 中
|
||||||
|
///
|
||||||
|
/// `scope_condition` 是 `(sql_fragment, params)` 元组。
|
||||||
|
/// sql_fragment 格式为 `"field = $N OR ..."`,可直接拼接到 WHERE 子句。
|
||||||
|
fn merge_scope_condition(sql: String, scope_condition: &(String, Vec<sea_orm::Value>)) -> String {
|
||||||
|
if scope_condition.0.is_empty() {
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
// 在 "deleted_at IS NULL" 之后追加 scope 条件
|
||||||
|
// 因为所有查询都包含 WHERE ... AND "deleted_at" IS NULL ...
|
||||||
|
// 我们在合适的位置追加 AND (scope_condition)
|
||||||
|
if sql.contains("\"deleted_at\" IS NULL") {
|
||||||
|
sql.replace(
|
||||||
|
"\"deleted_at\" IS NULL",
|
||||||
|
&format!("\"deleted_at\" IS NULL AND ({})", scope_condition.0),
|
||||||
|
)
|
||||||
|
} else if sql.contains("deleted_at IS NULL") {
|
||||||
|
sql.replace(
|
||||||
|
"deleted_at IS NULL",
|
||||||
|
&format!("deleted_at IS NULL AND ({})", scope_condition.0),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 回退:直接追加到 WHERE 子句末尾
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod validate_tests {
|
mod validate_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::data_dto::{
|
|||||||
PatchPluginDataReq, PluginDataListParams, PluginDataResp, TimeseriesItem, TimeseriesParams,
|
PatchPluginDataReq, PluginDataListParams, PluginDataResp, TimeseriesItem, TimeseriesParams,
|
||||||
UpdatePluginDataReq,
|
UpdatePluginDataReq,
|
||||||
};
|
};
|
||||||
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
use crate::data_service::{DataScopeParams, PluginDataService, resolve_manifest_id};
|
||||||
use crate::state::PluginState;
|
use crate::state::PluginState;
|
||||||
|
|
||||||
/// 获取当前用户对指定权限的 data_scope 等级
|
/// 获取当前用户对指定权限的 data_scope 等级
|
||||||
@@ -105,12 +105,10 @@ where
|
|||||||
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
|
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
|
||||||
require_permission(&ctx, &fine_perm)?;
|
require_permission(&ctx, &fine_perm)?;
|
||||||
|
|
||||||
// TODO(data_scope): 此处注入行级数据权限过滤
|
// 解析数据权限范围
|
||||||
// 1. 解析 entity 定义检查 data_scope == Some(true)
|
let scope = resolve_data_scope(
|
||||||
// 2. 调用 get_data_scope(&ctx, &fine_perm, &state.db) 获取当前用户的 scope 等级
|
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
|
||||||
// 3. 若 scope != "all",调用 get_dept_members 获取部门成员列表
|
).await?;
|
||||||
// 4. 将 scope 条件合并到 filter 中传给 PluginDataService::list
|
|
||||||
// 参考: crates/erp-plugin/src/dynamic_table.rs build_data_scope_condition()
|
|
||||||
|
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
@@ -133,6 +131,7 @@ where
|
|||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.sort_order,
|
params.sort_order,
|
||||||
&state.entity_cache,
|
&state.entity_cache,
|
||||||
|
scope,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -498,3 +497,54 @@ where
|
|||||||
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析数据权限范围 — 检查 entity 是否启用 data_scope,
|
||||||
|
/// 若启用则查询用户对该权限的 scope 等级,返回 DataScopeParams。
|
||||||
|
async fn resolve_data_scope(
|
||||||
|
ctx: &TenantContext,
|
||||||
|
manifest_id: &str,
|
||||||
|
entity: &str,
|
||||||
|
fine_perm: &str,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> Result<Option<DataScopeParams>, AppError> {
|
||||||
|
let entity_has_scope = check_entity_data_scope(manifest_id, entity, db).await?;
|
||||||
|
if !entity_has_scope {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let scope_level = get_data_scope(ctx, fine_perm, db).await?;
|
||||||
|
if scope_level == "all" {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let dept_members = get_dept_members(ctx, false).await;
|
||||||
|
Ok(Some(DataScopeParams {
|
||||||
|
scope_level,
|
||||||
|
user_id: ctx.user_id,
|
||||||
|
dept_member_ids: dept_members,
|
||||||
|
owner_field: "owner_id".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询 entity 定义是否启用了 data_scope
|
||||||
|
async fn check_entity_data_scope(
|
||||||
|
_manifest_id: &str,
|
||||||
|
entity_name: &str,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
|
use crate::entity::plugin_entity;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
let entity = plugin_entity::Entity::find()
|
||||||
|
.filter(plugin_entity::Column::EntityName.eq(entity_name))
|
||||||
|
.filter(plugin_entity::Column::DeletedAt.is_null())
|
||||||
|
.one(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
|
let Some(e) = entity else { return Ok(false) };
|
||||||
|
|
||||||
|
let schema: crate::manifest::PluginEntity =
|
||||||
|
serde_json::from_value(e.schema_json)
|
||||||
|
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(schema.data_scope.unwrap_or(false))
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ mod m20260417_000033_create_plugins;
|
|||||||
mod m20260417_000034_seed_plugin_permissions;
|
mod m20260417_000034_seed_plugin_permissions;
|
||||||
mod m20260418_000035_pg_trgm_and_entity_columns;
|
mod m20260418_000035_pg_trgm_and_entity_columns;
|
||||||
mod m20260418_000036_add_data_scope_to_role_permissions;
|
mod m20260418_000036_add_data_scope_to_role_permissions;
|
||||||
|
mod m20260419_000037_create_user_departments;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260417_000034_seed_plugin_permissions::Migration),
|
Box::new(m20260417_000034_seed_plugin_permissions::Migration),
|
||||||
Box::new(m20260418_000035_pg_trgm_and_entity_columns::Migration),
|
Box::new(m20260418_000035_pg_trgm_and_entity_columns::Migration),
|
||||||
Box::new(m20260418_000036_add_data_scope_to_role_permissions::Migration),
|
Box::new(m20260418_000036_add_data_scope_to_role_permissions::Migration),
|
||||||
|
Box::new(m20260419_000037_create_user_departments::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Alias::new("user_departments"))
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("user_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("department_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("tenant_id"))
|
||||||
|
.uuid()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("is_primary"))
|
||||||
|
.boolean()
|
||||||
|
.not_null()
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::current_timestamp()),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Alias::new("created_by")).uuid())
|
||||||
|
.col(ColumnDef::new(Alias::new("updated_by")).uuid())
|
||||||
|
.col(ColumnDef::new(Alias::new("deleted_at")).timestamp_with_time_zone())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("version"))
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.default(1),
|
||||||
|
)
|
||||||
|
.primary_key(
|
||||||
|
Index::create()
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.col(Alias::new("department_id")),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 索引:按租户 + 用户查询部门
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_user_departments_tenant_user")
|
||||||
|
.table(Alias::new("user_departments"))
|
||||||
|
.col(Alias::new("tenant_id"))
|
||||||
|
.col(Alias::new("user_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 索引:按部门查询成员
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_user_departments_dept")
|
||||||
|
.table(Alias::new("user_departments"))
|
||||||
|
.col(Alias::new("department_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Alias::new("user_departments")).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -426,10 +426,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
state.clone(),
|
state.clone(),
|
||||||
middleware::rate_limit::rate_limit_by_user,
|
middleware::rate_limit::rate_limit_by_user,
|
||||||
))
|
))
|
||||||
.layer(axum_middleware::from_fn(move |req, next| {
|
.layer({
|
||||||
let secret = jwt_secret.clone();
|
let db = state.db.clone();
|
||||||
async move { jwt_auth_middleware_fn(secret, req, next).await }
|
axum_middleware::from_fn(move |req, next| {
|
||||||
}))
|
let secret = jwt_secret.clone();
|
||||||
|
let db = db.clone();
|
||||||
|
async move { jwt_auth_middleware_fn(secret, Some(db), req, next).await }
|
||||||
|
})
|
||||||
|
})
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
|
||||||
// Merge public + protected into the final application router
|
// Merge public + protected into the final application router
|
||||||
|
|||||||
Reference in New Issue
Block a user