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'),
]
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-25 10:52+0800\n"
"POT-Creation-Date: 2019-10-30 11:52+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
......@@ -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:373 users/templates/users/_select_user_modal.html:13
#: users/models/user.py:375 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:414 users/serializers/v1.py:143
#: users/models/user.py:416 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
......@@ -219,7 +219,8 @@ msgstr "创建者"
#: assets/templates/assets/system_user_detail.html:96
#: common/mixins/models.py:51 ops/models/adhoc.py:45
#: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64
#: orgs/models.py:17 perms/models/base.py:55
#: orders/templates/orders/login_confirm_order_detail.html:60 orgs/models.py:17
#: perms/models/base.py:55
#: perms/templates/perms/asset_permission_detail.html:94
#: perms/templates/perms/remote_app_permission_detail.html:86
#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17
......@@ -253,12 +254,14 @@ msgstr "创建日期"
#: assets/templates/assets/domain_list.html:28
#: assets/templates/assets/system_user_detail.html:104
#: assets/templates/assets/system_user_list.html:55 ops/models/adhoc.py:43
#: orgs/models.py:18 perms/models/base.py:56
#: orders/serializers.py:23
#: orders/templates/orders/login_confirm_order_detail.html:96 orgs/models.py:18
#: perms/models/base.py:56
#: perms/templates/perms/asset_permission_detail.html:102
#: 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:406 users/templates/users/user_detail.html:129
#: users/models/user.py:408 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
......@@ -516,6 +519,7 @@ msgstr "创建远程应用"
#: authentication/templates/authentication/_access_key_modal.html:34
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:18
#: orders/templates/orders/login_confirm_order_list.html:19
#: perms/forms/asset_permission.py:21
#: perms/templates/perms/asset_permission_create_update.html:50
#: perms/templates/perms/asset_permission_list.html:56
......@@ -699,12 +703,12 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC"
#: assets/templates/assets/system_user_list.html:48 audits/models.py:80
#: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13
#: authentication/templates/authentication/login.html:65
#: authentication/templates/authentication/new_login.html:92
#: authentication/templates/authentication/xpack_login.html:92
#: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:70
#: 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:371 users/templates/users/_select_user_modal.html:14
#: users/models/user.py:373 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
......@@ -729,7 +733,7 @@ msgstr "密码或密钥密码"
#: assets/templates/assets/_asset_user_auth_view_modal.html:27
#: authentication/forms.py:15
#: authentication/templates/authentication/login.html:68
#: authentication/templates/authentication/new_login.html:95
#: authentication/templates/authentication/xpack_login.html:95
#: settings/forms.py:114 users/forms.py:15 users/forms.py:27
#: users/templates/users/reset_password.html:53
#: users/templates/users/user_password_authentication.html:18
......@@ -744,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:400
#: users/models/user.py:402
msgid "Private key"
msgstr "ssh私钥"
......@@ -793,6 +797,8 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig"
#: assets/templates/assets/domain_gateway_list.html:68
#: 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
#: 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
......@@ -842,6 +848,7 @@ msgstr "系统平台"
#: assets/models/asset.py:146 assets/models/authbook.py:27
#: assets/models/cmd_filter.py:22 assets/models/domain.py:54
#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:110
#: authentication/models.py:45
msgid "Is active"
msgstr "激活"
......@@ -957,7 +964,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
#: assets/models/cluster.py:22 users/models/user.py:392
#: assets/models/cluster.py:22 users/models/user.py:394
#: users/templates/users/user_detail.html:76
msgid "Phone"
msgstr "手机"
......@@ -983,7 +990,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:512
#: users/models/user.py:514
msgid "System"
msgstr "系统"
......@@ -1011,7 +1018,7 @@ msgstr "BGP全网通"
msgid "Regex"
msgstr "正则表达式"
#: assets/models/cmd_filter.py:40 ops/models/command.py:21
#: assets/models/cmd_filter.py:40 ops/models/command.py:22
#: ops/templates/ops/command_execution_list.html:64 terminal/models.py:163
#: terminal/templates/terminal/command_list.html:28
#: terminal/templates/terminal/command_list.html:68
......@@ -1034,7 +1041,7 @@ msgstr "过滤器"
#: assets/models/cmd_filter.py:51
#: assets/templates/assets/cmd_filter_rule_list.html:58
#: audits/templates/audits/login_log_list.html:58
#: audits/templates/audits/login_log_list.html:58 orders/models.py:41
#: perms/templates/perms/remote_app_permission_remote_app.html:54
#: settings/templates/settings/command_storage_create.html:31
#: settings/templates/settings/replay_storage_create.html:31
......@@ -1097,8 +1104,11 @@ msgstr "默认资产组"
#: audits/templates/audits/operate_log_list.html:72
#: audits/templates/audits/password_change_log_list.html:39
#: audits/templates/audits/password_change_log_list.html:56
#: ops/templates/ops/command_execution_list.html:38
#: ops/templates/ops/command_execution_list.html:63
#: authentication/models.py:43 ops/templates/ops/command_execution_list.html:38
#: 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
#: 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
......@@ -1111,8 +1121,9 @@ 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:127 users/models/user.py:143 users/models/user.py:500
#: users/serializers/v1.py:132 users/templates/users/user_group_detail.html:78
#: users/models/user.py:129 users/models/user.py:145 users/models/user.py:502
#: 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
#: xpack/plugins/orgs/forms.py:28
#: xpack/plugins/orgs/templates/orgs/org_detail.html:113
......@@ -1235,7 +1246,7 @@ msgid "Reachable"
msgstr "可连接"
#: assets/models/utils.py:45 assets/tasks/const.py:86
#: authentication/utils.py:13 xpack/plugins/license/models.py:78
#: authentication/utils.py:16 xpack/plugins/license/models.py:78
msgid "Unknown"
msgstr "未知"
......@@ -1266,7 +1277,7 @@ msgid "Backend"
msgstr "后端"
#: assets/serializers/asset_user.py:67 users/forms.py:262
#: users/models/user.py:403 users/templates/users/first_login.html:42
#: users/models/user.py:405 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,6 +1481,7 @@ msgid "Asset user auth"
msgstr "资产用户信息"
#: assets/templates/assets/_asset_user_auth_view_modal.html:54
#: authentication/templates/authentication/login_wait_confirm.html:117
msgid "Copy success"
msgstr "复制成功"
......@@ -1490,6 +1502,7 @@ msgstr "关闭"
#: audits/templates/audits/operate_log_list.html:77
#: audits/templates/audits/password_change_log_list.html:59
#: ops/templates/ops/task_adhoc.html:63
#: orders/templates/orders/login_confirm_order_list.html:18
#: terminal/templates/terminal/command_list.html:33
#: terminal/templates/terminal/session_detail.html:50
msgid "Datetime"
......@@ -1768,7 +1781,7 @@ msgstr "硬盘"
msgid "Date joined"
msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:148 authentication/models.py:15
#: assets/templates/assets/asset_detail.html:148 authentication/models.py:19
#: authentication/templates/authentication/_access_key_modal.html:32
#: perms/models/base.py:51
#: perms/templates/perms/asset_permission_create_update.html:55
......@@ -1787,6 +1800,7 @@ msgid "Refresh hardware"
msgstr "更新硬件信息"
#: assets/templates/assets/asset_detail.html:168
#: authentication/templates/authentication/login_wait_confirm.html:42
msgid "Refresh"
msgstr "刷新"
......@@ -2264,7 +2278,7 @@ msgstr "Agent"
#: audits/models.py:85 audits/templates/audits/login_log_list.html:62
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: users/forms.py:174 users/models/user.py:395
#: users/forms.py:174 users/models/user.py:397
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
......@@ -2278,6 +2292,8 @@ msgid "Reason"
msgstr "原因"
#: audits/models.py:87 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
#: 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
......@@ -2338,6 +2354,8 @@ msgid "UA"
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 "城市"
......@@ -2348,23 +2366,23 @@ msgid "Date"
msgstr "日期"
#: audits/views.py:86 audits/views.py:130 audits/views.py:167
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:129
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:139
msgid "Audits"
msgstr "日志审计"
#: audits/views.py:87 templates/_nav.html:133
#: audits/views.py:87 templates/_nav.html:143
msgid "FTP log"
msgstr "FTP日志"
#: audits/views.py:131 templates/_nav.html:134
#: audits/views.py:131 templates/_nav.html:144
msgid "Operate log"
msgstr "操作日志"
#: audits/views.py:168 templates/_nav.html:135
#: audits/views.py:168 templates/_nav.html:145
msgid "Password change log"
msgstr "改密日志"
#: audits/views.py:213 templates/_nav.html:132
#: audits/views.py:213 templates/_nav.html:142
msgid "Login log"
msgstr "登录日志"
......@@ -2372,25 +2390,33 @@ msgstr "登录日志"
msgid "Command execution log"
msgstr "命令执行"
#: authentication/api/auth.py:61 authentication/api/token.py:45
#: authentication/api/auth.py:58 authentication/api/token.py:45
#: authentication/templates/authentication/login.html:52
#: authentication/templates/authentication/new_login.html:77
#: authentication/templates/authentication/xpack_login.html:77
msgid "Log in frequently and try again later"
msgstr "登录频繁, 稍后重试"
#: authentication/api/auth.py:86
#: authentication/api/auth.py:83
msgid "Please carry seed value and conduct MFA secondary certification"
msgstr "请携带seed值, 进行MFA二次认证"
#: authentication/api/auth.py:176
#: authentication/api/auth.py:173
msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证"
#: authentication/api/auth.py:181
#: authentication/api/auth.py:178
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: authentication/api/token.py:80
#: authentication/api/auth.py:222
msgid "No order found or order expired"
msgstr "没有找到工单,或者已过期"
#: authentication/api/auth.py:228
msgid "Order was rejected by {}"
msgstr "工单被拒绝 {}"
#: authentication/api/token.py:81
msgid "MFA required"
msgstr ""
......@@ -2491,10 +2517,38 @@ msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
msgid "MFA code"
msgstr "MFA 验证码"
#: authentication/models.py:35
#: authentication/models.py:39
msgid "Private Token"
msgstr "ssh密钥"
#: authentication/models.py:43
msgid "login_confirmation_setting"
msgstr ""
#: authentication/models.py:44
msgid "Reviewers"
msgstr ""
#: authentication/models.py:44
msgid "review_login_confirmation_settings"
msgstr ""
#: authentication/models.py:53
msgid "User login request: {}"
msgstr "用户登录请求: {}"
#: authentication/models.py:57
msgid ""
"User: {}\n"
"IP: {}\n"
"City: {}\n"
"Date: {}\n"
msgstr ""
"用户: {}\n"
"IP: {}\n"
"城市: {}\n"
"日期: {}\n"
#: authentication/templates/authentication/_access_key_modal.html:6
msgid "API key list"
msgstr "API Key列表"
......@@ -2517,14 +2571,14 @@ msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
#: users/models/user.py:330 users/templates/users/user_profile.html:94
#: users/models/user.py:332 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:331 users/templates/users/user_profile.html:92
#: users/models/user.py:333 users/templates/users/user_profile.html:92
#: users/templates/users/user_profile.html:170
msgid "Enable"
msgstr "启用"
......@@ -2586,23 +2640,23 @@ msgstr "改变世界,从一点点开始。"
#: authentication/templates/authentication/login.html:46
#: authentication/templates/authentication/login.html:73
#: authentication/templates/authentication/new_login.html:101
#: authentication/templates/authentication/xpack_login.html:101
#: templates/_header_bar.html:83
msgid "Login"
msgstr "登录"
#: authentication/templates/authentication/login.html:54
#: authentication/templates/authentication/new_login.html:80
#: authentication/templates/authentication/xpack_login.html:80
msgid "The user password has expired"
msgstr "用户密码已过期"
#: authentication/templates/authentication/login.html:57
#: authentication/templates/authentication/new_login.html:83
#: authentication/templates/authentication/xpack_login.html:83
msgid "Captcha invalid"
msgstr "验证码错误"
#: authentication/templates/authentication/login.html:84
#: authentication/templates/authentication/new_login.html:105
#: authentication/templates/authentication/xpack_login.html:105
#: users/templates/users/forgot_password.html:10
#: users/templates/users/forgot_password.html:25
msgid "Forgot password"
......@@ -2653,24 +2707,45 @@ msgstr "下一步"
msgid "Can't provide security? Please contact the administrator!"
msgstr "如果不能提供MFA验证码,请联系管理员!"
#: authentication/templates/authentication/new_login.html:67
#: authentication/templates/authentication/login_wait_confirm.html:47
msgid "Copy link"
msgstr "复制链接"
#: authentication/templates/authentication/login_wait_confirm.html:52
#: templates/flash_message_standalone.html:47
msgid "Return"
msgstr "返回"
#: authentication/templates/authentication/xpack_login.html:67
msgid "Welcome back, please enter username and password to login"
msgstr "欢迎回来,请输入用户名和密码登录"
#: authentication/views/login.py:81
#: authentication/views/login.py:82
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: authentication/views/login.py:174 users/views/user.py:393
#: authentication/views/login.py:156 users/views/user.py:393
#: users/views/user.py:418
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对"
#: authentication/views/login.py:205
#: authentication/views/login.py:226
msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page"
msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面"
#: authentication/views/login.py:231
msgid "No order found"
msgstr "没有发现工单"
#: authentication/views/login.py:254
msgid "Logout success"
msgstr "退出登录成功"
#: authentication/views/login.py:206
#: authentication/views/login.py:255
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
......@@ -2757,6 +2832,21 @@ msgstr ""
msgid "Websocket server run on port: {}, you should proxy it on nginx"
msgstr ""
#: jumpserver/views.py:241
#, fuzzy
#| msgid ""
#| "<div>Luna is a separately deployed program, you need to deploy Luna, "
#| "koko, configure nginx for url distribution,</div> </div>If you see this "
#| "page, prove that you are not accessing the nginx listening port. Good "
#| "luck.</div>"
msgid ""
"<div>Koko is a separately deployed program, you need to deploy Koko, "
"configure nginx for url distribution,</div> </div>If you see this page, "
"prove that you are not accessing the nginx listening port. Good luck.</div>"
msgstr ""
"<div>Luna是单独部署的一个程序,你需要部署luna,koko, </div><div>如果你看到了"
"这个页面,证明你访问的不是nginx监听的端口,祝你好运</div>"
#: ops/api/celery.py:54
msgid "Waiting task start"
msgstr "等待任务开始"
......@@ -2872,21 +2962,21 @@ msgstr "结果"
msgid "Adhoc result summary"
msgstr "汇总"
#: ops/models/command.py:22
#: ops/models/command.py:23
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56
#: xpack/plugins/cloud/models.py:273
msgid "Result"
msgstr "结果"
#: ops/models/command.py:57
#: ops/models/command.py:58
msgid "Task start"
msgstr "任务开始"
#: ops/models/command.py:71
#: ops/models/command.py:75
msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 不允许被执行 ......."
#: ops/models/command.py:77
#: ops/models/command.py:81
msgid "Task end"
msgstr "任务结束"
......@@ -3028,7 +3118,7 @@ msgstr "没有输入命令"
msgid "No system user was selected"
msgstr "没有选择系统用户"
#: ops/templates/ops/command_execution_create.html:296
#: ops/templates/ops/command_execution_create.html:296 orders/models.py:26
msgid "Pending"
msgstr "等待"
......@@ -3112,7 +3202,93 @@ msgstr "命令执行列表"
msgid "Command execution"
msgstr "命令执行"
#: orgs/mixins/models.py:58 orgs/mixins/serializers.py:26 orgs/models.py:31
#: orders/models.py:12 orders/models.py:33
#, fuzzy
#| msgid "User is inactive"
msgid "User display name"
msgstr "用户已禁用"
#: orders/models.py:13 orders/models.py:36
msgid "Body"
msgstr ""
#: orders/models.py:24
#, fuzzy
#| msgid "Accept"
msgid "Accepted"
msgstr "接受"
#: orders/models.py:25
msgid "Rejected"
msgstr "拒绝"
#: orders/models.py:35 orders/templates/orders/login_confirm_order_list.html:13
msgid "Title"
msgstr "标题"
#: orders/models.py:37
#: orders/templates/orders/login_confirm_order_detail.html:59
msgid "Assignee"
msgstr "处理人"
#: orders/models.py:38
msgid "Assignee display name"
msgstr "处理人名称"
#: orders/models.py:39
#: orders/templates/orders/login_confirm_order_detail.html:34
msgid "Assignees"
msgstr "待处理人"
#: orders/models.py:40
msgid "Assignees display name"
msgstr "待处理人名称"
#: orders/serializers.py:21
#: orders/templates/orders/login_confirm_order_detail.html:94
#: orders/templates/orders/login_confirm_order_list.html:53
#: 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
#: terminal/templates/terminal/terminal_list.html:80
msgid "Reject"
msgstr "拒绝"
#: orders/serializers.py:43
msgid "this order"
msgstr "这个工单"
#: orders/templates/orders/login_confirm_order_detail.html:75
msgid "ago"
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/views.py:15 orders/views.py:31 templates/_nav.html:127
msgid "Orders"
msgstr "工单管理"
#: orders/views.py:16
msgid "Login confirm order list"
msgstr "登录复核工单列表"
#: orders/views.py:32
msgid "Login confirm order detail"
msgstr "登录复核工单详情"
#: orgs/mixins/models.py:44 orgs/mixins/serializers.py:26 orgs/models.py:31
msgid "Organization"
msgstr "组织"
......@@ -3136,7 +3312,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:379 users/templates/users/_select_user_modal.html:16
#: users/models/user.py:381 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
......@@ -3178,7 +3354,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:411 users/templates/users/user_detail.html:107
#: users/models/user.py:413 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:120
msgid "Date expired"
msgstr "失效日期"
......@@ -3742,7 +3918,7 @@ msgid "Please submit the LDAP configuration before import"
msgstr "请先提交LDAP配置再进行导入"
#: settings/templates/settings/_ldap_list_users_modal.html:32
#: users/models/user.py:375 users/templates/users/user_detail.html:71
#: users/models/user.py:377 users/templates/users/user_detail.html:71
#: users/templates/users/user_profile.html:59
msgid "Email"
msgstr "邮件"
......@@ -3946,7 +4122,7 @@ msgstr "用户来源不是LDAP"
#: settings/views.py:19 settings/views.py:46 settings/views.py:73
#: settings/views.py:103 settings/views.py:131 settings/views.py:144
#: settings/views.py:158 settings/views.py:185 templates/_nav.html:170
#: settings/views.py:158 settings/views.py:185 templates/_nav.html:180
msgid "Settings"
msgstr "系统设置"
......@@ -4139,7 +4315,7 @@ msgstr "终端管理"
msgid "Job Center"
msgstr "作业中心"
#: templates/_nav.html:116 templates/_nav.html:136
#: templates/_nav.html:116 templates/_nav.html:146
msgid "Batch command"
msgstr "批量命令"
......@@ -4147,15 +4323,19 @@ msgstr "批量命令"
msgid "Task monitor"
msgstr "任务监控"
#: templates/_nav.html:146
#: templates/_nav.html:130
msgid "Login confirm"
msgstr "登录复核"
#: templates/_nav.html:156
msgid "XPack"
msgstr ""
#: templates/_nav.html:154 xpack/plugins/cloud/views.py:28
#: templates/_nav.html:164 xpack/plugins/cloud/views.py:28
msgid "Account list"
msgstr "账户列表"
#: templates/_nav.html:155
#: templates/_nav.html:165
msgid "Sync instance"
msgstr "同步实例"
......@@ -4184,10 +4364,6 @@ msgstr "语言播放验证码"
msgid "Captcha"
msgstr "验证码"
#: templates/flash_message_standalone.html:47
msgid "Return"
msgstr "返回"
#: templates/index.html:11
msgid "Total users"
msgstr "用户总数"
......@@ -4496,14 +4672,6 @@ msgstr "地址"
msgid "Alive"
msgstr "在线"
#: terminal/templates/terminal/terminal_list.html:78
msgid "Accept"
msgstr "接受"
#: terminal/templates/terminal/terminal_list.html:80
msgid "Reject"
msgstr "拒绝"
#: terminal/templates/terminal/terminal_modal_accept.html:5
msgid "Accept terminal registration"
msgstr "接受终端注册"
......@@ -4541,7 +4709,7 @@ msgstr "你可以使用ssh客户端工具连接终端"
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
#: users/forms.py:32 users/models/user.py:383
#: users/forms.py:32 users/models/user.py:385
#: users/templates/users/_select_user_modal.html:15
#: users/templates/users/user_detail.html:87
#: users/templates/users/user_list.html:37
......@@ -4570,7 +4738,7 @@ msgstr "添加到用户组"
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
#: users/forms.py:90 users/forms.py:251 users/serializers/v1.py:116
#: users/forms.py:90 users/forms.py:251 users/serializers/user.py:110
msgid "Not a valid ssh public key"
msgstr "ssh密钥不合法"
......@@ -4655,98 +4823,98 @@ msgstr "复制你的公钥到这里"
msgid "Select users"
msgstr "选择用户"
#: users/models/user.py:50 users/templates/users/user_update.html:22
#: users/models/user.py:52 users/templates/users/user_update.html:22
#: users/views/login.py:46 users/views/login.py:107
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
#: users/models/user.py:126 users/models/user.py:508
#: users/models/user.py:128 users/models/user.py:510
msgid "Administrator"
msgstr "管理员"
#: users/models/user.py:128
#: users/models/user.py:130
msgid "Application"
msgstr "应用程序"
#: users/models/user.py:129 xpack/plugins/orgs/forms.py:30
#: users/models/user.py:131 xpack/plugins/orgs/forms.py:30
#: xpack/plugins/orgs/templates/orgs/org_list.html:14
msgid "Auditor"
msgstr "审计员"
#: users/models/user.py:139
#: users/models/user.py:141
msgid "Org admin"
msgstr "组织管理员"
#: users/models/user.py:141
#: users/models/user.py:143
msgid "Org auditor"
msgstr "组织审计员"
#: users/models/user.py:332 users/templates/users/user_profile.html:90
#: users/models/user.py:334 users/templates/users/user_profile.html:90
msgid "Force enable"
msgstr "强制启用"
#: users/models/user.py:386
#: users/models/user.py:388
msgid "Avatar"
msgstr "头像"
#: users/models/user.py:389 users/templates/users/user_detail.html:82
#: users/models/user.py:391 users/templates/users/user_detail.html:82
msgid "Wechat"
msgstr "微信"
#: users/models/user.py:418 users/templates/users/user_detail.html:103
#: users/models/user.py:420 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:422
#: users/models/user.py:424
msgid "Date password last updated"
msgstr "最后更新密码日期"
#: users/models/user.py:511
#: users/models/user.py:513
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
#: users/serializers/v1.py:45
#: users/serializers/group.py:46
msgid "Auditors cannot be join in the user group"
msgstr "审计员不能被加入到用户组"
#: users/serializers/user.py:39
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/v1.py:46
#: users/serializers/user.py:40
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/v1.py:47
#: users/serializers/user.py:41
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/v1.py:48
#: users/serializers/user.py:42
msgid "Role name"
msgstr "角色名"
#: users/serializers/v1.py:49
#: users/serializers/user.py:43
msgid "Is valid"
msgstr "账户是否有效"
#: users/serializers/v1.py:50
#: users/serializers/user.py:44
msgid "Is expired"
msgstr " 是否过期"
#: users/serializers/v1.py:51
#: users/serializers/user.py:45
msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/v1.py:72
#: users/serializers/user.py:66
msgid "Role limit to {}"
msgstr "角色只能为 {}"
#: users/serializers/v1.py:84
#: users/serializers/user.py:78
msgid "Password does not match security rules"
msgstr "密码不满足安全规则"
#: users/serializers/v1.py:157
msgid "Auditors cannot be join in the user group"
msgstr "审计员不能被加入到用户组"
#: users/serializers_v2/user.py:36
msgid "name not unique"
msgstr "名称重复"
......@@ -5092,14 +5260,6 @@ 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"
......@@ -6391,9 +6551,6 @@ msgstr "创建"
#~ msgid "Start"
#~ msgstr "开始"
#~ msgid "User login settings"
#~ msgstr "用户登录设置"
#~ msgid "Bit"
#~ msgstr " 位"
......
# -*- 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