- 增加登录日志

- 增加数据库索引
- 修改原生安装文档
This commit is contained in:
dushixiang
2021-01-20 22:57:26 +08:00
parent 1d4653a561
commit f0157dbaeb
20 changed files with 700 additions and 78 deletions

View File

@ -22,11 +22,14 @@ import {
DisconnectOutlined,
IdcardOutlined,
LinkOutlined,
LoginOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
SettingOutlined,
SolutionOutlined, TeamOutlined,
UserOutlined, UserSwitchOutlined
SolutionOutlined,
TeamOutlined,
UserOutlined,
UserSwitchOutlined
} from '@ant-design/icons';
import Info from "./components/user/Info";
import request from "./common/request";
@ -36,6 +39,7 @@ import BatchCommand from "./components/command/BatchCommand";
import {NT_PACKAGE} from "./utils/utils";
import {isAdmin} from "./service/permission";
import UserGroup from "./components/user/UserGroup";
import LoginLog from "./components/session/LoginLog";
const {Footer, Sider} = Layout;
@ -142,17 +146,16 @@ class App extends Component {
</Menu.Item>
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</Link>
</Menu.Item>
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
<Link to={'/asset'}>
资产列表
</Link>
</Menu.Item>
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='command-manage' title='指令管理' icon={<CodeOutlined/>}>
@ -183,15 +186,21 @@ class App extends Component {
历史会话
</Link>
</Menu.Item>
<Menu.Item key="login-log" icon={<LoginOutlined/>}>
<Link to={'/login-log'}>
登录日志
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined />}>
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
</Link>
</Menu.Item>
<Menu.Item key="user-group" icon={<TeamOutlined />}>
<Menu.Item key="user-group" icon={<TeamOutlined/>}>
<Link to={'/user-group'}>
用户组管理
</Link>
@ -243,6 +252,7 @@ class App extends Component {
<Route path="/batch-command" component={BatchCommand}/>
<Route path="/online-session" component={OnlineSession}/>
<Route path="/offline-session" component={OfflineSession}/>
<Route path="/login-log" component={LoginLog}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>

View File

@ -0,0 +1,399 @@
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";
import Logout from "../user/Logout";
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 page-herder"
title="登录日志"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
extra={[
<Logout key='logout'/>
]}
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}
>
{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

@ -474,20 +474,24 @@ class OfflineSession extends Component {
loading={this.state.loading}
/>
<Modal
className='monitor'
title="会话回放"
centered
visible={this.state.playbackVisible}
onCancel={this.hidePlayback}
{
this.state.playbackVisible ?
<Modal
className='monitor'
title="会话回放"
centered
visible={this.state.playbackVisible}
onCancel={this.hidePlayback}
width={window.innerWidth * 0.8}
footer={null}
destroyOnClose
maskClosable={false}
>
<Playback sessionId={this.state.playbackSessionId}/>
</Modal> : undefined
}
width={window.innerWidth * 0.8}
footer={null}
destroyOnClose
maskClosable={false}
>
<Playback sessionId={this.state.playbackSessionId}/>
</Modal>
</Content>
</>
);

View File

@ -142,7 +142,9 @@ class Playback extends Component {
this.setState({
recording: recording
})
}, () => {
this.handlePlayPause();
});
}
handlePlayPause = () => {
@ -150,7 +152,6 @@ class Playback extends Component {
if (recording) {
if (this.state.percent === this.state.max) {
// 重播
console.log('重新播放')
this.setState({
percent: 0
}, () => {