mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-22 22:36:06 +08:00
增加登录日志
This commit is contained in:
parent
0cbfca979e
commit
eb196bd40d
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
20
back/data/auth.ts
Normal 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';
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
2
src/pages/env/index.tsx
vendored
2
src/pages/env/index.tsx
vendored
|
@ -280,7 +280,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
|
|||
<>
|
||||
确认删除变量{' '}
|
||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||
{record.value}
|
||||
{record.name}: {record.value}
|
||||
</Text>{' '}
|
||||
吗
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}>
|
||||
|
|
78
src/pages/setting/loginLog.tsx
Normal file
78
src/pages/setting/loginLog.tsx
Normal 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;
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user