fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复: 1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查 2. 仪表盘统计容错:单个查询失败返回零值而非 500 3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致 4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径 5. 积分端点权限码:health.health-data.list → health.points.list 6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage 7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档 Clippy 全 workspace 清零(14→0 errors): - erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处 - erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处 - erp-ai: 修复 dead_code、unused import 等 11 处 - erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处 - erp-server-migration: 修复 enum_variant_names 5 处 - erp-auth/config/workflow/message: 各 1-3 处 工程改进: - lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy) - cargo fmt 统一格式化
This commit is contained in:
@@ -39,22 +39,20 @@ struct ApiDoc;
|
||||
erp_auth::handler::role_handler::get_role_permissions,
|
||||
erp_auth::handler::role_handler::list_permissions,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_auth::dto::LoginReq,
|
||||
erp_auth::dto::LoginResp,
|
||||
erp_auth::dto::RefreshReq,
|
||||
erp_auth::dto::UserResp,
|
||||
erp_auth::dto::CreateUserReq,
|
||||
erp_auth::dto::UpdateUserReq,
|
||||
erp_auth::dto::RoleResp,
|
||||
erp_auth::dto::CreateRoleReq,
|
||||
erp_auth::dto::UpdateRoleReq,
|
||||
erp_auth::dto::PermissionResp,
|
||||
erp_auth::dto::AssignPermissionsReq,
|
||||
erp_auth::dto::ChangePasswordReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_auth::dto::LoginReq,
|
||||
erp_auth::dto::LoginResp,
|
||||
erp_auth::dto::RefreshReq,
|
||||
erp_auth::dto::UserResp,
|
||||
erp_auth::dto::CreateUserReq,
|
||||
erp_auth::dto::UpdateUserReq,
|
||||
erp_auth::dto::RoleResp,
|
||||
erp_auth::dto::CreateRoleReq,
|
||||
erp_auth::dto::UpdateRoleReq,
|
||||
erp_auth::dto::PermissionResp,
|
||||
erp_auth::dto::AssignPermissionsReq,
|
||||
erp_auth::dto::ChangePasswordReq,
|
||||
))
|
||||
)]
|
||||
struct AuthApiDoc;
|
||||
|
||||
@@ -86,23 +84,21 @@ struct AuthApiDoc;
|
||||
erp_config::handler::setting_handler::update_setting,
|
||||
erp_config::handler::setting_handler::delete_setting,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_config::dto::DictionaryResp,
|
||||
erp_config::dto::CreateDictionaryReq,
|
||||
erp_config::dto::UpdateDictionaryReq,
|
||||
erp_config::dto::DictionaryItemResp,
|
||||
erp_config::dto::CreateDictionaryItemReq,
|
||||
erp_config::dto::UpdateDictionaryItemReq,
|
||||
erp_config::dto::MenuResp,
|
||||
erp_config::dto::CreateMenuReq,
|
||||
erp_config::dto::UpdateMenuReq,
|
||||
erp_config::dto::NumberingRuleResp,
|
||||
erp_config::dto::CreateNumberingRuleReq,
|
||||
erp_config::dto::UpdateNumberingRuleReq,
|
||||
erp_config::dto::ThemeResp,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_config::dto::DictionaryResp,
|
||||
erp_config::dto::CreateDictionaryReq,
|
||||
erp_config::dto::UpdateDictionaryReq,
|
||||
erp_config::dto::DictionaryItemResp,
|
||||
erp_config::dto::CreateDictionaryItemReq,
|
||||
erp_config::dto::UpdateDictionaryItemReq,
|
||||
erp_config::dto::MenuResp,
|
||||
erp_config::dto::CreateMenuReq,
|
||||
erp_config::dto::UpdateMenuReq,
|
||||
erp_config::dto::NumberingRuleResp,
|
||||
erp_config::dto::CreateNumberingRuleReq,
|
||||
erp_config::dto::UpdateNumberingRuleReq,
|
||||
erp_config::dto::ThemeResp,
|
||||
))
|
||||
)]
|
||||
struct ConfigApiDoc;
|
||||
|
||||
@@ -126,18 +122,16 @@ struct ConfigApiDoc;
|
||||
erp_workflow::handler::task_handler::complete_task,
|
||||
erp_workflow::handler::task_handler::delegate_task,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_workflow::dto::ProcessDefinitionResp,
|
||||
erp_workflow::dto::CreateProcessDefinitionReq,
|
||||
erp_workflow::dto::UpdateProcessDefinitionReq,
|
||||
erp_workflow::dto::ProcessInstanceResp,
|
||||
erp_workflow::dto::StartInstanceReq,
|
||||
erp_workflow::dto::TaskResp,
|
||||
erp_workflow::dto::CompleteTaskReq,
|
||||
erp_workflow::dto::DelegateTaskReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_workflow::dto::ProcessDefinitionResp,
|
||||
erp_workflow::dto::CreateProcessDefinitionReq,
|
||||
erp_workflow::dto::UpdateProcessDefinitionReq,
|
||||
erp_workflow::dto::ProcessInstanceResp,
|
||||
erp_workflow::dto::StartInstanceReq,
|
||||
erp_workflow::dto::TaskResp,
|
||||
erp_workflow::dto::CompleteTaskReq,
|
||||
erp_workflow::dto::DelegateTaskReq,
|
||||
))
|
||||
)]
|
||||
struct WorkflowApiDoc;
|
||||
|
||||
@@ -155,18 +149,16 @@ struct WorkflowApiDoc;
|
||||
erp_message::handler::template_handler::create_template,
|
||||
erp_message::handler::subscription_handler::update_subscription,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
erp_message::dto::MessageResp,
|
||||
erp_message::dto::SendMessageReq,
|
||||
erp_message::dto::MessageQuery,
|
||||
erp_message::dto::UnreadCountResp,
|
||||
erp_message::dto::MessageTemplateResp,
|
||||
erp_message::dto::CreateTemplateReq,
|
||||
erp_message::dto::MessageSubscriptionResp,
|
||||
erp_message::dto::UpdateSubscriptionReq,
|
||||
)
|
||||
)
|
||||
components(schemas(
|
||||
erp_message::dto::MessageResp,
|
||||
erp_message::dto::SendMessageReq,
|
||||
erp_message::dto::MessageQuery,
|
||||
erp_message::dto::UnreadCountResp,
|
||||
erp_message::dto::MessageTemplateResp,
|
||||
erp_message::dto::CreateTemplateReq,
|
||||
erp_message::dto::MessageSubscriptionResp,
|
||||
erp_message::dto::UpdateSubscriptionReq,
|
||||
))
|
||||
)]
|
||||
struct MessageApiDoc;
|
||||
|
||||
@@ -190,31 +182,31 @@ async fn main() -> anyhow::Result<()> {
|
||||
let config = AppConfig::load()?;
|
||||
|
||||
// ── 安全检查:拒绝默认密钥 ──────────────────────────
|
||||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
|
||||
tracing::error!(
|
||||
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
|
||||
);
|
||||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production"
|
||||
{
|
||||
tracing::error!("JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.database.url == "__MUST_SET_VIA_ENV__" {
|
||||
tracing::error!(
|
||||
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
|
||||
);
|
||||
tracing::error!("数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.redis.url == "__MUST_SET_VIA_ENV__" {
|
||||
tracing::error!(
|
||||
"Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL"
|
||||
);
|
||||
tracing::error!("Redis URL 为默认占位值,拒绝启动。请设置环境变量 ERP__REDIS__URL");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if !config.wechat.dev_mode && (config.wechat.appid == "__MUST_SET_VIA_ENV__" || config.wechat.secret == "__MUST_SET_VIA_ENV__") {
|
||||
if !config.wechat.dev_mode
|
||||
&& (config.wechat.appid == "__MUST_SET_VIA_ENV__"
|
||||
|| config.wechat.secret == "__MUST_SET_VIA_ENV__")
|
||||
{
|
||||
tracing::error!(
|
||||
"微信凭据为默认占位值,拒绝启动。请设置环境变量 ERP__WECHAT__APPID 和 ERP__WECHAT__SECRET"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if config.health.aes_key == "__MUST_SET_VIA_ENV__" || config.health.hmac_key == "__MUST_SET_VIA_ENV__" {
|
||||
if config.health.aes_key == "__MUST_SET_VIA_ENV__"
|
||||
|| config.health.hmac_key == "__MUST_SET_VIA_ENV__"
|
||||
{
|
||||
// 注: health 密钥已被统一 KEK (ERP__CRYPTO__KEK) 替代,此处仅保留兼容性检查
|
||||
tracing::warn!(
|
||||
"ERP__HEALTH__AES_KEY/HMAC_KEY 未设置(已迁移到 ERP__CRYPTO__KEK 统一密钥体系)"
|
||||
@@ -292,12 +284,21 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(tenant_id = %new_tenant_id, "Default tenant ready with auth seed data");
|
||||
|
||||
// Seed AI workflow definitions
|
||||
if let Err(e) = erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id).await {
|
||||
if let Err(e) =
|
||||
erp_workflow::service::ai_workflow_seed::ensure_ai_workflows(&db, new_tenant_id)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to seed AI workflow definitions");
|
||||
}
|
||||
|
||||
// Seed dialysis session workflow definition
|
||||
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(&db, new_tenant_id, new_tenant_id).await {
|
||||
if let Err(e) = dialysis_workflow::seed_dialysis_session_workflow(
|
||||
&db,
|
||||
new_tenant_id,
|
||||
new_tenant_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "Failed to seed dialysis session workflow");
|
||||
}
|
||||
|
||||
@@ -363,7 +364,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Points module 已统一到 erp-health(/health/points/* 路由)
|
||||
|
||||
|
||||
// Initialize dialysis module
|
||||
let dialysis_module = erp_dialysis::DialysisModule;
|
||||
tracing::info!(
|
||||
@@ -388,11 +388,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Initialize plugin engine
|
||||
let plugin_config = erp_plugin::engine::PluginEngineConfig::default();
|
||||
let plugin_engine = erp_plugin::engine::PluginEngine::new(
|
||||
db.clone(),
|
||||
event_bus.clone(),
|
||||
plugin_config,
|
||||
)?;
|
||||
let plugin_engine =
|
||||
erp_plugin::engine::PluginEngine::new(db.clone(), event_bus.clone(), plugin_config)?;
|
||||
tracing::info!("Plugin engine initialized");
|
||||
|
||||
// Register plugin module
|
||||
@@ -466,7 +463,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
panic!("ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes).");
|
||||
panic!(
|
||||
"ERP__CRYPTO__KEK must be set in production. Use a 64-char hex string (32 bytes)."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
erp_core::crypto::PiiCrypto::from_kek_hex(&config.crypto.kek)
|
||||
@@ -480,9 +479,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// 始终注册默认 Claude provider(兼容旧配置)
|
||||
{
|
||||
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
|
||||
config.ai.api_key.clone(),
|
||||
);
|
||||
let mut claude =
|
||||
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
@@ -496,22 +494,31 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
match pcfg.provider_type.as_str() {
|
||||
"openai" => {
|
||||
let api_key = pcfg.api_key_env.as_ref()
|
||||
let api_key = pcfg
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg.base_url.clone()
|
||||
let base_url = pcfg
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let provider = erp_ai::provider::openai::OpenAIProvider::new(
|
||||
api_key, base_url, pcfg.default_model.clone(),
|
||||
api_key,
|
||||
base_url,
|
||||
pcfg.default_model.clone(),
|
||||
);
|
||||
registry.register(name.clone(), std::sync::Arc::new(provider));
|
||||
tracing::info!(provider = %name, "已注册 OpenAI 兼容提供商");
|
||||
}
|
||||
"ollama" => {
|
||||
let base_url = pcfg.base_url.clone()
|
||||
let base_url = pcfg
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let provider = erp_ai::provider::ollama::OllamaProvider::new(
|
||||
base_url, pcfg.default_model.clone(),
|
||||
base_url,
|
||||
pcfg.default_model.clone(),
|
||||
);
|
||||
registry.register(name.clone(), std::sync::Arc::new(provider));
|
||||
tracing::info!(provider = %name, "已注册 Ollama 本地提供商");
|
||||
@@ -528,46 +535,58 @@ async fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成");
|
||||
|
||||
// 根据 default_provider 配置构建 AnalysisService 的默认 provider
|
||||
let default_provider: Box<dyn erp_ai::provider::AiProvider> =
|
||||
match config.ai.default_provider.as_str() {
|
||||
"ollama" => {
|
||||
let pcfg = config.ai.providers.get("ollama");
|
||||
let base_url = pcfg.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let model = pcfg.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
|
||||
Box::new(erp_ai::provider::ollama::OllamaProvider::new(base_url, model))
|
||||
let default_provider: Box<dyn erp_ai::provider::AiProvider> = match config
|
||||
.ai
|
||||
.default_provider
|
||||
.as_str()
|
||||
{
|
||||
"ollama" => {
|
||||
let pcfg = config.ai.providers.get("ollama");
|
||||
let base_url = pcfg
|
||||
.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
let model = pcfg
|
||||
.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商");
|
||||
Box::new(erp_ai::provider::ollama::OllamaProvider::new(
|
||||
base_url, model,
|
||||
))
|
||||
}
|
||||
"openai" => {
|
||||
let pcfg = config.ai.providers.get("openai");
|
||||
let api_key = pcfg
|
||||
.and_then(|c| c.api_key_env.as_ref())
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg
|
||||
.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let model = pcfg
|
||||
.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
Box::new(erp_ai::provider::openai::OpenAIProvider::new(
|
||||
api_key, base_url, model,
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
// 默认 Claude
|
||||
let mut claude =
|
||||
erp_ai::provider::claude::ClaudeProvider::new(config.ai.api_key.clone());
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
"openai" => {
|
||||
let pcfg = config.ai.providers.get("openai");
|
||||
let api_key = pcfg.and_then(|c| c.api_key_env.as_ref())
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.unwrap_or_default();
|
||||
let base_url = pcfg.and_then(|c| c.base_url.clone())
|
||||
.unwrap_or_else(|| "https://api.openai.com".to_string());
|
||||
let model = pcfg.map(|c| c.default_model.clone())
|
||||
.unwrap_or_else(|| config.ai.model.clone());
|
||||
Box::new(erp_ai::provider::openai::OpenAIProvider::new(api_key, base_url, model))
|
||||
}
|
||||
_ => {
|
||||
// 默认 Claude
|
||||
let mut claude = erp_ai::provider::claude::ClaudeProvider::new(
|
||||
config.ai.api_key.clone(),
|
||||
);
|
||||
if let Some(ref base_url) = config.ai.base_url {
|
||||
claude = claude.with_base_url(base_url.clone());
|
||||
}
|
||||
Box::new(claude)
|
||||
}
|
||||
};
|
||||
Box::new(claude)
|
||||
}
|
||||
};
|
||||
|
||||
let analysis_svc = erp_ai::service::analysis::AnalysisService::new(
|
||||
default_provider,
|
||||
db.clone(),
|
||||
).with_knowledge_source(std::sync::Arc::new(
|
||||
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(db.clone()),
|
||||
));
|
||||
let analysis_svc =
|
||||
erp_ai::service::analysis::AnalysisService::new(default_provider, db.clone())
|
||||
.with_knowledge_source(std::sync::Arc::new(
|
||||
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(
|
||||
db.clone(),
|
||||
),
|
||||
));
|
||||
let analysis = std::sync::Arc::new(analysis_svc);
|
||||
let prompt = std::sync::Arc::new(erp_ai::service::prompt::PromptService::new(db.clone()));
|
||||
let usage = std::sync::Arc::new(erp_ai::service::usage::UsageService::new(db.clone()));
|
||||
@@ -684,6 +703,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
"/analytics/batch",
|
||||
axum::routing::post(handlers::analytics::batch),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(
|
||||
middleware::frozen_module::frozen_module_middleware,
|
||||
))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
middleware::rate_limit::rate_limit_by_user,
|
||||
@@ -716,9 +738,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
let secret = secret_for_uploads.clone();
|
||||
async move { upload_auth_middleware(secret, req, next).await }
|
||||
}));
|
||||
let fhir_routes = erp_health::HealthModule::fhir_routes().with_state(state.clone());
|
||||
let app = Router::new()
|
||||
.nest("/api/v1", unthrottled_routes.merge(public_routes).merge(protected_routes))
|
||||
.nest("/fhir", erp_health::HealthModule::fhir_routes().with_state(state.clone()))
|
||||
.nest(
|
||||
"/api/v1",
|
||||
unthrottled_routes
|
||||
.merge(public_routes)
|
||||
.merge(protected_routes)
|
||||
.nest("/fhir", fhir_routes),
|
||||
)
|
||||
.nest(
|
||||
"/health/gateway",
|
||||
erp_health::HealthModule::gateway_routes()
|
||||
@@ -729,7 +757,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
.with_state(state.clone()),
|
||||
)
|
||||
.nest("/uploads", uploads_router)
|
||||
.layer(axum::middleware::from_fn(middleware::metrics::metrics_middleware))
|
||||
.layer(axum::middleware::from_fn(
|
||||
middleware::metrics::metrics_middleware,
|
||||
))
|
||||
.layer(cors);
|
||||
|
||||
// Start Prometheus metrics exporter on a separate port
|
||||
@@ -811,7 +841,9 @@ fn build_cors_layer(allowed_origins: &str) -> tower_http::cors::CorsLayer {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
tracing::error!("CORS wildcard '*' is not allowed in production builds");
|
||||
panic!("Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains.");
|
||||
panic!(
|
||||
"Refusing to start with CORS wildcard in release mode. Set ERP__CORS__ALLOWED_ORIGINS to specific domains."
|
||||
);
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -879,6 +911,7 @@ async fn shutdown_signal() {
|
||||
/// 对每个模块的 `permissions()` 返回的权限执行 upsert:
|
||||
/// - 新权限:INSERT
|
||||
/// - 已有权限(同 tenant_id + code):跳过
|
||||
///
|
||||
/// 同时将新权限分配给 admin 角色。
|
||||
async fn sync_module_permissions(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
@@ -906,7 +939,7 @@ async fn sync_module_permissions(
|
||||
perm.code.clone().into(),
|
||||
perm.name.clone().into(),
|
||||
perm.module.clone().into(),
|
||||
perm.code.split('.').last().unwrap_or("manage").into(),
|
||||
perm.code.split('.').next_back().unwrap_or("manage").into(),
|
||||
perm.description.clone().into(),
|
||||
system_user_id.into(),
|
||||
],
|
||||
@@ -932,7 +965,10 @@ async fn sync_module_permissions(
|
||||
)).await?;
|
||||
|
||||
if total_new > 0 {
|
||||
tracing::info!(total_new, "New module permissions synced and bound to admin role");
|
||||
tracing::info!(
|
||||
total_new,
|
||||
"New module permissions synced and bound to admin role"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user