增加登录日志

This commit is contained in:
hanhh 2021-09-07 01:40:25 +08:00
parent 0cbfca979e
commit eb196bd40d
11 changed files with 233 additions and 12 deletions

View File

@ -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);
}
},
);
}; };

View File

@ -24,6 +24,7 @@ const dbPath = path.join(rootPath, 'db/');
const cronDbFile = path.join(rootPath, 'db/crontab.db'); const cronDbFile = path.join(rootPath, 'db/crontab.db');
const envDbFile = path.join(rootPath, 'db/env.db'); const envDbFile = path.join(rootPath, 'db/env.db');
const appDbFile = path.join(rootPath, 'db/app.db'); const appDbFile = path.join(rootPath, 'db/app.db');
const authDbFile = path.join(rootPath, 'db/auth.db');
const configFound = dotenv.config({ path: confFile }); const configFound = dotenv.config({ path: confFile });
@ -60,6 +61,7 @@ export default {
cronDbFile, cronDbFile,
envDbFile, envDbFile,
appDbFile, appDbFile,
authDbFile,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,

View File

@ -158,11 +158,12 @@ export async function getNetIp(req: any) {
} }
try { try {
const baiduApi = got const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 100000 }) .get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
.text(); .text();
const ipApi = got const ipApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, { .get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 100000, timeout: 10000,
retry: 0,
}) })
.buffer(); .buffer();
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]); const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);

20
back/data/auth.ts Normal file
View File

@ -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';

View File

@ -6,10 +6,20 @@ import * as fs from 'fs';
import _ from 'lodash'; import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { exec } from 'child_process';
import DataStore from 'nedb';
import { AuthInfo, LoginStatus } from '../data/auth';
@Service() @Service()
export default class AuthService { 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( public async login(
payloads: { payloads: {
@ -84,6 +94,16 @@ export default class AuthService {
lastaddr: address, lastaddr: address,
isTwoFactorChecking: false, 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 { return {
code: 200, code: 200,
data: { token, lastip, lastaddr, lastlogon, retries }, data: { token, lastip, lastaddr, lastlogon, retries },
@ -95,6 +115,16 @@ export default class AuthService {
lastip: ip, lastip: ip,
lastaddr: address, 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 }; return { code: 400, message: config.authError };
} }
} else { } else {
@ -102,6 +132,36 @@ export default class AuthService {
} }
} }
public async getLoginLog(): Promise<AuthInfo[]> {
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<AuthInfo> {
return new Promise((resolve) => {
this.authDb.insert(payload, (err, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
});
});
}
private initAuthInfo() { private initAuthInfo() {
const newPassword = createRandomString(16, 22); const newPassword = createRandomString(16, 22);
fs.writeFileSync( fs.writeFileSync(

View File

@ -127,7 +127,12 @@ export default function (props: any) {
{...defaultProps} {...defaultProps}
> >
{React.Children.map(props.children, (child) => { {React.Children.map(props.children, (child) => {
return React.cloneElement(child, { ...ctx, ...theme, user }); return React.cloneElement(child, {
...ctx,
...theme,
user,
reloadUser: getUser,
});
})} })}
</ProLayout> </ProLayout>
); );

View File

@ -280,7 +280,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
<> <>
{' '} {' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value} {record.name}: {record.value}
</Text>{' '} </Text>{' '}
</> </>

View File

@ -15,7 +15,6 @@ import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import EditModal from './editModal'; import EditModal from './editModal';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
const { Text } = Typography; const { Text } = Typography;
@ -82,9 +81,29 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
getDetail(node); getDetail(node);
}; };
const onTreeSelect = useCallback((keys: Key[], e: any) => { const onTreeSelect = useCallback(
onSelect(keys[0], e.node); (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( const onSearch = useCallback(
(e) => { (e) => {
@ -132,6 +151,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
.then((_data: any) => { .then((_data: any) => {
if (_data.code === 200) { if (_data.code === 200) {
message.success(`保存成功`); message.success(`保存成功`);
setValue(content);
setIsEditing(false); setIsEditing(false);
} else { } else {
message.error(_data); message.error(_data);

View File

@ -29,6 +29,7 @@ import {
ReloadOutlined, ReloadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import SecuritySettings from './security'; import SecuritySettings from './security';
import LoginLog from './loginLog';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [ const optionsWithDisabled = [
@ -37,7 +38,7 @@ const optionsWithDisabled = [
{ label: '跟随系统', value: 'auto' }, { label: '跟随系统', value: 'auto' },
]; ];
const Setting = ({ headerStyle, isPhone, user }: any) => { const Setting = ({ headerStyle, isPhone, user, reloadUser }: any) => {
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@ -110,6 +111,7 @@ const Setting = ({ headerStyle, isPhone, user }: any) => {
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState(); const [editedApp, setEditedApp] = useState();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]);
const themeChange = (e: any) => { const themeChange = (e: any) => {
setTheme(e.target.value); setTheme(e.target.value);
@ -219,10 +221,23 @@ const Setting = ({ headerStyle, isPhone, user }: any) => {
setDataSource(result); 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) => { const tabChange = (activeKey: string) => {
setTabActiveKey(activeKey); setTabActiveKey(activeKey);
if (activeKey === 'app') { if (activeKey === 'app') {
getApps(); getApps();
} else if (activeKey === 'login') {
getLoginLog();
} }
}; };
@ -261,7 +276,7 @@ const Setting = ({ headerStyle, isPhone, user }: any) => {
onChange={tabChange} onChange={tabChange}
> >
<Tabs.TabPane tab="安全设置" key="security"> <Tabs.TabPane tab="安全设置" key="security">
<SecuritySettings user={user} /> <SecuritySettings user={user} userChange={reloadUser} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="应用设置" key="app"> <Tabs.TabPane tab="应用设置" key="app">
<Table <Table
@ -274,6 +289,9 @@ const Setting = ({ headerStyle, isPhone, user }: any) => {
loading={loading} loading={loading}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="登陆日志" key="login">
<LoginLog data={loginLogData} />
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="theme"> <Tabs.TabPane tab="其他设置" key="theme">
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="主题设置" name="theme" initialValue={theme}> <Form.Item label="主题设置" name="theme" initialValue={theme}>

View File

@ -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 <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => {
return <span>{new Date(record.timestamp).toLocaleString()}</span>;
},
},
{
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 (
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
{LoginStatus[record.status]}
</Tag>
);
},
},
];
const LoginLog = ({ data }: any) => {
return (
<>
<Table
columns={columns}
pagination={false}
dataSource={data}
rowKey="_id"
size="middle"
scroll={{ x: 768 }}
/>
</>
);
};
export default LoginLog;

View File

@ -7,7 +7,7 @@ import QRCode from 'qrcode.react';
const { Title, Link } = Typography; const { Title, Link } = Typography;
const SecuritySettings = ({ user }: any) => { const SecuritySettings = ({ user, userChange }: any) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [twoFactorActived, setTwoFactorActived] = useState<boolean>(); const [twoFactorActived, setTwoFactorActived] = useState<boolean>();
const [twoFactoring, setTwoFactoring] = useState(false); const [twoFactoring, setTwoFactoring] = useState(false);
@ -46,6 +46,7 @@ const SecuritySettings = ({ user }: any) => {
.then((data: any) => { .then((data: any) => {
if (data.data) { if (data.data) {
setTwoFactorActived(false); setTwoFactorActived(false);
userChange();
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@ -61,6 +62,7 @@ const SecuritySettings = ({ user }: any) => {
message.success('激活成功'); message.success('激活成功');
setTwoFactoring(false); setTwoFactoring(false);
setTwoFactorActived(true); setTwoFactorActived(true);
userChange();
} }
}) })
.catch((error: any) => { .catch((error: any) => {