fix: close permission review gaps
fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -85,15 +85,24 @@ interface StudentFilterLookups {
|
||||
|
||||
const StudentsPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission, hasAnyPermission } = usePermission();
|
||||
const { hasPermission, hasAnyPermission, hasAllPermissions } = usePermission();
|
||||
const canViewOrganizations = hasPermission('organization:view');
|
||||
const canChooseOrganization =
|
||||
canViewOrganizations && hasAnyPermission('student:create', 'student:edit');
|
||||
const canLoadOrganizations = hasAnyPermission(
|
||||
'organization:view',
|
||||
'student:create',
|
||||
'student:edit',
|
||||
);
|
||||
const canChooseOrganization = hasAnyPermission('student:create', 'student:edit');
|
||||
const canCreateStudent = hasPermission('student:create');
|
||||
const canEditStudent = hasPermission('student:edit');
|
||||
const canDeleteStudent = hasPermission('student:delete');
|
||||
const canSyncJinshuju = hasAllPermissions('sync:read', 'sync:trigger');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [organizations, setOrganizations] = useState<any[]>([]);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const canSaveStudent = editing ? canEditStudent : canCreateStudent;
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
||||
const [filterOrganizationId, setFilterOrganizationId] = useState<number | undefined>(undefined);
|
||||
@@ -118,13 +127,40 @@ const StudentsPage: React.FC = () => {
|
||||
|
||||
const [jinshujuOpen, setJinshujuOpen] = useState(false);
|
||||
|
||||
// Sensitive info modal — command-style; destroy when log:create is lost or comp unmounts.
|
||||
// Close the student form modal when the user loses the required permission.
|
||||
useEffect(() => {
|
||||
if (!canSaveStudent && modalOpen) {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [canSaveStudent, modalOpen, form]);
|
||||
|
||||
// Close sensitive modal when log:create is lost (imperative ref already set above).
|
||||
const logCreateRef = React.useRef(hasPermission('log:create'));
|
||||
const sensitiveModalRef = React.useRef<ReturnType<typeof modal.confirm> | null>(null);
|
||||
logCreateRef.current = hasPermission('log:create');
|
||||
useEffect(() => {
|
||||
if (!logCreateRef.current && sensitiveModalRef.current) {
|
||||
sensitiveModalRef.current.destroy();
|
||||
sensitiveModalRef.current = null;
|
||||
}
|
||||
return () => {
|
||||
sensitiveModalRef.current?.destroy();
|
||||
sensitiveModalRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleViewSensitive = (studentId: number, field: string, value: string) => {
|
||||
modal.confirm({
|
||||
if (!logCreateRef.current) return;
|
||||
sensitiveModalRef.current = modal.confirm({
|
||||
title: '查看敏感信息',
|
||||
content: `您即将查看 "${field}" 的完整信息。此操作将被记录。`,
|
||||
okText: '确认查看',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
if (!logCreateRef.current) return;
|
||||
try {
|
||||
await api.post('/operation-logs/audit', {
|
||||
module: '学生管理',
|
||||
@@ -142,6 +178,9 @@ const StudentsPage: React.FC = () => {
|
||||
message.error('审计日志记录失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
afterClose: () => {
|
||||
sensitiveModalRef.current = null;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -194,16 +233,25 @@ const StudentsPage: React.FC = () => {
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canLoadOrganizations) {
|
||||
setOrganizations([]);
|
||||
setFilterOrganizationId(undefined);
|
||||
return;
|
||||
}
|
||||
if (canViewOrganizations) {
|
||||
api
|
||||
.get('/organizations', { params: { includeArchived: 'false' } })
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
setOrganizations([]);
|
||||
setFilterOrganizationId(undefined);
|
||||
api
|
||||
.get('/organizations/options')
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string; isHost?: boolean }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
api
|
||||
.get<StudentFilterLookups>('/students/filter-lookups')
|
||||
@@ -212,7 +260,7 @@ const StudentsPage: React.FC = () => {
|
||||
setTeacherOptions(res.teachers || []);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [canViewOrganizations]);
|
||||
}, [canLoadOrganizations]);
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
@@ -614,21 +662,18 @@ const StudentsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<Popconfirm
|
||||
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
|
||||
onConfirm={() => handleRestore(record.id)}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:edit"
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
type="link"
|
||||
canEditStudent ? (
|
||||
<Popconfirm
|
||||
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
|
||||
onConfirm={() => handleRestore(record.id)}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
恢复
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null
|
||||
) : (
|
||||
<>
|
||||
<PermissionButton
|
||||
@@ -650,20 +695,21 @@ const StudentsPage: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title="归档后不会删除数据,可随时恢复。确定归档?"
|
||||
onConfirm={() => handleArchive(record.id)}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:delete"
|
||||
size="small"
|
||||
{canDeleteStudent ? (
|
||||
<Popconfirm
|
||||
title="归档后不会删除数据,可随时恢复。确定归档?"
|
||||
onConfirm={() => handleArchive(record.id)}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<InboxOutlined />}
|
||||
>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -765,23 +811,24 @@ const StudentsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="student:delete"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
{canDeleteStudent ? (
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchLoading}
|
||||
>
|
||||
批量归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="student:create"
|
||||
type="primary"
|
||||
@@ -814,13 +861,11 @@ const StudentsPage: React.FC = () => {
|
||||
</Upload>
|
||||
</>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="sync:read"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => setJinshujuOpen(true)}
|
||||
>
|
||||
同步金数据
|
||||
</PermissionButton>
|
||||
{canSyncJinshuju ? (
|
||||
<Button icon={<CloudUploadOutlined />} onClick={() => setJinshujuOpen(true)}>
|
||||
同步金数据
|
||||
</Button>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="student:view"
|
||||
icon={<DownloadOutlined />}
|
||||
@@ -929,8 +974,8 @@ const StudentsPage: React.FC = () => {
|
||||
title={editing ? '编辑学生' : '添加学生'}
|
||||
className="student-form-modal"
|
||||
width={720}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
open={modalOpen && canSaveStudent}
|
||||
onOk={canSaveStudent ? handleSave : undefined}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
@@ -1007,7 +1052,7 @@ const StudentsPage: React.FC = () => {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{hasPermission('sync:read') ? (
|
||||
{canSyncJinshuju ? (
|
||||
<JinshujuMatchModal
|
||||
open={jinshujuOpen}
|
||||
onClose={() => setJinshujuOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user