A 添加系统管理/登录日志

pull/586/head
vapao 2022-07-13 18:11:44 +08:00
parent 5330f05211
commit 2d9c2fc8b4
8 changed files with 187 additions and 26 deletions

View File

@ -1,20 +1,12 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from django.db.models import F
from libs import json_response, auth
from libs.mixins import AdminView
from libs import json_response
from apps.account.models import History
class HistoryView(View):
@auth('dashboard.dashboard.view')
class HistoryView(AdminView):
def get(self, request):
histories = []
for item in History.objects.annotate(nickname=F('user__nickname')):
histories.append({
'nickname': item.nickname,
'ip': item.ip,
'created_at': item.created_at.split('-', 1)[1],
})
histories = History.objects.all()
return json_response(histories)

View File

@ -123,8 +123,12 @@ class Role(models.Model, ModelMixin):
class History(models.Model, ModelMixin):
user = models.ForeignKey(User, on_delete=models.CASCADE)
username = models.CharField(max_length=100, null=True)
type = models.CharField(max_length=20, default='default')
ip = models.CharField(max_length=50)
agent = models.CharField(max_length=255, null=True)
message = models.CharField(max_length=255, null=True)
is_success = models.BooleanField(default=True)
created_at = models.CharField(max_length=20, default=human_datetime)
class Meta:

View File

@ -11,6 +11,8 @@ from apps.account.models import User, Role, History
from apps.setting.utils import AppSetting
from apps.account.utils import verify_password
from libs.ldap import LDAP
from functools import partial
import user_agents
import ipaddress
import time
import uuid
@ -190,59 +192,76 @@ def login(request):
Argument('type', required=False)
).parse(request.body)
if error is None:
handle_response = partial(handle_login_record, request, form.username, form.type)
user = User.objects.filter(username=form.username, type=form.type).first()
if user and not user.is_active:
return json_response(error="账户已被系统禁用")
return handle_response(error="账户已被系统禁用")
if form.type == 'ldap':
config = AppSetting.get_default('ldap_service')
if not config:
return json_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
return handle_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
ldap = LDAP(**config)
is_success, message = ldap.valid_user(form.username, form.password)
if is_success:
if not user:
user = User.objects.create(username=form.username, nickname=form.username, type=form.type)
return handle_user_info(request, user, form.captcha)
return handle_user_info(handle_response, request, user, form.captcha)
elif message:
return json_response(error=message)
return handle_response(error=message)
else:
if user and user.deleted_by is None:
if user.verify_password(form.password):
return handle_user_info(request, user, form.captcha)
return handle_user_info(handle_response, request, user, form.captcha)
value = cache.get_or_set(form.username, 0, 86400)
if value >= 3:
if user and user.is_active:
user.is_active = False
user.save()
return json_response(error='账户已被系统禁用')
return handle_response(error='账户已被系统禁用')
cache.set(form.username, value + 1, 86400)
return json_response(error="用户名或密码错误,连续多次错误账户将会被禁用")
return handle_response(error="用户名或密码错误,连续多次错误账户将会被禁用")
return json_response(error=error)
def handle_user_info(request, user, captcha):
def handle_login_record(request, username, login_type, error=None):
x_real_ip = get_request_real_ip(request.headers)
user_agent = user_agents.parse(request.headers.get('User-Agent'))
History.objects.create(
username=username,
type=login_type,
ip=x_real_ip,
agent=user_agent,
is_success=False if error else True,
message=error
)
if error:
return json_response(error=error)
def handle_user_info(handle_response, request, user, captcha):
cache.delete(user.username)
key = f'{user.username}:code'
if captcha:
code = cache.get(key)
if not code:
return json_response(error='验证码已失效,请重新获取')
return handle_response(error='验证码已失效,请重新获取')
if code != captcha:
ttl = cache.ttl(key)
cache.expire(key, ttl - 100)
return json_response(error='验证码错误')
return handle_response(error='验证码错误')
cache.delete(key)
else:
mfa = AppSetting.get_default('MFA', {'enable': False})
if mfa['enable']:
if not user.wx_token:
return json_response(error='已启用登录双重认证但您的账户未配置微信Token请联系管理员')
return handle_response(error='已启用登录双重认证但您的账户未配置微信Token请联系管理员')
code = generate_random_str(6)
send_login_wx_code(user.wx_token, code)
cache.set(key, code, 300)
return json_response({'required_mfa': True})
handle_response()
x_real_ip = get_request_real_ip(request.headers)
token_isvalid = user.access_token and len(user.access_token) == 32 and user.token_expired >= time.time()
user.access_token = user.access_token if token_isvalid else uuid.uuid4().hex
@ -250,7 +269,6 @@ def handle_user_info(request, user, captcha):
user.last_login = human_datetime()
user.last_ip = x_real_ip
user.save()
History.objects.create(user=user, ip=x_real_ip)
verify_ip = AppSetting.get_default('verify_ip', True)
return json_response({
'id': user.id,

View File

@ -8,4 +8,5 @@ django-redis==4.10.0
requests==2.22.0
GitPython==3.0.8
python-ldap==3.4.0
openpyxl==3.0.3
openpyxl==3.0.3
user_agents==2.2.0

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React from 'react';
import { observer } from 'mobx-react';
import { Radio, Tag } from 'antd';
import { TableCard } from 'components';
import store from './store';
@observer
class ComTable extends React.Component {
constructor(props) {
super(props);
this.state = {
password: ''
}
}
componentDidMount() {
store.fetchRecords()
}
columns = [{
title: '时间',
width: 200,
dataIndex: 'created_at'
}, {
title: '账户名',
width: 120,
dataIndex: 'username',
}, {
title: '登录方式',
width: 100,
hide: true,
dataIndex: 'type',
render: text => text === 'ldap' ? 'LDAP' : '普通登录'
}, {
title: '状态',
width: 90,
render: text => text['is_success'] ? <Tag color="success">成功</Tag> : <Tag color="error"></Tag>
}, {
title: '登录IP',
width: 160,
dataIndex: 'ip',
}, {
title: 'User Agent',
ellipsis: true,
dataIndex: 'agent'
}, {
title: '提示信息',
ellipsis: true,
dataIndex: 'message'
}];
render() {
return (
<TableCard
tKey="sl"
rowKey="id"
title="登录记录"
loading={store.isFetching}
dataSource={store.dataSource}
onReload={store.fetchRecords}
actions={[
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
<Radio.Button value="">全部</Radio.Button>
<Radio.Button value="true">成功</Radio.Button>
<Radio.Button value="false">失败</Radio.Button>
</Radio.Group>
]}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
}}
columns={this.columns}/>
)
}
}
export default ComTable

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React from 'react';
import { observer } from 'mobx-react';
import { Input } from 'antd';
import { SearchForm, AuthDiv, Breadcrumb } from 'components';
import ComTable from './Table';
import store from './store';
export default observer(function () {
return (
<AuthDiv auth="system.account.view">
<Breadcrumb>
<Breadcrumb.Item>首页</Breadcrumb.Item>
<Breadcrumb.Item>系统管理</Breadcrumb.Item>
<Breadcrumb.Item>账户管理</Breadcrumb.Item>
</Breadcrumb>
<SearchForm>
<SearchForm.Item span={8} title="账户名称">
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
</SearchForm.Item>
</SearchForm>
<ComTable/>
</AuthDiv>
)
})

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import { observable, computed } from 'mobx';
import { http, includes } from 'libs';
class Store {
@observable records = [];
@observable isFetching = false;
@observable f_name;
@observable f_status = '';
@computed get dataSource() {
let records = this.records;
if (this.f_name) records = records.filter(x => includes(x.username, this.f_name));
if (this.f_status) records = records.filter(x => String(x.is_success) === this.f_status);
return records
}
fetchRecords = () => {
this.isFetching = true;
http.get('/api/account/login/history/')
.then(res => this.records = res)
.finally(() => this.isFetching = false)
};
}
export default new Store()

View File

@ -38,6 +38,7 @@ import AlarmContact from './pages/alarm/contact';
import SystemAccount from './pages/system/account';
import SystemRole from './pages/system/role';
import SystemSetting from './pages/system/setting';
import SystemLogin from './pages/system/login';
import WelcomeIndex from './pages/welcome/index';
import WelcomeInfo from './pages/welcome/info';
@ -90,6 +91,7 @@ export default [
},
{
icon: <SettingOutlined/>, title: '系统管理', auth: "system.account.view|system.role.view|system.setting.view", child: [
{title: '登录日志', auth: 'system.login.view', path: '/system/login', component: SystemLogin},
{title: '账户管理', auth: 'system.account.view', path: '/system/account', component: SystemAccount},
{title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole},
{title: '系统设置', auth: 'system.setting.view', path: '/system/setting', component: SystemSetting},