From eb196bd40da8c2f3570a0c869e0302dad003821b Mon Sep 17 00:00:00 2001 From: hanhh <18330117883@163.com> Date: Tue, 7 Sep 2021 01:40:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=99=BB=E5=BD=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/api/auth.ts | 15 +++++++ back/config/index.ts | 2 + back/config/util.ts | 5 ++- back/data/auth.ts | 20 +++++++++ back/services/auth.ts | 62 ++++++++++++++++++++++++++- src/layouts/index.tsx | 7 ++- src/pages/env/index.tsx | 2 +- src/pages/script/index.tsx | 28 ++++++++++-- src/pages/setting/index.tsx | 22 +++++++++- src/pages/setting/loginLog.tsx | 78 ++++++++++++++++++++++++++++++++++ src/pages/setting/security.tsx | 4 +- 11 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 back/data/auth.ts create mode 100644 src/pages/setting/loginLog.tsx diff --git a/back/api/auth.ts b/back/api/auth.ts index 06c86003..76a2255a 100644 --- a/back/api/auth.ts +++ b/back/api/auth.ts @@ -167,4 +167,19 @@ export default (app: Router) => { } }, ); + + route.get( + '/user/login-log', + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const authService = Container.get(AuthService); + const data = await authService.getLoginLog(); + res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); }; diff --git a/back/config/index.ts b/back/config/index.ts index 54bcab01..bd0c3e3b 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -24,6 +24,7 @@ const dbPath = path.join(rootPath, 'db/'); const cronDbFile = path.join(rootPath, 'db/crontab.db'); const envDbFile = path.join(rootPath, 'db/env.db'); const appDbFile = path.join(rootPath, 'db/app.db'); +const authDbFile = path.join(rootPath, 'db/auth.db'); const configFound = dotenv.config({ path: confFile }); @@ -60,6 +61,7 @@ export default { cronDbFile, envDbFile, appDbFile, + authDbFile, configPath, scriptPath, samplePath, diff --git a/back/config/util.ts b/back/config/util.ts index 7384151c..232c4d9a 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -158,11 +158,12 @@ export async function getNetIp(req: any) { } try { const baiduApi = got - .get(`https://www.cip.cc/${ip}`, { timeout: 100000 }) + .get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 }) .text(); const ipApi = got .get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, { - timeout: 100000, + timeout: 10000, + retry: 0, }) .buffer(); const [data, ipApiBody] = await await Promise.all([baiduApi, ipApi]); diff --git a/back/data/auth.ts b/back/data/auth.ts new file mode 100644 index 00000000..bddccf79 --- /dev/null +++ b/back/data/auth.ts @@ -0,0 +1,20 @@ +export class AuthInfo { + ip?: string; + type: AuthInfoType; + info?: any; + _id?: string; + + constructor(options: AuthInfo) { + this.ip = options.ip; + this.info = options.info; + this.type = options.type; + this._id = options._id; + } +} + +export enum LoginStatus { + 'success', + 'fail', +} + +export type AuthInfoType = 'loginLog' | 'authToken'; diff --git a/back/services/auth.ts b/back/services/auth.ts index 0c05394d..acb8e917 100644 --- a/back/services/auth.ts +++ b/back/services/auth.ts @@ -6,10 +6,20 @@ import * as fs from 'fs'; import _ from 'lodash'; import jwt from 'jsonwebtoken'; import { authenticator } from '@otplib/preset-default'; +import { exec } from 'child_process'; +import DataStore from 'nedb'; +import { AuthInfo, LoginStatus } from '../data/auth'; @Service() export default class AuthService { - constructor(@Inject('logger') private logger: winston.Logger) {} + private authDb = new DataStore({ filename: config.authDbFile }); + + constructor(@Inject('logger') private logger: winston.Logger) { + this.authDb.loadDatabase((err) => { + if (err) throw err; + }); + this.authDb.persistence.setAutocompactionInterval(30000); + } public async login( payloads: { @@ -84,6 +94,16 @@ export default class AuthService { lastaddr: address, isTwoFactorChecking: false, }); + exec( + `notify 登陆通知 你于${new Date( + timestamp, + ).toLocaleString()}在${address}登陆成功,ip地址${ip}`, + ); + await this.getLoginLog(); + await this.insertDb({ + type: 'loginLog', + info: { timestamp, address, ip, status: LoginStatus.success }, + }); return { code: 200, data: { token, lastip, lastaddr, lastlogon, retries }, @@ -95,6 +115,16 @@ export default class AuthService { lastip: ip, lastaddr: address, }); + exec( + `notify 登陆通知 你于${new Date( + timestamp, + ).toLocaleString()}在${address}登陆失败,ip地址${ip}`, + ); + await this.getLoginLog(); + await this.insertDb({ + type: 'loginLog', + info: { timestamp, address, ip, status: LoginStatus.fail }, + }); return { code: 400, message: config.authError }; } } else { @@ -102,6 +132,36 @@ export default class AuthService { } } + public async getLoginLog(): Promise { + return new Promise((resolve) => { + this.authDb.find({ type: 'loginLog' }).exec((err, docs) => { + if (err || docs.length === 0) { + resolve(docs); + } else { + const result = docs.sort( + (a, b) => b.info.timestamp - a.info.timestamp, + ); + if (result.length > 100) { + this.authDb.remove({ _id: result[result.length - 1]._id }); + } + resolve(result.map((x) => x.info)); + } + }); + }); + } + + private async insertDb(payload: AuthInfo): Promise { + return new Promise((resolve) => { + this.authDb.insert(payload, (err, doc) => { + if (err) { + this.logger.error(err); + } else { + resolve(doc); + } + }); + }); + } + private initAuthInfo() { const newPassword = createRandomString(16, 22); fs.writeFileSync( diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index cf6d3c3d..852892ad 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -127,7 +127,12 @@ export default function (props: any) { {...defaultProps} > {React.Children.map(props.children, (child) => { - return React.cloneElement(child, { ...ctx, ...theme, user }); + return React.cloneElement(child, { + ...ctx, + ...theme, + user, + reloadUser: getUser, + }); })} ); diff --git a/src/pages/env/index.tsx b/src/pages/env/index.tsx index c320148a..3b58102d 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -280,7 +280,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => { <> 确认删除变量{' '} - {record.value} + {record.name}: {record.value} {' '} 吗 diff --git a/src/pages/script/index.tsx b/src/pages/script/index.tsx index 71e699eb..e51e52c4 100644 --- a/src/pages/script/index.tsx +++ b/src/pages/script/index.tsx @@ -15,7 +15,6 @@ import { request } from '@/utils/http'; import styles from './index.module.less'; import EditModal from './editModal'; import { Controlled as CodeMirror } from 'react-codemirror2'; -import { useCtx, useTheme } from '@/utils/hooks'; import SplitPane from 'react-split-pane'; const { Text } = Typography; @@ -82,9 +81,29 @@ const Script = ({ headerStyle, isPhone, theme }: any) => { getDetail(node); }; - const onTreeSelect = useCallback((keys: Key[], e: any) => { - onSelect(keys[0], e.node); - }, []); + const onTreeSelect = useCallback( + (keys: Key[], e: any) => { + const content = editorRef.current + ? editorRef.current.getValue().replace(/\r\n/g, '\n') + : value; + if (content !== value) { + Modal.confirm({ + title: `确认离开`, + content: <>当前修改未保存,确定离开吗, + onOk() { + onSelect(keys[0], e.node); + setIsEditing(false); + }, + onCancel() { + console.log('Cancel'); + }, + }); + } else { + onSelect(keys[0], e.node); + } + }, + [value], + ); const onSearch = useCallback( (e) => { @@ -132,6 +151,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => { .then((_data: any) => { if (_data.code === 200) { message.success(`保存成功`); + setValue(content); setIsEditing(false); } else { message.error(_data); diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index ef35df99..32dd5f53 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -29,6 +29,7 @@ import { ReloadOutlined, } from '@ant-design/icons'; import SecuritySettings from './security'; +import LoginLog from './loginLog'; const { Text } = Typography; const optionsWithDisabled = [ @@ -37,7 +38,7 @@ const optionsWithDisabled = [ { label: '跟随系统', value: 'auto' }, ]; -const Setting = ({ headerStyle, isPhone, user }: any) => { +const Setting = ({ headerStyle, isPhone, user, reloadUser }: any) => { const columns = [ { title: '名称', @@ -110,6 +111,7 @@ const Setting = ({ headerStyle, isPhone, user }: any) => { const [isModalVisible, setIsModalVisible] = useState(false); const [editedApp, setEditedApp] = useState(); const [tabActiveKey, setTabActiveKey] = useState('security'); + const [loginLogData, setLoginLogData] = useState([]); const themeChange = (e: any) => { setTheme(e.target.value); @@ -219,10 +221,23 @@ const Setting = ({ headerStyle, isPhone, user }: any) => { setDataSource(result); }; + const getLoginLog = () => { + request + .get(`${config.apiPrefix}user/login-log`) + .then((data: any) => { + setLoginLogData(data.data); + }) + .catch((error: any) => { + console.log(error); + }); + }; + const tabChange = (activeKey: string) => { setTabActiveKey(activeKey); if (activeKey === 'app') { getApps(); + } else if (activeKey === 'login') { + getLoginLog(); } }; @@ -261,7 +276,7 @@ const Setting = ({ headerStyle, isPhone, user }: any) => { onChange={tabChange} > - + { loading={loading} /> + + +
diff --git a/src/pages/setting/loginLog.tsx b/src/pages/setting/loginLog.tsx new file mode 100644 index 00000000..c97b2c9f --- /dev/null +++ b/src/pages/setting/loginLog.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import { Typography, Table, Tag, Button, Spin, message } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; + +const { Text, Link } = Typography; + +enum LoginStatus { + '成功', + '失败', +} + +enum LoginStatusColor { + 'success', + 'error', +} + +const columns = [ + { + title: '序号', + align: 'center' as const, + width: 50, + render: (text: string, record: any, index: number) => { + return {index + 1} ; + }, + }, + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + align: 'center' as const, + render: (text: string, record: any) => { + return {new Date(record.timestamp).toLocaleString()}; + }, + }, + { + title: '地址', + dataIndex: 'address', + key: 'address', + align: 'center' as const, + }, + { + title: 'IP', + dataIndex: 'ip', + key: 'ip', + align: 'center' as const, + }, + { + title: '登陆状态', + dataIndex: 'status', + key: 'status', + align: 'center' as const, + render: (text: string, record: any) => { + return ( + + {LoginStatus[record.status]} + + ); + }, + }, +]; + +const LoginLog = ({ data }: any) => { + return ( + <> +
+ + ); +}; + +export default LoginLog; diff --git a/src/pages/setting/security.tsx b/src/pages/setting/security.tsx index 643cb82a..b2925258 100644 --- a/src/pages/setting/security.tsx +++ b/src/pages/setting/security.tsx @@ -7,7 +7,7 @@ import QRCode from 'qrcode.react'; const { Title, Link } = Typography; -const SecuritySettings = ({ user }: any) => { +const SecuritySettings = ({ user, userChange }: any) => { const [loading, setLoading] = useState(false); const [twoFactorActived, setTwoFactorActived] = useState(); const [twoFactoring, setTwoFactoring] = useState(false); @@ -46,6 +46,7 @@ const SecuritySettings = ({ user }: any) => { .then((data: any) => { if (data.data) { setTwoFactorActived(false); + userChange(); } }) .catch((error: any) => { @@ -61,6 +62,7 @@ const SecuritySettings = ({ user }: any) => { message.success('激活成功'); setTwoFactoring(false); setTwoFactorActived(true); + userChange(); } }) .catch((error: any) => {