Merge branch 'dev' into pr@dev@connectivity_choice

This commit is contained in:
feng626 2025-05-07 17:30:30 +08:00 committed by GitHub
commit dd5bcab4ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 379 additions and 187 deletions

View File

@ -1,9 +1,12 @@
import time
from django.conf import settings from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from common.api import JMSModelViewSet from common.api import JMSModelViewSet
@ -44,6 +47,9 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
@action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny]) @action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny])
def login(self, request): def login(self, request):
confirm_mfa = request.GET.get('mfa')
if confirm_mfa:
request.session['passkey_confirm_mfa'] = '1'
return render(request, 'authentication/passkey.html', {}) return render(request, 'authentication/passkey.html', {})
def redirect_to_error(self, error): def redirect_to_error(self, error):
@ -64,8 +70,16 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
if not user: if not user:
return self.redirect_to_error(_('Auth failed')) return self.redirect_to_error(_('Auth failed'))
confirm_mfa = request.session.get('passkey_confirm_mfa')
if confirm_mfa:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index('mfa') + 1
request.session['CONFIRM_TIME'] = int(time.time())
request.session['passkey_confirm_mfa'] = ''
return Response('ok')
try: try:
self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY) self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY)
self.mark_mfa_ok('passkey', user)
return self.redirect_to_guard_view() return self.redirect_to_guard_view()
except Exception as e: except Exception as e:
msg = getattr(e, 'msg', '') or str(e) msg = getattr(e, 'msg', '') or str(e)

View File

@ -34,6 +34,7 @@ class MFAType(TextChoices):
Email = 'email', _('Email') Email = 'email', _('Email')
Face = 'face', _('Face Recognition') Face = 'face', _('Face Recognition')
Radius = 'otp_radius', _('Radius') Radius = 'otp_radius', _('Radius')
Passkey = 'passkey', _('Passkey')
Custom = 'mfa_custom', _('Custom') Custom = 'mfa_custom', _('Custom')

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
class BaseMFA(abc.ABC): class BaseMFA(abc.ABC):
placeholder = _('Please input security code') placeholder = _('Please input security code')
skip_cache_check = False skip_cache_check = False
has_code = True
def __init__(self, user): def __init__(self, user):
""" """

View File

@ -11,6 +11,7 @@ class MFAFace(BaseMFA, AuthFaceMixin):
display_name = MFAType.Face.name display_name = MFAType.Face.name
placeholder = 'Face Recognition' placeholder = 'Face Recognition'
skip_cache_check = True skip_cache_check = True
has_code = False
def _check_code(self, code): def _check_code(self, code):
assert self.is_authenticated() assert self.is_authenticated()

View File

@ -49,4 +49,3 @@ class MFAOtp(BaseMFA):
def help_text_of_disable(self): def help_text_of_disable(self):
return '' return ''

View File

@ -0,0 +1,46 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from authentication.mfa.base import BaseMFA
from ..const import MFAType
class MFAPasskey(BaseMFA):
name = MFAType.Passkey.value
display_name = MFAType.Passkey.name
placeholder = 'Passkey'
has_code = False
def _check_code(self, code):
assert self.is_authenticated()
return False, ''
def is_active(self):
if not self.is_authenticated():
return True
return self.user.passkey_set.count()
@staticmethod
def global_enabled():
return settings.AUTH_PASSKEY
def get_enable_url(self) -> str:
return '/ui/#/profile/passkeys'
def get_disable_url(self) -> str:
return '/ui/#/profile/passkeys'
def disable(self):
pass
def can_disable(self) -> bool:
return False
@staticmethod
def help_text_of_enable():
return _("Using passkey as MFA")
@staticmethod
def help_text_of_disable():
return _("Using passkey as MFA")

View File

@ -5,12 +5,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Login passkey</title> <title>Login passkey</title>
<script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script> <script src="{% static 'js/jquery-3.6.1.min.js' %}?_=9"></script>
</head> </head>
<body> <body>
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm"> <form action="{% url 'api-auth:passkey-auth' %}" method="post" id="loginForm">
<input type="hidden" name="passkeys" id="passkeys"/> {% csrf_token %}
</form> <input type="hidden" name="passkeys" id="passkeys"/>
</form>
</body> </body>
<script> <script>
const loginUrl = "/core/auth/login/"; const loginUrl = "/core/auth/login/";

View File

@ -5,11 +5,14 @@ from datetime import datetime, timedelta
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
from django.conf import settings from django.conf import settings
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from audits.models import UserLoginLog from audits.models import UserLoginLog
from common.utils import get_ip_city, get_request_ip from common.utils import get_ip_city, get_request_ip
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.utils import static_or_direct
from users.models import User from users.models import User
from .notifications import DifferentCityLoginMessage from .notifications import DifferentCityLoginMessage
@ -75,3 +78,72 @@ def check_user_property_is_correct(username, **properties):
user = None user = None
break break
return user return user
def get_auth_methods():
return [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': reverse('authentication:oauth2:login'),
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': reverse('authentication:dingtalk-qr-login'),
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': reverse('authentication:lark-qr-login'),
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': reverse('authentication:slack-qr-login'),
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': reverse('api-auth:passkey-login'),
'logo': static('img/login_passkey.png')
}
]

View File

@ -14,7 +14,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
from django.db import IntegrityError from django.db import IntegrityError
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import reverse, redirect from django.shortcuts import reverse, redirect
from django.templatetags.static import static
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _, get_language from django.utils.translation import gettext as _, get_language
@ -25,13 +24,14 @@ from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from common.const import Language from common.const import Language
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url from common.utils import FlashMessageUtil, safe_next_url
from users.utils import ( from users.utils import (
redirect_user_first_login_or_index redirect_user_first_login_or_index
) )
from .. import mixins, errors from .. import mixins, errors
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
from ..forms import get_user_login_form_cls from ..forms import get_user_login_form_cls
from ..utils import get_auth_methods
__all__ = [ __all__ = [
'UserLoginView', 'UserLogoutView', 'UserLoginView', 'UserLogoutView',
@ -46,73 +46,17 @@ class UserLoginContextMixin:
def get_support_auth_methods(self): def get_support_auth_methods(self):
query_string = self.request.GET.urlencode() query_string = self.request.GET.urlencode()
auth_methods = [ all_methods = get_auth_methods()
{ methods = []
'name': 'OpenID', for method in all_methods:
'enabled': settings.AUTH_OPENID, method = method.copy()
'url': f"{reverse('authentication:openid:login')}?{query_string}", if not method.get('enabled', False):
'logo': static('img/login_oidc_logo.png'), continue
'auto_redirect': True # 是否支持自动重定向 url = method.get('url', '')
}, if query_string and url:
{ method['url'] = '{}?{}'.format(url, query_string)
'name': 'CAS', methods.append(method)
'enabled': settings.AUTH_CAS, return methods
'url': f"{reverse('authentication:cas:cas-login')}?{query_string}",
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': f"{reverse('authentication:saml2:saml2-login')}?{query_string}",
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': f"{reverse('authentication:oauth2:login')}?{query_string}",
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': f"{reverse('authentication:wecom-qr-login')}?{query_string}",
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': f"{reverse('authentication:dingtalk-qr-login')}?{query_string}",
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': f"{reverse('authentication:feishu-qr-login')}?{query_string}",
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': f"{reverse('authentication:lark-qr-login')}?{query_string}",
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': f"{reverse('authentication:slack-qr-login')}?{query_string}",
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': f"{reverse('api-auth:passkey-login')}?{query_string}",
'logo': static('img/login_passkey.png')
}
]
return [method for method in auth_methods if method['enabled']]
@staticmethod @staticmethod
def get_support_langs(): def get_support_langs():

View File

@ -40,6 +40,8 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
if mfa_type == MFAType.Face: if mfa_type == MFAType.Face:
return redirect(reverse('authentication:login-face-capture')) return redirect(reverse('authentication:login-face-capture'))
elif mfa_type == MFAType.Passkey:
return redirect('/api/v1/authentication/passkeys/login/')
return self.do_mfa_check(form, code, mfa_type) return self.do_mfa_check(form, code, mfa_type)
def do_mfa_check(self, form, code, mfa_type): def do_mfa_check(self, form, code, mfa_type):

View File

@ -136,6 +136,8 @@ msgstr ">>> 开始执行测试网关账号可连接性任务"
#: users/templates/users/_msg_user_created.html:13 #: users/templates/users/_msg_user_created.html:13
#: users/templates/users/user_password_verify.html:18 #: users/templates/users/user_password_verify.html:18
#: xpack/plugins/cloud/serializers/account_attrs.py:27 #: xpack/plugins/cloud/serializers/account_attrs.py:27
#: xpack/plugins/cloud/serializers/account_attrs.py:89
#: xpack/plugins/cloud/serializers/account_attrs.py:96
msgid "Password" msgid "Password"
msgstr "密码" msgstr "密码"
@ -746,7 +748,7 @@ msgstr "状态"
#: accounts/serializers/account/account.py:278 #: accounts/serializers/account/account.py:278
#: accounts/templates/accounts/change_secret_failed_info.html:13 #: accounts/templates/accounts/change_secret_failed_info.html:13
#: assets/const/automation.py:9 #: assets/const/automation.py:9
#: authentication/templates/authentication/passkey.html:173 #: authentication/templates/authentication/passkey.html:174
#: authentication/views/base.py:42 authentication/views/base.py:43 #: authentication/views/base.py:42 authentication/views/base.py:43
#: authentication/views/base.py:44 common/const/choices.py:67 #: authentication/views/base.py:44 common/const/choices.py:67
#: settings/templates/ldap/_msg_import_ldap_user.html:26 #: settings/templates/ldap/_msg_import_ldap_user.html:26
@ -855,6 +857,8 @@ msgstr "重复密码"
#: users/serializers/profile.py:186 #: users/serializers/profile.py:186
#: users/templates/users/_msg_user_created.html:12 #: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/cloud/serializers/account_attrs.py:25 #: xpack/plugins/cloud/serializers/account_attrs.py:25
#: xpack/plugins/cloud/serializers/account_attrs.py:87
#: xpack/plugins/cloud/serializers/account_attrs.py:94
msgid "Username" msgid "Username"
msgstr "用户名" msgstr "用户名"
@ -1905,8 +1909,8 @@ msgid ""
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name "
"support)" "support)"
msgstr "" msgstr ""
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:" "* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, "
"db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" "2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19 #: acls/serializers/base.py:41 assets/serializers/asset/host.py:19
msgid "IP/Host" msgid "IP/Host"
@ -1934,8 +1938,8 @@ msgid ""
"With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, " "With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, "
"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 "
msgstr "" msgstr ""
"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:" "* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, "
"db8:2de::e13, 2001:db8:1a:1110::/64" "2001:db8:2de::e13, 2001:db8:1a:1110::/64"
#: acls/serializers/rules/rules.py:33 #: acls/serializers/rules/rules.py:33
#: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_oauth_bind.html:12
@ -2179,7 +2183,8 @@ msgstr "云服务"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: assets/const/category.py:16 common/sdk/sms/endpoint.py:20 #: assets/const/category.py:16 common/sdk/sms/custom_file.py:47
#: common/sdk/sms/endpoint.py:20
msgid "Custom type" msgid "Custom type"
msgstr "自定义类型" msgstr "自定义类型"
@ -2286,7 +2291,7 @@ msgid "Any"
msgstr "任意" msgstr "任意"
#: assets/const/protocol.py:88 rbac/tree.py:62 #: assets/const/protocol.py:88 rbac/tree.py:62
#: settings/serializers/security.py:245 #: settings/serializers/security.py:266
msgid "Security" msgid "Security"
msgstr "安全" msgstr "安全"
@ -2884,7 +2889,7 @@ msgstr "端口超出范围 (0-65535)"
msgid "Protocol is required: {}" msgid "Protocol is required: {}"
msgstr "协议是必填的: {}" msgstr "协议是必填的: {}"
#: assets/serializers/asset/common.py:353 #: assets/serializers/asset/common.py:353 labels/api.py:107
msgid "Invalid data" msgid "Invalid data"
msgstr "无效的数据" msgstr "无效的数据"
@ -3553,29 +3558,29 @@ msgid "Auth Token"
msgstr "认证令牌" msgstr "认证令牌"
#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73 #: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
#: authentication/views/login.py:79 notifications/backends/__init__.py:11 #: authentication/utils.py:114 notifications/backends/__init__.py:11
#: settings/serializers/auth/wecom.py:11 settings/serializers/auth/wecom.py:16 #: settings/serializers/auth/wecom.py:11 settings/serializers/auth/wecom.py:16
#: users/models/user/__init__.py:130 users/models/user/_source.py:19 #: users/models/user/__init__.py:130 users/models/user/_source.py:19
msgid "WeCom" msgid "WeCom"
msgstr "企业微信" msgstr "企业微信"
#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:97 #: audits/signal_handlers/login_log.py:38 authentication/utils.py:126
#: authentication/views/login.py:91 notifications/backends/__init__.py:14 #: authentication/views/feishu.py:97 notifications/backends/__init__.py:14
#: settings/serializers/auth/feishu.py:12 #: settings/serializers/auth/feishu.py:12
#: settings/serializers/auth/feishu.py:14 users/models/user/__init__.py:136 #: settings/serializers/auth/feishu.py:14 users/models/user/__init__.py:136
#: users/models/user/_source.py:21 #: users/models/user/_source.py:21
msgid "FeiShu" msgid "FeiShu"
msgstr "飞书" msgstr "飞书"
#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:103 #: audits/signal_handlers/login_log.py:40 authentication/utils.py:138
#: authentication/views/slack.py:79 notifications/backends/__init__.py:16 #: authentication/views/slack.py:79 notifications/backends/__init__.py:16
#: settings/serializers/auth/slack.py:11 settings/serializers/auth/slack.py:13 #: settings/serializers/auth/slack.py:11 settings/serializers/auth/slack.py:13
#: users/models/user/__init__.py:142 users/models/user/_source.py:23 #: users/models/user/__init__.py:142 users/models/user/_source.py:23
msgid "Slack" msgid "Slack"
msgstr "Slack" msgstr "Slack"
#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:151 #: audits/signal_handlers/login_log.py:41 authentication/utils.py:120
#: authentication/views/login.py:85 notifications/backends/__init__.py:12 #: authentication/views/dingtalk.py:151 notifications/backends/__init__.py:12
#: settings/serializers/auth/dingtalk.py:11 users/models/user/__init__.py:133 #: settings/serializers/auth/dingtalk.py:11 users/models/user/__init__.py:133
#: users/models/user/_source.py:20 #: users/models/user/_source.py:20
msgid "DingTalk" msgid "DingTalk"
@ -3586,8 +3591,8 @@ msgstr "钉钉"
msgid "Temporary token" msgid "Temporary token"
msgstr "临时密码" msgstr "临时密码"
#: audits/signal_handlers/login_log.py:43 authentication/views/login.py:109 #: audits/signal_handlers/login_log.py:43 authentication/const.py:37
#: settings/serializers/auth/passkey.py:8 #: authentication/utils.py:144 settings/serializers/auth/passkey.py:8
#: settings/serializers/auth/passkey.py:11 #: settings/serializers/auth/passkey.py:11
msgid "Passkey" msgid "Passkey"
msgstr "Passkey" msgstr "Passkey"
@ -3671,8 +3676,8 @@ msgstr "ACL 动作是人脸在线"
msgid "No available face feature" msgid "No available face feature"
msgstr "没有可用的人脸特征" msgstr "没有可用的人脸特征"
#: authentication/api/face.py:100 authentication/mfa/face.py:21 #: authentication/api/face.py:100 authentication/mfa/face.py:22
#: authentication/mfa/face.py:23 users/views/profile/face.py:72 #: authentication/mfa/face.py:24 users/views/profile/face.py:72
msgid "Facial comparison failed" msgid "Facial comparison failed"
msgstr "人脸比对失败" msgstr "人脸比对失败"
@ -3739,11 +3744,11 @@ msgstr "OpenID 错误"
msgid "Please check if a user with the same username or email already exists" msgid "Please check if a user with the same username or email already exists"
msgstr "请检查是否已经存在相同用户名或邮箱的用户" msgstr "请检查是否已经存在相同用户名或邮箱的用户"
#: authentication/backends/passkey/api.py:37 #: authentication/backends/passkey/api.py:40
msgid "Only register passkey for local user" msgid "Only register passkey for local user"
msgstr "仅为本地用户注册密钥" msgstr "仅为本地用户注册密钥"
#: authentication/backends/passkey/api.py:65 #: authentication/backends/passkey/api.py:71
msgid "Auth failed" msgid "Auth failed"
msgstr "认证失败" msgstr "认证失败"
@ -3793,7 +3798,7 @@ msgstr "人脸识别"
msgid "Radius" msgid "Radius"
msgstr "Radius" msgstr "Radius"
#: authentication/const.py:37 #: authentication/const.py:38
msgid "Custom" msgid "Custom"
msgstr "自定义" msgstr "自定义"
@ -3993,7 +3998,7 @@ msgstr "动态码"
msgid "Please input security code" msgid "Please input security code"
msgstr "请输入动态安全码" msgstr "请输入动态安全码"
#: authentication/mfa/base.py:27 #: authentication/mfa/base.py:28
msgid "" msgid ""
"The two-factor code you entered has either already been used or has expired. " "The two-factor code you entered has either already been used or has expired. "
"Please request a new one." "Please request a new one."
@ -4019,11 +4024,11 @@ msgstr "邮件验证码校验失败"
msgid "Email verification code" msgid "Email verification code"
msgstr "邮件验证码" msgstr "邮件验证码"
#: authentication/mfa/face.py:55 #: authentication/mfa/face.py:56
msgid "Bind face to enable" msgid "Bind face to enable"
msgstr "绑定人脸特征以启用" msgstr "绑定人脸特征以启用"
#: authentication/mfa/face.py:59 #: authentication/mfa/face.py:60
msgid "Unbind face to disable" msgid "Unbind face to disable"
msgstr "解绑人脸特征以禁用" msgstr "解绑人脸特征以禁用"
@ -4039,6 +4044,10 @@ msgstr "虚拟 MFA 验证码"
msgid "Virtual OTP based MFA" msgid "Virtual OTP based MFA"
msgstr "虚拟 MFA(OTP)" msgstr "虚拟 MFA(OTP)"
#: authentication/mfa/passkey.py:42 authentication/mfa/passkey.py:46
msgid "Using passkey as MFA"
msgstr ""
#: authentication/mfa/radius.py:8 #: authentication/mfa/radius.py:8
msgid "Radius verify code invalid" msgid "Radius verify code invalid"
msgstr "Radius 校验失败" msgstr "Radius 校验失败"
@ -4537,17 +4546,17 @@ msgstr "返回"
msgid "Copy success" msgid "Copy success"
msgstr "复制成功" msgstr "复制成功"
#: authentication/templates/authentication/passkey.html:162 #: authentication/templates/authentication/passkey.html:163
msgid "" msgid ""
"This page is not served over HTTPS. Please use HTTPS to ensure security of " "This page is not served over HTTPS. Please use HTTPS to ensure security of "
"your credentials." "your credentials."
msgstr "本页面未使用 HTTPS 协议,请使用 HTTPS 协议以确保您的凭据安全。" msgstr "本页面未使用 HTTPS 协议,请使用 HTTPS 协议以确保您的凭据安全。"
#: authentication/templates/authentication/passkey.html:173 #: authentication/templates/authentication/passkey.html:174
msgid "Do you want to retry ?" msgid "Do you want to retry ?"
msgstr "是否重试 " msgstr "是否重试 "
#: authentication/utils.py:24 common/utils/ip/geoip/utils.py:24 #: authentication/utils.py:27 common/utils/ip/geoip/utils.py:24
#: xpack/plugins/cloud/const.py:33 #: xpack/plugins/cloud/const.py:33
msgid "LAN" msgid "LAN"
msgstr "局域网" msgstr "局域网"
@ -4632,23 +4641,23 @@ msgstr "Lark 已经绑定"
msgid "Failed to get user from Lark" msgid "Failed to get user from Lark"
msgstr "从 Lark 获取用户失败" msgstr "从 Lark 获取用户失败"
#: authentication/views/login.py:219 #: authentication/views/login.py:163
msgid "Redirecting" msgid "Redirecting"
msgstr "跳转中" msgstr "跳转中"
#: authentication/views/login.py:220 #: authentication/views/login.py:164
msgid "Redirecting to {} authentication" msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证" msgstr "正在跳转到 {} 认证"
#: authentication/views/login.py:247 #: authentication/views/login.py:191
msgid "Login timeout, please try again." msgid "Login timeout, please try again."
msgstr "登录超时,请重新登录" msgstr "登录超时,请重新登录"
#: authentication/views/login.py:292 #: authentication/views/login.py:236
msgid "User email already exists ({})" msgid "User email already exists ({})"
msgstr "用户邮箱已存在 ({})" msgstr "用户邮箱已存在 ({})"
#: authentication/views/login.py:370 #: authentication/views/login.py:314
msgid "" msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n" "Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page" " Don't close this page"
@ -4656,15 +4665,15 @@ msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n" "等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面" " 不要关闭本页面"
#: authentication/views/login.py:375 #: authentication/views/login.py:319
msgid "No ticket found" msgid "No ticket found"
msgstr "没有发现工单" msgstr "没有发现工单"
#: authentication/views/login.py:411 #: authentication/views/login.py:355
msgid "Logout success" msgid "Logout success"
msgstr "退出登录成功" msgstr "退出登录成功"
#: authentication/views/login.py:412 #: authentication/views/login.py:356
msgid "Logout success, return login page" msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面" msgstr "退出登录成功,返回到登录页面"
@ -4759,7 +4768,7 @@ msgstr "企业专业版"
msgid "Ultimate edition" msgid "Ultimate edition"
msgstr "企业旗舰版" msgstr "企业旗舰版"
#: common/const/common.py:5 xpack/plugins/cloud/manager.py:454 #: common/const/common.py:5 xpack/plugins/cloud/manager.py:450
#, python-format #, python-format
msgid "%(name)s was created successfully" msgid "%(name)s was created successfully"
msgstr "%(name)s 创建成功" msgstr "%(name)s 创建成功"
@ -5033,6 +5042,10 @@ msgstr "自定义短信文件无效"
msgid "SMS sending failed[%s]: %s" msgid "SMS sending failed[%s]: %s"
msgstr "短信发送失败[%s]: %s" msgstr "短信发送失败[%s]: %s"
#: common/sdk/sms/custom_file.py:47 common/serializers/common.py:98
msgid "File"
msgstr "文件"
#: common/sdk/sms/endpoint.py:16 #: common/sdk/sms/endpoint.py:16
msgid "Alibaba cloud" msgid "Alibaba cloud"
msgstr "阿里云" msgstr "阿里云"
@ -5077,10 +5090,6 @@ msgstr "请在 {} 秒后发送"
msgid "Children" msgid "Children"
msgstr "节点" msgstr "节点"
#: common/serializers/common.py:98
msgid "File"
msgstr "文件"
#: common/serializers/fields.py:139 #: common/serializers/fields.py:139
msgid "Invalid data type" msgid "Invalid data type"
msgstr "无效的数据" msgstr "无效的数据"
@ -5202,6 +5211,10 @@ msgstr "你的账号已创建成功"
msgid "JumpServer - An open-source PAM" msgid "JumpServer - An open-source PAM"
msgstr "JumpServer 开源堡垒机" msgstr "JumpServer 开源堡垒机"
#: jumpserver/context_processor.py:28
msgid "FIT2CLOUD"
msgstr ""
#: jumpserver/views/celery_flower.py:22 #: jumpserver/views/celery_flower.py:22
msgid "<h1>Flower service unavailable, check it</h1>" msgid "<h1>Flower service unavailable, check it</h1>"
msgstr "Flower 服务不可用,请检查" msgstr "Flower 服务不可用,请检查"
@ -5233,7 +5246,7 @@ msgstr ""
msgid "App Labels" msgid "App Labels"
msgstr "标签管理" msgstr "标签管理"
#: labels/models.py:15 #: labels/models.py:15 settings/serializers/security.py:205
msgid "Color" msgid "Color"
msgstr "颜色" msgstr "颜色"
@ -7207,7 +7220,7 @@ msgstr "租户 ID"
#: settings/serializers/feature.py:110 terminal/serializers/storage.py:68 #: settings/serializers/feature.py:110 terminal/serializers/storage.py:68
#: xpack/plugins/cloud/manager.py:119 xpack/plugins/cloud/manager.py:124 #: xpack/plugins/cloud/manager.py:119 xpack/plugins/cloud/manager.py:124
#: xpack/plugins/cloud/models.py:292 #: xpack/plugins/cloud/manager.py:161 xpack/plugins/cloud/models.py:292
msgid "Region" msgid "Region"
msgstr "地域" msgstr "地域"
@ -7618,63 +7631,88 @@ msgstr "每天检测一次,超过预设时间的用户自动禁用"
msgid "Watermark" msgid "Watermark"
msgstr "开启水印" msgstr "开启水印"
#: settings/serializers/security.py:200 #: settings/serializers/security.py:199
msgid "Watermark session content"
msgstr "会话水印自定义内容"
#: settings/serializers/security.py:202
msgid "Watermark console content"
msgstr "管理页面水印自定义内容"
#: settings/serializers/security.py:208
msgid "Watermark font size"
msgstr "字体字号"
#: settings/serializers/security.py:211
msgid "Watermark height"
msgstr "单个水印高度"
#: settings/serializers/security.py:214
msgid "Watermark width"
msgstr "单个水印宽度"
#: settings/serializers/security.py:217
msgid "Watermark rotate"
msgstr "水印旋转角度"
#: settings/serializers/security.py:221
msgid "Max idle time (minute)" msgid "Max idle time (minute)"
msgstr "连接最大空闲时间 (分)" msgstr "连接最大空闲时间 (分)"
#: settings/serializers/security.py:201 #: settings/serializers/security.py:222
msgid "If idle time more than it, disconnect connection." msgid "If idle time more than it, disconnect connection."
msgstr "提示:如果超过该配置没有操作,连接会被断开" msgstr "提示:如果超过该配置没有操作,连接会被断开"
#: settings/serializers/security.py:204 #: settings/serializers/security.py:225
msgid "Session expire at browser closed" msgid "Session expire at browser closed"
msgstr "会话在浏览器关闭时过期" msgstr "会话在浏览器关闭时过期"
#: settings/serializers/security.py:205 #: settings/serializers/security.py:226
msgid "Whether to expire the session when the user closes their browser." msgid "Whether to expire the session when the user closes their browser."
msgstr "当用户关闭浏览器时是否使会话过期。" msgstr "当用户关闭浏览器时是否使会话过期。"
#: settings/serializers/security.py:210 #: settings/serializers/security.py:231
msgid "Allow users to view asset session information" msgid "Allow users to view asset session information"
msgstr "允许用户查看资产在线会话信息" msgstr "允许用户查看资产在线会话信息"
#: settings/serializers/security.py:212 #: settings/serializers/security.py:233
msgid "" msgid ""
"When a user connects to an asset, the account selection popup displays the " "When a user connects to an asset, the account selection popup displays the "
"number of active sessions for the current asset (RDP protocol only)." "number of active sessions for the current asset (RDP protocol only)."
msgstr "" msgstr ""
"当用户连接资产时,账号选择弹窗中显示当前资产的在线会话数量(仅 rdp 协议)" "当用户连接资产时,账号选择弹窗中显示当前资产的在线会话数量(仅 rdp 协议)"
#: settings/serializers/security.py:218 #: settings/serializers/security.py:239
msgid "Max online time (hour)" msgid "Max online time (hour)"
msgstr "会话连接最大时间 (时)" msgstr "会话连接最大时间 (时)"
#: settings/serializers/security.py:219 #: settings/serializers/security.py:240
msgid "If session connection time more than it, disconnect connection." msgid "If session connection time more than it, disconnect connection."
msgstr "提示:如果会话连接超过该配置,连接会被断开" msgstr "提示:如果会话连接超过该配置,连接会被断开"
#: settings/serializers/security.py:222 #: settings/serializers/security.py:243
msgid "Remember manual auth" msgid "Remember manual auth"
msgstr "保存手动输入密码" msgstr "保存手动输入密码"
#: settings/serializers/security.py:225 #: settings/serializers/security.py:246
#: terminal/templates/terminal/_msg_session_sharing.html:10 #: terminal/templates/terminal/_msg_session_sharing.html:10
msgid "Session share" msgid "Session share"
msgstr "会话分享" msgstr "会话分享"
#: settings/serializers/security.py:226 #: settings/serializers/security.py:247
msgid "Enabled, Allows user active session to be shared with other users" msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作"
#: settings/serializers/security.py:232 #: settings/serializers/security.py:253
msgid "Insecure command alert" msgid "Insecure command alert"
msgstr "危险命令告警" msgstr "危险命令告警"
#: settings/serializers/security.py:235 #: settings/serializers/security.py:256
msgid "Email recipient" msgid "Email recipient"
msgstr "邮件收件人" msgstr "邮件收件人"
#: settings/serializers/security.py:236 #: settings/serializers/security.py:257
msgid "Multiple user using , split" msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割" msgstr "多个用户,使用 , 分割"
@ -8039,17 +8077,17 @@ msgstr ""
"您的SSH密钥没有设置或已失效请点击 <a href=\"%(user_pubkey_update)s\"> 链接 " "您的SSH密钥没有设置或已失效请点击 <a href=\"%(user_pubkey_update)s\"> 链接 "
"</a> 更新" "</a> 更新"
#: templates/_mfa_login_field.html:31 #: templates/_mfa_login_field.html:29
#: users/templates/users/forgot_password.html:101 #: users/templates/users/forgot_password.html:101
msgid "Send" msgid "Send"
msgstr "发送" msgstr "发送"
#: templates/_mfa_login_field.html:110 #: templates/_mfa_login_field.html:108
#: users/templates/users/forgot_password.html:176 #: users/templates/users/forgot_password.html:176
msgid "Wait: " msgid "Wait: "
msgstr "等待:" msgstr "等待:"
#: templates/_mfa_login_field.html:120 #: templates/_mfa_login_field.html:118
#: users/templates/users/forgot_password.html:192 #: users/templates/users/forgot_password.html:192
msgid "The verification code has been sent" msgid "The verification code has been sent"
msgstr "验证码已发送" msgstr "验证码已发送"
@ -8162,7 +8200,7 @@ msgstr "会话不存在: {}"
msgid "Session is finished or the protocol not supported" msgid "Session is finished or the protocol not supported"
msgstr "会话已经完成或协议不支持" msgstr "会话已经完成或协议不支持"
#: terminal/api/session/session.py:345 #: terminal/api/session/session.py:345 tickets/api/ticket.py:140
msgid "User does not have permission" msgid "User does not have permission"
msgstr "用户没有权限" msgstr "用户没有权限"
@ -8710,9 +8748,9 @@ msgid ""
"days. <a href=\"https://learn.microsoft.com/en-us/windows-server/remote/" "days. <a href=\"https://learn.microsoft.com/en-us/windows-server/remote/"
"remote-desktop-services/rds-client-access-license\">Detail</a>" "remote-desktop-services/rds-client-access-license\">Detail</a>"
msgstr "" msgstr ""
"如果不存在RDS 将处于试用模式,试用期为 120 天。<a href='https://learn." "如果不存在RDS 将处于试用模式,试用期为 120 天。<a href='https://"
"microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-client-" "learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-"
"access-license'>详情</a>" "client-access-license'>详情</a>"
#: terminal/serializers/applet_host.py:55 #: terminal/serializers/applet_host.py:55
msgid "RDS License Server" msgid "RDS License Server"
@ -8930,8 +8968,8 @@ msgid ""
"If there are multiple hosts, use a comma (,) to separate them. <br>(For " "If there are multiple hosts, use a comma (,) to separate them. <br>(For "
"example: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100)" "example: http://www.jumpserver.a.com:9100, http://www.jumpserver.b.com:9100)"
msgstr "" msgstr ""
"如果有多个主机,请用逗号 (,) 分隔它们。<br>例如http://www.jumpserver.a." "如果有多个主机,请用逗号 (,) 分隔它们。<br>例如http://"
"com:9100http://www.jumpserver.b.com:9100" "www.jumpserver.a.com:9100http://www.jumpserver.b.com:9100"
#: terminal/serializers/storage.py:199 #: terminal/serializers/storage.py:199
msgid "Index by date" msgid "Index by date"
@ -10529,59 +10567,63 @@ msgstr "同步地区"
msgid "Get instances of region \"%s\" error, error: %s" msgid "Get instances of region \"%s\" error, error: %s"
msgstr "获取区域 \"%s\" 的实例错误,错误:%s" msgstr "获取区域 \"%s\" 的实例错误,错误:%s"
#: xpack/plugins/cloud/manager.py:161 xpack/plugins/cloud/models.py:289
msgid "Instance"
msgstr "实例"
#: xpack/plugins/cloud/manager.py:187 #: xpack/plugins/cloud/manager.py:187
#, python-format #, python-format
msgid "Failed to synchronize the instance \"%s\"" msgid "Failed to synchronize the instance \"%s\""
msgstr "无法同步实例 %s" msgstr "无法同步实例 %s"
#: xpack/plugins/cloud/manager.py:379 #: xpack/plugins/cloud/manager.py:378
#, python-format #, python-format
msgid "" msgid ""
"The updated platform of asset \"%s\" is inconsistent with the original " "The updated platform of asset \"%s\" is inconsistent with the original "
"platform type. Skip platform and protocol updates" "platform type. Skip platform and protocol updates"
msgstr "资产 \"%s\" 的更新平台与原平台类型不一致。跳过平台和协议更新" msgstr "资产 \"%s\" 的更新平台与原平台类型不一致。跳过平台和协议更新"
#: xpack/plugins/cloud/manager.py:436 #: xpack/plugins/cloud/manager.py:432
#, python-format #, python-format
msgid "The asset \"%s\" already exists" msgid "The asset \"%s\" already exists"
msgstr "资产 \"%s\" 已存在" msgstr "资产 \"%s\" 已存在"
#: xpack/plugins/cloud/manager.py:438 #: xpack/plugins/cloud/manager.py:434
#, python-format #, python-format
msgid "Update asset \"%s\"" msgid "Update asset \"%s\""
msgstr "更新资产 \"%s\"" msgstr "更新资产 \"%s\""
#: xpack/plugins/cloud/manager.py:441 #: xpack/plugins/cloud/manager.py:437
#, python-format #, python-format
msgid "Asset \"%s\" has been updated" msgid "Asset \"%s\" has been updated"
msgstr "资产 \"%s\" 已更新" msgstr "资产 \"%s\" 已更新"
#: xpack/plugins/cloud/manager.py:450 #: xpack/plugins/cloud/manager.py:446
#, python-format #, python-format
msgid "Prepare to create asset \"%s\"" msgid "Prepare to create asset \"%s\""
msgstr "准备创建资产 %s" msgstr "准备创建资产 %s"
#: xpack/plugins/cloud/manager.py:471 #: xpack/plugins/cloud/manager.py:467
#, python-format #, python-format
msgid "Set nodes \"%s\"" msgid "Set nodes \"%s\""
msgstr "设置节点: \"%s\"" msgstr "设置节点: \"%s\""
#: xpack/plugins/cloud/manager.py:497 #: xpack/plugins/cloud/manager.py:493
#, python-format #, python-format
msgid "Set accounts \"%s\"" msgid "Set accounts \"%s\""
msgstr "设置账号: %s" msgstr "设置账号: %s"
#: xpack/plugins/cloud/manager.py:513 #: xpack/plugins/cloud/manager.py:509
#, python-format #, python-format
msgid "Set protocols \"%s\"" msgid "Set protocols \"%s\""
msgstr "设置协议 \"%s\"" msgstr "设置协议 \"%s\""
#: xpack/plugins/cloud/manager.py:521 #: xpack/plugins/cloud/manager.py:517
#, python-format #, python-format
msgid "Set labels \"%s\"" msgid "Set labels \"%s\""
msgstr "设置标签: \"%s\"" msgstr "设置标签: \"%s\""
#: xpack/plugins/cloud/manager.py:535 xpack/plugins/cloud/tasks.py:31 #: xpack/plugins/cloud/manager.py:531 xpack/plugins/cloud/tasks.py:31
msgid "Run sync instance task" msgid "Run sync instance task"
msgstr "执行同步实例任务" msgstr "执行同步实例任务"
@ -10671,10 +10713,6 @@ msgstr "同步任务"
msgid "Sync instance task history" msgid "Sync instance task history"
msgstr "同步实例任务历史" msgstr "同步实例任务历史"
#: xpack/plugins/cloud/models.py:289
msgid "Instance"
msgstr "实例"
#: xpack/plugins/cloud/models.py:306 #: xpack/plugins/cloud/models.py:306
msgid "Sync instance detail" msgid "Sync instance detail"
msgstr "同步实例详情" msgstr "同步实例详情"
@ -10979,6 +11017,12 @@ msgstr "订阅 ID"
msgid "Auto node classification" msgid "Auto node classification"
msgstr "自动节点分类" msgstr "自动节点分类"
#: xpack/plugins/cloud/serializers/account_attrs.py:92
#, fuzzy
#| msgid "Domain name"
msgid "domain_name"
msgstr "域名称"
#: xpack/plugins/cloud/serializers/account_attrs.py:98 #: xpack/plugins/cloud/serializers/account_attrs.py:98
#: xpack/plugins/cloud/serializers/account_attrs.py:102 #: xpack/plugins/cloud/serializers/account_attrs.py:102
#: xpack/plugins/cloud/serializers/account_attrs.py:126 #: xpack/plugins/cloud/serializers/account_attrs.py:126

View File

@ -1536,5 +1536,13 @@
"disallowSelfUpdateFields": "Not allowed to modify the current fields yourself", "disallowSelfUpdateFields": "Not allowed to modify the current fields yourself",
"forceEnableMFAHelpText": "If force enable, user can not disable by themselves", "forceEnableMFAHelpText": "If force enable, user can not disable by themselves",
"removeWarningMsg": "Are you sure you want to remove", "removeWarningMsg": "Are you sure you want to remove",
"setVariable": "Set variable" "setVariable": "Set variable",
"WatermarkVariableHelpText": "You can use ${key} to read built-in variables in watermark content",
"isConsoleCanUse": "is Console page can use",
"userId": "User ID",
"userName": "User name",
"currentTime": "Current time",
"assetId": "Asset ID",
"assetName": "Asset name",
"assetAddress": "Asset address"
} }

View File

@ -1538,5 +1538,15 @@
"disallowSelfUpdateFields": "不允许自己修改当前字段", "disallowSelfUpdateFields": "不允许自己修改当前字段",
"forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用", "forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用",
"removeWarningMsg": "你确定要移除", "removeWarningMsg": "你确定要移除",
"setVariable": "设置参数" "setVariable": "设置参数",
"Watermark": "水印",
"WatermarkVariableHelpText": "您可以在自定义水印内容中使用 ${key} 读取内置变量",
"isConsoleCanUse": "管理页面是否可用",
"userId": "用户ID",
"name": "用户名称",
"userName": "用户名",
"currentTime": "当前时间",
"assetId": "资产 ID",
"assetName": "资产名称",
"assetAddress": "资产地址"
} }

View File

@ -589,6 +589,13 @@ class Config(dict):
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '', 'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'SECURITY_LUNA_REMEMBER_AUTH': True, 'SECURITY_LUNA_REMEMBER_AUTH': True,
'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_WATERMARK_ENABLED': True,
'SECURITY_WATERMARK_SESSION_CONTENT': '${name}(${userName})\n${assetName}',
'SECURITY_WATERMARK_CONSOLE_CONTENT': '${userName}(${name})',
'SECURITY_WATERMARK_COLOR': 'rgba(184, 184, 184, 0.8)',
'SECURITY_WATERMARK_FONT_SIZE': 13,
'SECURITY_WATERMARK_HEIGHT': 200,
'SECURITY_WATERMARK_WIDTH': 200,
'SECURITY_WATERMARK_ROTATE': 45,
'SECURITY_MFA_VERIFY_TTL': 3600, 'SECURITY_MFA_VERIFY_TTL': 3600,
'SECURITY_UNCOMMON_USERS_TTL': 999, 'SECURITY_UNCOMMON_USERS_TTL': 999,
'VERIFY_CODE_TTL': 60, 'VERIFY_CODE_TTL': 60,

View File

@ -328,9 +328,13 @@ MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace'
MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius' MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius'
MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms' MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms'
MFA_BACKEND_EMAIL = 'authentication.mfa.email.MFAEmail' MFA_BACKEND_EMAIL = 'authentication.mfa.email.MFAEmail'
MFA_BACKEND_PASSKEY = 'authentication.mfa.passkey.MFAPasskey'
MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom' MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom'
MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS, MFA_BACKEND_FACE, MFA_BACKEND_EMAIL] MFA_BACKENDS = [
MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS,
MFA_BACKEND_PASSKEY, MFA_BACKEND_FACE, MFA_BACKEND_EMAIL
]
MFA_CUSTOM = CONFIG.MFA_CUSTOM MFA_CUSTOM = CONFIG.MFA_CUSTOM
MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5 MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5

View File

@ -160,9 +160,17 @@ HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
# 水印
SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED
SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE SECURITY_WATERMARK_SESSION_CONTENT = CONFIG.SECURITY_WATERMARK_SESSION_CONTENT
SECURITY_WATERMARK_COLOR = CONFIG.SECURITY_WATERMARK_COLOR
SECURITY_WATERMARK_FONT_SIZE = CONFIG.SECURITY_WATERMARK_FONT_SIZE
SECURITY_WATERMARK_HEIGHT = CONFIG.SECURITY_WATERMARK_HEIGHT
SECURITY_WATERMARK_WIDTH = CONFIG.SECURITY_WATERMARK_WIDTH
SECURITY_WATERMARK_ROTATE = CONFIG.SECURITY_WATERMARK_ROTATE
SECURITY_WATERMARK_CONSOLE_CONTENT = CONFIG.SECURITY_WATERMARK_CONSOLE_CONTENT
SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE
LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND
LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED

View File

@ -37,12 +37,16 @@ class ComponentI18nApi(RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
name = kwargs.get('name') name = kwargs.get('name')
lang = request.query_params.get('lang') lang = request.query_params.get('lang')
flat = request.query_params.get('flat', '1')
data = self.get_component_translations(name) data = self.get_component_translations(name)
if lang:
code = Language.to_internal_code(lang, with_filename=True) if not lang:
data = data.get(code) or {} return Response(data)
flat = request.query_params.get('flat', '1') if lang not in Language.choices:
if flat == '0': lang = 'en'
# 这里要使用原始的 lang, lina 会 merge code = Language.to_internal_code(lang, with_filename=True)
data = {lang: data} data = data.get(code) or {}
if flat == '0':
# 这里要使用原始的 lang, lina 会 merge
data = {lang: data}
return Response(data) return Response(data)

View File

@ -29,6 +29,13 @@ class PrivateSettingSerializer(PublicSettingSerializer):
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField() SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField()
SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField() SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField()
SECURITY_WATERMARK_ENABLED = serializers.BooleanField() SECURITY_WATERMARK_ENABLED = serializers.BooleanField()
SECURITY_WATERMARK_SESSION_CONTENT = serializers.CharField()
SECURITY_WATERMARK_CONSOLE_CONTENT = serializers.CharField()
SECURITY_WATERMARK_COLOR = serializers.CharField()
SECURITY_WATERMARK_FONT_SIZE = serializers.IntegerField()
SECURITY_WATERMARK_HEIGHT = serializers.IntegerField()
SECURITY_WATERMARK_WIDTH = serializers.IntegerField()
SECURITY_WATERMARK_ROTATE = serializers.IntegerField()
SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField() SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField()
VIEW_ASSET_ONLINE_SESSION_INFO = serializers.BooleanField() VIEW_ASSET_ONLINE_SESSION_INFO = serializers.BooleanField()
PASSWORD_RULE = serializers.DictField() PASSWORD_RULE = serializers.DictField()

View File

@ -195,6 +195,27 @@ class SecuritySessionSerializer(serializers.Serializer):
SECURITY_WATERMARK_ENABLED = serializers.BooleanField( SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
required=True, label=_('Watermark'), required=True, label=_('Watermark'),
) )
SECURITY_WATERMARK_SESSION_CONTENT = serializers.CharField(
required=False, label=_('Watermark session content'),
)
SECURITY_WATERMARK_CONSOLE_CONTENT = serializers.CharField(
required=False, label=_("Watermark console content")
)
SECURITY_WATERMARK_COLOR = serializers.CharField(
max_length=32, default="", label=_("Color")
)
SECURITY_WATERMARK_FONT_SIZE = serializers.IntegerField(
required=False, label=_('Watermark font size'), min_value=1, max_value=100,
)
SECURITY_WATERMARK_HEIGHT = serializers.IntegerField(
required=False, label=_('Watermark height'), default=200
)
SECURITY_WATERMARK_WIDTH = serializers.IntegerField(
required=False, label=_('Watermark width'), default=200
)
SECURITY_WATERMARK_ROTATE = serializers.IntegerField(
required=False, label=_('Watermark rotate'), default=45
)
SECURITY_MAX_IDLE_TIME = serializers.IntegerField( SECURITY_MAX_IDLE_TIME = serializers.IntegerField(
min_value=1, max_value=99999, required=False, min_value=1, max_value=99999, required=False,
label=_('Max idle time (minute)'), label=_('Max idle time (minute)'),

View File

@ -5,34 +5,32 @@
onchange="selectChange(this.value)" onchange="selectChange(this.value)"
> >
{% for backend in mfa_backends %} {% for backend in mfa_backends %}
<option value="{{ backend.name }}" <option value="{{ backend.name }}"
{% if not backend.is_active %} disabled {% endif %} {% if not backend.is_active %} disabled {% endif %}
> >
{{ backend.display_name }} {{ backend.display_name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<div class="mfa-div"> <div class="mfa-div">
{% for backend in mfa_backends %} {% for backend in mfa_backends %}
<div id="mfa-{{ backend.name }}" class="mfa-field <div id="mfa-{{ backend.name }}" class="mfa-field
{% if backend.challenge_required %}challenge-required{% endif %}" {% if backend.challenge_required %}challenge-required{% endif %}"
style="display: none" style="display: none"
>
{% if backend.has_code %}
<input type="text" class="form-control input-style"
placeholder="{{ backend.placeholder }}"
> >
{% if backend.challenge_required %}
{% if backend.name == 'face' %} <button class="btn btn-primary full-width btn-challenge"
{% else %} type='button' onclick="sendChallengeCode(this)"
<input type="text" class="form-control input-style" >
placeholder="{{ backend.placeholder }}" {% trans 'Send' %}
> </button>
{% if backend.challenge_required %} {% endif %}
<button class="btn btn-primary full-width btn-challenge" {% endif %}
type='button' onclick="sendChallengeCode(this)" </div>
>
{% trans 'Send' %}
</button>
{% endif %}
{% endif %}
</div>
{% endfor %} {% endfor %}
</div> </div>
@ -121,7 +119,7 @@
}) })
} }
function onError (responseText, responseJson, status) { function onError(responseText, responseJson, status) {
setTimeout(function () { setTimeout(function () {
toastr.error(responseJson.detail || responseJson.error); toastr.error(responseJson.detail || responseJson.error);
}); });