fix: 前端深度审计全量修复 — 安全/功能/代码质量
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

严重 BUG 修复:
- 修复 Token 过期后 hash 重定向导致无法跳转登录页
- 修复文章编辑器新建后提交审核使用错误 ID

安全加固:
- HTML 清理函数替换为 ammonia 专业库(替代自定义解析器)
- 文件上传添加 magic bytes 校验(防 Content-Type 伪造)
- 登录添加账户级失败锁定(5次失败→15分钟锁定)
- 审计日志 9 个关键更新操作补充变更前后值(with_changes)

功能缺陷修复:
- 登录/登出时清理 API 缓存(防多账户数据污染)
- 文章编辑器上传改用统一 HTTP 客户端(自动 token 刷新)
- 添加全局 HTTP 错误处理和后端错误消息展示
- PrivateRoute 增加路由级权限检查(系统管理页面)
- 健康数据三个 Tab 添加编辑/删除功能
- 预约创建增加排班可用性校验提示
- 医生详情 API 返回解密后的原始执照号

代码清理:
- 删除未使用的 auth.ts refresh() 函数
- 删除重复的 AuthGuard.tsx 组件
- 删除未使用的 getHealthSummary API
This commit is contained in:
iven
2026-04-26 21:47:26 +08:00
parent f0c3426792
commit 787e64d9a9
23 changed files with 1152 additions and 482 deletions

View File

@@ -411,6 +411,14 @@ pub async fn update_schedule(
}
}
// 记录变更前的关键字段
let old_values = serde_json::json!({
"start_time": model.start_time,
"end_time": model.end_time,
"max_appointments": model.max_appointments,
"status": model.status,
});
let mut active: doctor_schedule::ActiveModel = model.into();
if let Some(v) = req.start_time { active.start_time = Set(v); }
if let Some(v) = req.end_time { active.end_time = Set(v); }
@@ -422,9 +430,18 @@ pub async fn update_schedule(
let m = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"start_time": m.start_time,
"end_time": m.end_time,
"max_appointments": m.max_appointments,
"status": m.status,
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "doctor_schedule.updated", "doctor_schedule")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;

View File

@@ -170,6 +170,14 @@ pub async fn update_task(
validate_follow_up_status_transition(&model.status, new_status)?;
}
// 记录变更前的关键字段
let old_values = serde_json::json!({
"assigned_to": model.assigned_to,
"follow_up_type": model.follow_up_type,
"planned_date": model.planned_date,
"status": model.status,
});
let mut active: follow_up_task::ActiveModel = model.into();
if let Some(v) = req.assigned_to { active.assigned_to = Set(Some(v)); }
if let Some(v) = req.follow_up_type { active.follow_up_type = Set(v); }
@@ -182,9 +190,18 @@ pub async fn update_task(
let m = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"assigned_to": m.assigned_to,
"follow_up_type": m.follow_up_type,
"planned_date": m.planned_date,
"status": m.status,
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "follow_up_task.updated", "follow_up_task")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;

View File

@@ -156,6 +156,19 @@ pub async fn update_vital_signs(
let next_ver = check_version(expected_version, model.version)
.map_err(|_| HealthError::VersionMismatch)?;
// 记录变更前的关键体征值
let old_values = serde_json::json!({
"record_date": model.record_date,
"systolic_bp_morning": model.systolic_bp_morning,
"diastolic_bp_morning": model.diastolic_bp_morning,
"systolic_bp_evening": model.systolic_bp_evening,
"diastolic_bp_evening": model.diastolic_bp_evening,
"heart_rate": model.heart_rate,
"weight": model.weight,
"blood_sugar": model.blood_sugar,
"notes": model.notes,
});
let mut active: vital_signs::ActiveModel = model.into();
if let Some(v) = req.record_date { active.record_date = Set(v); }
if let Some(v) = req.systolic_bp_morning { active.systolic_bp_morning = Set(Some(v)); }
@@ -174,6 +187,19 @@ pub async fn update_vital_signs(
let m = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"record_date": m.record_date,
"systolic_bp_morning": m.systolic_bp_morning,
"diastolic_bp_morning": m.diastolic_bp_morning,
"systolic_bp_evening": m.systolic_bp_evening,
"diastolic_bp_evening": m.diastolic_bp_evening,
"heart_rate": m.heart_rate,
"weight": m.weight,
"blood_sugar": m.blood_sugar,
"notes": m.notes,
});
// 更新后也触发危急值检测(修改后的值可能触发告警)
let check_req = CreateVitalSignsReq {
record_date: m.record_date,
@@ -193,7 +219,8 @@ pub async fn update_vital_signs(
audit_service::record(
AuditLog::new(tenant_id, operator_id, "vital_signs.updated", "vital_signs")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;
@@ -406,6 +433,16 @@ pub async fn update_lab_report(
let next_ver = check_version(expected_version, model.version)
.map_err(|_| HealthError::VersionMismatch)?;
// 记录变更前的关键字段items 为加密值,记录 meta 信息)
let old_values = serde_json::json!({
"report_date": model.report_date,
"report_type": model.report_type,
"status": model.status,
"has_items": model.items.is_some(),
"has_image_urls": model.image_urls.is_some(),
"has_doctor_notes": model.doctor_notes.is_some(),
});
let mut active: lab_report::ActiveModel = model.into();
if let Some(v) = req.report_date { active.report_date = Set(v); }
if let Some(v) = req.report_type { active.report_type = Set(v); }
@@ -430,9 +467,20 @@ pub async fn update_lab_report(
let m = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"report_date": m.report_date,
"report_type": m.report_type,
"status": m.status,
"has_items": m.items.is_some(),
"has_image_urls": m.image_urls.is_some(),
"has_doctor_notes": m.doctor_notes.is_some(),
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "lab_report.updated", "lab_report")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;
@@ -514,6 +562,7 @@ pub async fn review_lab_report(
validate_lab_report_status_transition(&model.status, "reviewed")?;
let old_status = model.status.clone();
let mut active: lab_report::ActiveModel = model.into();
active.status = Set("reviewed".to_string());
active.reviewed_by = Set(Some(reviewer_id));
@@ -539,7 +588,11 @@ pub async fn review_lab_report(
audit_service::record(
AuditLog::new(tenant_id, Some(reviewer_id), "lab_report.reviewed", "lab_report")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(
Some(serde_json::json!({ "status": old_status })),
Some(serde_json::json!({ "status": m.status, "reviewed_by": m.reviewed_by })),
),
&state.db,
).await;
@@ -675,6 +728,14 @@ pub async fn update_health_record(
let next_ver = check_version(expected_version, model.version)
.map_err(|_| HealthError::VersionMismatch)?;
// 记录变更前的关键字段
let old_values = serde_json::json!({
"record_type": model.record_type,
"record_date": model.record_date,
"overall_assessment": model.overall_assessment,
"notes": model.notes,
});
let mut active: health_record::ActiveModel = model.into();
if let Some(ref v) = req.record_type { validate_record_type(v)?; active.record_type = Set(v.clone()); }
if let Some(v) = req.record_date { active.record_date = Set(v); }
@@ -688,9 +749,18 @@ pub async fn update_health_record(
let m = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"record_type": m.record_type,
"record_date": m.record_date,
"overall_assessment": m.overall_assessment,
"notes": m.notes,
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "health_record.updated", "health_record")
.with_resource_id(m.id),
.with_resource_id(m.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;

View File

@@ -603,6 +603,15 @@ pub async fn update_family_member(
let kek = state.crypto.kek();
let hmac_key = state.crypto.hmac_key();
// 记录变更前的关键字段phone 为加密值,不记录原文)
let old_values = serde_json::json!({
"name": model.name,
"relationship": model.relationship,
"birth_date": model.birth_date,
"notes": model.notes,
});
let mut active: patient_family_member::ActiveModel = model.into();
active.name = Set(req.name);
active.relationship = Set(req.relationship);
@@ -621,9 +630,18 @@ pub async fn update_family_member(
let updated = active.update(&state.db).await?;
// 变更后快照
let new_values = serde_json::json!({
"name": updated.name,
"relationship": updated.relationship,
"birth_date": updated.birth_date,
"notes": updated.notes,
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "patient.family_member_updated", "patient_family_member")
.with_resource_id(updated.id),
.with_resource_id(updated.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;
@@ -958,6 +976,13 @@ pub async fn update_tag(
if tag.tenant_id != tenant_id { return Err(HealthError::TagNotFound); }
check_version(req.version, tag.version)?;
// 记录变更前的关键字段
let old_values = serde_json::json!({
"name": tag.name,
"color": tag.color,
"description": tag.description,
});
let mut active: patient_tag::ActiveModel = tag.into();
if let Some(name) = req.name { active.name = Set(name); }
if let Some(color) = req.color { active.color = Set(Some(color)); }
@@ -969,9 +994,17 @@ pub async fn update_tag(
let updated = active.update(&state.db).await
.map_err(|e: sea_orm::DbErr| HealthError::DbError(e.to_string()))?;
// 变更后快照
let new_values = serde_json::json!({
"name": updated.name,
"color": updated.color,
"description": updated.description,
});
audit_service::record(
AuditLog::new(tenant_id, operator_id, "patient_tag.update", "patient_tag")
.with_resource_id(updated.id),
.with_resource_id(updated.id)
.with_changes(Some(old_values), Some(new_values)),
&state.db,
).await;