mirror of https://github.com/jumpserver/jumpserver
Export login log (#2511)
* [Add]增加登录日志导出功能 * [Update]优化导出登录日志代码 * [Update]优化导出登录日志代码 * [Update]更改导出登录日志按钮pull/2512/head^2
parent
c8aa9d006f
commit
e4e6f59589
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -100,5 +101,22 @@ class UserLoginLog(models.Model):
|
||||||
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
||||||
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
|
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_login_logs(cls, date_form=None, date_to=None, user=None, keyword=None):
|
||||||
|
login_logs = cls.objects.all()
|
||||||
|
if date_form and date_to:
|
||||||
|
login_logs = login_logs.filter(
|
||||||
|
datetime__gt=date_form, datetime__lt=date_to
|
||||||
|
)
|
||||||
|
if user:
|
||||||
|
login_logs = login_logs.filter(username=user)
|
||||||
|
if keyword:
|
||||||
|
login_logs = login_logs.filter(
|
||||||
|
Q(ip__contains=keyword) |
|
||||||
|
Q(city__contains=keyword) |
|
||||||
|
Q(username__contains=keyword)
|
||||||
|
)
|
||||||
|
return login_logs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-datetime', 'username']
|
ordering = ['-datetime', 'username']
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
<div class="form-group" id="date">
|
<div class="form-group" id="date">
|
||||||
<div class="input-daterange input-group" id="datepicker">
|
<div class="input-daterange input-group" id="datepicker">
|
||||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
|
<input type="text" id="id_date_from" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
|
||||||
{# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#}
|
{# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#}
|
||||||
<span class="input-group-addon">to</span>
|
<span class="input-group-addon">to</span>
|
||||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
|
<input type="text" id="id_date_to" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
|
<input type="text" id="search" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
|
@ -43,38 +43,57 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block table_container %}
|
||||||
|
<table class="table table-striped table-bordered table-hover " id="login_log_table" >
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">{% trans 'ID' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Username' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Type' %}</th>
|
||||||
|
<th class="text-center">{% trans 'UA' %}</th>
|
||||||
|
<th class="text-center">{% trans 'IP' %}</th>
|
||||||
|
<th class="text-center">{% trans 'City' %}</th>
|
||||||
|
<th class="text-center">{% trans 'MFA' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Reason' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Status' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Date' %}</th>
|
||||||
|
</tr>
|
||||||
|
<thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for login_log in object_list %}
|
||||||
|
<tr class="gradeX">
|
||||||
|
<td class="text-center">{{ forloop.counter }}</td>
|
||||||
|
<td class="text-center">{{ login_log.username }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_type_display }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ login_log.ip }}</td>
|
||||||
|
<td class="text-center">{{ login_log.city }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||||
|
<td class="text-center">{{ login_log.datetime }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="actions" class="" style="margin-top: -20px">
|
||||||
|
<div class="input-group">
|
||||||
|
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
|
||||||
|
<option value="export">{% trans 'Export login log' %}</option>
|
||||||
|
</select>
|
||||||
|
<div class="input-group-btn pull-left" style="padding-left: 5px;">
|
||||||
|
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary btn_export">
|
||||||
|
{% trans 'Submit' %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block table_head %}
|
|
||||||
<th class="text-center">{% trans 'ID' %}</th>
|
|
||||||
<th class="text-center">{% trans 'Username' %}</th>
|
|
||||||
<th class="text-center">{% trans 'Type' %}</th>
|
|
||||||
<th class="text-center">{% trans 'UA' %}</th>
|
|
||||||
<th class="text-center">{% trans 'IP' %}</th>
|
|
||||||
<th class="text-center">{% trans 'City' %}</th>
|
|
||||||
<th class="text-center">{% trans 'MFA' %}</th>
|
|
||||||
<th class="text-center">{% trans 'Reason' %}</th>
|
|
||||||
<th class="text-center">{% trans 'Status' %}</th>
|
|
||||||
<th class="text-center">{% trans 'Date' %}</th>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block table_body %}
|
|
||||||
{% for login_log in object_list %}
|
|
||||||
<tr class="gradeX">
|
|
||||||
<td class="text-center">{{ forloop.counter }}</td>
|
|
||||||
<td class="text-center">{{ login_log.username }}</td>
|
|
||||||
<td class="text-center">{{ login_log.get_type_display }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">{{ login_log.ip }}</td>
|
|
||||||
<td class="text-center">{{ login_log.city }}</td>
|
|
||||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
|
||||||
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
|
||||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
|
||||||
<td class="text-center">{{ login_log.datetime }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block custom_foot_js %}
|
{% block custom_foot_js %}
|
||||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||||
|
@ -95,6 +114,29 @@
|
||||||
width: 'auto'
|
width: 'auto'
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.on('click', '.btn_export', function () {
|
||||||
|
var date_form = $('#id_date_from').val();
|
||||||
|
var date_to = $('#id_date_to').val();
|
||||||
|
var user = $('.select2 option:selected').val();
|
||||||
|
var keyword = $('#search').val();
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url "audits:login-log-export" %}",
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
'date_form':date_form,
|
||||||
|
'date_to':date_to,
|
||||||
|
'user':user,
|
||||||
|
'keyword':keyword
|
||||||
|
}),
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data, textStatus) {
|
||||||
|
window.open(data.redirect)
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
toastr.error('Export failed');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -14,4 +14,5 @@ urlpatterns = [
|
||||||
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
|
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
|
||||||
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
|
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
|
||||||
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
|
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
|
||||||
|
path('login-log/export/', views.LoginLogExportView.as_view(), name='login-log-export'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import csv
|
||||||
|
import codecs
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
|
def get_excel_response(filename):
|
||||||
|
excel_response = HttpResponse(content_type='text/csv')
|
||||||
|
excel_response[
|
||||||
|
'Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||||
|
excel_response.write(codecs.BOM_UTF8)
|
||||||
|
return excel_response
|
||||||
|
|
||||||
|
|
||||||
|
def write_content_to_excel(response, header=None, login_logs=None, fields=None):
|
||||||
|
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
|
||||||
|
if header:
|
||||||
|
writer.writerow(header)
|
||||||
|
if login_logs:
|
||||||
|
for log in login_logs:
|
||||||
|
data = [getattr(log, field.name) for field in fields]
|
||||||
|
writer.writerow(data)
|
||||||
|
return response
|
|
@ -1,8 +1,23 @@
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from audits.utils import get_excel_response, write_content_to_excel
|
||||||
from common.mixins import DatetimeSearchMixin
|
from common.mixins import DatetimeSearchMixin
|
||||||
from common.permissions import AdminUserRequiredMixin
|
from common.permissions import AdminUserRequiredMixin
|
||||||
|
|
||||||
|
@ -232,3 +247,38 @@ class CommandExecutionListView(UserCommandExecutionListView):
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class LoginLogExportView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
fields = [
|
||||||
|
field for field in UserLoginLog._meta.fields
|
||||||
|
]
|
||||||
|
filename = 'login-logs-{}.csv'.format(
|
||||||
|
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
|
)
|
||||||
|
excel_response = get_excel_response(filename)
|
||||||
|
header = [field.verbose_name for field in fields]
|
||||||
|
login_logs = cache.get(request.GET.get('spm', ''), [])
|
||||||
|
|
||||||
|
response = write_content_to_excel(excel_response, login_logs=login_logs,
|
||||||
|
header=header, fields=fields)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
date_form = json.loads(request.body).get('date_form', [])
|
||||||
|
date_to = json.loads(request.body).get('date_to', [])
|
||||||
|
user = json.loads(request.body).get('user', [])
|
||||||
|
keyword = json.loads(request.body).get('keyword', [])
|
||||||
|
|
||||||
|
login_logs = UserLoginLog.get_login_logs(
|
||||||
|
date_form=date_form, date_to=date_to, user=user, keyword=keyword)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponse('Json object not valid', status=400)
|
||||||
|
spm = uuid.uuid4().hex
|
||||||
|
cache.set(spm, login_logs, 300)
|
||||||
|
url = reverse('audits:login-log-export') + '?spm=%s' % spm
|
||||||
|
return JsonResponse({'redirect': url})
|
Loading…
Reference in New Issue