Commit d0ba67ed authored by ibuler's avatar ibuler

[Update] 基本完成登录二次审核

parent 517c6822
...@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin ...@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from common.permissions import PermissionsMixin ,IsOrgAdmin from common.permissions import PermissionsMixin, IsOrgAdmin
from common.const import create_success_msg, update_success_msg from common.const import create_success_msg, update_success_msg
from common.utils import get_object_or_none from common.utils import get_object_or_none
from ..models import Domain, Gateway from ..models import Domain, Gateway
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import time import time
...@@ -8,19 +7,17 @@ from django.core.cache import cache ...@@ -8,19 +7,17 @@ from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip from common.utils import get_logger, get_request_ip, get_object_or_none
from common.permissions import IsOrgAdminOrAppUser, IsValidUser from common.permissions import IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer from users.serializers import UserSerializer
from users.models import User from users.models import User
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import ( from users.utils import (
check_otp_code, increase_login_failed_count, check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count is_block_login, clean_failed_count
...@@ -33,7 +30,7 @@ from ..signals import post_auth_success, post_auth_failed ...@@ -33,7 +30,7 @@ from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', 'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
'UserOtpVerifyApi', 'UserOtpVerifyApi', 'UserOrderAcceptAuthApi',
] ]
...@@ -209,3 +206,26 @@ class UserOtpVerifyApi(CreateAPIView): ...@@ -209,3 +206,26 @@ class UserOtpVerifyApi(CreateAPIView):
else: else:
return Response({"error": "Code not valid"}, status=400) return Response({"error": "Code not valid"}, status=400)
class UserOrderAcceptAuthApi(APIView):
permission_classes = ()
def get(self, request, *args, **kwargs):
from orders.models import LoginConfirmOrder
order_id = self.request.session.get("auth_order_id")
logger.debug('Login confirm order id: {}'.format(order_id))
if not order_id:
order = None
else:
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
if not order:
error = _("No order found or order expired")
return Response({"error": error, "status": "not found"}, status=404)
if order.status == order.STATUS_ACCEPTED:
self.request.session["auth_confirm"] = "1"
return Response({"msg": "ok"})
elif order.status == order.STATUS_REJECTED:
error = _("Order was rejected by {}").format(order.assignee_display)
else:
error = "Order status: {}".format(order.status)
return Response({"error": error, "status": order.status}, status=400)
...@@ -71,7 +71,8 @@ class TokenCreateApi(CreateAPIView): ...@@ -71,7 +71,8 @@ class TokenCreateApi(CreateAPIView):
raise MFARequiredError() raise MFARequiredError()
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)
clean_failed_count(username, ip) clean_failed_count(username, ip)
return super().create(request, *args, **kwargs) resp = super().create(request, *args, **kwargs)
return resp
except AuthFailedError as e: except AuthFailedError as e:
increase_login_failed_count(username, ip) increase_login_failed_count(username, ip)
self.send_auth_signal(success=False, user=user, username=username, reason=str(e)) self.send_auth_signal(success=False, user=user, username=username, reason=str(e))
...@@ -80,8 +81,8 @@ class TokenCreateApi(CreateAPIView): ...@@ -80,8 +81,8 @@ class TokenCreateApi(CreateAPIView):
msg = _("MFA required") msg = _("MFA required")
seed = uuid.uuid4().hex seed = uuid.uuid4().hex
cache.set(seed, user.username, 300) cache.set(seed, user.username, 300)
resp = {'msg': msg, "choices": ["otp"], "req": seed} data = {'msg': msg, "choices": ["otp"], "req": seed}
return Response(resp, status=300) return Response(data, status=300)
def send_auth_signal(self, success=True, user=None, username='', reason=''): def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success: if success:
......
...@@ -49,8 +49,8 @@ class LoginConfirmSetting(CommonModelMixin): ...@@ -49,8 +49,8 @@ class LoginConfirmSetting(CommonModelMixin):
return get_object_or_none(cls, user=user) return get_object_or_none(cls, user=user)
def create_confirm_order(self, request=None): def create_confirm_order(self, request=None):
from orders.models import Order from orders.models import LoginConfirmOrder
title = _('User login request confirm: {}'.format(self.user)) title = _('User login request: {}'.format(self.user))
if request: if request:
remote_addr = get_request_ip(request) remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr) city = get_ip_city(remote_addr)
...@@ -58,14 +58,17 @@ class LoginConfirmSetting(CommonModelMixin): ...@@ -58,14 +58,17 @@ class LoginConfirmSetting(CommonModelMixin):
self.user, remote_addr, city, timezone.now() self.user, remote_addr, city, timezone.now()
) )
else: else:
city = ''
remote_addr = ''
body = '' body = ''
reviewer = self.reviewers.all() reviewer = self.reviewers.all()
reviewer_names = ','.join([u.name for u in reviewer]) reviewer_names = ','.join([u.name for u in reviewer])
order = Order.objects.create( order = LoginConfirmOrder.objects.create(
user=self.user, user_display=str(self.user), user=self.user, user_display=str(self.user),
title=title, body=body, title=title, body=body,
city=city, ip=remote_addr,
assignees_display=reviewer_names, assignees_display=reviewer_names,
type=Order.TYPE_LOGIN_REQUEST, type=LoginConfirmOrder.TYPE_LOGIN_CONFIRM,
) )
order.assignees.set(reviewer) order.assignees.set(reviewer)
return order return order
......
...@@ -6,13 +6,11 @@ ...@@ -6,13 +6,11 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<title>{{ title }}</title> <title>{{ title }}</title>
{% include '_head_css_js.html' %} {% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet"> <link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" <script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script> <script src="{% static "js/jumpserver.js" %}"></script>
</head> </head>
...@@ -29,23 +27,29 @@ ...@@ -29,23 +27,29 @@
</h2> </h2>
</div> </div>
<p></p> <p></p>
<div class="alert alert-success" id="messages"> <div class="alert alert-success info-messages" >
Wait for Guanghongwei confirm, You also can copy link to her/his <br/> {{ msg|safe }}
Don't close .... </div>
<div class="alert alert-danger error-messages" style="display: none">
</div> </div>
<div class="progress progress-bar-default"> <div class="progress progress-bar-default progress-striped active">
<div style="width: 43%" aria-valuemax="100" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar"> <div aria-valuemax="3600" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-3"> <div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b"> <a class="btn btn-primary btn-sm block btn-refresh">
{% trans 'Refresh' %} <i class="fa fa-refresh"></i> {% trans 'Refresh' %}
</a>
</div>
<div class="col-lg-3">
<a class="btn btn-primary btn-sm block btn-copy" data-link="{{ order_detail_url }}">
<i class="fa fa-clipboard"></i> {% trans 'Copy link' %}
</a> </a>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b"> <a class="btn btn-default btn-sm block btn-return" href="{% url 'authentication:login' %}">
{% trans 'Copy link' %} <i class="fa fa-reply"></i> {% trans 'Return' %}
</a> </a>
</div> </div>
</div> </div>
...@@ -63,26 +67,76 @@ ...@@ -63,26 +67,76 @@
</div> </div>
</div> </div>
</body> </body>
{% include '_foot_js.html' %}
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script> <script>
var time = '{{ interval }}'; var errorMsgShow = false;
if (!time) { var errorMsgRef = $(".error-messages");
time = 5; var infoMsgRef = $(".info-messages");
} else { var timestamp = '{{ timestamp }}';
time = parseInt(time); var progressBarRef = $(".progress-bar");
} var interval, checkInterval;
var url = "{% url 'api-auth:user-order-auth' %}";
var successUrl = "{% url 'authentication:login-guard' %}";
function redirect_page() { function doRequestAuth() {
if (time >= 0) { requestApi({
var messages = '{{ messages|safe }}, <b>' + time + '</b> ...'; url: url,
$('#messages').html(messages); method: "GET",
time--; success: function () {
setTimeout(redirect_page, 1000); clearInterval(interval);
} else { clearInterval(checkInterval);
window.location.href = "{{ redirect_url }}"; window.location = successUrl;
},
error: function (text, data) {
if (data.status !== "pending") {
if (!errorMsgShow) {
infoMsgRef.hide();
errorMsgRef.show();
progressBarRef.addClass('progress-bar-danger');
errorMsgShow = true;
}
clearInterval(interval);
clearInterval(checkInterval);
$(".copy-btn").attr('disabled', 'disabled')
}
errorMsgRef.html(data.error)
},
flash_message: false
})
}
function initClipboard() {
var clipboard = new Clipboard('.btn-copy', {
text: function (trigger) {
var origin = window.location.origin;
var link = origin + $(".btn-copy").data('link');
return link
} }
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function handleProgressBar() {
var now = new Date().getTime() / 1000;
var offset = now - timestamp;
var percent = offset / 3600 * 100;
if (percent > 100) {
percent = 100
} }
{% if auto_redirect %} progressBarRef.css("width", percent + '%');
window.onload = redirect_page; progressBarRef.attr('aria-valuenow', offset);
{% endif %} }
$(document).ready(function () {
interval = setInterval(handleProgressBar, 1000);
checkInterval = setInterval(doRequestAuth, 5000);
doRequestAuth();
initClipboard();
}).on('click', '.btn-refresh', function () {
window.location.reload();
})
</script> </script>
</html> </html>
# coding:utf-8 # coding:utf-8
# #
from __future__ import absolute_import
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .. import api from .. import api
app_name = 'authentication'
router = DefaultRouter() router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key') router.register('access-keys', api.AccessKeyViewSet, 'access-key')
app_name = 'authentication'
urlpatterns = [ urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'), # path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
...@@ -24,6 +19,7 @@ urlpatterns = [ ...@@ -24,6 +19,7 @@ urlpatterns = [
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth')
] ]
urlpatterns += router.urls urlpatterns += router.urls
......
...@@ -16,7 +16,7 @@ urlpatterns = [ ...@@ -16,7 +16,7 @@ urlpatterns = [
# login # login
path('login/', views.UserLoginView.as_view(), name='login'), path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('login/continue/', views.UserLoginContinueView.as_view(), name='login-continue'), path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
path('login/wait/', views.UserLoginWaitConfirmView.as_view(), name='login-wait'), path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
path('logout/', views.UserLogoutView.as_view(), name='logout'), path('logout/', views.UserLogoutView.as_view(), name='logout'),
] ]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _, ugettext_lazy as __
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils import timezone
from common.utils import get_ip_city, get_object_or_none, validate_ip from common.utils import (
get_ip_city, get_object_or_none, validate_ip, get_request_ip
)
from users.models import User from users.models import User
from . import const from . import const
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import datetime
from django.core.cache import cache from django.core.cache import cache
from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse from django.http import HttpResponse
...@@ -12,17 +13,18 @@ from django.utils.translation import ugettext as _ ...@@ -12,17 +13,18 @@ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView, View, RedirectView from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django.conf import settings from django.conf import settings
from common.utils import get_request_ip from common.utils import get_request_ip, get_object_or_none
from users.models import User from users.models import User
from users.utils import ( from users.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user, check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user,
set_tmp_user_to_cache, increase_login_failed_count, set_tmp_user_to_cache, increase_login_failed_count,
redirect_user_first_login_or_index, redirect_user_first_login_or_index
) )
from ..models import LoginConfirmSetting
from ..signals import post_auth_success, post_auth_failed from ..signals import post_auth_success, post_auth_failed
from .. import forms from .. import forms
from .. import const from .. import const
...@@ -30,7 +32,7 @@ from .. import const ...@@ -30,7 +32,7 @@ from .. import const
__all__ = [ __all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
'UserLoginContinueView', 'UserLoginWaitConfirmView', 'UserLoginGuardView', 'UserLoginWaitConfirmView',
] ]
...@@ -91,7 +93,7 @@ class UserLoginView(FormView): ...@@ -91,7 +93,7 @@ class UserLoginView(FormView):
# 登陆成功,清除缓存计数 # 登陆成功,清除缓存计数
clean_failed_count(username, ip) clean_failed_count(username, ip)
self.request.session['auth_password'] = '1' self.request.session['auth_password'] = '1'
return self.redirect_to_continue_view() return self.redirect_to_guard_view()
def form_invalid(self, form): def form_invalid(self, form):
# write login failed log # write login failed log
...@@ -112,8 +114,8 @@ class UserLoginView(FormView): ...@@ -112,8 +114,8 @@ class UserLoginView(FormView):
return super().form_invalid(form) return super().form_invalid(form)
@staticmethod @staticmethod
def redirect_to_continue_view(): def redirect_to_guard_view():
continue_url = reverse('authentication:login-continue') continue_url = reverse('authentication:login-guard')
return redirect(continue_url) return redirect(continue_url)
def get_form_class(self): def get_form_class(self):
...@@ -144,7 +146,7 @@ class UserLoginOtpView(FormView): ...@@ -144,7 +146,7 @@ class UserLoginOtpView(FormView):
if check_otp_code(otp_secret_key, otp_code): if check_otp_code(otp_secret_key, otp_code):
self.request.session['auth_otp'] = '1' self.request.session['auth_otp'] = '1'
return UserLoginView.redirect_to_continue_view() return UserLoginView.redirect_to_guard_view()
else: else:
self.send_auth_signal( self.send_auth_signal(
success=False, username=user.username, success=False, username=user.username,
...@@ -165,7 +167,7 @@ class UserLoginOtpView(FormView): ...@@ -165,7 +167,7 @@ class UserLoginOtpView(FormView):
) )
class UserLoginContinueView(RedirectView): class UserLoginGuardView(RedirectView):
redirect_field_name = 'next' redirect_field_name = 'next'
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
...@@ -173,11 +175,18 @@ class UserLoginContinueView(RedirectView): ...@@ -173,11 +175,18 @@ class UserLoginContinueView(RedirectView):
return reverse('authentication:login') return reverse('authentication:login')
user = get_user_or_tmp_user(self.request) user = get_user_or_tmp_user(self.request)
# 启用并设置了otp
if user.otp_enabled and user.otp_secret_key and \ if user.otp_enabled and user.otp_secret_key and \
not self.request.session.get('auth_otp'): not self.request.session.get('auth_otp'):
return reverse('authentication:login-otp') return reverse('authentication:login-otp')
confirm_setting = LoginConfirmSetting.get_user_confirm_setting(user)
if confirm_setting and not self.request.session.get('auth_confirm'):
order = confirm_setting.create_confirm_order(self.request)
self.request.session['auth_order_id'] = str(order.id)
url = reverse('authentication:login-wait-confirm')
return url
self.login_success(user) self.login_success(user)
# 启用但是没有设置otp
if user.otp_enabled and not user.otp_secret_key: if user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F # 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication') return reverse('users:user-otp-enable-authentication')
...@@ -204,7 +213,28 @@ class UserLoginWaitConfirmView(TemplateView): ...@@ -204,7 +213,28 @@ class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html' template_name = 'authentication/login_wait_confirm.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) from orders.models import LoginConfirmOrder
order_id = self.request.session.get("auth_order_id")
if not order_id:
order = None
else:
order = get_object_or_none(LoginConfirmOrder, pk=order_id)
context = super().get_context_data(**kwargs)
if order:
order_detail_url = reverse('orders:login-confirm-order-detail', kwargs={'pk': order_id})
timestamp_created = datetime.datetime.timestamp(order.date_created)
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
Don't close this page""").format(order.assignees_display)
else:
timestamp_created = 0
order_detail_url = ''
msg = _("No order found")
context.update({
"msg": msg,
"timestamp": timestamp_created,
"order_detail_url": order_detail_url
})
return context
@method_decorator(never_cache, name='dispatch') @method_decorator(never_cache, name='dispatch')
......
...@@ -13,22 +13,23 @@ from .celery_flower import celery_flower_view ...@@ -13,22 +13,23 @@ from .celery_flower import celery_flower_view
from .swagger import get_swagger_view from .swagger import get_swagger_view
api_v1 = [ api_v1 = [
path('users/', include('users.urls.api_urls', namespace='api-users')), path('users/', include('users.urls.api_urls', namespace='api-users')),
path('assets/', include('assets.urls.api_urls', namespace='api-assets')), path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/', include('perms.urls.api_urls', namespace='api-perms')), path('perms/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/', include('ops.urls.api_urls', namespace='api-ops')), path('ops/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/', include('audits.urls.api_urls', namespace='api-audits')), path('audits/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/', include('settings.urls.api_urls', namespace='api-settings')), path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')), path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('orders/', include('orders.urls.api_urls', namespace='api-orders')),
] ]
api_v2 = [ api_v2 = [
path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
] ]
...@@ -42,6 +43,7 @@ app_view_patterns = [ ...@@ -42,6 +43,7 @@ app_view_patterns = [
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('applications/', include('applications.urls.views_urls', namespace='applications')), path('applications/', include('applications.urls.views_urls', namespace='applications')),
path('orders/', include('orders.urls.views_urls', namespace='orders')),
re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'), re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'),
] ]
......
This diff is collapsed.
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets, generics
from django.shortcuts import get_object_or_404
from common.permissions import IsValidUser
from common.mixins import CommonApiMixin
from . import serializers
from .models import LoginConfirmOrder
class LoginConfirmOrderViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.LoginConfirmOrderSerializer
permission_classes = (IsValidUser,)
search_fields = ['user_display', 'title', 'ip', 'city']
def get_queryset(self):
queryset = LoginConfirmOrder.objects.all()\
.filter(assignees=self.request.user)
return queryset
class LoginConfirmOrderCreateActionApi(generics.CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = serializers.LoginConfirmOrderActionSerializer
def get_order(self):
order_id = self.kwargs.get('pk')
queryset = LoginConfirmOrder.objects.all()\
.filter(assignees=self.request.user)
order = get_object_or_404(queryset, id=order_id)
return order
def get_serializer_context(self):
context = super().get_serializer_context()
order = self.get_order()
context['order'] = order
return context
...@@ -3,3 +3,7 @@ from django.apps import AppConfig ...@@ -3,3 +3,7 @@ from django.apps import AppConfig
class OrdersConfig(AppConfig): class OrdersConfig(AppConfig):
name = 'orders' name = 'orders'
def ready(self):
from . import signals_handler
return super().ready()
...@@ -3,31 +3,56 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -3,31 +3,56 @@ from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
__all__ = ['LoginConfirmOrder', 'Comment']
class Order(CommonModelMixin):
class Comment(CommonModelMixin):
order_id = models.UUIDField()
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
class Meta:
ordering = ('date_created', )
class BaseOrder(CommonModelMixin):
STATUS_ACCEPTED = 'accepted'
STATUS_REJECTED = 'rejected'
STATUS_PENDING = 'pending'
STATUS_CHOICES = ( STATUS_CHOICES = (
('accepted', _("Accepted")), (STATUS_ACCEPTED, _("Accepted")),
('rejected', _("Rejected")), (STATUS_REJECTED, _("Rejected")),
('pending', _("Pending")) (STATUS_PENDING, _("Pending"))
) )
TYPE_LOGIN_REQUEST = 'login_request' TYPE_LOGIN_CONFIRM = 'login_confirm'
TYPE_CHOICES = ( TYPE_CHOICES = (
(TYPE_LOGIN_REQUEST, _("Login request")), (TYPE_LOGIN_CONFIRM, 'Login confirm'),
) )
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name=_("User")) user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
user_display = models.CharField(max_length=128, verbose_name=_("User display name")) user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
title = models.CharField(max_length=256, verbose_name=_("Title")) title = models.CharField(max_length=256, verbose_name=_("Title"))
body = models.TextField(verbose_name=_("Body")) body = models.TextField(verbose_name=_("Body"))
assignees = models.ManyToManyField('users.User', related_name='assign_orders', verbose_name=_("Assignees")) assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee"))
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True) assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
type = models.CharField(choices=TYPE_CHOICES, max_length=16, verbose_name=_('Type'))
type = models.CharField(choices=TYPE_CHOICES, max_length=64)
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='pending') status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='pending')
def __str__(self): def __str__(self):
return '{}: {}'.format(self.user_display, self.title) return '{}: {}'.format(self.user_display, self.title)
@property
def comments(self):
return Comment.objects.filter(order_id=self.id)
class Meta: class Meta:
ordering = ('date_created',) abstract = True
ordering = ('-date_created',)
class LoginConfirmOrder(BaseOrder):
ip = models.GenericIPAddressField(blank=True, null=True)
city = models.CharField(max_length=16, blank=True, default='')
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from .models import LoginConfirmOrder, Comment
class LoginConfirmOrderSerializer(serializers.ModelSerializer):
class Meta:
model = LoginConfirmOrder
fields = [
'id', 'user', 'user_display', 'title', 'body',
'ip', 'city', 'assignees', 'assignees_display',
'type', 'status', 'date_created', 'date_updated',
]
class LoginConfirmOrderActionSerializer(serializers.Serializer):
ACTION_CHOICES = (
('accept', _('Accept')),
('reject', _('Reject')),
('comment', _('Comment'))
)
action = serializers.ChoiceField(choices=ACTION_CHOICES)
comment = serializers.CharField(allow_blank=True)
def update(self, instance, validated_data):
pass
def create_comments(self, order, user, validated_data):
comment_data = validated_data.get('comment')
action = validated_data.get('action')
comments_data = []
if comment_data:
comments_data.append(comment_data)
Comment.objects.create(
order_id=order.id, body=comment_data, user=user,
user_display=str(user)
)
if action != "comment":
action_display = dict(self.ACTION_CHOICES).get(action)
comment_data = '{} {} {}'.format(user, action_display, _("this order"))
comments_data.append(comment_data)
comments = [
Comment(order_id=order.id, body=data, user=user, user_display=str(user))
for data in comments_data
]
Comment.objects.bulk_create(comments)
@staticmethod
def perform_action(order, user, validated_data):
action = validated_data.get('action')
if action == "accept":
status = "accepted"
elif action == "reject":
status = "rejected"
else:
status = None
if status:
order.status = status
order.assignee = user
order.assignee_display = str(user)
order.save()
def create(self, validated_data):
order = self.context['order']
user = self.context['request'].user
self.create_comments(order, user, validated_data)
self.perform_action(order, user, validated_data)
return validated_data
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from django.dispatch import receiver
from django.db.models.signals import m2m_changed
from django.conf import settings
from common.tasks import send_mail_async
from common.utils import get_logger, reverse
from .models import LoginConfirmOrder
logger = get_logger(__name__)
def send_mail(order, assignees):
recipient_list = [user.email for user in assignees]
user = order.user
if not recipient_list:
logger.error("Order not has assignees: {}".format(order.id))
return
subject = '{}: {}'.format(_("New order"), order.title)
detail_url = reverse('orders:login-confirm-order-detail',
kwargs={'pk': order.id}, external=True)
message = _("""
<div>
<p>Your has a new order</p>
<div>
<b>Title:</b> {order.title}
<br/>
<b>User:</b> {user}
<br/>
<b>City:</b> {order.city}
<br/>
<b>IP:</b> {order.ip}
<br/>
<a href={url}>click here to review</a>
</div>
</div>
""").format(order=order, user=user, url=detail_url)
if settings.DEBUG:
try:
print(message)
except OSError:
pass
send_mail_async.delay(subject, message, recipient_list, html_message=message)
@receiver(m2m_changed, sender=LoginConfirmOrder.assignees.through)
def on_login_confirm_order_assignee_set(sender, instance=None, action=None,
model=None, pk_set=None, **kwargs):
print(">>>>>>>>>>>>>>>>>>>>>>>.")
print(action)
if action == 'post_add':
print("<<<<<<<<<<<<<<<<<<<<")
logger.debug('New order create, send mail: {}'.format(instance.id))
assignees = model.objects.filter(pk__in=pk_set)
send_mail(instance, assignees)
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>
{{ object.title }}
</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<div class="row">
<div class="col-lg-11">
<div class="row">
<div class="col-lg-6">
<dl class="dl-horizontal">
<dt>{% trans 'User' %}:</dt> <dd>{{ object.user_display }}</dd>
<dt>{% trans 'IP' %}:</dt> <dd>{{ object.ip }}</dd>
<dt>{% trans 'Assignees' %}:</dt> <dd> {{ object.assignees_display }}</dd>
<dt>{% trans 'Status' %}:</dt>
<dd>
{% if object.status == "accpeted" %}
<span class="label label-primary">
{{ object.get_status_display }}
</span>
{% endif %}
{% if object.status == "rejected" %}
<span class="label label-danger">
{{ object.get_status_display }}
</span>
{% endif %}
{% if object.status == "pending" %}
<span class="label label-info">
{{ object.get_status_display }}
</span>
{% endif %}
</dd>
</dl>
</div>
<div class="col-lg-6">
<dl class="dl-horizontal">
<dt><br></dt><dd></dd>
<dt>{% trans 'City' %}:</dt> <dd>{{ object.city }}</dd>
<dt>{% trans 'Assignee' %}:</dt> <dd>{{ object.assignee_display | default_if_none:"" }}</dd>
<dt>{% trans 'Date created' %}:</dt> <dd> {{ object.date_created }}</dd>
</dl>
</div>
</div>
<div class="row m-t-sm">
<div class="col-lg-12">
<div class="panel blank-panel">
<div class="panel-body">
<div class="feed-activity-list">
{% for comment in object.comments %}
<div class="feed-element">
<a href="#" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
</a>
<div class="media-body ">
<strong>{{ comment.user_display }}</strong> <small class="text-muted"> {{ comment.date_created|timesince}} {% trans 'ago' %}</small>
<br/>
<small class="text-muted">{{ comment.date_created }} </small>
<div style="padding-top: 10px">
{{ comment.body }}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="feed-element">
<a href="" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
</a>
<div class="media-body">
<textarea class="form-control" placeholder="" id="comment"></textarea>
</div>
</div>
<div class="text-right">
<a class="btn btn-sm btn-primary btn-action btn-update" data-action="accept"><i class="fa fa-check"></i> {% trans 'Accept' %}</a>
<a class="btn btn-sm btn-danger btn-action btn-update" data-action="reject"><i class="fa fa-times"></i> {% trans 'Reject' %}</a>
<a class="btn btn-sm btn-info btn-action" data-action="comment"><i class="fa fa-pencil"></i> {% trans 'Comment' %}</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-1">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var orderId = "{{ object.id }}";
var status = "{{ object.status }}";
var actionCreateUrl = "{% url 'api-orders:login-confirm-order-create-action' pk=object.id %}";
$(document).ready(function () {
if (status !== "pending") {
$('.btn-update').attr('disabled', '1')
}
})
.on('click', '.btn-action', function () {
var action = $(this).data('action');
var comment = $("#comment").val();
var data = {
url: actionCreateUrl,
method: 'POST',
body: JSON.stringify({action: action, comment: comment}),
success: function () {
window.location.reload();
}
};
requestApi(data);
})
</script>
{% endblock %}
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
{% endblock %}
{% block table_container %}
<table class="table table-striped table-bordered table-hover " id="login_confirm_order_list_table" >
<thead>
<tr>
<th class="text-center">
<input id="" type="checkbox" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'City' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
var orderTable = 0;
function initTable() {
var options = {
ele: $('#login_confirm_order_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "orders:login-confirm-order-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
if (cellData === "accepted") {
$(td).html('<i class="fa fa-check text-navy"></i>')
} else if (cellData === "rejected") {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else if (cellData === "pending") {
$(td).html('<i class="fa fa-spinner text-info"></i>')
}
}},
{targets: 6, createdCell: function (td, cellData) {
var d = toSafeLocalDateStr(cellData);
$(td).html(d)
}},
{targets: 7, createdCell: function (td, cellData, rowData) {
var acceptBtn = '<a class="btn btn-xs btn-info" data-uid="{{ DEFAULT_PK }}" >{% trans "Accept" %}</a> ';
var rejectBtn = '<a class="btn btn-xs btn-danger " data-uid="{{ DEFAULT_PK }}" >{% trans "Reject" %}</a>';
acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData);
rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData);
var acceptBtnRef = $(acceptBtn);
var rejectBtnRef = $(rejectBtn);
if (rowData.status !== "pending") {
acceptBtnRef.attr('disabled', 'disabled');
rejectBtnRef.attr('disabled', 'disabled');
}
var btn = acceptBtnRef.prop('outerHTML') + ' ' + rejectBtnRef.prop('outerHTML');
$(td).html(btn)
}}],
ajax_url: '{% url "api-orders:login-confirm-order-list" %}',
columns: [
{data: "id"}, {data: "title", className: "text-left"}, {data: "user_display"},
{data: "ip"}, {data: "city"},
{data: "status", orderable: false},
{data: "date_created"},
{data: "id", orderable: false, width: "100px"}
],
op_html: $('#actions').html()
};
orderTable = jumpserver.initServerSideDataTable(options);
return orderTable
}
$(document).ready(function(){
initTable();
}).on('click', '.expired', function () {
var msg = '{% trans "User is expired" %}';
toastr.error(msg)
}).on('click', '.inactive', function () {
var msg = '{% trans 'User is inactive' %}';
toastr.error(msg)
})
</script>
{% endblock %}
# -*- coding: utf-8 -*-
#
# -*- coding: utf-8 -*-
#
from django.urls import path
from rest_framework.routers import DefaultRouter
from .. import api
app_name = 'orders'
router = DefaultRouter()
router.register('login-confirm-orders', api.LoginConfirmOrderViewSet, 'login-confirm-order')
urlpatterns = [
path('login-confirm-order/<uuid:pk>/actions/',
api.LoginConfirmOrderCreateActionApi.as_view(),
name='login-confirm-order-create-action'
),
]
urlpatterns += router.urls
# -*- coding: utf-8 -*-
#
from django.urls import path
from .. import views
app_name = 'orders'
urlpatterns = [
path('login-confirm-orders/', views.LoginConfirmOrderListView.as_view(), name='login-confirm-order-list'),
path('login-confirm-orders/<uuid:pk>/', views.LoginConfirmOrderDetailView.as_view(), name='login-confirm-order-detail')
]
# -*- coding: utf-8 -*-
#
from django.shortcuts import render from django.views.generic import TemplateView, DetailView
from django.utils.translation import ugettext as _
# Create your views here. from common.permissions import PermissionsMixin, IsOrgAdmin
from .models import LoginConfirmOrder
class LoginConfirmOrderListView(PermissionsMixin, TemplateView):
template_name = 'orders/login_confirm_order_list.html'
permission_classes = (IsOrgAdmin,)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'app': _("Orders"),
'action': _("Login confirm order list")
})
return context
class LoginConfirmOrderDetailView(PermissionsMixin, DetailView):
template_name = 'orders/login_confirm_order_detail.html'
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
return LoginConfirmOrder.objects.filter(assignees=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'app': _("Orders"),
'action': _("Login confirm order detail")
})
return context
...@@ -307,7 +307,7 @@ function requestApi(props) { ...@@ -307,7 +307,7 @@ function requestApi(props) {
toastr.error(msg); toastr.error(msg);
} }
if (typeof props.error === 'function') { if (typeof props.error === 'function') {
return props.error(jqXHR.responseText, jqXHR.status); return props.error(jqXHR.responseText, jqXHR.responseJSON, jqXHR.status);
} }
}); });
// return true; // return true;
......
...@@ -121,6 +121,16 @@ ...@@ -121,6 +121,16 @@
</li> </li>
{% endif %} {% endif %}
{% if request.user.can_admin_current_org %}
<li id="orders">
<a>
<i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Orders' %}</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li id="login-confirm-orders"><a href="{% url 'orders:login-confirm-order-list' %}">{% trans 'Login confirm' %}</a></li>
</ul>
</li>
{% endif %}
{# Audits #} {# Audits #}
{% if request.user.can_admin_or_audit_current_org %} {% if request.user.can_admin_or_audit_current_org %}
...@@ -175,4 +185,4 @@ ...@@ -175,4 +185,4 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
}) })
</script> </script>
\ No newline at end of file
...@@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user): ...@@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user):
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)
def get_user_or_tmp_user(request): def get_user_or_tmp_user(request):
user = request.user user = request.user
tmp_user = get_tmp_user_from_cache(request) tmp_user = get_tmp_user_from_cache(request)
...@@ -212,8 +211,8 @@ def get_tmp_user_from_cache(request): ...@@ -212,8 +211,8 @@ def get_tmp_user_from_cache(request):
return user return user
def set_tmp_user_to_cache(request, user): def set_tmp_user_to_cache(request, user, ttl=3600):
cache.set(request.session.session_key+'user', user, 600) cache.set(request.session.session_key+'user', user, ttl)
def redirect_user_first_login_or_index(request, redirect_field_name): def redirect_user_first_login_or_index(request, redirect_field_name):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment