Commit d0ba67ed authored by ibuler's avatar ibuler

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

parent 517c6822
......@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
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.utils import get_object_or_none
from ..models import Domain, Gateway
......
# -*- coding: utf-8 -*-
#
import uuid
import time
......@@ -8,19 +7,17 @@ from django.core.cache import cache
from django.urls import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
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 orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer
from users.models import User
from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
......@@ -33,7 +30,7 @@ from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__)
__all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
'UserOtpVerifyApi',
'UserOtpVerifyApi', 'UserOrderAcceptAuthApi',
]
......@@ -209,3 +206,26 @@ class UserOtpVerifyApi(CreateAPIView):
else:
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):
raise MFARequiredError()
self.send_auth_signal(success=True, user=user)
clean_failed_count(username, ip)
return super().create(request, *args, **kwargs)
resp = super().create(request, *args, **kwargs)
return resp
except AuthFailedError as e:
increase_login_failed_count(username, ip)
self.send_auth_signal(success=False, user=user, username=username, reason=str(e))
......@@ -80,8 +81,8 @@ class TokenCreateApi(CreateAPIView):
msg = _("MFA required")
seed = uuid.uuid4().hex
cache.set(seed, user.username, 300)
resp = {'msg': msg, "choices": ["otp"], "req": seed}
return Response(resp, status=300)
data = {'msg': msg, "choices": ["otp"], "req": seed}
return Response(data, status=300)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
......
......@@ -49,8 +49,8 @@ class LoginConfirmSetting(CommonModelMixin):
return get_object_or_none(cls, user=user)
def create_confirm_order(self, request=None):
from orders.models import Order
title = _('User login request confirm: {}'.format(self.user))
from orders.models import LoginConfirmOrder
title = _('User login request: {}'.format(self.user))
if request:
remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr)
......@@ -58,14 +58,17 @@ class LoginConfirmSetting(CommonModelMixin):
self.user, remote_addr, city, timezone.now()
)
else:
city = ''
remote_addr = ''
body = ''
reviewer = self.reviewers.all()
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),
title=title, body=body,
city=city, ip=remote_addr,
assignees_display=reviewer_names,
type=Order.TYPE_LOGIN_REQUEST,
type=LoginConfirmOrder.TYPE_LOGIN_CONFIRM,
)
order.assignees.set(reviewer)
return order
......
......@@ -6,13 +6,11 @@
<head>
<meta charset="utf-8">
<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>
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript"
src="{% url 'javascript-catalog' %}"></script>
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>
......@@ -29,23 +27,29 @@
</h2>
</div>
<p></p>
<div class="alert alert-success" id="messages">
Wait for Guanghongwei confirm, You also can copy link to her/his <br/>
Don't close ....
<div class="alert alert-success info-messages" >
{{ msg|safe }}
</div>
<div class="alert alert-danger error-messages" style="display: none">
</div>
<div class="progress progress-bar-default">
<div style="width: 43%" aria-valuemax="100" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar">
<div class="progress progress-bar-default progress-striped active">
<div aria-valuemax="3600" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar">
</div>
</div>
<div class="row">
<div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
{% trans 'Refresh' %}
<a class="btn btn-primary btn-sm block btn-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>
</div>
<div class="col-lg-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
{% trans 'Copy link' %}
<a class="btn btn-default btn-sm block btn-return" href="{% url 'authentication:login' %}">
<i class="fa fa-reply"></i> {% trans 'Return' %}
</a>
</div>
</div>
......@@ -63,26 +67,76 @@
</div>
</div>
</body>
{% include '_foot_js.html' %}
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
var time = '{{ interval }}';
if (!time) {
time = 5;
} else {
time = parseInt(time);
}
var errorMsgShow = false;
var errorMsgRef = $(".error-messages");
var infoMsgRef = $(".info-messages");
var timestamp = '{{ timestamp }}';
var progressBarRef = $(".progress-bar");
var interval, checkInterval;
var url = "{% url 'api-auth:user-order-auth' %}";
var successUrl = "{% url 'authentication:login-guard' %}";
function redirect_page() {
if (time >= 0) {
var messages = '{{ messages|safe }}, <b>' + time + '</b> ...';
$('#messages').html(messages);
time--;
setTimeout(redirect_page, 1000);
} else {
window.location.href = "{{ redirect_url }}";
function doRequestAuth() {
requestApi({
url: url,
method: "GET",
success: function () {
clearInterval(interval);
clearInterval(checkInterval);
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 %}
window.onload = redirect_page;
{% endif %}
progressBarRef.css("width", percent + '%');
progressBarRef.attr('aria-valuenow', offset);
}
$(document).ready(function () {
interval = setInterval(handleProgressBar, 1000);
checkInterval = setInterval(doRequestAuth, 5000);
doRequestAuth();
initClipboard();
}).on('click', '.btn-refresh', function () {
window.location.reload();
})
</script>
</html>
# coding:utf-8
#
from __future__ import absolute_import
from django.urls import path
from rest_framework.routers import DefaultRouter
from .. import api
app_name = 'authentication'
router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
app_name = 'authentication'
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
......@@ -24,6 +19,7 @@ urlpatterns = [
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
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
......
......@@ -16,7 +16,7 @@ urlpatterns = [
# login
path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('login/continue/', views.UserLoginContinueView.as_view(), name='login-continue'),
path('login/wait/', views.UserLoginWaitConfirmView.as_view(), name='login-wait'),
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
]
# -*- 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.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 . import const
......
......@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import os
import datetime
from django.core.cache import cache
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse
......@@ -12,17 +13,18 @@ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
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.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.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user,
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 .. import forms
from .. import const
......@@ -30,7 +32,7 @@ from .. import const
__all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
'UserLoginContinueView', 'UserLoginWaitConfirmView',
'UserLoginGuardView', 'UserLoginWaitConfirmView',
]
......@@ -91,7 +93,7 @@ class UserLoginView(FormView):
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
self.request.session['auth_password'] = '1'
return self.redirect_to_continue_view()
return self.redirect_to_guard_view()
def form_invalid(self, form):
# write login failed log
......@@ -112,8 +114,8 @@ class UserLoginView(FormView):
return super().form_invalid(form)
@staticmethod
def redirect_to_continue_view():
continue_url = reverse('authentication:login-continue')
def redirect_to_guard_view():
continue_url = reverse('authentication:login-guard')
return redirect(continue_url)
def get_form_class(self):
......@@ -144,7 +146,7 @@ class UserLoginOtpView(FormView):
if check_otp_code(otp_secret_key, otp_code):
self.request.session['auth_otp'] = '1'
return UserLoginView.redirect_to_continue_view()
return UserLoginView.redirect_to_guard_view()
else:
self.send_auth_signal(
success=False, username=user.username,
......@@ -165,7 +167,7 @@ class UserLoginOtpView(FormView):
)
class UserLoginContinueView(RedirectView):
class UserLoginGuardView(RedirectView):
redirect_field_name = 'next'
def get_redirect_url(self, *args, **kwargs):
......@@ -173,11 +175,18 @@ class UserLoginContinueView(RedirectView):
return reverse('authentication:login')
user = get_user_or_tmp_user(self.request)
# 启用并设置了otp
if user.otp_enabled and user.otp_secret_key and \
not self.request.session.get('auth_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)
# 启用但是没有设置otp
if user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
......@@ -204,7 +213,28 @@ class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html'
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')
......
......@@ -13,22 +13,23 @@ from .celery_flower import celery_flower_view
from .swagger import get_swagger_view
api_v1 = [
path('users/', include('users.urls.api_urls', namespace='api-users')),
path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('users/', include('users.urls.api_urls', namespace='api-users')),
path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('orders/', include('orders.urls.api_urls', namespace='api-orders')),
]
api_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('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
]
......@@ -42,6 +43,7 @@ app_view_patterns = [
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'),
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'),
]
......
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
class OrdersConfig(AppConfig):
name = 'orders'
def ready(self):
from . import signals_handler
return super().ready()
......@@ -3,31 +3,56 @@ from django.utils.translation import ugettext_lazy as _
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 = (
('accepted', _("Accepted")),
('rejected', _("Rejected")),
('pending', _("Pending"))
(STATUS_ACCEPTED, _("Accepted")),
(STATUS_REJECTED, _("Rejected")),
(STATUS_PENDING, _("Pending"))
)
TYPE_LOGIN_REQUEST = 'login_request'
TYPE_LOGIN_CONFIRM = 'login_confirm'
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"))
title = models.CharField(max_length=256, verbose_name=_("Title"))
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)
type = models.CharField(choices=TYPE_CHOICES, max_length=64)
type = models.CharField(choices=TYPE_CHOICES, max_length=16, verbose_name=_('Type'))
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='pending')
def __str__(self):
return '{}: {}'.format(self.user_display, self.title)
@property
def comments(self):
return Comment.objects.filter(order_id=self.id)
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) {
toastr.error(msg);
}
if (typeof props.error === 'function') {
return props.error(jqXHR.responseText, jqXHR.status);
return props.error(jqXHR.responseText, jqXHR.responseJSON, jqXHR.status);
}
});
// return true;
......
......@@ -121,6 +121,16 @@
</li>
{% 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 #}
{% if request.user.can_admin_or_audit_current_org %}
......@@ -175,4 +185,4 @@
<script>
$(document).ready(function () {
})
</script>
\ No newline at end of file
</script>
......@@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user):
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def get_user_or_tmp_user(request):
user = request.user
tmp_user = get_tmp_user_from_cache(request)
......@@ -212,8 +211,8 @@ def get_tmp_user_from_cache(request):
return user
def set_tmp_user_to_cache(request, user):
cache.set(request.session.session_key+'user', user, 600)
def set_tmp_user_to_cache(request, user, ttl=3600):
cache.set(request.session.session_key+'user', user, ttl)
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