- 修复RDP协议连接导致的任意文件读取漏洞

- RDP协议增加「域」参数
- 增加安全访问功能
- 优化代码
This commit is contained in:
dushixiang
2021-03-11 21:16:29 +08:00
parent ba5bff1b38
commit 7f7edaa33c
45 changed files with 1020 additions and 158 deletions

View File

@ -0,0 +1,12 @@
.cron-log {
overflow: auto;
border: 0 none;
line-height: 23px;
padding: 15px;
margin: 0;
white-space: pre-wrap;
height: 500px;
background-color: rgb(51, 51, 51);
color: #f1f1f1;
border-radius: 0;
}

View File

@ -0,0 +1,612 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Dropdown,
Input,
Layout,
Menu,
Modal,
PageHeader,
Row,
Space,
Spin,
Switch,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import dayjs from "dayjs";
import JobModal from "./JobModal";
import './Job.css'
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'job',
breadcrumbName: '计划任务',
}
];
class Job extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
logPending: false,
logs: []
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/jobs/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/jobs/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此任务吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, obj = null) {
if (obj['func'] === 'shell-job') {
obj['shell'] = JSON.parse(obj['metadata'])['shell'];
}
if (obj['mode'] === 'custom') {
obj['resourceIds'] = obj['resourceIds'].split(',');
}
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
console.log(formData)
if (formData['func'] === 'shell-job') {
console.log(formData['shell'], JSON.stringify({'shell': formData['shell']}))
formData['metadata'] = JSON.stringify({'shell': formData['shell']});
formData['shell'] = undefined;
}
if (formData['mode'] === 'custom') {
let resourceIds = formData['resourceIds'];
formData['resourceIds'] = resourceIds.join(',');
}
if (formData.id) {
// 向后台提交数据
const result = await request.put('/jobs/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/jobs', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/jobs/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '任务名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
}
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
},
sorter: true,
}, {
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status, record) => {
return <Switch checkedChildren="开启" unCheckedChildren="关闭" checked={status === 'running'}
onChange={async (checked) => {
let jobStatus = checked ? 'running' : 'not-running';
let result = await request.post(`/jobs/${record['id']}/change-status?status=${jobStatus}`);
if (result['code'] === 1) {
message.success('操作成功');
await this.loadTableData();
} else {
message.error(result['message']);
}
}}
/>
}
}, {
title: '任务类型',
dataIndex: 'func',
key: 'func',
render: (func, record) => {
switch (func) {
case "check-asset-status-job":
return <Tag color="green">资产状态检测</Tag>;
case "shell-job":
return <Tag color="volcano">Shell脚本</Tag>;
default:
return '';
}
}
}, {
title: 'cron表达式',
dataIndex: 'cron',
key: 'cron'
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
title: '最后执行日期',
dataIndex: 'updated',
key: 'updated',
render: (text, record) => {
if (text === '0001-01-01 00:00:00') {
return '';
}
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
title: '操作',
key: 'action',
render: (text, record, index) => {
const menu = (
<Menu>
<Menu.Item key="0">
<Button type="text" size='small'
onClick={() => this.showModal('更新计划任务', record)}>编辑</Button>
</Menu.Item>
<Menu.Item key="2">
<Button type="text" size='small'
onClick={async () => {
this.setState({
logVisible: true,
logPending: true
})
let result = await request.get(`/jobs/${record['id']}/logs`);
if (result['code'] === 1) {
this.setState({
logPending: false,
logs: result['data'],
selectedRow: record
})
}
}}>日志</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3">
<Button type="text" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</Menu.Item>
</Menu>
);
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={async () => {
let items = this.state.items;
items[index]['execLoading'] = true;
this.setState({
items: items
});
let result = await request.post(`/jobs/${record['id']}/exec`);
if (result['code'] === 1) {
message.success('执行成功');
await this.loadTableData();
} else {
message.error(result['message']);
items[index]['execLoading'] = false;
this.setState({
items: items
});
}
}}>执行</Button>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
</Button>
</Dropdown>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper"
title="计划任务"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="计划任务"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>任务列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="任务名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增计划任务', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<JobModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</JobModal> : undefined
}
{
this.state.logVisible ?
<Modal
className='modal-no-padding'
width={window.innerWidth * 0.8}
title={'日志'}
visible={true}
maskClosable={false}
centered={true}
onOk={async () => {
let result = await request.delete(`/jobs/${this.state.selectedRow['id']}/logs`);
if (result['code'] === 1) {
this.setState({
logVisible: false,
selectedRow: undefined
})
message.success('日志清空成功');
} else {
message.error(result['message'], 10);
}
}}
onCancel={() => {
this.setState({
logVisible: false,
selectedRow: undefined
})
}}
okText='清空'
okType={'danger'}
cancelText='取消'
>
<Spin tip='加载中...' spinning={this.state.logPending}>
<pre className='cron-log'>
{
this.state.logs.map(item => {
return <><Divider
orientation="left"
style={{color: 'white'}}>{item['timestamp']}</Divider>{item['message']}</>;
})
}
</pre>
</Spin>
</Modal> : undefined
}
</Content>
</>
);
}
}
export default Job;

View File

@ -0,0 +1,129 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, Modal, Radio, Select, Spin} from "antd/lib/index";
import TextArea from "antd/es/input/TextArea";
import request from "../../common/request";
import {message} from "antd";
const JobModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model.func === undefined) {
model.func = 'shell-job';
}
if (model.mode === undefined) {
model.mode = 'all';
}
let [func, setFunc] = useState(model.func);
let [mode, setMode] = useState(model.mode);
let [resources, setResources] = useState([]);
useEffect(() => {
const fetchData = async () => {
setResourcesLoading(true);
let result = await request.get('/assets?protocol=ssh');
if (result['code'] === 1) {
setResources(result['data']);
} else {
message.error(result['message'], 10);
}
setResourcesLoading(false);
};
fetchData();
}, []);
let [resourcesLoading, setResourcesLoading] = useState(false);
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
centered={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="任务类型" name='func' rules={[{required: true, message: '请选择任务类型'}]}>
<Select onChange={(value) => {
setFunc(value);
}}>
<Select.Option value="shell-job">Shell脚本</Select.Option>
<Select.Option value="check-asset-status-job">资产状态检测</Select.Option>
</Select>
</Form.Item>
<Form.Item label="任务名称" name='name' rules={[{required: true, message: '请输入任务名称'}]}>
<Input autoComplete="off" placeholder="请输入任务名称"/>
</Form.Item>
{
func === 'shell-job' ?
<Form.Item label="Shell脚本" name='shell' rules={[{required: true, message: '请输入Shell脚本'}]}>
<TextArea autoSize={{minRows: 5, maxRows: 10}} placeholder="在此处填写Shell脚本内容"/>
</Form.Item> : undefined
}
<Form.Item label="cron表达式" name='cron' rules={[{required: true, message: '请输入cron表达式'}]}>
<Input placeholder="请输入cron表达式"/>
</Form.Item>
<Form.Item label="资产选择" name='mode' rules={[{required: true, message: '请选择资产'}]}>
<Radio.Group onChange={async (e) => {
setMode(e.target.value);
}}>
<Radio value={'all'}>全部资产</Radio>
<Radio value={'custom'}>自定义</Radio>
</Radio.Group>
</Form.Item>
{
mode === 'custom' ?
<Spin tip='加载中...' spinning={resourcesLoading}>
<Form.Item label="已选择资产" name='resourceIds' rules={[{required: true}]}>
<Select
mode="multiple"
allowClear
placeholder="请选择资产"
>
{
resources.map(item => {
return <Select.Option key={item['id']}>{item['name']}</Select.Option>
})
}
</Select>
</Form.Item>
</Spin>
: undefined
}
</Form>
</Modal>
)
};
export default JobModal;

View File

@ -0,0 +1,398 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
PageHeader,
Row,
Select,
Space,
Table,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {formatDate, isEmpty, itemRender} from "../../utils/utils";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'loginLog',
breadcrumbName: '登录日志',
}
];
class LoginLog extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
userId: undefined,
},
loading: false,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
};
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
}
async loadTableData(queryParams) {
queryParams = queryParams || this.state.queryParams;
this.setState({
queryParams: queryParams,
loading: true
});
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/login-logs/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
}
this.loadTableData(query);
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/login-logs/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'userName',
key: 'userName'
}, {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '浏览器',
dataIndex: 'clientUserAgent',
key: 'clientUserAgent',
render: (text, record) => {
if (isEmpty(text)) {
return '未知';
}
return (
<Tooltip placement="topLeft" title={text}>
{text.split(' ')[0]}
</Tooltip>
)
}
}, {
title: '登录时间',
dataIndex: 'loginTime',
key: 'loginTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '注销时间',
dataIndex: 'logoutTime',
key: 'logoutTime',
render: (text, record) => {
if (isEmpty(text) || text === '0001-01-01 00:00:00') {
return '';
}
return text;
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' onClick={() => {
confirm({
title: '您确定要删除此条登录日志吗?',
content: '删除用户未注销的登录日志将会强制用户下线',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
del(record.id)
}
});
const del = async (id) => {
const result = await request.delete(`/login-logs/${id}`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '删除成功',
});
this.loadTableData();
} else {
notification['error']({
message: '提示',
description: '删除失败 :( ' + result.message,
});
}
}
}}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper"
title="登录日志"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="只有登录成功的才会保存日志"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>登录日志列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 150}}
showSearch
value={this.state.queryParams.userId}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
allowClear
>
{userOptions}
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
</Content>
</>
);
}
}
export default LoginLog;

View File

@ -0,0 +1,421 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import './Job.css'
import SecurityModal from "./SecurityModal";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'security',
breadcrumbName: '访问安全',
}
];
class Security extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/securities/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/securities/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此任务吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, obj = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/securities/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/securities', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/securities/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: 'IP',
dataIndex: 'ip',
key: 'ip',
sorter: true,
}, {
title: '规则',
dataIndex: 'rule',
key: 'rule',
render: (rule) => {
if (rule === 'allow') {
return <Tag color={'green'}>允许</Tag>
} else {
return <Tag color={'red'}>禁止</Tag>
}
}
}, {
title: '优先级',
dataIndex: 'priority',
key: 'priority',
sorter: true,
}, {
title: '来源',
dataIndex: 'source',
key: 'source',
}, {
title: '操作',
key: 'action',
render: (text, record, index) => {
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.showModal('更新计划任务', record)}>编辑</Button>
<Button type="text" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper"
title="访问安全"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="IP访问规则限制"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>IP访问规则列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="IP"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增安全访问规则', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<SecurityModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</SecurityModal> : undefined
}
</Content>
</>
);
}
}
export default Security;

View File

@ -0,0 +1,66 @@
import React from 'react';
import {Form, Input, InputNumber, Modal, Radio} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model['priority'] === undefined) {
model['priority'] = 0;
}
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
centered={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="IP地址" name='ip' rules={[{required: true, message: '请输入IP地址'}]}>
<Input autoComplete="off" placeholder="支持单个IPCIDR或使用-连接的两个IP"/>
</Form.Item>
<Form.Item label="规则" name='rule' rules={[{required: true, message: '请选择规则'}]}>
<Radio.Group onChange={async (e) => {
}}>
<Radio value={'allow'}>允许</Radio>
<Radio value={'reject'}>拒绝</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]}>
<InputNumber min={0} max={100}/>
</Form.Item>
</Form>
</Modal>
)
};
export default SecurityModal;