修复 「1.2.2 用户管理-用户列表勾选单一用户会全选 」 close #216

This commit is contained in:
dushixiang
2022-01-23 17:53:22 +08:00
parent 29c066ca3a
commit d35b348a33
130 changed files with 5467 additions and 4554 deletions

View File

@ -11,7 +11,7 @@
}
.logo {
margin: 24px;
margin: 30px 17px;
text-align: center;
}
@ -128,15 +128,7 @@
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
}
.km-header-logo {
font-size: 18px;
/*font-weight: 500;*/
font-weight: bold;
cursor: pointer;
color: white;
/*padding: 0 16px;*/
}
.km-header-right {
@ -145,12 +137,6 @@
margin: 0 8px;
}
.km-header-right {
text-align: right;
height: 100%;
margin: 0 8px;
}
.km-header-right-item {
cursor: pointer;
/*padding: 23px 12px;*/

View File

@ -12,6 +12,8 @@ import OfflineSession from "./components/session/OfflineSession";
import Login from "./components/Login";
import DynamicCommand from "./components/command/DynamicCommand";
import Credential from "./components/credential/Credential";
import LogoWithName from './images/logo-with-name.svg'
import Logo from './images/logo.svg'
import {
ApiOutlined,
AuditOutlined,
@ -23,7 +25,8 @@ import {
DesktopOutlined,
DisconnectOutlined,
DownOutlined,
FolderOutlined, GithubOutlined,
FolderOutlined,
GithubOutlined,
HddOutlined,
IdcardOutlined,
InsuranceOutlined,
@ -73,13 +76,26 @@ class App extends Component {
'nickname': '未定义'
},
package: NT_PACKAGE(),
triggerMenu: true
triggerMenu: true,
logo: LogoWithName,
logoWidth: 140
};
onCollapse = () => {
this.setState({
collapsed: !this.state.collapsed,
});
let collapsed = !this.state.collapsed;
if (collapsed) {
this.setState({
logo: Logo,
logoWidth: 46,
collapsed: collapsed,
});
} else {
this.setState({
logo: LogoWithName,
logoWidth: 140,
collapsed: collapsed,
});
}
};
componentDidMount() {
@ -94,7 +110,7 @@ class App extends Component {
async getInfo() {
let result = await request.get('/info');
let result = await request.get('/account/info');
if (result['code'] === 1) {
sessionStorage.setItem('user', JSON.stringify(result['data']));
this.setState({
@ -126,8 +142,8 @@ class App extends Component {
sessionStorage.setItem('openKeys', JSON.stringify(openKeys));
}
confirm = async (e) => {
let result = await request.post('/logout');
confirm = async () => {
let result = await request.post('/account/logout');
if (result['code'] !== 1) {
message.error(result['message']);
} else {
@ -180,14 +196,7 @@ class App extends Component {
<>
<Sider collapsible collapsed={this.state.collapsed} trigger={null}>
<div className="logo">
<img
src='data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik0yNzIgMTIyLjI0aDQ4MHYxNTcuMDU2aDk2Vi40NDhoLTk2TDI3MiAwYy01Mi44IDAtOTYgLjQ0OC05NiAuNDQ4djI3OC44NDhoOTZ2LTE1Ny4xMnptNDAzLjY0OCA2MDMuMzkyTDg5NiA1MTIgNjc1LjY0OCAyOTguMzY4IDYwOCAzNjQuNDggNzYwLjEyOCA1MTIgNjA4IDY1OS41Mmw2Ny42NDggNjYuMTEyek00MTYgNjU5LjUyTDI2My44MDggNTEyIDQxNiAzNjQuNDhsLTY3LjcxMi02Ni4xMTJMMTI4IDUxMmwyMjAuMjg4IDIxMy42MzJMNDE2IDY1OS41MnptMzM2IDI0Mi4zMDRIMjcydi0xNTcuMTJoLTk2VjEwMjRoNjcyVjc0NC43MDRoLTk2djE1Ny4xMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='
alt='logo'/>
{
!this.state.collapsed ?
<>&nbsp;<h1>Next Terminal</h1></> :
null
}
<img src={this.state.logo} alt='logo' width={this.state.logoWidth}/>
</div>
<Menu
@ -314,8 +323,10 @@ class App extends Component {
<div className='layout-header-right'>
<div className={'layout-header-right-item'}>
<a style={{color: 'black'}} target='_blank' href='https://github.com/dushixiang/next-terminal' rel='noreferrer noopener'>
<GithubOutlined />
<a style={{color: 'black'}} target='_blank'
href='https://github.com/dushixiang/next-terminal'
rel='noreferrer noopener'>
<GithubOutlined/>
</a>
</div>
</div>
@ -350,7 +361,8 @@ class App extends Component {
<Route path="/strategy" component={Strategy}/>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
Copyright © 2020-2022 dushixiang, All Rights Reserved.
Version:{this.state.package['version']}
</Footer>
</Layout>
</> :
@ -359,11 +371,7 @@ class App extends Component {
<div className='km-header'>
<div style={{flex: '1 1 0%'}}>
<Link to={'/'}>
<img
style={{paddingBottom: 4, marginRight: 5}}
src='data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik0yNzIgMTIyLjI0aDQ4MHYxNTcuMDU2aDk2Vi40NDhoLTk2TDI3MiAwYy01Mi44IDAtOTYgLjQ0OC05NiAuNDQ4djI3OC44NDhoOTZ2LTE1Ny4xMnptNDAzLjY0OCA2MDMuMzkyTDg5NiA1MTIgNjc1LjY0OCAyOTguMzY4IDYwOCAzNjQuNDggNzYwLjEyOCA1MTIgNjA4IDY1OS41Mmw2Ny42NDggNjYuMTEyek00MTYgNjU5LjUyTDI2My44MDggNTEyIDQxNiAzNjQuNDhsLTY3LjcxMi02Ni4xMTJMMTI4IDUxMmwyMjAuMjg4IDIxMy42MzJMNDE2IDY1OS41MnptMzM2IDI0Mi4zMDRIMjcydi0xNTcuMTJoLTk2VjEwMjRoNjcyVjc0NC43MDRoLTk2djE1Ny4xMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='
alt='logo'/>
<span className='km-header-logo'>Next Terminal</span>
<img src={this.state.logo} alt='logo' width={120}/>
</Link>
<Link to={'/my-file'}>
@ -400,7 +408,8 @@ class App extends Component {
</Layout>
</Content>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
Copyright © 2020-2022 dushixiang, All Rights Reserved.
Version:{this.state.package['version']}
</Footer>
</>
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,9 @@ import request from "../common/request";
import {message} from "antd/es";
import {withRouter} from "react-router-dom";
import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons';
import Particles from "react-tsparticles";
import Background from '../images/bg.png'
import {setToken} from "../utils/utils";
const {Title} = Typography;
@ -56,7 +59,7 @@ class LoginForm extends Component {
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
localStorage.setItem('X-Auth-Token', result['data']);
setToken(result['data']);
// this.props.history.push();
window.location.href = "/"
} catch (e) {
@ -85,7 +88,7 @@ class LoginForm extends Component {
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
localStorage.setItem('X-Auth-Token', result['data']);
setToken(result['data']);
// this.props.history.push();
window.location.href = "/"
} catch (e) {
@ -106,7 +109,90 @@ class LoginForm extends Component {
render() {
return (
<div className='login-bg'
style={{width: this.state.width, height: this.state.height, backgroundColor: '#F0F2F5'}}>
style={{width: this.state.width, height: this.state.height}}>
<Particles
id="tsparticles"
options={{
background: {
color: {
// value: "#0d47a1",
},
image: `url(${Background})`,
repeat: 'no-repeat',
size: '100% 100%'
},
fpsLimit: 60,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: true,
mode: "repulse",
},
resize: true,
},
modes: {
bubble: {
distance: 400,
duration: 2,
opacity: 0.8,
size: 40,
},
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#ffffff",
},
links: {
color: "#ffffff",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
collisions: {
enable: true,
},
move: {
direction: "none",
enable: true,
outMode: "bounce",
random: false,
speed: 6,
straight: false,
},
number: {
density: {
enable: true,
value_area: 800,
},
value: 80,
},
opacity: {
value: 0.5,
},
shape: {
type: "circle",
},
size: {
random: true,
value: 5,
},
},
detectRetina: true,
}}
/>
<Card className='login-card' title={null}>
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
<Title level={1}>Next Terminal</Title>
@ -140,9 +226,6 @@ class LoginForm extends Component {
.then(values => {
this.handleOk(values);
// this.formRef.current.resetFields();
})
.catch(info => {
});
}}
onCancel={this.handleCancel}>

View File

@ -1,3 +1,3 @@
.container div {
.container > div {
margin: 0 auto;
}

View File

@ -13,7 +13,7 @@ import {
LineChartOutlined,
WindowsOutlined
} from '@ant-design/icons';
import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils";
import {exitFull, getToken, isEmpty, requestFullScreen, setToken} from "../../utils/utils";
import './Access.css'
import Draggable from 'react-draggable';
import FileSystem from "../devops/FileSystem";
@ -38,13 +38,14 @@ class Access extends Component {
state = {
session: {},
sessionId: '',
client: {},
client: undefined,
scale: 1,
clientState: STATE_IDLE,
clipboardVisible: false,
clipboardText: '',
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
containerWidth: 1024,
containerHeight: 768,
uploadAction: '',
uploadHeaders: {},
keyboard: {},
@ -57,15 +58,39 @@ class Access extends Component {
fullScreenBtnText: '进入全屏',
sink: undefined,
commands: [],
showFileSystem: false
showFileSystem: false,
external: false,
fixedSize: false,
};
async componentDidMount() {
let urlParams = new URLSearchParams(this.props.location.search);
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
let protocol = urlParams.get('protocol');
let width = urlParams.get('width');
let height = urlParams.get('height');
let fixedSize = false;
if (width && height) {
fixedSize = true
} else {
width = window.innerWidth;
height = window.innerHeight;
}
let shareSessionId = urlParams.get('shareSessionId');
let external = false;
if (shareSessionId && shareSessionId !== '') {
setToken(shareSessionId);
external = true;
let shareSession = await this.getShareSession(shareSessionId);
if (!shareSession) {
return
}
assetId = shareSession['assetId'];
}
let session = await this.createSession(assetId);
if (!session) {
return;
@ -79,10 +104,14 @@ class Access extends Component {
session: session,
sessionId: sessionId,
protocol: protocol,
showFileSystem: session['fileSystem'] === '1'
showFileSystem: session['fileSystem'] === '1',
external: external,
fixedSize: fixedSize,
containerWidth: width,
containerHeight: height,
});
this.renderDisplay(sessionId, protocol);
this.renderDisplay(sessionId, protocol, width, height);
window.addEventListener('resize', this.onWindowResize);
window.onfocus = this.onWindowFocus;
@ -95,6 +124,10 @@ class Access extends Component {
}
sendClipboard(data) {
if (this.state.session['paste'] === '0') {
message.warn('禁止粘贴');
return
}
let writer;
// Create stream with proper mimetype
@ -133,6 +166,7 @@ class Access extends Component {
}
onTunnelStateChange = (state) => {
console.log(state)
if (state === Guacamole.Tunnel.State.CLOSED) {
console.log('web socket 已关闭');
}
@ -175,12 +209,13 @@ class Access extends Component {
break;
case STATE_CONNECTED:
this.onWindowResize(null);
Modal.destroyAll();
message.destroy();
message.success('连接成功');
// 向后台发送请求,更新会话的状态
this.updateSessionStatus(this.state.sessionId).then(_ => {
})
if (this.state.protocol === 'ssh') {
if (this.state.protocol === 'ssh' && !this.state.external) {
// 加载指令
this.getCommands();
}
@ -300,9 +335,11 @@ class Access extends Component {
}
clientClipboardReceived = (stream, mimetype) => {
console.log('clientClipboardReceived', mimetype)
if (this.state.session['copy'] === '0') {
message.warn('禁止复制');
return
}
let reader;
// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {
reader = new Guacamole.StringReader(stream);
@ -378,9 +415,18 @@ class Access extends Component {
return result['data'];
}
async renderDisplay(sessionId, protocol) {
async getShareSession(shareSessionId) {
let result = await request.get(`/share-sessions/${shareSessionId}`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return undefined;
}
return result['data'];
}
let tunnel = new Guacamole.WebSocketTunnel(wsServer + '/tunnel');
async renderDisplay(sessionId, protocol, width, height) {
let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel`);
tunnel.onstatechange = this.onTunnelStateChange;
// Get new client instance
@ -404,17 +450,16 @@ class Access extends Component {
const element = client.getDisplay().getElement();
display.appendChild(element);
let width = window.innerWidth;
let height = window.innerHeight;
let scale = 1;
let dpi = 96;
if (protocol === 'ssh' || protocol === 'telnet') {
dpi = dpi * 2;
scale = 0.5;
}
let token = getToken();
let params = {
'sessionId': sessionId,
'width': width,
'height': height,
'dpi': dpi,
@ -439,13 +484,9 @@ class Access extends Component {
};
mouse.onmousemove = function (mouseState) {
if (protocol === 'ssh' || protocol === 'telnet') {
mouseState.x = mouseState.x * 2;
mouseState.y = mouseState.y * 2;
client.sendMouseState(mouseState);
} else {
client.sendMouseState(mouseState);
}
mouseState.x = mouseState.x / scale;
mouseState.y = mouseState.y / scale;
client.sendMouseState(mouseState);
};
const sink = new Guacamole.InputSink();
@ -460,8 +501,7 @@ class Access extends Component {
this.setState({
client: client,
containerWidth: width,
containerHeight: height,
scale: scale,
keyboard: keyboard,
sink: sink
});
@ -469,19 +509,14 @@ class Access extends Component {
onWindowResize = (e) => {
if (this.state.client) {
if (this.state.client && !this.state.fixedSize) {
const display = this.state.client.getDisplay();
let scale = this.state.scale;
display.scale(scale);
let width = window.innerWidth;
let height = window.innerHeight;
const width = window.innerWidth;
const height = window.innerHeight;
if (this.state.protocol === 'ssh' || this.state.protocol === 'telnet') {
let r = 2;
display.scale(1 / r);
this.state.client.sendSize(width * r, height * r);
} else {
this.state.client.sendSize(width, height);
}
this.state.client.sendSize(width / scale, height / scale);
this.setState({
containerWidth: width,
@ -502,47 +537,17 @@ class Access extends Component {
onWindowFocus = (e) => {
if (navigator.clipboard && this.state.clientState === STATE_CONNECTED) {
navigator.clipboard.readText().then((text) => {
this.sendClipboard({
'data': text,
'type': 'text/plain'
});
})
}
};
onPaste = (e) => {
const cbd = e.clipboardData;
const ua = window.navigator.userAgent;
// 如果是 Safari 直接 return
if (!(e.clipboardData && e.clipboardData.items)) {
return;
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if (cbd.items && cbd.items.length === 2 && cbd.items[0].kind === "string" && cbd.items[1].kind === "file" &&
cbd.types && cbd.types.length === 2 && cbd.types[0] === "text/plain" && cbd.types[1] === "Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49) {
return;
}
for (let i = 0; i < cbd.items.length; i++) {
let item = cbd.items[i];
if (item.kind === "file") {
let blob = item.getAsFile();
if (blob.size === 0) {
return;
}
// blob 就是从剪切板获得的文件 可以进行上传或其他操作
} else if (item.kind === 'string') {
item.getAsString((str) => {
try {
navigator.clipboard.readText().then((text) => {
this.sendClipboard({
'data': str,
'data': text,
'type': 'text/plain'
});
})
} catch (e) {
// console.error(e);
}
}
};
@ -616,7 +621,8 @@ class Access extends Component {
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight
height: this.state.containerHeight,
margin: '0 auto'
}}>
<div id="display"/>
</div>
@ -630,16 +636,20 @@ class Access extends Component {
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button icon={<CopyOutlined/>} disabled={this.state.clientState !== STATE_CONNECTED}
onClick={() => {
this.setState({
clipboardVisible: true
});
}}/>
</Affix>
</Draggable>
{
this.state.session['copy'] === '1' || this.state.session['paste'] === '1' ?
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button icon={<CopyOutlined/>} disabled={this.state.clientState !== STATE_CONNECTED}
onClick={() => {
this.setState({
clipboardVisible: true
});
}}/>
</Affix>
</Draggable> : undefined
}
{
this.state.protocol === 'vnc' ?
@ -734,7 +744,6 @@ class Access extends Component {
placement="right"
width={window.innerWidth * 0.8}
closable={true}
// maskClosable={false}
onClose={() => {
this.focus();
this.setState({

View File

@ -53,8 +53,8 @@ class Term extends Component {
},
rightClickSelectsWord: true,
});
term.open(document.getElementById('terminal'));
let elementTerm = document.getElementById('terminal');
term.open(elementTerm);
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
@ -64,7 +64,6 @@ class Term extends Component {
term.onSelectionChange(async () => {
let selection = term.getSelection();
console.log(`selection: [${selection}]`);
this.setState({
selection: selection
})
@ -80,6 +79,29 @@ class Term extends Component {
return !(e.ctrlKey && e.key === 'v');
});
document.body.oncopy = (event) => {
event.preventDefault();
if (this.state.session['copy'] === '0') {
message.warn('禁止复制')
if (event.clipboardData) {
return event.clipboardData.setData('text', '');
} else {
// 兼容IE
return window.clipboardData.setData("text", '');
}
}
return true;
}
document.body.onpaste = (event) => {
event.preventDefault();
if (this.state.session['paste'] === '0') {
message.warn('禁止粘贴')
return false;
}
return true;
}
term.onData(data => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {

View File

@ -38,8 +38,6 @@ const AccessGatewayModal = ({title, visible, handleOk, handleCancel, confirmLoad
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
@ -102,10 +100,6 @@ const AccessGatewayModal = ({title, visible, handleOk, handleCancel, confirmLoad
</Form.Item>
</>
}
<Form.Item label="本地映射地址" name='localhost' tooltip='隧道映射到本地的地址请确保Guacd可以访问到此IP'>
<Input placeholder="localhost"/>
</Form.Item>
</Form>
</Modal>
)

View File

@ -284,6 +284,7 @@ class Asset extends Component {
asset['ignore-cert'] = asset['ignore-cert'] === 'true';
asset['enable-drive'] = asset['enable-drive'] === 'true';
asset['socks-proxy-enable'] = asset['socks-proxy-enable'] === 'true';
asset['force-lossless'] = asset['force-lossless'] === 'true';
this.setState({
modalTitle: title,

View File

@ -1,5 +1,6 @@
import React, {useEffect, useState} from 'react';
import {
Alert,
Col,
Collapse,
Form,
@ -239,20 +240,29 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
{
accountType === 'credential' ?
<Form.Item label="授权凭证" name='credentialId'
rules={[{required: true, message: '请选择授权凭证'}]}>
<Select onChange={() => null}>
{credentials.map(item => {
return (
<Option key={item.id} value={item.id}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
<>
{protocol === 'ssh' ?
<Form.Item wrapperCol={{offset: 6}}>
<Alert
message="GUACD 对ED25519、RSA等密钥类型支持不完善请选择原生模式进行连接。"
type="info"
/>
</Form.Item> : null}
<Form.Item label="授权凭证" name='credentialId'
rules={[{required: true, message: '请选择授权凭证'}]}>
<Select onChange={() => null}>
{credentials.map(item => {
return (
<Option key={item.id} value={item.id}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
</>
: null
}
@ -274,6 +284,13 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
{
accountType === 'private-key' ?
<>
<Form.Item wrapperCol={{offset: 6}}>
<Alert
message="GUACD 对ED25519、RSA等密钥类型支持不完善请选择原生模式进行连接。"
type="info"
/>
</Form.Item>
<Form.Item label="授权账户" name='username'>
<Input placeholder="输入授权账户"/>
</Form.Item>
@ -321,11 +338,39 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
</Form.Item>
</Col>
<Col span={11}>
<Collapse defaultActiveKey={['remote-app', '认证', 'VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为', 'socks']}
ghost>
<Collapse
defaultActiveKey={['remote-app', '认证', 'VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为', 'socks']}
ghost>
{
protocol === 'rdp' ?
<>
<Panel header={<Text strong>显示设置</Text>} key="">
<Form.Item
name="color-depth"
label="色彩深度"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="16">低色16</Option>
<Option value="24">真彩24</Option>
<Option value="32">真彩32</Option>
<Option value="8">256</Option>
</Select>
</Form.Item>
<Form.Item
name="force-lossless"
label="无损压缩"
valuePropName="checked"
rules={[
{
required: true,
},
]}
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>认证</Text>} key="">
<Form.Item
name="domain"

View File

@ -131,7 +131,7 @@ class Dashboard extends Component {
<div style={{margin: 16, marginBottom: 0}}>
<Row gutter={16}>
<Col span={6}>
<Card bordered={true} hoverable={true}>
<Card bordered={true} hoverable>
<Link to={'/user'}>
<Statistic title="在线用户" value={this.state.counter['user']}
prefix={<UserOutlined/>}/>
@ -139,7 +139,7 @@ class Dashboard extends Component {
</Card>
</Col>
<Col span={6}>
<Card bordered={true} hoverable={true}>
<Card bordered={true} hoverable>
<Link to={'/asset'}>
<Statistic title="资产数量" value={this.state.counter['asset']}
prefix={<DesktopOutlined/>}/>
@ -147,7 +147,7 @@ class Dashboard extends Component {
</Card>
</Col>
<Col span={6}>
<Card bordered={true} hoverable={true}>
<Card bordered={true} hoverable>
<Link to={'/credential'} hoverable>
<Statistic title="授权凭证" value={this.state.counter['credential']}
prefix={<IdcardOutlined/>}/>
@ -156,7 +156,7 @@ class Dashboard extends Component {
</Card>
</Col>
<Col span={6}>
<Card bordered={true} hoverable={true}>
<Card bordered={true} hoverable>
<Link to={'/online-session'}>
<Statistic title="在线会话" value={this.state.counter['onlineSession']}
prefix={<LinkOutlined/>}/>

View File

@ -1,8 +1,7 @@
import React, {Component} from 'react';
import {Alert, Button, Form, Input, Layout, Select, Space, Switch, Tabs, Tooltip, Typography} from "antd";
import {Alert, Button, Form, Input, Layout, Select, Space, Switch, Tabs, Typography} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import {download, getToken} from "../../utils/utils";
import {server} from "../../common/env";
@ -24,7 +23,9 @@ const formTailLayout = {
class Setting extends Component {
state = {
properties: {}
refs: [],
properties: {},
ldapUserSyncLoading: false
}
rdpSettingFormRef = React.createRef();
@ -32,10 +33,18 @@ class Setting extends Component {
vncSettingFormRef = React.createRef();
guacdSettingFormRef = React.createRef();
mailSettingFormRef = React.createRef();
ldapSettingFormRef = React.createRef();
logSettingFormRef = React.createRef();
componentDidMount() {
this.getProperties();
// eslint-disable-next-line no-extend-native
String.prototype.bool = function () {
return (/^true$/i).test(this);
};
this.setState({
refs: [this.rdpSettingFormRef, this.sshSettingFormRef, this.vncSettingFormRef, this.guacdSettingFormRef, this.mailSettingFormRef, this.logSettingFormRef]
}, this.getProperties)
}
changeProperties = async (values) => {
@ -49,11 +58,6 @@ class Setting extends Component {
getProperties = async () => {
// eslint-disable-next-line no-extend-native
String.prototype.bool = function () {
return (/^true$/i).test(this);
};
let result = await request.get('/properties');
if (result['code'] === 1) {
let properties = result['data'];
@ -74,28 +78,10 @@ class Setting extends Component {
properties: properties
})
if (this.rdpSettingFormRef.current) {
this.rdpSettingFormRef.current.setFieldsValue(properties)
}
if (this.sshSettingFormRef.current) {
this.sshSettingFormRef.current.setFieldsValue(properties)
}
if (this.vncSettingFormRef.current) {
this.vncSettingFormRef.current.setFieldsValue(properties)
}
if (this.guacdSettingFormRef.current) {
this.guacdSettingFormRef.current.setFieldsValue(properties)
}
if (this.mailSettingFormRef.current) {
this.mailSettingFormRef.current.setFieldsValue(properties)
}
if (this.logSettingFormRef.current) {
this.logSettingFormRef.current.setFieldsValue(properties)
for (let ref of this.state.refs) {
if (ref.current) {
ref.current.setFieldsValue(properties)
}
}
} else {
message.error(result['message']);
@ -135,6 +121,26 @@ class Setting extends Component {
reader.readAsText(files[0]);
}
ldapUserSync = async () => {
const id = 'ldap-user-sync'
try {
this.setState({
ldapUserSyncLoading: true
});
message.info({content: '同步中...', key: id, duration: 5});
let result = await request.post(`/properties/ldap-user-sync`);
if (result.code !== 1) {
message.error({content: result.message, key: id, duration: 10});
return;
}
message.success({content: '同步成功。', key: id, duration: 3});
} finally {
this.setState({
ldapUserSyncLoading: false
});
}
}
render() {
return (
<>
@ -254,20 +260,6 @@ class Setting extends Component {
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-glyph-caching"
label="禁用字形缓存"
valuePropName="checked"
rules={[
{
required: true,
},
]}
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
更新
@ -409,18 +401,18 @@ class Setting extends Component {
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label={<Tooltip
title="连接到VNC代理例如UltraVNC Repeater时要请求的目标主机。">目标主机&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
{...formItemLayout}
name='dest-host'>
<Form.Item
{...formItemLayout}
label='目标主机'
tooltip='连接到VNC代理例如UltraVNC Repeater时要请求的目标主机。'
name='dest-host'>
<Input placeholder="目标主机"/>
</Form.Item>
<Form.Item label={<Tooltip
title="连接到VNC代理例如UltraVNC Repeater时要请求的目标端口。">目标端口&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
{...formItemLayout}
name='dest-port'>
<Form.Item
{...formItemLayout}
label='目标端口'
tooltip='连接到VNC代理例如UltraVNC Repeater时要请求的目标端口。'
name='dest-port'>
<Input type='number' min={1} max={65535}
placeholder='目标端口'/>
</Form.Item>
@ -457,26 +449,21 @@ class Setting extends Component {
})
}}/>
</Form.Item>
{
this.state.properties['enable-recording'] === true ?
<>
<Form.Item
{...formItemLayout}
name="session-saved-limit"
label="会话录屏保存时长"
initialValue=""
>
<Select onChange={null}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
</> : null
}
<Form.Item
{...formItemLayout}
name="session-saved-limit"
label="会话录屏保存时长"
initialValue=""
>
<Select onChange={null} disabled={!this.state.properties['enable-recording']}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
@ -487,6 +474,11 @@ class Setting extends Component {
</TabPane>
<TabPane tab="邮箱配置" key="mail">
<Title level={3}>邮箱配置</Title>
<Alert
message="配置邮箱后,添加用户将向对方的邮箱发送账号密码。"
type="info"
style={{marginBottom: 10}}
/>
<Form ref={this.mailSettingFormRef} name='mail' onFinish={this.changeProperties}
layout="vertical">
@ -534,7 +526,7 @@ class Setting extends Component {
>
<Input type='email' placeholder="请输入邮箱账号"/>
</Form.Item>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
{...formItemLayout}
name="mail-password"
@ -555,6 +547,7 @@ class Setting extends Component {
</Button>
</Form.Item>
</Form>
</TabPane>
<TabPane tab="日志配置" key="log">

View File

@ -1,5 +1,20 @@
import React, {Component} from 'react';
import {Button, Card, Divider, Form, Image, Input, Layout, Modal, Result, Space, Typography} from "antd";
import {
Button,
Card,
Col,
Descriptions,
Divider,
Form,
Image,
Input,
Layout,
Modal,
Result,
Row,
Space,
Typography
} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
@ -7,12 +22,13 @@ import {isAdmin} from "../../service/permission";
const {Content} = Layout;
const {Meta} = Card;
const {Title} = Typography;
const {Title, Text} = Typography;
const formItemLayout = {
labelCol: {span: 4},
wrapperCol: {span: 10},
};
const formTailLayout = {
labelCol: {span: 4},
wrapperCol: {span: 10, offset: 4},
@ -24,17 +40,19 @@ class Info extends Component {
state = {
user: {
enableTotp: false
}
},
accessToken: {}
}
passwordFormRef = React.createRef();
componentDidMount() {
this.loadInfo();
this.loadAccessToken();
}
loadInfo = async () => {
let result = await request.get('/info');
let result = await request.get('/account/info');
if (result['code'] === 1) {
this.setState({
user: result['data']
@ -45,6 +63,26 @@ class Info extends Component {
}
}
loadAccessToken = async () => {
let result = await request.get('/account/access-token');
if (result['code'] === 1) {
this.setState({
accessToken: result['data']
})
} else {
message.error(result['message']);
}
}
genAccessToken = async () => {
let result = await request.post('/account/access-token');
if (result['code'] === 1) {
this.loadAccessToken();
} else {
message.error(result['message']);
}
}
onNewPasswordChange(value) {
this.setState({
'newPassword': value.target.value
@ -72,7 +110,7 @@ class Info extends Component {
}
changePassword = async (values) => {
let result = await request.post('/change-password', values);
let result = await request.post('/account/change-password', values);
if (result.code === 1) {
message.success('密码修改成功,即将跳转至登录页面');
window.location.href = '/#';
@ -83,7 +121,7 @@ class Info extends Component {
confirmTOTP = async (values) => {
values['secret'] = this.state.secret
let result = await request.post('/confirm-totp', values);
let result = await request.post('/account/confirm-totp', values);
if (result.code === 1) {
message.success('TOTP启用成功');
await this.loadInfo();
@ -97,7 +135,7 @@ class Info extends Component {
}
resetTOTP = async () => {
let result = await request.get('/reload-totp');
let result = await request.get('/account/reload-totp');
if (result.code === 1) {
this.setState({
qr: result.data.qr,
@ -113,60 +151,82 @@ class Info extends Component {
return (
<>
<Content className={["site-layout-background", contentClassName]}>
<Title level={3}>修改密码</Title>
<Form ref={this.passwordFormRef} name="password" onFinish={this.changePassword}>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
{...formItemLayout}
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码" style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码"
onChange={(value) => this.onNewPasswordChange(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={this.state.validateStatus}
help={this.state.errorMsg || ''}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致"
onChange={(value) => this.onNewPassword2Change(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
<Row>
<Col span={12}>
<Title level={3}>修改密码</Title>
<Form ref={this.passwordFormRef} name="password" onFinish={this.changePassword}>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
{...formItemLayout}
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码" style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码"
onChange={(value) => this.onNewPasswordChange(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={this.state.validateStatus}
help={this.state.errorMsg || ''}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致"
onChange={(value) => this.onNewPassword2Change(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
<Divider/>
</Col>
<Col span={12}>
<Title level={3}>授权信息</Title>
<Descriptions column={1}>
<Descriptions.Item label="授权令牌">
<Text strong copyable>{this.state.accessToken.token}</Text>
</Descriptions.Item>
<Descriptions.Item label="生成时间">
<Text strong>{this.state.accessToken.created}</Text>
</Descriptions.Item>
</Descriptions>
<Space>
<Button type="primary" onClick={this.genAccessToken}>
重新生成
</Button>
</Space>
</Col>
</Row>
<Divider/>
<Title level={3}>双因素认证</Title>
<Form hidden={this.state.qr}>
@ -187,7 +247,7 @@ class Info extends Component {
okType: 'danger',
cancelText: '取消',
onOk: async () => {
let result = await request.post('/reset-totp');
let result = await request.post('/account/reset-totp');
if (result.code === 1) {
message.success('双因素认证解除成功');
await this.loadInfo();

View File

@ -13,7 +13,7 @@ const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const keys = ['upload', 'download', 'delete', 'rename', 'edit'];
const keys = ['upload', 'download', 'delete', 'rename', 'edit', 'copy', 'paste'];
class Strategy extends Component {
@ -289,6 +289,20 @@ class Strategy extends Component {
render: (text) => {
return renderStatus(text);
}
}, {
title: '复制',
dataIndex: 'copy',
key: 'copy',
render: (text) => {
return renderStatus(text);
}
}, {
title: '粘贴',
dataIndex: 'paste',
key: 'paste',
render: (text) => {
return renderStatus(text);
}
}, {
title: '创建时间',
dataIndex: 'created',

View File

@ -17,6 +17,8 @@ const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading,
'delete': false,
'rename': false,
'edit': false,
'copy': false,
'paste': false,
};
}
@ -32,8 +34,6 @@ const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading,
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
@ -59,7 +59,8 @@ const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading,
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="编辑" name='edit' rules={[{required: true}]} valuePropName="checked" tooltip={'编辑需要先开启下载'}>
<Form.Item label="编辑" name='edit' rules={[{required: true}]} valuePropName="checked"
tooltip={'编辑需要先开启下载'}>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
@ -70,6 +71,14 @@ const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading,
<Form.Item label="重命名" name='rename' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="复制" name='copy' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="粘贴" name='paste' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Form>
</Modal>
)

View File

@ -450,6 +450,17 @@ class User extends Component {
)
},
sorter: true,
}, {
title: '来源',
dataIndex: 'source',
key: 'source',
render: (text) => {
if (text === 'ldap') {
return (
<Tag color="gold">域同步</Tag>
);
}
}
},
{
title: '操作',
@ -460,6 +471,7 @@ class User extends Component {
<Menu>
<Menu.Item key="1">
<Button type="text" size='small'
disabled={record['source'] === 'ldap'}
onClick={() => {
this.setState({
changePasswordVisible: true,
@ -626,6 +638,7 @@ class User extends Component {
</div>
<Table rowSelection={rowSelection}
rowKey='id'
dataSource={this.state.items}
columns={columns}
position={'both'}

View File

@ -73,11 +73,8 @@ class UserGroup extends Component {
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
items: data.items,
total: data.total,
queryParams: queryParams,
loading: false
@ -95,8 +92,7 @@ class UserGroup extends Component {
queryParams: queryParams
});
this.loadTableData(queryParams).then(r => {
})
this.loadTableData(queryParams);
};
showDeleteConfirm(id, content) {
@ -139,7 +135,6 @@ class UserGroup extends Component {
}
await this.handleSearchByNickname('');
console.log(model)
this.setState({
model: model,
modalVisible: true,
@ -147,7 +142,7 @@ class UserGroup extends Component {
});
};
handleCancelModal = e => {
handleCancelModal = () => {
this.setState({
modalVisible: false,
modalTitle: '',
@ -161,37 +156,43 @@ class UserGroup extends Component {
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/user-groups/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
try {
if (formData.id) {
// 向后台提交数据
const result = await request.put('/user-groups/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
return true;
} else {
message.error(result.message, 10);
return false;
}
} else {
message.error(result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/user-groups', formData);
if (result.code === 1) {
message.success('操作成功', 3);
// 向后台提交数据
const result = await request.post('/user-groups', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
return true;
} else {
message.error(result.message, 10);
return false;
}
}
} finally {
this.setState({
modalConfirmLoading: false
});
}
this.setState({
modalConfirmLoading: false
});
};
handleSearchByName = name => {
@ -280,7 +281,7 @@ class UserGroup extends Component {
title: '授权资产',
dataIndex: 'assetCount',
key: 'assetCount',
render: (text, record, index) => {
render: (text, record) => {
return <Button type='link' onClick={async () => {
this.setState({
assetVisible: true,
@ -292,7 +293,7 @@ class UserGroup extends Component {
title: '创建日期',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
render: (text) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
@ -328,7 +329,7 @@ class UserGroup extends Component {
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
onChange: (selectedRowKeys) => {
this.setState({selectedRowKeys});
},
};
@ -408,6 +409,7 @@ class UserGroup extends Component {
</div>
<Table rowSelection={rowSelection}
rowKey='id'
dataSource={this.state.items}
columns={columns}
position={'both'}

View File

@ -27,11 +27,11 @@ const UserGroupModal = ({
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}

BIN
web/src/images/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -0,0 +1,14 @@
<svg width="400" height="140" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path fill="#40a9ff" d="M 10 10 V 110 L 65 135 L 120 110 V 10 Z"/>
<path fill="white" d="M 65 50 l -55 20 v 20 l 55 -20 l 55 20 v -20 l -55 -20 Z"/>
<path fill="#0050b3" d="M 65 70 l -55 20 v 20 l 55 25 l 55 -25 v -20 l -55 -20 Z"/>
</g>
<g>
<text transform="translate(150 20)" fill="white">
<tspan class="name" font-size="48" font-weight="bold" x="0" y="30">NEXT </tspan>
<tspan class="name" font-size="48" font-weight="bold" x="0" y="90">TERMINAL</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 638 B

10
web/src/images/logo.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="130" height="140" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path fill="#40a9ff" d="M 10 10 V 110 L 65 135 L 120 110 V 10 Z"/>
<g fill="none">
<path fill="#40a9ff" d="M 10 10 V 110 L 65 135 L 120 110 V 10 Z"/>
<path fill="white" d="M 65 50 l -55 20 v 20 l 55 -20 l 55 20 v -20 l -55 -20 Z"/>
<path fill="#0050b3" d="M 65 70 l -55 20 v 20 l 55 25 l 55 -25 v -20 l -55 -20 Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@ -5,6 +5,10 @@ export const sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export const setToken = function (token) {
localStorage.setItem('X-Auth-Token', token);
}
export const getToken = function () {
return localStorage.getItem('X-Auth-Token');
}