fix: audit remediation — SSE user scoping, FK transactional safety, UI error handling
- H4: scoped SSE import progress to exact userId match; non-HTTP events excluded from all subscribers - H2: moved PRAGMA foreign_key_check inside SQLite transaction before COMMIT; violations rollback preserving old tables - M1: removed dead axios-style error branch from extractErrorMessage (interceptor already unwraps) - M2: split handleSave try/catch — save errors vs reload errors shown distinctly - M3: added provider field validation before AI config test request - Added SSE scoping regression tests (import service + controller) - Added FK check failure rollback test (database-migrations.spec) - Updated controller spec expectations for userId parameter Co-authored-by: Code Review <branch-review>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined,
|
||||
SyncOutlined, BankOutlined, UserOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import type { TreeSelectProps } from 'antd/es/tree-select';
|
||||
@@ -64,6 +65,28 @@ interface ImportResult {
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
interface DingTalkAttendanceGroup {
|
||||
group_id: number;
|
||||
group_name: string;
|
||||
type: string;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
interface AttendanceGroupResponse {
|
||||
success: boolean;
|
||||
data: DingTalkAttendanceGroup[];
|
||||
}
|
||||
|
||||
interface DeleteAttendanceGroupsResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
total: number;
|
||||
deleted: Array<{ groupId: number; groupName: string }>;
|
||||
failed: Array<{ groupId: number; groupName: string; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const IntegrationConfigPage: React.FC = () => {
|
||||
const { hasAllPermissions } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -85,6 +108,10 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
const [classes, setClasses] = useState<ClassItem[]>([]);
|
||||
const [classForm] = Form.useForm();
|
||||
const [classModalOpen, setClassModalOpen] = useState(false);
|
||||
const [attendanceGroups, setAttendanceGroups] = useState<DingTalkAttendanceGroup[]>([]);
|
||||
const [deleteGroupsOpen, setDeleteGroupsOpen] = useState(false);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
const [deletingGroups, setDeletingGroups] = useState(false);
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
@@ -194,6 +221,7 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const buildTreeData = useCallback((nodes: DingOrgTreeNodeExt[]): DataNode[] => {
|
||||
return nodes.map((node) => {
|
||||
const users = node.users ?? [];
|
||||
@@ -282,6 +310,41 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteAllGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await api.get<AttendanceGroupResponse>('/sync/dingtalk/attendance-groups');
|
||||
setAttendanceGroups(response.data);
|
||||
setDeleteGroupsOpen(true);
|
||||
} catch (error: unknown) {
|
||||
message.error(error instanceof Error ? error.message : '获取钉钉考勤组失败');
|
||||
} finally {
|
||||
setLoadingGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAllGroups = async () => {
|
||||
setDeletingGroups(true);
|
||||
try {
|
||||
const response = await api.post<DeleteAttendanceGroupsResponse>(
|
||||
'/sync/dingtalk/attendance-groups/delete-all',
|
||||
);
|
||||
setDeleteGroupsOpen(false);
|
||||
setAttendanceGroups([]);
|
||||
if (response.data.failed.length > 0) {
|
||||
message.warning(
|
||||
`已删除 ${response.data.deleted.length} 个,失败 ${response.data.failed.length} 个`,
|
||||
);
|
||||
} else {
|
||||
message.success(`已删除钉钉全部 ${response.data.deleted.length} 个考勤组`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
message.error(error instanceof Error ? error.message : '删除钉钉考勤组失败');
|
||||
} finally {
|
||||
setDeletingGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncTabItems = config && hasAllPermissions('sync:read', 'class:view', 'class:edit')
|
||||
? [
|
||||
{
|
||||
@@ -314,6 +377,15 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
>
|
||||
获取组织架构
|
||||
</Button>
|
||||
<PermissionButton
|
||||
permission="sync:trigger"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={loadingGroups}
|
||||
onClick={openDeleteAllGroups}
|
||||
>
|
||||
删除钉钉全部考勤组
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
|
||||
{drawerOpen && (
|
||||
@@ -414,6 +486,38 @@ const IntegrationConfigPage: React.FC = () => {
|
||||
</Modal>
|
||||
</Drawer>
|
||||
)}
|
||||
<Modal
|
||||
title="确认删除钉钉全部考勤组"
|
||||
open={deleteGroupsOpen}
|
||||
okText="确认全部删除"
|
||||
okButtonProps={{ danger: true, disabled: attendanceGroups.length === 0 }}
|
||||
cancelText="取消"
|
||||
confirmLoading={deletingGroups}
|
||||
onOk={deleteAllGroups}
|
||||
onCancel={() => setDeleteGroupsOpen(false)}
|
||||
>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={`将永久删除钉钉上的 ${attendanceGroups.length} 个考勤组`}
|
||||
description="本地班级和排课不会删除。删除后需在排课管理中重新同步,才能重建考勤组。"
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<List
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={attendanceGroups}
|
||||
style={{ maxHeight: 280, overflow: 'auto' }}
|
||||
renderItem={(group) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={group.group_name}
|
||||
description={`ID ${group.group_id} · ${group.member_count} 人`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user