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) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
# Copyright: (c) <spug.dev@gmail.com>
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
# Released under the AGPL-3.0 License.
|
# Released under the AGPL-3.0 License.
|
||||||
from django.views.generic import View
|
from libs.mixins import AdminView
|
||||||
from django.db.models import F
|
from libs import json_response
|
||||||
from libs import json_response, auth
|
|
||||||
from apps.account.models import History
|
from apps.account.models import History
|
||||||
|
|
||||||
|
|
||||||
class HistoryView(View):
|
class HistoryView(AdminView):
|
||||||
@auth('dashboard.dashboard.view')
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
histories = []
|
histories = History.objects.all()
|
||||||
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],
|
|
||||||
})
|
|
||||||
return json_response(histories)
|
return json_response(histories)
|
||||||
|
|
|
@ -123,8 +123,12 @@ class Role(models.Model, ModelMixin):
|
||||||
|
|
||||||
|
|
||||||
class History(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)
|
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)
|
created_at = models.CharField(max_length=20, default=human_datetime)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -11,6 +11,8 @@ from apps.account.models import User, Role, History
|
||||||
from apps.setting.utils import AppSetting
|
from apps.setting.utils import AppSetting
|
||||||
from apps.account.utils import verify_password
|
from apps.account.utils import verify_password
|
||||||
from libs.ldap import LDAP
|
from libs.ldap import LDAP
|
||||||
|
from functools import partial
|
||||||
|
import user_agents
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -190,59 +192,76 @@ def login(request):
|
||||||
Argument('type', required=False)
|
Argument('type', required=False)
|
||||||
).parse(request.body)
|
).parse(request.body)
|
||||||
if error is None:
|
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()
|
user = User.objects.filter(username=form.username, type=form.type).first()
|
||||||
if user and not user.is_active:
|
if user and not user.is_active:
|
||||||
return json_response(error="账户已被系统禁用")
|
return handle_response(error="账户已被系统禁用")
|
||||||
if form.type == 'ldap':
|
if form.type == 'ldap':
|
||||||
config = AppSetting.get_default('ldap_service')
|
config = AppSetting.get_default('ldap_service')
|
||||||
if not config:
|
if not config:
|
||||||
return json_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
|
return handle_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
|
||||||
ldap = LDAP(**config)
|
ldap = LDAP(**config)
|
||||||
is_success, message = ldap.valid_user(form.username, form.password)
|
is_success, message = ldap.valid_user(form.username, form.password)
|
||||||
if is_success:
|
if is_success:
|
||||||
if not user:
|
if not user:
|
||||||
user = User.objects.create(username=form.username, nickname=form.username, type=form.type)
|
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:
|
elif message:
|
||||||
return json_response(error=message)
|
return handle_response(error=message)
|
||||||
else:
|
else:
|
||||||
if user and user.deleted_by is None:
|
if user and user.deleted_by is None:
|
||||||
if user.verify_password(form.password):
|
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)
|
value = cache.get_or_set(form.username, 0, 86400)
|
||||||
if value >= 3:
|
if value >= 3:
|
||||||
if user and user.is_active:
|
if user and user.is_active:
|
||||||
user.is_active = False
|
user.is_active = False
|
||||||
user.save()
|
user.save()
|
||||||
return json_response(error='账户已被系统禁用')
|
return handle_response(error='账户已被系统禁用')
|
||||||
cache.set(form.username, value + 1, 86400)
|
cache.set(form.username, value + 1, 86400)
|
||||||
return json_response(error="用户名或密码错误,连续多次错误账户将会被禁用")
|
return handle_response(error="用户名或密码错误,连续多次错误账户将会被禁用")
|
||||||
return json_response(error=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)
|
cache.delete(user.username)
|
||||||
key = f'{user.username}:code'
|
key = f'{user.username}:code'
|
||||||
if captcha:
|
if captcha:
|
||||||
code = cache.get(key)
|
code = cache.get(key)
|
||||||
if not code:
|
if not code:
|
||||||
return json_response(error='验证码已失效,请重新获取')
|
return handle_response(error='验证码已失效,请重新获取')
|
||||||
if code != captcha:
|
if code != captcha:
|
||||||
ttl = cache.ttl(key)
|
ttl = cache.ttl(key)
|
||||||
cache.expire(key, ttl - 100)
|
cache.expire(key, ttl - 100)
|
||||||
return json_response(error='验证码错误')
|
return handle_response(error='验证码错误')
|
||||||
cache.delete(key)
|
cache.delete(key)
|
||||||
else:
|
else:
|
||||||
mfa = AppSetting.get_default('MFA', {'enable': False})
|
mfa = AppSetting.get_default('MFA', {'enable': False})
|
||||||
if mfa['enable']:
|
if mfa['enable']:
|
||||||
if not user.wx_token:
|
if not user.wx_token:
|
||||||
return json_response(error='已启用登录双重认证,但您的账户未配置微信Token,请联系管理员')
|
return handle_response(error='已启用登录双重认证,但您的账户未配置微信Token,请联系管理员')
|
||||||
code = generate_random_str(6)
|
code = generate_random_str(6)
|
||||||
send_login_wx_code(user.wx_token, code)
|
send_login_wx_code(user.wx_token, code)
|
||||||
cache.set(key, code, 300)
|
cache.set(key, code, 300)
|
||||||
return json_response({'required_mfa': True})
|
return json_response({'required_mfa': True})
|
||||||
|
|
||||||
|
handle_response()
|
||||||
x_real_ip = get_request_real_ip(request.headers)
|
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()
|
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
|
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_login = human_datetime()
|
||||||
user.last_ip = x_real_ip
|
user.last_ip = x_real_ip
|
||||||
user.save()
|
user.save()
|
||||||
History.objects.create(user=user, ip=x_real_ip)
|
|
||||||
verify_ip = AppSetting.get_default('verify_ip', True)
|
verify_ip = AppSetting.get_default('verify_ip', True)
|
||||||
return json_response({
|
return json_response({
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
|
|
|
@ -9,3 +9,4 @@ requests==2.22.0
|
||||||
GitPython==3.0.8
|
GitPython==3.0.8
|
||||||
python-ldap==3.4.0
|
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 SystemAccount from './pages/system/account';
|
||||||
import SystemRole from './pages/system/role';
|
import SystemRole from './pages/system/role';
|
||||||
import SystemSetting from './pages/system/setting';
|
import SystemSetting from './pages/system/setting';
|
||||||
|
import SystemLogin from './pages/system/login';
|
||||||
import WelcomeIndex from './pages/welcome/index';
|
import WelcomeIndex from './pages/welcome/index';
|
||||||
import WelcomeInfo from './pages/welcome/info';
|
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: [
|
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.account.view', path: '/system/account', component: SystemAccount},
|
||||||
{title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole},
|
{title: '角色管理', auth: 'system.role.view', path: '/system/role', component: SystemRole},
|
||||||
{title: '系统设置', auth: 'system.setting.view', path: '/system/setting', component: SystemSetting},
|
{title: '系统设置', auth: 'system.setting.view', path: '/system/setting', component: SystemSetting},
|
||||||
|
|
Loading…
Reference in New Issue