mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-23 23:06: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 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,
|
||||||
|
|
|
@ -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
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 _ 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(
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
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">
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
{record.value}
|
{record.name}: {record.value}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
吗
|
吗
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
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 { 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) => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user