diff --git a/pkg/api/account.go b/pkg/api/account.go index ae898c3..64bf4c0 100644 --- a/pkg/api/account.go +++ b/pkg/api/account.go @@ -48,6 +48,45 @@ func LoginEndpoint(c echo.Context) error { return Fail(c, -1, "您输入的账号或密码不正确") } + if user.TOTPSecret != "" { + return Fail(c, 0, "") + } + + token := utils.UUID() + + authorization := Authorization{ + Token: token, + Remember: loginAccount.Remember, + User: user, + } + + if authorization.Remember { + // 记住登录有效期两周 + global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14)) + } else { + global.Cache.Set(token, authorization, time.Hour*time.Duration(2)) + } + + model.UpdateUserById(&model.User{Online: true}, user.ID) + + return Success(c, token) +} + +func loginWithTotpEndpoint(c echo.Context) error { + var loginAccount LoginAccount + if err := c.Bind(&loginAccount); err != nil { + return err + } + + user, err := model.FindUserByUsername(loginAccount.Username) + if err != nil { + return Fail(c, -1, "您输入的账号或密码不正确") + } + + if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { + return Fail(c, -1, "您输入的账号或密码不正确") + } + if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) { return Fail(c, -2, "您的TOTP不匹配") } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index a95fd23..fd10274 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -30,6 +30,7 @@ func SetupRoutes() *echo.Echo { e.Use(Auth) e.POST("/login", LoginEndpoint) + e.POST("/loginWithTotp", loginWithTotpEndpoint) e.GET("/tunnel", TunEndpoint) e.GET("/ssh", SSHEndpoint) diff --git a/web/src/common/request.js b/web/src/common/request.js index 4c914cc..657006c 100644 --- a/web/src/common/request.js +++ b/web/src/common/request.js @@ -25,6 +25,7 @@ const handleError = (error) => { const handleResult = (result) => { if (result['code'] === 403) { window.location.href = '#/login'; + return; } } diff --git a/web/src/components/Login.js b/web/src/components/Login.js index e54a185..5a07ff3 100644 --- a/web/src/components/Login.js +++ b/web/src/components/Login.js @@ -1,18 +1,24 @@ -import React, { Component } from 'react'; -import { Button, Card, Checkbox, Form, Input, Typography } from "antd"; +import React, {Component} from 'react'; +import {Button, Card, Checkbox, Form, Input, Modal, Typography} from "antd"; import './Login.css' import request from "../common/request"; -import { message } from "antd/es"; -import { withRouter } from "react-router-dom"; -import { OneToOneOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; +import {message} from "antd/es"; +import {withRouter} from "react-router-dom"; +import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons'; + +const {Title} = Typography; -const { Title } = Typography; class LoginForm extends Component { + formRef = React.createRef() + state = { inLogin: false, height: window.innerHeight, - width: window.innerWidth + width: window.innerWidth, + loginAccount: undefined, + totpModalVisible: false, + confirmLoading: false }; componentDidMount() { @@ -31,6 +37,15 @@ class LoginForm extends Component { try { let result = await request.post('/login', params); + + if (result.code === 0) { + // 进行双因子认证 + this.setState({ + loginAccount: params, + totpModalVisible: true + }) + return; + } if (result.code !== 1) { throw new Error(result.message); } @@ -50,35 +65,88 @@ class LoginForm extends Component { } }; + handleOk = async (values) => { + this.setState({ + confirmLoading: true + }) + let loginAccount = this.state.loginAccount; + loginAccount['totp'] = values['totp']; + try { + let result = await request.post('/loginWithTotp', loginAccount); + + if (result.code !== 1) { + throw new Error(result.message); + } + + // 跳转登录 + sessionStorage.removeItem('current'); + sessionStorage.removeItem('openKeys'); + localStorage.setItem('X-Auth-Token', result['data']); + // this.props.history.push(); + window.location.href = "/" + } catch (e) { + message.error(e.message); + } finally { + this.setState({ + confirmLoading: false + }); + } + } + + handleCancel = () => { + this.setState({ + totpModalVisible: false + }) + } + render() { return (
+ style={{width: this.state.width, height: this.state.height, backgroundColor: '#F0F2F5'}}> -
+
Next Terminal
- - } placeholder="登录账号" /> + + } placeholder="登录账号"/> - - } placeholder="登录密码" /> - - - } placeholder="TOTP" /> + + } placeholder="登录密码"/> 记住登录 + + { + this.formRef.current + .validateFields() + .then(values => { + this.formRef.current.resetFields(); + this.handleOk(values); + }) + .catch(info => { + + }); + }} + onCancel={this.handleCancel}> + +
+ + + } placeholder="请输入双因素认证APP中显示的授权码"/> + +
+
); diff --git a/web/src/components/user/Info.js b/web/src/components/user/Info.js index 2b5ec5b..40114d5 100644 --- a/web/src/components/user/Info.js +++ b/web/src/components/user/Info.js @@ -1,11 +1,12 @@ -import React, { Component } from 'react'; -import { Button, Form, Input, Layout, PageHeader, Image } from "antd"; -import { itemRender } from '../../utils/utils' +import React, {Component} from 'react'; +import {Button, Card, Form, Image, Input, Layout, PageHeader} from "antd"; +import {itemRender} from '../../utils/utils' import request from "../../common/request"; -import { message } from "antd/es"; +import {message} from "antd/es"; import Logout from "./Logout"; -const { Content } = Layout; +const {Content} = Layout; +const {Meta} = Card; const routes = [ { @@ -19,12 +20,12 @@ const routes = [ ]; const formItemLayout = { - labelCol: { span: 3 }, - wrapperCol: { span: 6 }, + labelCol: {span: 3}, + wrapperCol: {span: 6}, }; const formTailLayout = { - labelCol: { span: 3 }, - wrapperCol: { span: 6, offset: 3 }, + labelCol: {span: 3}, + wrapperCol: {span: 6, offset: 3}, }; class Info extends Component { @@ -106,7 +107,7 @@ class Info extends Component { itemRender: itemRender }} extra={[ - + ]} subTitle="个人中心" /> @@ -125,7 +126,7 @@ class Info extends Component { }, ]} > - + this.onNewPasswordChange(value)} /> + onChange={(value) => this.onNewPasswordChange(value)}/> this.onNewPassword2Change(value)} /> + onChange={(value) => this.onNewPassword2Change(value)}/>