fix(server+health): 修复路由 middleware 泄漏 — FHIR/Gateway 改用 .nest() 隔离
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Axum 的 .merge() 会将子 Router 的 middleware 泄漏到整个路由树,
导致 FHIR OAuth middleware 和 Gateway auth middleware 拦截所有请求。

修复方式:
- fhir_routes 内部路径去掉 /fhir 前缀,main.rs 用 .nest("/fhir", ...) 注册
- gateway_routes 内部路径去掉 /health/gateway 前缀,main.rs 用 .nest("/health/gateway", ...) 注册
- 透析患者查询表名 patients → patient(与 Entity 一致)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-05 11:56:42 +08:00
parent 062b4493e4
commit 84b671d1e5
3 changed files with 23 additions and 22 deletions

View File

@@ -75,7 +75,7 @@ pub async fn create_dialysis_record(
// 患者存在性校验
let patient_sql = sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"SELECT EXISTS(SELECT 1 FROM patients WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL)",
"SELECT EXISTS(SELECT 1 FROM patient WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL)",
[req.patient_id.into(), tenant_id.into()],
);
if let Ok(row) = state.db.query_one(patient_sql).await {

View File

@@ -151,32 +151,32 @@ impl HealthModule {
use crate::fhir::handler as fhir;
Router::new()
.route("/fhir/R4/metadata", axum::routing::get(fhir::capability_statement))
.route("/R4/metadata", axum::routing::get(fhir::capability_statement))
// Patient
.route("/fhir/R4/Patient", axum::routing::get(fhir::search_patients))
.route("/fhir/R4/Patient/{id}", axum::routing::get(fhir::get_patient))
.route("/R4/Patient", axum::routing::get(fhir::search_patients))
.route("/R4/Patient/{id}", axum::routing::get(fhir::get_patient))
// Observation
.route("/fhir/R4/Observation", axum::routing::get(fhir::search_observations))
.route("/R4/Observation", axum::routing::get(fhir::search_observations))
// Device
.route("/fhir/R4/Device", axum::routing::get(fhir::search_devices))
.route("/fhir/R4/Device/{id}", axum::routing::get(fhir::get_device))
.route("/R4/Device", axum::routing::get(fhir::search_devices))
.route("/R4/Device/{id}", axum::routing::get(fhir::get_device))
// Practitioner
.route("/fhir/R4/Practitioner", axum::routing::get(fhir::search_practitioners))
.route("/fhir/R4/Practitioner/{id}", axum::routing::get(fhir::get_practitioner))
.route("/R4/Practitioner", axum::routing::get(fhir::search_practitioners))
.route("/R4/Practitioner/{id}", axum::routing::get(fhir::get_practitioner))
// Appointment
.route("/fhir/R4/Appointment", axum::routing::get(fhir::search_appointments))
.route("/fhir/R4/Appointment/{id}", axum::routing::get(fhir::get_appointment))
.route("/R4/Appointment", axum::routing::get(fhir::search_appointments))
.route("/R4/Appointment/{id}", axum::routing::get(fhir::get_appointment))
// DiagnosticReport
.route("/fhir/R4/DiagnosticReport", axum::routing::get(fhir::search_diagnostic_reports))
.route("/fhir/R4/DiagnosticReport/{id}", axum::routing::get(fhir::get_diagnostic_report))
.route("/R4/DiagnosticReport", axum::routing::get(fhir::search_diagnostic_reports))
.route("/R4/DiagnosticReport/{id}", axum::routing::get(fhir::get_diagnostic_report))
// Encounter
.route("/fhir/R4/Encounter", axum::routing::get(fhir::search_encounters))
.route("/fhir/R4/Encounter/{id}", axum::routing::get(fhir::get_encounter))
.route("/R4/Encounter", axum::routing::get(fhir::search_encounters))
.route("/R4/Encounter/{id}", axum::routing::get(fhir::get_encounter))
// Task
.route("/fhir/R4/Task", axum::routing::get(fhir::search_tasks))
.route("/fhir/R4/Task/{id}", axum::routing::get(fhir::get_task))
.route("/R4/Task", axum::routing::get(fhir::search_tasks))
.route("/R4/Task/{id}", axum::routing::get(fhir::get_task))
// $everything
.route("/fhir/R4/Patient/{id}/$everything", axum::routing::get(fhir::patient_everything))
.route("/R4/Patient/{id}/$everything", axum::routing::get(fhir::patient_everything))
// metadata 端点不需要认证,其他端点需要 OAuth Bearer token
.layer(axum::middleware::from_fn(
crate::oauth::middleware::oauth_auth_middleware,
@@ -945,8 +945,8 @@ impl HealthModule {
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/health/gateway/upload", axum::routing::post(ble_gateway_handler::gateway_upload))
.route("/health/gateway/heartbeat", axum::routing::post(ble_gateway_handler::gateway_heartbeat))
.route("/upload", axum::routing::post(ble_gateway_handler::gateway_upload))
.route("/heartbeat", axum::routing::post(ble_gateway_handler::gateway_heartbeat))
}
}

View File

@@ -616,8 +616,9 @@ async fn main() -> anyhow::Result<()> {
}));
let app = Router::new()
.nest("/api/v1", unthrottled_routes.merge(public_routes).merge(protected_routes))
.merge(erp_health::HealthModule::fhir_routes().with_state(state.clone()))
.merge(
.nest("/fhir", erp_health::HealthModule::fhir_routes().with_state(state.clone()))
.nest(
"/health/gateway",
erp_health::HealthModule::gateway_routes()
.layer(axum::middleware::from_fn_with_state(
state.clone(),