From dc3a9561c293f55b796730282b127369db0b46c3 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 31 Oct 2019 18:23:43 +0800 Subject: [PATCH] =?UTF-8?q?[Update]=20=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=99=BB=E9=99=86=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/__init__.py | 1 + apps/authentication/api/login_confirm.py | 25 ++ .../migrations/0003_loginconfirmsetting.py | 32 ++ apps/authentication/models.py | 8 +- apps/authentication/serializers.py | 11 +- .../authentication/login_wait_confirm.html | 3 - apps/authentication/urls/api_urls.py | 3 +- apps/authentication/views/login.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 82485 -> 84548 bytes apps/locale/zh/LC_MESSAGES/django.po | 287 ++++++++++-------- apps/orders/migrations/0001_initial.py | 59 ++++ apps/orders/models.py | 8 + apps/orders/signals_handler.py | 61 ++-- .../orders/login_confirm_order_list.html | 47 +-- apps/orders/utils.py | 60 ++++ apps/static/js/jumpserver.js | 3 + apps/templates/flash_message_standalone.html | 3 - apps/users/models/user.py | 7 + apps/users/templates/users/user_detail.html | 154 +++++++--- 19 files changed, 532 insertions(+), 242 deletions(-) create mode 100644 apps/authentication/api/login_confirm.py create mode 100644 apps/authentication/migrations/0003_loginconfirmsetting.py create mode 100644 apps/orders/migrations/0001_initial.py diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index c2a4a740f..4f6475124 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -5,3 +5,4 @@ from .auth import * from .token import * from .mfa import * from .access_key import * +from .login_confirm import * diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py new file mode 100644 index 000000000..3ce26f84d --- /dev/null +++ b/apps/authentication/api/login_confirm.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +from rest_framework.generics import UpdateAPIView +from django.shortcuts import get_object_or_404 + +from common.permissions import IsOrgAdmin +from ..models import LoginConfirmSetting +from ..serializers import LoginConfirmSettingSerializer + +__all__ = ['LoginConfirmSettingUpdateApi'] + + +class LoginConfirmSettingUpdateApi(UpdateAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = LoginConfirmSettingSerializer + + def get_object(self): + from users.models import User + user_id = self.kwargs.get('user_id') + user = get_object_or_404(User, pk=user_id) + defaults = {'user': user} + s, created = LoginConfirmSetting.objects.get_or_create( + defaults, user=user, + ) + return s diff --git a/apps/authentication/migrations/0003_loginconfirmsetting.py b/apps/authentication/migrations/0003_loginconfirmsetting.py new file mode 100644 index 000000000..c8043bc87 --- /dev/null +++ b/apps/authentication/migrations/0003_loginconfirmsetting.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.5 on 2019-10-31 10:23 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authentication', '0002_auto_20190729_1423'), + ] + + operations = [ + migrations.CreateModel( + name='LoginConfirmSetting', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('reviewers', models.ManyToManyField(blank=True, related_name='review_login_confirm_settings', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='login_confirm_setting', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index f50305651..4f0e06fb6 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,7 +1,7 @@ import uuid from django.db import models from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ugettext as __ from rest_framework.authtoken.models import Token from django.conf import settings @@ -40,8 +40,8 @@ class PrivateToken(Token): class LoginConfirmSetting(CommonModelMixin): - user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name=_("login_confirmation_setting")) - reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name=_("review_login_confirmation_settings")) + user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name="login_confirm_setting") + reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) @classmethod @@ -50,7 +50,7 @@ class LoginConfirmSetting(CommonModelMixin): def create_confirm_order(self, request=None): from orders.models import LoginConfirmOrder - title = _('User login confirm: {}'.format(self.user)) + title = _('User login confirm: {}').format(self.user) if request: remote_addr = get_request_ip(request) city = get_ip_city(remote_addr) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 584da768f..7463d30ca 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -4,17 +4,16 @@ from django.core.cache import cache from rest_framework import serializers from users.models import User -from .models import AccessKey +from .models import AccessKey, LoginConfirmSetting __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', - 'MFAChallengeSerializer', + 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', ] class AccessKeySerializer(serializers.ModelSerializer): - class Meta: model = AccessKey fields = ['id', 'secret', 'is_active', 'date_created'] @@ -87,3 +86,9 @@ class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer): username = self.context["username"] return self.create_response(username) + +class LoginConfirmSettingSerializer(serializers.ModelSerializer): + class Meta: + model = LoginConfirmSetting + fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated'] + read_only_fields = ['date_created', 'date_updated'] diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html index 0a14e8515..0167236df 100644 --- a/apps/authentication/templates/authentication/login_wait_confirm.html +++ b/apps/authentication/templates/authentication/login_wait_confirm.html @@ -61,9 +61,6 @@
{% include '_copyright.html' %}
-
- 2014-2019 -
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index a90b328cc..b47e5eb72 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -19,7 +19,8 @@ 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') + path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'), + path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] urlpatterns += router.urls diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 646268eea..761e656a1 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -179,7 +179,7 @@ class UserLoginGuardView(RedirectView): 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) + confirm_setting = user.get_login_confirm_setting() 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) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index fe18ec6df3287cda3353a310f4bd41666bc8eca2..7d21e0d682223b8118cb8487069700ed5d42e1ea 100644 GIT binary patch delta 25414 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%0D1VrV_$3hUp@^P-O~_1c30fQ8F5Qj$1nQ$e?JA` znb>Lkf^lwxu{*xFzG6e{!X>e*ru$lXh|RJ8oqR2*?o{H*a73s@<{Er^K(_vLm)kK<1D%@$hm!2biN&e!Px delta 24277 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\n" "Language-Team: Jumpserver team\n" @@ -83,7 +83,7 @@ msgstr "运行参数" #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 #: assets/templates/assets/label_list.html:16 -#: assets/templates/assets/system_user_list.html:51 audits/models.py:19 +#: assets/templates/assets/system_user_list.html:51 audits/models.py:20 #: audits/templates/audits/ftp_log_list.html:44 #: audits/templates/audits/ftp_log_list.html:74 #: perms/forms/asset_permission.py:84 perms/models/asset_permission.py:80 @@ -144,7 +144,7 @@ msgstr "资产" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:23 #: terminal/models.py:260 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:375 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:382 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 @@ -197,7 +197,7 @@ msgstr "参数" #: orgs/models.py:16 perms/models/base.py:54 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:416 users/serializers/group.py:32 +#: users/models/user.py:423 users/serializers/group.py:32 #: users/templates/users/user_detail.html:111 #: xpack/plugins/change_auth_plan/models.py:108 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 @@ -261,7 +261,7 @@ msgstr "创建日期" #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:33 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:408 users/templates/users/user_detail.html:129 +#: users/models/user.py:415 users/templates/users/user_detail.html:129 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:138 @@ -413,7 +413,7 @@ msgstr "详情" #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:26 #: assets/templates/assets/system_user_list.html:29 -#: assets/templates/assets/system_user_list.html:81 audits/models.py:33 +#: assets/templates/assets/system_user_list.html:81 audits/models.py:34 #: perms/templates/perms/asset_permission_detail.html:30 #: perms/templates/perms/asset_permission_list.html:178 #: perms/templates/perms/remote_app_permission_detail.html:30 @@ -457,7 +457,7 @@ msgstr "更新" #: assets/templates/assets/domain_list.html:55 #: assets/templates/assets/label_list.html:40 #: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:82 audits/models.py:34 +#: assets/templates/assets/system_user_list.html:82 audits/models.py:35 #: authentication/templates/authentication/_access_key_modal.html:65 #: ops/templates/ops/task_list.html:69 #: perms/templates/perms/asset_permission_detail.html:34 @@ -513,7 +513,7 @@ msgstr "创建远程应用" #: assets/templates/assets/domain_gateway_list.html:73 #: assets/templates/assets/domain_list.html:29 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_list.html:56 audits/models.py:38 +#: assets/templates/assets/system_user_list.html:56 audits/models.py:39 #: audits/templates/audits/operate_log_list.html:47 #: audits/templates/audits/operate_log_list.html:73 #: authentication/templates/authentication/_access_key_modal.html:34 @@ -700,7 +700,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/admin_user_list.html:45 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:48 audits/models.py:80 +#: assets/templates/assets/system_user_list.html:48 audits/models.py:81 #: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13 #: authentication/templates/authentication/login.html:65 #: authentication/templates/authentication/xpack_login.html:92 @@ -708,7 +708,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 #: settings/templates/settings/_ldap_list_users_modal.html:30 users/forms.py:13 -#: users/models/user.py:373 users/templates/users/_select_user_modal.html:14 +#: users/models/user.py:380 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 @@ -748,7 +748,7 @@ msgstr "密码" #: assets/forms/user.py:30 assets/serializers/asset_user.py:71 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:402 +#: users/models/user.py:409 msgid "Private key" msgstr "ssh私钥" @@ -798,7 +798,7 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" #: assets/templates/assets/user_asset_list.html:76 #: audits/templates/audits/login_log_list.html:60 #: orders/templates/orders/login_confirm_order_detail.html:33 -#: orders/templates/orders/login_confirm_order_list.html:15 +#: orders/templates/orders/login_confirm_order_list.html:16 #: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:144 #: users/templates/users/_granted_assets.html:31 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:54 @@ -964,7 +964,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:394 +#: assets/models/cluster.py:22 users/models/user.py:401 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -990,7 +990,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:514 +#: users/models/user.py:521 msgid "System" msgstr "系统" @@ -1097,8 +1097,8 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 audits/models.py:17 audits/models.py:37 -#: audits/models.py:50 audits/templates/audits/ftp_log_list.html:36 +#: assets/models/label.py:15 audits/models.py:18 audits/models.py:38 +#: audits/models.py:51 audits/templates/audits/ftp_log_list.html:36 #: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/operate_log_list.html:39 #: audits/templates/audits/operate_log_list.html:72 @@ -1108,7 +1108,7 @@ msgstr "默认资产组" #: ops/templates/ops/command_execution_list.html:63 orders/models.py:11 #: orders/models.py:32 #: orders/templates/orders/login_confirm_order_detail.html:32 -#: orders/templates/orders/login_confirm_order_list.html:14 +#: orders/templates/orders/login_confirm_order_list.html:15 #: perms/forms/asset_permission.py:78 perms/forms/remote_app_permission.py:34 #: perms/models/base.py:49 #: perms/templates/perms/asset_permission_create_update.html:41 @@ -1121,7 +1121,7 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:65 #: terminal/templates/terminal/session_list.html:27 #: terminal/templates/terminal/session_list.html:71 users/forms.py:319 -#: users/models/user.py:129 users/models/user.py:145 users/models/user.py:502 +#: users/models/user.py:136 users/models/user.py:152 users/models/user.py:509 #: users/serializers/group.py:21 #: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:36 users/views/user.py:250 @@ -1211,7 +1211,7 @@ msgid "Login mode" msgstr "登录模式" #: assets/models/user.py:166 assets/templates/assets/user_asset_list.html:79 -#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:52 +#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:52 #: audits/templates/audits/ftp_log_list.html:75 #: perms/forms/asset_permission.py:90 perms/forms/remote_app_permission.py:43 #: perms/models/asset_permission.py:82 perms/models/remote_app_permission.py:16 @@ -1277,7 +1277,7 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:67 users/forms.py:262 -#: users/models/user.py:405 users/templates/users/first_login.html:42 +#: users/models/user.py:412 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1470,8 +1470,8 @@ msgstr "请输入密码" #: assets/templates/assets/_asset_user_auth_update_modal.html:68 #: assets/templates/assets/asset_detail.html:302 -#: users/templates/users/user_detail.html:313 -#: users/templates/users/user_detail.html:340 +#: users/templates/users/user_detail.html:364 +#: users/templates/users/user_detail.html:391 #: xpack/plugins/interface/views.py:35 msgid "Update successfully!" msgstr "更新成功" @@ -1668,10 +1668,11 @@ msgstr "选择节点" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 #: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:112 -#: users/templates/users/user_detail.html:394 -#: users/templates/users/user_detail.html:420 -#: users/templates/users/user_detail.html:443 -#: users/templates/users/user_detail.html:488 +#: users/templates/users/user_detail.html:271 +#: users/templates/users/user_detail.html:445 +#: users/templates/users/user_detail.html:471 +#: users/templates/users/user_detail.html:494 +#: users/templates/users/user_detail.html:539 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:120 #: users/templates/users/user_list.html:256 @@ -1871,9 +1872,9 @@ msgstr "显示所有子节点资产" #: assets/templates/assets/asset_list.html:417 #: assets/templates/assets/system_user_list.html:129 -#: users/templates/users/user_detail.html:388 -#: users/templates/users/user_detail.html:414 -#: users/templates/users/user_detail.html:482 +#: users/templates/users/user_detail.html:439 +#: users/templates/users/user_detail.html:465 +#: users/templates/users/user_detail.html:533 #: users/templates/users/user_group_list.html:114 #: users/templates/users/user_list.html:250 #: xpack/plugins/interface/templates/interface/interface.html:97 @@ -1887,9 +1888,9 @@ msgstr "删除选择资产" #: assets/templates/assets/asset_list.html:421 #: assets/templates/assets/system_user_list.html:133 #: settings/templates/settings/terminal_setting.html:166 -#: users/templates/users/user_detail.html:392 -#: users/templates/users/user_detail.html:418 -#: users/templates/users/user_detail.html:486 +#: users/templates/users/user_detail.html:443 +#: users/templates/users/user_detail.html:469 +#: users/templates/users/user_detail.html:537 #: users/templates/users/user_group_list.html:118 #: users/templates/users/user_list.html:254 #: xpack/plugins/interface/templates/interface/interface.html:101 @@ -2194,7 +2195,7 @@ msgstr "资产管理" msgid "System user asset" msgstr "系统用户资产" -#: audits/models.py:18 audits/models.py:41 audits/models.py:52 +#: audits/models.py:19 audits/models.py:42 audits/models.py:53 #: audits/templates/audits/ftp_log_list.html:76 #: audits/templates/audits/operate_log_list.html:76 #: audits/templates/audits/password_change_log_list.html:58 @@ -2204,86 +2205,86 @@ msgstr "系统用户资产" msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:77 +#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:77 msgid "Operate" msgstr "操作" -#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:59 +#: audits/models.py:23 audits/templates/audits/ftp_log_list.html:59 #: audits/templates/audits/ftp_log_list.html:78 msgid "Filename" msgstr "文件名" -#: audits/models.py:23 audits/models.py:76 +#: audits/models.py:24 audits/models.py:77 #: audits/templates/audits/ftp_log_list.html:79 #: ops/templates/ops/command_execution_list.html:68 #: ops/templates/ops/task_list.html:15 -#: users/templates/users/user_detail.html:464 +#: users/templates/users/user_detail.html:515 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14 #: xpack/plugins/cloud/api.py:61 msgid "Success" msgstr "成功" -#: audits/models.py:32 +#: audits/models.py:33 #: authentication/templates/authentication/_access_key_modal.html:22 #: xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" -#: audits/models.py:39 audits/templates/audits/operate_log_list.html:55 +#: audits/models.py:40 audits/templates/audits/operate_log_list.html:55 #: audits/templates/audits/operate_log_list.html:74 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:40 audits/templates/audits/operate_log_list.html:75 +#: audits/models.py:41 audits/templates/audits/operate_log_list.html:75 msgid "Resource" msgstr "资源" -#: audits/models.py:51 audits/templates/audits/password_change_log_list.html:57 +#: audits/models.py:52 audits/templates/audits/password_change_log_list.html:57 msgid "Change by" msgstr "修改者" -#: audits/models.py:70 users/templates/users/user_detail.html:98 +#: audits/models.py:71 users/templates/users/user_detail.html:98 msgid "Disabled" msgstr "禁用" -#: audits/models.py:71 settings/models.py:33 +#: audits/models.py:72 settings/models.py:33 #: users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" -#: audits/models.py:72 +#: audits/models.py:73 msgid "-" msgstr "" -#: audits/models.py:77 xpack/plugins/cloud/models.py:264 +#: audits/models.py:78 xpack/plugins/cloud/models.py:264 #: xpack/plugins/cloud/models.py:287 msgid "Failed" msgstr "失败" -#: audits/models.py:81 +#: audits/models.py:82 msgid "Login type" msgstr "登录方式" -#: audits/models.py:82 +#: audits/models.py:83 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:83 +#: audits/models.py:84 msgid "Login city" msgstr "登录城市" -#: audits/models.py:84 +#: audits/models.py:85 msgid "User agent" msgstr "Agent" -#: audits/models.py:85 audits/templates/audits/login_log_list.html:62 +#: audits/models.py:86 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:174 users/models/user.py:397 +#: users/forms.py:174 users/models/user.py:404 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" -#: audits/models.py:86 audits/templates/audits/login_log_list.html:63 +#: audits/models.py:87 audits/templates/audits/login_log_list.html:63 #: xpack/plugins/change_auth_plan/models.py:416 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:278 @@ -2291,16 +2292,17 @@ msgstr "MFA" msgid "Reason" msgstr "原因" -#: audits/models.py:87 audits/templates/audits/login_log_list.html:64 +#: audits/models.py:88 audits/templates/audits/login_log_list.html:64 #: orders/templates/orders/login_confirm_order_detail.html:35 #: orders/templates/orders/login_confirm_order_list.html:17 +#: orders/templates/orders/login_confirm_order_list.html:91 #: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 msgid "Status" msgstr "状态" -#: audits/models.py:88 +#: audits/models.py:89 msgid "Date login" msgstr "登录日期" @@ -2355,7 +2357,6 @@ msgstr "Agent" #: audits/templates/audits/login_log_list.html:61 #: orders/templates/orders/login_confirm_order_detail.html:58 -#: orders/templates/orders/login_confirm_order_list.html:16 msgid "City" msgstr "城市" @@ -2522,15 +2523,15 @@ msgid "Private Token" msgstr "ssh密钥" #: authentication/models.py:43 -msgid "login_confirmation_setting" -msgstr "" +msgid "login_confirm_setting" +msgstr "登录复核设置" -#: authentication/models.py:44 +#: authentication/models.py:44 users/templates/users/user_detail.html:265 msgid "Reviewers" -msgstr "" +msgstr "审批人" #: authentication/models.py:44 -msgid "review_login_confirmation_settings" +msgid "review_login_confirm_settings" msgstr "" #: authentication/models.py:53 @@ -2571,14 +2572,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:332 users/templates/users/user_profile.html:94 +#: users/models/user.py:339 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:333 users/templates/users/user_profile.html:92 +#: users/models/user.py:340 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2838,8 +2839,9 @@ msgid "" "configure nginx for url distribution, If you see this page, " "prove that you are not accessing the nginx listening port. Good luck." msgstr "" -"
Koko是单独部署的一个程序,你需要部署Koko, 并确保nginx配置转发,
如果你看到了" -"这个页面,证明你访问的不是nginx监听的端口,祝你好运
" +"
Koko是单独部署的一个程序,你需要部署Koko, 并确保nginx配置转发,
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" #: ops/api/celery.py:54 msgid "Waiting task start" @@ -3113,6 +3115,7 @@ msgid "No system user was selected" msgstr "没有选择系统用户" #: ops/templates/ops/command_execution_create.html:296 orders/models.py:26 +#: orders/templates/orders/login_confirm_order_list.html:92 msgid "Pending" msgstr "等待" @@ -3197,26 +3200,23 @@ msgid "Command execution" msgstr "命令执行" #: orders/models.py:12 orders/models.py:33 -#, fuzzy -#| msgid "User is inactive" msgid "User display name" -msgstr "用户已禁用" +msgstr "用户显示名称" #: orders/models.py:13 orders/models.py:36 msgid "Body" -msgstr "" +msgstr "内容" -#: orders/models.py:24 -#, fuzzy -#| msgid "Accept" +#: orders/models.py:24 orders/templates/orders/login_confirm_order_list.html:93 msgid "Accepted" -msgstr "接受" +msgstr "已接受" -#: orders/models.py:25 +#: orders/models.py:25 orders/templates/orders/login_confirm_order_list.html:94 msgid "Rejected" -msgstr "拒绝" +msgstr "已拒绝" -#: orders/models.py:35 orders/templates/orders/login_confirm_order_list.html:13 +#: orders/models.py:35 orders/templates/orders/login_confirm_order_list.html:14 +#: orders/templates/orders/login_confirm_order_list.html:90 msgid "Title" msgstr "标题" @@ -3240,14 +3240,14 @@ msgstr "待处理人名称" #: orders/serializers.py:21 #: orders/templates/orders/login_confirm_order_detail.html:94 -#: orders/templates/orders/login_confirm_order_list.html:53 +#: orders/templates/orders/login_confirm_order_list.html:59 #: terminal/templates/terminal/terminal_list.html:78 msgid "Accept" msgstr "接受" #: orders/serializers.py:22 #: orders/templates/orders/login_confirm_order_detail.html:95 -#: orders/templates/orders/login_confirm_order_list.html:54 +#: orders/templates/orders/login_confirm_order_list.html:60 #: terminal/templates/terminal/terminal_list.html:80 msgid "Reject" msgstr "拒绝" @@ -3256,16 +3256,16 @@ msgstr "拒绝" msgid "this order" msgstr "这个工单" -#: orders/signals_handler.py:21 -#, fuzzy -#| msgid "New node" -msgid "New order" -msgstr "新节点" +#: orders/templates/orders/login_confirm_order_detail.html:75 +msgid "ago" +msgstr "前" -# msgid "Update user" -# msgstr "更新用户" -#: orders/signals_handler.py:24 -#, fuzzy, python-brace-format +#: orders/utils.py:18 +msgid "New order" +msgstr "新工单" + +#: orders/utils.py:21 +#, python-brace-format msgid "" "\n" "
\n" @@ -3275,6 +3275,8 @@ msgid "" "
\n" " User: {user}\n" "
\n" +" Assignees: {order.assignees_display}\n" +"
\n" " City: {order.city}\n" "
\n" " IP: {order.ip}\n" @@ -3292,6 +3294,8 @@ msgstr "" "
\n" " 用户: {user}\n" "
\n" +" 待处理人: {order.assignees_display}\n" +"
\n" " 城市: {order.city}\n" "
\n" " IP: {order.ip}\n" @@ -3301,19 +3305,40 @@ msgstr "" "
\n" " " -#: orders/templates/orders/login_confirm_order_detail.html:75 -msgid "ago" -msgstr "前" +#: orders/utils.py:52 +msgid "Order has been reply" +msgstr "工单已被回复" -#: orders/templates/orders/login_confirm_order_list.html:83 -#: users/templates/users/user_list.html:327 -msgid "User is expired" -msgstr "用户已失效" - -#: orders/templates/orders/login_confirm_order_list.html:86 -#: users/templates/users/user_list.html:330 -msgid "User is inactive" -msgstr "用户已禁用" +#: orders/utils.py:53 +#, python-brace-format +msgid "" +"\n" +"
\n" +"

Your order has been replay

\n" +"
\n" +" Title: {order.title}\n" +"
\n" +" Assignee: {order.assignee_display}\n" +"
\n" +" Status: {order.status_display}\n" +"
\n" +"
\n" +"
\n" +" " +msgstr "" +"\n" +"
\n" +"

您的工单已被回复

\n" +"
\n" +" 标题: {order.title}\n" +"
\n" +" 处理人: {order.assignee_display}\n" +"
\n" +" 状态: {order.status_display}\n" +"
\n" +"
\n" +"
\n" +" " #: orders/views.py:15 orders/views.py:31 templates/_nav.html:127 msgid "Orders" @@ -3351,7 +3376,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/templates/perms/asset_permission_list.html:118 #: perms/templates/perms/remote_app_permission_list.html:16 #: templates/_nav.html:21 users/forms.py:293 users/models/group.py:26 -#: users/models/user.py:381 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:388 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:218 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 @@ -3393,7 +3418,7 @@ msgstr "资产授权" #: perms/models/base.py:53 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:413 users/templates/users/user_detail.html:107 +#: users/models/user.py:420 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3957,7 +3982,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: users/models/user.py:377 users/templates/users/user_detail.html:71 +#: users/models/user.py:384 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -4362,7 +4387,7 @@ msgstr "批量命令" msgid "Task monitor" msgstr "任务监控" -#: templates/_nav.html:130 +#: templates/_nav.html:130 users/templates/users/user_detail.html:257 msgid "Login confirm" msgstr "登录复核" @@ -4748,7 +4773,7 @@ msgstr "你可以使用ssh客户端工具连接终端" msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:32 users/models/user.py:385 +#: users/forms.py:32 users/models/user.py:392 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4867,50 +4892,50 @@ msgstr "选择用户" msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:128 users/models/user.py:510 +#: users/models/user.py:135 users/models/user.py:517 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:130 +#: users/models/user.py:137 msgid "Application" msgstr "应用程序" -#: users/models/user.py:131 xpack/plugins/orgs/forms.py:30 +#: users/models/user.py:138 xpack/plugins/orgs/forms.py:30 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:141 +#: users/models/user.py:148 msgid "Org admin" msgstr "组织管理员" -#: users/models/user.py:143 +#: users/models/user.py:150 msgid "Org auditor" msgstr "组织审计员" -#: users/models/user.py:334 users/templates/users/user_profile.html:90 +#: users/models/user.py:341 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:388 +#: users/models/user.py:395 msgid "Avatar" msgstr "头像" -#: users/models/user.py:391 users/templates/users/user_detail.html:82 +#: users/models/user.py:398 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:420 users/templates/users/user_detail.html:103 +#: users/models/user.py:427 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:102 msgid "Source" msgstr "用户来源" -#: users/models/user.py:424 +#: users/models/user.py:431 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:513 +#: users/models/user.py:520 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -5082,7 +5107,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" #: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:379 users/utils.py:83 +#: users/templates/users/user_detail.html:430 users/utils.py:83 msgid "Reset password" msgstr "重置密码" @@ -5199,7 +5224,7 @@ msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" #: users/templates/users/user_detail.html:203 -#: users/templates/users/user_detail.html:467 +#: users/templates/users/user_detail.html:518 msgid "Unblock user" msgstr "解除登录限制" @@ -5207,46 +5232,46 @@ msgstr "解除登录限制" msgid "Unblock" msgstr "解除" -#: users/templates/users/user_detail.html:322 +#: users/templates/users/user_detail.html:373 msgid "Goto profile page enable MFA" msgstr "请去个人信息页面启用自己的MFA" -#: users/templates/users/user_detail.html:378 +#: users/templates/users/user_detail.html:429 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:389 +#: users/templates/users/user_detail.html:440 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:404 +#: users/templates/users/user_detail.html:455 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:405 +#: users/templates/users/user_detail.html:456 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:415 +#: users/templates/users/user_detail.html:466 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:433 +#: users/templates/users/user_detail.html:484 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:434 -#: users/templates/users/user_detail.html:438 +#: users/templates/users/user_detail.html:485 +#: users/templates/users/user_detail.html:489 msgid "User SSH public key update" msgstr "ssh密钥" -#: users/templates/users/user_detail.html:483 +#: users/templates/users/user_detail.html:534 msgid "After unlocking the user, the user can log in normally." msgstr "解除用户登录限制后,此用户即可正常登录" -#: users/templates/users/user_detail.html:497 +#: users/templates/users/user_detail.html:548 msgid "Reset user MFA success" msgstr "重置用户MFA成功" @@ -5299,6 +5324,14 @@ msgstr "删除" msgid "User Deleting failed." msgstr "用户删除失败" +#: users/templates/users/user_list.html:327 +msgid "User is expired" +msgstr "用户已失效" + +#: users/templates/users/user_list.html:330 +msgid "User is inactive" +msgstr "用户已禁用" + #: users/templates/users/user_otp_authentication.html:6 #: users/templates/users/user_password_authentication.html:6 msgid "Authenticate" diff --git a/apps/orders/migrations/0001_initial.py b/apps/orders/migrations/0001_initial.py new file mode 100644 index 000000000..9b1099965 --- /dev/null +++ b/apps/orders/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 2.2.5 on 2019-10-31 10:23 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='LoginConfirmOrder', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('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')), + ('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')), + ('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')), + ('type', models.CharField(choices=[('login_confirm', 'Login confirm')], max_length=16, verbose_name='Type')), + ('status', models.CharField(choices=[('accepted', 'Accepted'), ('rejected', 'Rejected'), ('pending', 'Pending')], default='pending', max_length=16)), + ('ip', models.GenericIPAddressField(blank=True, null=True)), + ('city', models.CharField(blank=True, default='', max_length=16)), + ('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')), + ('assignees', models.ManyToManyField(related_name='loginconfirmorder_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'ordering': ('-date_created',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('order_id', models.UUIDField()), + ('user_display', models.CharField(max_length=128, verbose_name='User display name')), + ('body', models.TextField(verbose_name='Body')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'ordering': ('date_created',), + }, + ), + ] diff --git a/apps/orders/models.py b/apps/orders/models.py index c574cacb9..3ed12c8c8 100644 --- a/apps/orders/models.py +++ b/apps/orders/models.py @@ -48,6 +48,14 @@ class BaseOrder(CommonModelMixin): def comments(self): return Comment.objects.filter(order_id=self.id) + @property + def body_as_html(self): + return self.body.replace('\n', '
') + + @property + def status_display(self): + return self.get_status_display() + class Meta: abstract = True ordering = ('-date_created',) diff --git a/apps/orders/signals_handler.py b/apps/orders/signals_handler.py index 9e2cdd2e7..60db0c5e5 100644 --- a/apps/orders/signals_handler.py +++ b/apps/orders/signals_handler.py @@ -1,59 +1,32 @@ # -*- 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 django.db.models.signals import m2m_changed, post_save -from common.tasks import send_mail_async -from common.utils import get_logger, reverse +from common.utils import get_logger from .models import LoginConfirmOrder +from .utils import ( + send_login_confirm_order_mail_to_assignees, + send_login_confirm_action_mail_to_user +) + 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 = _(""" -
-

Your has a new order

-
- Title: {order.title} -
- User: {user} -
- City: {order.city} -
- IP: {order.ip} -
- click here to review -
-
- """).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, +def on_login_confirm_order_assignees_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) + print(assignees) + send_login_confirm_order_mail_to_assignees(instance, assignees) + +@receiver(post_save, sender=LoginConfirmOrder) +def on_login_confirm_order_status_change(sender, instance=None, created=False, **kwargs): + if created or instance.status == "pending": + return + logger.debug('Order changed, send mail: {}'.format(instance.id)) + send_login_confirm_action_mail_to_user(instance) diff --git a/apps/orders/templates/orders/login_confirm_order_list.html b/apps/orders/templates/orders/login_confirm_order_list.html index e7b8da90c..c54743c85 100644 --- a/apps/orders/templates/orders/login_confirm_order_list.html +++ b/apps/orders/templates/orders/login_confirm_order_list.html @@ -14,7 +14,6 @@ {% trans 'Title' %} {% trans 'User' %} {% trans 'IP' %} - {% trans 'City' %} {% trans 'Status' %} {% trans 'Datetime' %} {% trans 'Action' %} @@ -38,8 +37,12 @@ function initTable() { cellData = htmlEscape(cellData); var detailBtn = '' + cellData + ''; $(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id)); - }}, - {targets: 5, createdCell: function (td, cellData, rowData) { + }}, + {targets: 3, createdCell: function (td, cellData, rowData) { + var d = cellData + "(" + rowData.city + ")"; + $(td).html(d) + }}, + {targets: 4, createdCell: function (td, cellData, rowData) { if (cellData === "accepted") { $(td).html('') } else if (cellData === "rejected") { @@ -48,13 +51,13 @@ function initTable() { $(td).html('') } }}, - {targets: 6, createdCell: function (td, cellData) { + {targets: 5, createdCell: function (td, cellData) { var d = toSafeLocalDateStr(cellData); $(td).html(d) }}, - {targets: 7, createdCell: function (td, cellData, rowData) { - var acceptBtn = '{% trans "Accept" %} '; - var rejectBtn = '{% trans "Reject" %}'; + {targets: 6, createdCell: function (td, cellData, rowData) { + var acceptBtn = '{% trans "Accept" %} '; + var rejectBtn = '{% trans "Reject" %}'; acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData); rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData); var acceptBtnRef = $(acceptBtn); @@ -68,10 +71,10 @@ function initTable() { }}], 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"}, {data: "title", className: "text-left"}, + {data: "user_display"}, {data: "ip"}, + {data: "status", orderable: false, width: "30px"}, + {data: "date_created", width: "120px"}, {data: "id", orderable: false, width: "100px"} ], op_html: $('#actions').html() @@ -82,7 +85,6 @@ function initTable() { $(document).ready(function(){ initTable(); - $('') var menu = [ {title: "IP", value: "ip"}, {title: "{% trans 'Title' %}", value: "title"}, @@ -93,12 +95,21 @@ $(document).ready(function(){ ]} ]; initTableFilterDropdown('#login_confirm_order_list_table_filter input', menu) -}).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) +}).on('click', '.btn-action', function () { + var actionCreateUrl = "{% url 'api-orders:login-confirm-order-create-action' pk=DEFAULT_PK %}"; + var orderId = $(this).data('uid'); + actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", orderId); + var action = $(this).data('action'); + var comment = ''; + var data = { + url: actionCreateUrl, + method: 'POST', + body: JSON.stringify({action: action, comment: comment}), + success: function () { + window.location.reload(); + } + }; + requestApi(data); }) {% endblock %} diff --git a/apps/orders/utils.py b/apps/orders/utils.py index ec51c5a2b..6fd3d965d 100644 --- a/apps/orders/utils.py +++ b/apps/orders/utils.py @@ -1,2 +1,62 @@ # -*- coding: utf-8 -*- # +from django.conf import settings +from django.utils.translation import ugettext as _ + +from common.utils import get_logger, reverse +from common.tasks import send_mail_async + +logger = get_logger(__name__) + + +def send_login_confirm_order_mail_to_assignees(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 = _(""" +
+

Your has a new order

+
+ Title: {order.title} +
+ User: {user} +
+ Assignees: {order.assignees_display} +
+ City: {order.city} +
+ IP: {order.ip} +
+ click here to review +
+
+ """).format(order=order, user=user, url=detail_url) + send_mail_async.delay(subject, message, recipient_list, html_message=message) + + +def send_login_confirm_action_mail_to_user(order): + if not order.user: + logger.error("Order not has user: {}".format(order.id)) + return + user = order.user + recipient_list = [user.email] + subject = '{}: {}'.format(_("Order has been reply"), order.title) + message = _(""" +
+

Your order has been replay

+
+ Title: {order.title} +
+ Assignee: {order.assignee_display} +
+ Status: {order.status_display} +
+
+
+ """).format(order=order) + send_mail_async.delay(subject, message, recipient_list, html_message=message) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 89ac43364..8fddac5ed 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -416,6 +416,9 @@ function makeLabel(data) { function parseTableFilter(value) { var cleanValues = []; + if (!value) { + return {} + } var valuesArray = value.split(':'); for (var i=0; i {% include '_copyright.html' %}
-
- 2014-2019 -
diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 4c7c54def..36a60abc8 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -117,6 +117,13 @@ class AuthMixin: return True return False + def get_login_confirm_setting(self): + if hasattr(self, 'login_confirm_setting'): + s = self.login_confirm_setting + if s.reviewers.all().count() and s.is_active: + return s + return False + class RoleMixin: ROLE_ADMIN = 'Admin' diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 8e049938c..b57480b18 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -7,6 +7,7 @@ + {% endblock %} {% block content %}
@@ -212,46 +213,82 @@
- {% if user_object.is_current_org_admin %} -
-
- {% trans 'User group' %} -
-
- - - - - - - - - - - - {% for group in user_object.groups.all %} - - - - - {% endfor %} - -
- -
- -
- {{ group.name }} - - -
-
+ {% if user_object.is_current_org_admin or user_object.is_superuser %} +
+
+ {% trans 'User group' %}
+
+ + + + + + + + + + + + {% for group in user_object.groups.all %} + + + + + {% endfor %} + +
+ +
+ +
+ {{ group.name }} + + +
+
+
{% endif %} +
+
+ {% trans 'Login confirm' %} +
+
+ + + + + + + + + + + {% if user_object.get_login_confirm_setting %} + {% for u in user_object.login_confirm_setting.reviewers.all %} + + + + + {% endfor %} + {% endif %} + +
+ +
+ +
+ {{ u }} + + +
+
+
@@ -263,6 +300,7 @@ {% block custom_foot_js %} {% endblock %}