增加登录日志

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 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,

View File

@ -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<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 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<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() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(

View File

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

View File

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

View File

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

View File

@ -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<any[]>([]);
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}
>
<Tabs.TabPane tab="安全设置" key="security">
<SecuritySettings user={user} />
<SecuritySettings user={user} userChange={reloadUser} />
</Tabs.TabPane>
<Tabs.TabPane tab="应用设置" key="app">
<Table
@ -274,6 +289,9 @@ const Setting = ({ headerStyle, isPhone, user }: any) => {
loading={loading}
/>
</Tabs.TabPane>
<Tabs.TabPane tab="登陆日志" key="login">
<LoginLog data={loginLogData} />
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="theme">
<Form layout="vertical">
<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 SecuritySettings = ({ user }: any) => {
const SecuritySettings = ({ user, userChange }: any) => {
const [loading, setLoading] = useState(false);
const [twoFactorActived, setTwoFactorActived] = useState<boolean>();
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) => {