A 添加工作台最近30天登录记录

pull/220/head
vapao 2020-10-19 20:46:43 +08:00
parent 42f3ce1bf2
commit b283ff4c16
13 changed files with 117 additions and 96 deletions

View File

@ -0,0 +1,19 @@
# 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
from apps.account.models import History
class HistoryView(View):
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],
})
return json_response(histories)

View File

@ -110,3 +110,13 @@ class Role(models.Model, ModelMixin):
class Meta: class Meta:
db_table = 'roles' db_table = 'roles'
ordering = ('-id',) ordering = ('-id',)
class History(models.Model, ModelMixin):
user = models.ForeignKey(User, on_delete=models.CASCADE)
ip = models.CharField(max_length=50)
created_at = models.CharField(max_length=20, default=human_datetime)
class Meta:
db_table = 'login_histories'
ordering = ('-id',)

View File

@ -4,11 +4,13 @@
from django.conf.urls import url from django.conf.urls import url
from apps.account.views import * from apps.account.views import *
from apps.account.history import *
urlpatterns = [ urlpatterns = [
url(r'^login/', login), url(r'^login/$', login),
url(r'^logout/', logout), url(r'^logout/$', logout),
url(r'^user/$', UserView.as_view()), url(r'^user/$', UserView.as_view()),
url(r'^role/$', RoleView.as_view()), url(r'^role/$', RoleView.as_view()),
url(r'^self/$', SelfView.as_view()), url(r'^self/$', SelfView.as_view()),
url(r'^login/history/$', HistoryView.as_view())
] ]

View File

@ -0,0 +1,9 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Released under the AGPL-3.0 License.
from apps.account.models import History
from datetime import datetime, timedelta
def auto_clean_login_history():
date = datetime.now() - timedelta(days=30)
History.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()

View File

@ -6,7 +6,7 @@ from django.views.generic import View
from django.db.models import F from django.db.models import F
from libs import JsonParser, Argument, human_datetime, json_response from libs import JsonParser, Argument, human_datetime, json_response
from libs.utils import get_request_real_ip from libs.utils import get_request_real_ip
from apps.account.models import User, Role from apps.account.models import User, Role, History
from apps.setting.models import Setting from apps.setting.models import Setting
from libs.ldap import LDAP from libs.ldap import LDAP
import ipaddress import ipaddress
@ -198,6 +198,7 @@ def handle_user_info(user, x_real_ip):
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)
return json_response({ return json_response({
'access_token': user.access_token, 'access_token': user.access_token,
'nickname': user.nickname, 'nickname': user.nickname,

View File

@ -4,6 +4,6 @@ from apps.alarm.models import Alarm
from datetime import datetime, timedelta from datetime import datetime, timedelta
def auto_clean_records(): def auto_clean_alarm_records():
date = datetime.now() - timedelta(days=30) date = datetime.now() - timedelta(days=30)
Alarm.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete() Alarm.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()

View File

@ -15,9 +15,16 @@ import json
def get_statistic(request): def get_statistic(request):
if request.user.is_supper:
app = App.objects.count()
host = Host.objects.filter(deleted_at__isnull=True).count()
else:
deploy_perms, host_perms = request.user.deploy_perms, request.user.host_perms
app = App.objects.filter(id__in=deploy_perms['apps']).count()
host = Host.objects.filter(id__in=host_perms, deleted_at__isnull=True).count()
data = { data = {
'app': App.objects.count(), 'app': app,
'host': Host.objects.filter(deleted_at__isnull=True).count(), 'host': host,
'task': Task.objects.count(), 'task': Task.objects.count(),
'detection': Detection.objects.count() 'detection': Detection.objects.count()
} }

View File

@ -15,7 +15,8 @@ from apps.schedule.utils import send_fail_notify
from apps.notify.models import Notify from apps.notify.models import Notify
from apps.schedule.executors import dispatch from apps.schedule.executors import dispatch
from apps.schedule.utils import auto_clean_schedule_history from apps.schedule.utils import auto_clean_schedule_history
from apps.alarm.utils import auto_clean_records from apps.alarm.utils import auto_clean_alarm_records
from apps.account.utils import auto_clean_login_history
from apps.deploy.utils import auto_update_status from apps.deploy.utils import auto_update_status
from django.conf import settings from django.conf import settings
from libs import AttrDict, human_datetime from libs import AttrDict, human_datetime
@ -88,8 +89,9 @@ class Scheduler:
send_fail_notify(obj) send_fail_notify(obj)
def _init_builtin_jobs(self): def _init_builtin_jobs(self):
self.scheduler.add_job(auto_clean_records, 'cron', hour=0, minute=0) self.scheduler.add_job(auto_clean_alarm_records, 'cron', hour=0, minute=1)
self.scheduler.add_job(auto_clean_schedule_history, 'cron', hour=0, minute=0) self.scheduler.add_job(auto_clean_login_history, 'cron', hour=0, minute=2)
self.scheduler.add_job(auto_clean_schedule_history, 'cron', hour=0, minute=3)
self.scheduler.add_job(auto_update_status, 'interval', minutes=5) self.scheduler.add_job(auto_update_status, 'interval', minutes=5)
def _init(self): def _init(self):

View File

@ -45,6 +45,7 @@ class ComTable extends React.Component {
ellipsis: true ellipsis: true
}, { }, {
title: '操作', title: '操作',
width: 260,
className: hasPermission('deploy.app.edit|deploy.app.del') ? null : 'none', className: hasPermission('deploy.app.edit|deploy.app.del') ? null : 'none',
render: info => ( render: info => (
<Action> <Action>

View File

@ -1,85 +0,0 @@
/**
* 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 { Card } from 'antd';
import { Chart, Geom, Axis, Tooltip, Coord, Guide, Label } from 'bizcharts';
import DataSet from "@antv/data-set";
import { http } from 'libs';
export default class AlarmTrend extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
host: 0,
res: []
};
}
componentDidMount() {
http.get('/api/home/deploy/')
.then(res => this.setState(res))
.finally(() => this.setState({loading: false}))
}
render() {
const {res, host, loading} = this.state;
const dv = new DataSet.DataView();
dv.source(res).transform({
type: "percent",
field: "count",
dimension: "name",
as: "percent"
});
const cols = {
percent: {
formatter: val => {
val = val * 100 + "%";
return val;
}
}
};
return (
<Card loading={loading} title="应用部署">
<Chart height={300} data={dv} scale={cols} padding={[-30, 0, -30, 50]} forceFit>
<Coord type={"theta"} radius={0.75} innerRadius={0.6}/>
<Axis name="percent"/>
<Tooltip showTitle={false}/>
<Guide>
<Guide.Html
position={["50%", "50%"]}
html={`<div style="color:#8c8c8c;font-size:1.16em;text-align: center;width: 10em;">主机<br><span style="color:#262626;font-size:2.5em">${host}</span>台</div>`}
alignX="middle"
alignY="middle"
/>
</Guide>
<Geom
type="intervalStack"
position="percent"
color="name"
tooltip={[
"name*count",
(name, count) => {
return {
name: name,
value: count + '台'
};
}
]}
style={{lineWidth: 1, stroke: "#fff"}}>
<Label
content="percent"
formatter={(val, item) => {
const percent = (item.point['percent'] * 100).toFixed(2) + '%';
return item.point.name + ": " + percent;
}}
/>
</Geom>
</Chart>
</Card>
)
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState, useEffect } from 'react';
import { Card, List, Tag } from 'antd';
import { http } from 'libs';
import styles from './index.module.css';
export default function () {
const [name, setName] = useState(null);
const [ip, setIp] = useState(null);
const [rawData, setRawData] = useState([]);
const [dataSource, setDataSource] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
http.get('/api/account/login/history/')
.then(res => setRawData(res))
.finally(() => setLoading(false))
}, [])
useEffect(() => {
let data = rawData;
if (name) data = data.filter(x => x.nickname === name);
if (ip) data = data.filter(x => x.ip === ip);
setDataSource(data)
}, [name, ip, rawData])
return (
<Card loading={loading} title="最近30天登录" bodyStyle={{paddingTop: 0}} extra={(
<div>
{name !== null && <Tag closable color="#1890ff" onClose={() => setName(null)}>{name}</Tag>}
{ip !== null && <Tag closable color="#1890ff" onClose={() => setIp(null)}>{ip}</Tag>}
</div>
)}>
<List style={{height: 329, overflow: 'scroll'}} dataSource={dataSource} renderItem={item => (
<List.Item>
<span>{item.created_at}</span>
<span className={styles.spanText} onClick={() => setName(item.nickname)}>{item.nickname}</span>
<span>通过</span>
<span className={styles.spanText} onClick={() => setIp(item.ip)}>{item.ip}</span>
<span>登录</span>
</List.Item>
)}/>
</Card>
)
}

View File

@ -9,7 +9,7 @@ import { AuthDiv } from 'components';
import StatisticsCard from './StatisticCard'; import StatisticsCard from './StatisticCard';
import AlarmTrend from './AlarmTrend'; import AlarmTrend from './AlarmTrend';
import RequestTop from './RequestTop'; import RequestTop from './RequestTop';
import DeployPie from './DeployPie'; import LoginActive from './LoginActive';
class HomeIndex extends React.Component { class HomeIndex extends React.Component {
render() { render() {
@ -22,7 +22,7 @@ class HomeIndex extends React.Component {
<RequestTop/> <RequestTop/>
</Col> </Col>
<Col span={10} offset={1}> <Col span={10} offset={1}>
<DeployPie/> <LoginActive/>
</Col> </Col>
</Row> </Row>
</AuthDiv> </AuthDiv>

View File

@ -17,3 +17,9 @@
.spanButton:hover { .spanButton:hover {
color: #1890ff; color: #1890ff;
} }
.spanText {
cursor: pointer;
color: #1890ff;
padding: 0 4px;
}