fix(health): 审计问题修复 — 权限守卫 + OAuth中间件 + FHIR声明 + SSE聚合
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

- OAuthClientList/RealtimeMonitor/OfflineEventList/StatisticsDashboard 补权限守卫
- OAuth 中间件注入 TenantContext + FHIR scope→permission 映射
- FHIR CapabilityStatement 移除未实现的 $lastn 操作
- useVitalSSE 修复批量同步事件数据聚合逻辑
This commit is contained in:
iven
2026-05-04 12:02:50 +08:00
parent d436888ca5
commit 1135439403
7 changed files with 80 additions and 15 deletions

View File

@@ -41,15 +41,17 @@ export function useVitalSSE(options: UseVitalSSEOptions = {}): UseVitalSSEReturn
setPatientVitals((prev) => {
const next = new Map(prev);
if (data.device_model) {
const key = `${data.patient_id}_${data.device_model}`;
next.set(key, {
patient_id: data.patient_id,
device_type: data.device_model,
latest_value: data.count > 0 ? undefined : undefined,
updated_at: data.occurred_at ?? new Date().toISOString(),
});
}
const key = `${data.patient_id}_${data.device_model ?? 'unknown'}`;
const existing = next.get(key);
next.set(key, {
patient_id: data.patient_id,
device_type: data.device_model ?? 'unknown',
// 批量同步事件不含单值,用累计 count 表示活跃度
latest_value: data.count > 0
? (existing?.latest_value ?? 0) + data.count
: existing?.latest_value,
updated_at: data.occurred_at ?? new Date().toISOString(),
});
return next;
});
setLastUpdate(data);

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import { Button, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography } from 'antd';
import { Button, Form, Input, InputNumber, message, Modal, Popconfirm, Result, Select, Space, Switch, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
@@ -11,8 +11,10 @@ import {
FHIR_SCOPE_OPTIONS,
} from '../../api/health/oauthClients';
import { PageContainer } from '../../components/PageContainer';
import { usePermission } from '../../hooks/usePermission';
export default function OAuthClientList() {
const { hasPermission } = usePermission('health.oauth.manage');
const [data, setData] = useState<OAuthClient[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -162,6 +164,10 @@ export default function OAuthClientList() {
},
];
if (!hasPermission) {
return <Result status="403" title="权限不足" subTitle="您没有管理 FHIR API 合作方的权限" />;
}
return (
<PageContainer
title="FHIR API 合作方管理"

View File

@@ -31,6 +31,8 @@ import {
type CreateOfflineEventReq,
} from '../../api/health/points';
import { AuthButton } from '../../components/AuthButton';
import { usePermission } from '../../hooks/usePermission';
import { Result } from 'antd';
/** 活动状态映射 */
const STATUS_MAP: Record<string, { text: string; color: string }> = {
@@ -48,6 +50,7 @@ const STATUS_OPTIONS = Object.entries(STATUS_MAP).map(([value, { text }]) => ({
}));
export default function OfflineEventList() {
const { hasPermission } = usePermission('health.points.list');
const [data, setData] = useState<OfflineEvent[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
@@ -58,6 +61,10 @@ export default function OfflineEventList() {
const [editing, setEditing] = useState<OfflineEvent | null>(null);
const [form] = Form.useForm();
if (!hasPermission) {
return <Result status="403" title="权限不足" subTitle="您没有访问线下活动管理的权限" />;
}
// ---- 数据获取 ----
const fetchData = useCallback(async (p = page, ps = pageSize) => {
setLoading(true);

View File

@@ -1,7 +1,8 @@
import { useState, useCallback, useEffect } from 'react';
import { Card, Row, Col, Statistic, Tag, List, Select, Badge, Typography, Space, Empty } from 'antd';
import { Card, Row, Col, Statistic, Tag, List, Select, Badge, Typography, Space, Empty, Result } from 'antd';
import { AlertOutlined } from '@ant-design/icons';
import { useVitalSSE } from '../../hooks/useVitalSSE';
import { usePermission } from '../../hooks/usePermission';
import { alertApi, type Alert } from '../../api/health/alerts';
import { PageContainer } from '../../components/PageContainer';
import { SEVERITY_COLOR, SEVERITY_LABEL, VITAL_CARD_METRICS } from '../../constants/health';
@@ -20,6 +21,7 @@ interface PatientAlertSummary {
* SSE 实时接收体征更新,按告警严重度排序患者列表。
*/
export default function RealtimeMonitor() {
const { hasPermission } = usePermission('health.alerts.list');
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(null);
@@ -68,6 +70,10 @@ export default function RealtimeMonitor() {
const totalMedium = alertSummary.reduce((s, a) => s + a.medium, 0);
const totalLow = alertSummary.reduce((s, a) => s + a.low, 0);
if (!hasPermission) {
return <Result status="403" title="权限不足" subTitle="您没有访问实时体征监控的权限" />;
}
return (
<PageContainer
title="实时体征监控"

View File

@@ -1,4 +1,6 @@
import { useDashboardRole } from '../../hooks/useDashboardRole';
import { usePermission } from '../../hooks/usePermission';
import { Result } from 'antd';
import { DoctorDashboard } from './StatisticsDashboard/DoctorDashboard';
import { NurseDashboard } from './StatisticsDashboard/NurseDashboard';
import { AdminDashboard } from './StatisticsDashboard/AdminDashboard';
@@ -13,7 +15,13 @@ const DASHBOARD_MAP: Record<string, React.FC> = {
};
export default function StatisticsDashboard() {
const { hasPermission } = usePermission('health.dashboard.manage');
const role = useDashboardRole();
if (!hasPermission) {
return <Result status="403" title="权限不足" subTitle="您没有访问统计面板的权限" />;
}
const DashboardComponent = DASHBOARD_MAP[role];
return <DashboardComponent />;
}