mirror of https://github.com/openspug/spug
A 添加系统管理/登录日志
parent
5330f05211
commit
2d9c2fc8b4
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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()
|
|
@ -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},
|
||||
|
|
Loading…
Reference in New Issue