From 1e97a23bc5e2d8f9213aced7588f6130966c01b5 Mon Sep 17 00:00:00 2001 From: jiangweidong <80373698+Hi-JWD@users.noreply.github.com> Date: Fri, 4 Nov 2022 13:56:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BF=98=E8=AE=B0=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=8B=E6=9C=BA=E7=9F=AD=E4=BF=A1=E6=89=BE?= =?UTF-8?q?=E5=9B=9E=EF=BC=8C=E5=B9=B6=E4=BF=AE=E6=94=B9=E9=82=AE=E7=AE=B1?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E5=92=8C=E6=89=8B=E6=9C=BA=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20(#8960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 忘记密码支持通过手机找回,邮箱方式修改为和手机方式一致 * feat: 翻译 * feat: 修改翻译 * fix: 还原 Co-authored-by: Jiangjie.Bai --- apps/authentication/api/password.py | 62 ++++++- apps/authentication/mfa/sms.py | 6 +- .../serializers/password_mfa.py | 28 ++- .../_msg_reset_password_code.html | 21 +++ apps/authentication/urls/api_urls.py | 3 +- apps/common/sdk/sms/__init__.py | 1 - apps/common/sdk/sms/exceptions.py | 21 +++ apps/common/sdk/sms/utils.py | 90 ---------- apps/common/utils/verify_code.py | 94 ++++++++++ apps/locale/ja/LC_MESSAGES/django.po | 164 ++++++++++-------- apps/locale/zh/LC_MESSAGES/django.po | 157 ++++++++++------- apps/notifications/backends/sms.py | 12 +- apps/users/forms/profile.py | 7 +- .../templates/users/forgot_password.html | 130 ++++++++++++-- apps/users/views/profile/reset.py | 68 +++++--- 15 files changed, 583 insertions(+), 281 deletions(-) create mode 100644 apps/authentication/templates/authentication/_msg_reset_password_code.html create mode 100644 apps/common/sdk/sms/exceptions.py delete mode 100644 apps/common/sdk/sms/utils.py create mode 100644 apps/common/utils/verify_code.py diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py index 95ebe6edc..8c676117c 100644 --- a/apps/authentication/api/password.py +++ b/apps/authentication/api/password.py @@ -1,13 +1,73 @@ from rest_framework.generics import CreateAPIView from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from django.utils.translation import ugettext as _ +from django.template.loader import render_to_string -from authentication.serializers import PasswordVerifySerializer +from common.utils.verify_code import SendAndVerifyCodeUtil from common.permissions import IsValidUser +from common.utils.random import random_string +from common.utils import get_object_or_none +from authentication.serializers import ( + PasswordVerifySerializer, ResetPasswordCodeSerializer +) +from settings.utils import get_login_title +from users.models import User from authentication.mixins import authenticate from authentication.errors import PasswordInvalid from authentication.mixins import AuthMixin +class UserResetPasswordSendCodeApi(CreateAPIView): + permission_classes = (AllowAny,) + serializer_class = ResetPasswordCodeSerializer + + @staticmethod + def is_valid_user( **kwargs): + user = get_object_or_none(User, **kwargs) + if not user: + err_msg = _('User does not exist: {}').format(_("No user matched")) + return None, err_msg + if not user.is_local: + err_msg = _( + 'The user is from {}, please go to the corresponding system to change the password' + ).format(user.get_source_display()) + return None, err_msg + return user, None + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + form_type = serializer.validated_data['form_type'] + username = serializer.validated_data['username'] + code = random_string(6, lower=False, upper=False) + other_args = {} + + if form_type == 'phone': + backend = 'sms' + target = serializer.validated_data['phone'] + user, err = self.is_valid_user(username=username, phone=target) + if not user: + return Response({'error': err}, status=400) + else: + backend = 'email' + target = serializer.validated_data['email'] + user, err = self.is_valid_user(username=username, email=target) + if not user: + return Response({'error': err}, status=400) + + subject = '%s: %s' % (get_login_title(), _('Forgot password')) + context = { + 'user': user, 'title': subject, 'code': code, + } + message = render_to_string('authentication/_msg_reset_password_code.html', context) + other_args['subject'] = subject + other_args['message'] = message + + SendAndVerifyCodeUtil(target, code, backend=backend, **other_args).gen_and_send_async() + return Response({'data': 'ok'}, status=200) + + class UserPasswordVerifyApi(AuthMixin, CreateAPIView): permission_classes = (IsValidUser,) serializer_class = PasswordVerifySerializer diff --git a/apps/authentication/mfa/sms.py b/apps/authentication/mfa/sms.py index d23b68766..aa426bf57 100644 --- a/apps/authentication/mfa/sms.py +++ b/apps/authentication/mfa/sms.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from .base import BaseMFA -from common.sdk.sms import SendAndVerifySMSUtil +from common.utils.verify_code import SendAndVerifyCodeUtil sms_failed_msg = _("SMS verify code invalid") @@ -15,7 +15,7 @@ class MFASms(BaseMFA): def __init__(self, user): super().__init__(user) phone = user.phone if self.is_authenticated() else '' - self.sms = SendAndVerifySMSUtil(phone) + self.sms = SendAndVerifyCodeUtil(phone, backend=self.name) def check_code(self, code): assert self.is_authenticated() @@ -37,7 +37,7 @@ class MFASms(BaseMFA): return True def send_challenge(self): - self.sms.gen_and_send() + self.sms.gen_and_send_async() @staticmethod def global_enabled(): diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index f52274e49..152ba0fc6 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -1,15 +1,41 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.drf.fields import EncryptedField __all__ = [ 'MFAChallengeSerializer', 'MFASelectTypeSerializer', - 'PasswordVerifySerializer', + 'PasswordVerifySerializer', 'ResetPasswordCodeSerializer', ] +class ResetPasswordCodeSerializer(serializers.Serializer): + form_type = serializers.CharField(default='email') + username = serializers.CharField() + email = serializers.CharField(allow_blank=True) + phone = serializers.CharField(allow_blank=True) + + def create(self, attrs): + error = [] + form_type = attrs.get('form_type', 'email') + username = attrs.get('username') + if not username: + error.append(_('The {} cannot be empty').format(_('Username'))) + if form_type == 'phone': + phone = attrs.get('phone') + if not phone: + error.append(_('The {} cannot be empty').format(_('Phone'))) + else: + email = attrs.get('email') + if not email: + error.append(_('The {} cannot be empty').format(_('Email'))) + + if error: + raise serializers.ValidationError(error) + + class PasswordVerifySerializer(serializers.Serializer): password = EncryptedField() diff --git a/apps/authentication/templates/authentication/_msg_reset_password_code.html b/apps/authentication/templates/authentication/_msg_reset_password_code.html new file mode 100644 index 000000000..fc8d7a64e --- /dev/null +++ b/apps/authentication/templates/authentication/_msg_reset_password_code.html @@ -0,0 +1,21 @@ +{% load i18n %} + +
+ + + + + + + + + + + + + + + + +
{{ title }}
{% trans 'Hello' %} {{ user.name }},
{% trans 'Verify code' %}: {{ code }}
{% trans 'Copy the verification code to the Reset Password page to reset the password.' %}
{% trans 'The validity period of the verification code is one minute' %}
+
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index cfbac879f..99805a4e8 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -32,7 +32,8 @@ urlpatterns = [ path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'), path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'), path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'), - path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-codej'), + path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-code'), + path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'), path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), ] diff --git a/apps/common/sdk/sms/__init__.py b/apps/common/sdk/sms/__init__.py index 21a51e37b..69a0e13e3 100644 --- a/apps/common/sdk/sms/__init__.py +++ b/apps/common/sdk/sms/__init__.py @@ -1,2 +1 @@ from .endpoint import SMS, BACKENDS -from .utils import SendAndVerifySMSUtil diff --git a/apps/common/sdk/sms/exceptions.py b/apps/common/sdk/sms/exceptions.py new file mode 100644 index 000000000..1fa3956bb --- /dev/null +++ b/apps/common/sdk/sms/exceptions.py @@ -0,0 +1,21 @@ +from django.utils.translation import gettext_lazy as _ + +from common.exceptions import JMSException + + +class CodeExpired(JMSException): + default_code = 'verify_code_expired' + default_detail = _('The verification code has expired. Please resend it') + + +class CodeError(JMSException): + default_code = 'verify_code_error' + default_detail = _('The verification code is incorrect') + + +class CodeSendTooFrequently(JMSException): + default_code = 'code_send_too_frequently' + default_detail = _('Please wait {} seconds before sending') + + def __init__(self, ttl): + super().__init__(detail=self.default_detail.format(ttl)) diff --git a/apps/common/sdk/sms/utils.py b/apps/common/sdk/sms/utils.py deleted file mode 100644 index 7474acc55..000000000 --- a/apps/common/sdk/sms/utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import random - -from django.core.cache import cache -from django.utils.translation import gettext_lazy as _ - -from .endpoint import SMS -from common.utils import get_logger -from common.exceptions import JMSException - -logger = get_logger(__file__) - - -class CodeExpired(JMSException): - default_code = 'verify_code_expired' - default_detail = _('The verification code has expired. Please resend it') - - -class CodeError(JMSException): - default_code = 'verify_code_error' - default_detail = _('The verification code is incorrect') - - -class CodeSendTooFrequently(JMSException): - default_code = 'code_send_too_frequently' - default_detail = _('Please wait {} seconds before sending') - - def __init__(self, ttl): - super().__init__(detail=self.default_detail.format(ttl)) - - -class SendAndVerifySMSUtil: - KEY_TMPL = 'auth-verify-code-{}' - TIMEOUT = 60 - - def __init__(self, phone, key_suffix=None, timeout=None): - self.phone = phone - self.code = '' - self.timeout = timeout or self.TIMEOUT - self.key_suffix = key_suffix or str(phone) - self.key = self.KEY_TMPL.format(self.key_suffix) - - def gen_and_send(self): - """ - 生成,保存,发送 - """ - ttl = self.ttl() - if ttl > 0: - logger.error('Send sms too frequently, delay {}'.format(ttl)) - raise CodeSendTooFrequently(ttl) - - try: - code = self.generate() - self.send(code) - except JMSException: - self.clear() - raise - - def generate(self): - code = ''.join(random.sample('0123456789', 4)) - self.code = code - return code - - def clear(self): - cache.delete(self.key) - - def send(self, code): - """ - 发送信息的方法,如果有错误直接抛出 api 异常 - """ - sms = SMS() - sms.send_verify_code(self.phone, code) - cache.set(self.key, self.code, self.timeout) - logger.info(f'Send sms verify code to {self.phone}: {code}') - - def verify(self, code): - right = cache.get(self.key) - if not right: - raise CodeExpired - - if right != code: - raise CodeError - - self.clear() - return True - - def ttl(self): - return cache.ttl(self.key) - - def get_code(self): - return cache.get(self.key) diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py new file mode 100644 index 000000000..abf4d6056 --- /dev/null +++ b/apps/common/utils/verify_code.py @@ -0,0 +1,94 @@ +from django.core.cache import cache +from django.conf import settings +from django.core.mail import send_mail +from celery import shared_task + +from common.sdk.sms.exceptions import CodeError, CodeExpired, CodeSendTooFrequently +from common.sdk.sms.endpoint import SMS +from common.exceptions import JMSException +from common.utils.random import random_string +from common.utils import get_logger + + +logger = get_logger(__file__) + + +@shared_task +def send_async(sender): + sender.gen_and_send() + + +class SendAndVerifyCodeUtil(object): + KEY_TMPL = 'auth-verify-code-{}' + + def __init__(self, target, code=None, key=None, backend='email', timeout=60, **kwargs): + self.target = target + self.code = code + self.timeout = timeout + self.backend = backend + self.key = key or self.KEY_TMPL.format(target) + self.other_args = kwargs + + def gen_and_send_async(self): + return send_async.delay(self) + + def gen_and_send(self): + ttl = self.__ttl() + if ttl > 0: + logger.error('Send sms too frequently, delay {}'.format(ttl)) + raise CodeSendTooFrequently(ttl) + + try: + if not self.code: + self.code = self.__generate() + self.__send(self.code) + except JMSException: + self.__clear() + raise + + def verify(self, code): + right = cache.get(self.key) + if not right: + raise CodeExpired + + if right != code: + raise CodeError + + self.__clear() + return True + + def __clear(self): + cache.delete(self.key) + + def __ttl(self): + return cache.ttl(self.key) + + def __get_code(self): + return cache.get(self.key) + + def __generate(self): + code = random_string(4, lower=False, upper=False) + self.code = code + return code + + def __send_with_sms(self): + sms = SMS() + sms.send_verify_code(self.target, self.code) + + def __send_with_email(self): + subject = self.other_args.get('subject') + message = self.other_args.get('message') + from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER + send_mail(subject, message, from_email, [self.target], html_message=message) + + def __send(self, code): + """ + 发送信息的方法,如果有错误直接抛出 api 异常 + """ + if self.backend == 'sms': + self.__send_with_sms() + else: + self.__send_with_email() + + cache.set(self.key, self.code, self.timeout) + logger.info(f'Send verify code to {self.target}: {code}') diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 6b229d6fe..3a3c8395b 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-28 09:17+0800\n" +"POT-Creation-Date: 2022-11-04 11:37+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -157,11 +157,13 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること #: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 #: assets/models/gathered_user.py:15 audits/models.py:121 #: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 +#: authentication/models.py:260 authentication/serializers/password_mfa.py:25 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:667 -#: users/templates/users/_msg_user_created.html:12 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:102 +#: users/models/user.py:667 users/templates/users/_msg_user_created.html:12 +#: users/templates/users/forgot_password.html:51 +#: users/templates/users/forgot_password.html:98 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 #: xpack/plugins/cloud/serializers/account_attrs.py:26 @@ -837,7 +839,10 @@ msgstr "帯域幅" msgid "Contact" msgstr "連絡先" -#: assets/models/cluster.py:22 users/models/user.py:689 +#: assets/models/cluster.py:22 authentication/serializers/password_mfa.py:29 +#: users/forms/profile.py:104 users/models/user.py:689 +#: users/templates/users/forgot_password.html:59 +#: users/templates/users/forgot_password.html:103 msgid "Phone" msgstr "電話" @@ -1824,6 +1829,30 @@ msgstr "この操作には、MFAを検証する必要があります" msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" +#: authentication/api/password.py:29 terminal/api/session.py:224 +#: users/views/profile/reset.py:74 +msgid "User does not exist: {}" +msgstr "ユーザーが存在しない: {}" + +#: authentication/api/password.py:29 +msgid "No user matched" +msgstr "ユーザーにマッチしなかった" + +#: authentication/api/password.py:33 +msgid "" +"The user is from {}, please go to the corresponding system to change the " +"password" +msgstr "" +"ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" +"さい。" + +#: authentication/api/password.py:59 +#: authentication/templates/authentication/login.html:256 +#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:34 +msgid "Forgot password" +msgstr "パスワードを忘れた" + #: authentication/apps.py:7 msgid "Authentication" msgstr "認証" @@ -2246,6 +2275,20 @@ msgstr "期限切れ時間" msgid "Asset or application required" msgstr "アセットまたはアプリが必要" +#: authentication/serializers/password_mfa.py:25 +#: authentication/serializers/password_mfa.py:29 +#: authentication/serializers/password_mfa.py:33 +#: users/templates/users/forgot_password.html:95 +msgid "The {} cannot be empty" +msgstr "{} 空にしてはならない" + +#: authentication/serializers/password_mfa.py:33 +#: notifications/backends/__init__.py:10 users/forms/profile.py:103 +#: users/models/user.py:671 users/templates/users/forgot_password.html:55 +#: users/templates/users/forgot_password.html:108 +msgid "Email" +msgstr "メール" + #: authentication/serializers/token.py:79 #: perms/serializers/application/permission.py:20 #: perms/serializers/application/permission.py:41 @@ -2309,7 +2352,6 @@ msgid "Play CAPTCHA as audio file" msgstr "CAPTCHAをオーディオファイルとして再生する" #: authentication/templates/authentication/_captcha_field.html:15 -#: users/forms/profile.py:103 msgid "Captcha" msgstr "キャプチャ" @@ -2335,6 +2377,7 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_different_city.html:3 #: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3 +#: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 #: jumpserver/conf.py:402 ops/tasks.py:145 ops/tasks.py:148 @@ -2394,6 +2437,21 @@ msgstr "このリンクは1時間有効です。有効期限が切れた後" msgid "request new one" msgstr "新しいものを要求する" +#: authentication/templates/authentication/_msg_reset_password_code.html:12 +#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 +#: users/forms/profile.py:105 users/templates/users/forgot_password.html:63 +msgid "Verify code" +msgstr "コードの確認" + +#: authentication/templates/authentication/_msg_reset_password_code.html:15 +msgid "" +"Copy the verification code to the Reset Password page to reset the password." +msgstr "パスワードリセットページにcaptchaをコピーし、パスワードをリセットする" + +#: authentication/templates/authentication/_msg_reset_password_code.html:18 +msgid "The validity period of the verification code is one minute" +msgstr "認証コードの有効時間は 1 分" + #: authentication/templates/authentication/_msg_rest_password_success.html:5 msgid "Your password has just been successfully updated" msgstr "パスワードが正常に更新されました" @@ -2438,12 +2496,6 @@ msgid "Welcome back, please enter username and password to login" msgstr "" "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" -#: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:16 -#: users/templates/users/forgot_password.html:17 -msgid "Forgot password" -msgstr "パスワードを忘れた" - #: authentication/templates/authentication/login.html:264 #: templates/_header_bar.html:89 msgid "Login" @@ -2776,15 +2828,15 @@ msgstr "SMSプロバイダーはサポートしていません: {}" msgid "SMS verification code signature or template invalid" msgstr "SMS検証コードの署名またはテンプレートが無効" -#: common/sdk/sms/utils.py:15 +#: common/sdk/sms/exceptions.py:8 msgid "The verification code has expired. Please resend it" msgstr "確認コードの有効期限が切れています。再送信してください" -#: common/sdk/sms/utils.py:20 +#: common/sdk/sms/exceptions.py:13 msgid "The verification code is incorrect" msgstr "確認コードが正しくありません" -#: common/sdk/sms/utils.py:25 +#: common/sdk/sms/exceptions.py:18 msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" @@ -2852,11 +2904,6 @@ msgstr "" msgid "Notifications" msgstr "通知" -#: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:671 -msgid "Email" -msgstr "メール" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "サイトメッセージ" @@ -4750,14 +4797,17 @@ msgstr "" " " #: templates/_mfa_login_field.html:28 +#: users/templates/users/forgot_password.html:65 msgid "Send verification code" msgstr "確認コードを送信" #: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:122 msgid "Wait: " msgstr "待つ:" #: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:138 msgid "The verification code has been sent" msgstr "確認コードが送信されました" @@ -4822,10 +4872,6 @@ msgstr "セッションが存在しません: {}" msgid "Session is finished or the protocol not supported" msgstr "セッションが終了したか、プロトコルがサポートされていません" -#: terminal/api/session.py:224 -msgid "User does not exist: {}" -msgstr "ユーザーが存在しない: {}" - #: terminal/api/session.py:232 msgid "User does not have permission" msgstr "ユーザーに権限がありません" @@ -5030,10 +5076,6 @@ msgstr "セッションアクションのパーマを検証できます" msgid "Creator" msgstr "作成者" -#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 -msgid "Verify code" -msgstr "コードの確認" - #: terminal/models/sharing.py:31 msgid "Expired time (min)" msgstr "期限切れ時間 (分)" @@ -5320,7 +5362,7 @@ msgstr "" "使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の" "制限を確認して変更してください. " -#: terminal/utils/db_port_mapper.py:92 +#: terminal/utils/db_port_mapper.py:83 msgid "All available port count: {}, Already use port count: {}" msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" @@ -5764,40 +5806,40 @@ msgstr "パスワードの確認" msgid "Password does not match" msgstr "パスワードが一致しない" -#: users/forms/profile.py:109 +#: users/forms/profile.py:112 msgid "Old password" msgstr "古いパスワード" -#: users/forms/profile.py:119 +#: users/forms/profile.py:122 msgid "Old password error" msgstr "古いパスワードエラー" -#: users/forms/profile.py:129 +#: users/forms/profile.py:132 msgid "Automatically configure and download the SSH key" msgstr "SSHキーの自動設定とダウンロード" -#: users/forms/profile.py:131 +#: users/forms/profile.py:134 msgid "ssh public key" msgstr "ssh公開キー" -#: users/forms/profile.py:132 +#: users/forms/profile.py:135 msgid "ssh-rsa AAAA..." msgstr "ssh-rsa AAAA.." -#: users/forms/profile.py:133 +#: users/forms/profile.py:136 msgid "Paste your id_rsa.pub here." msgstr "ここにid_rsa.pubを貼り付けます。" -#: users/forms/profile.py:146 +#: users/forms/profile.py:149 msgid "Public key should not be the same as your old one." msgstr "公開鍵は古いものと同じであってはなりません。" -#: users/forms/profile.py:150 users/serializers/profile.py:100 +#: users/forms/profile.py:153 users/serializers/profile.py:100 #: users/serializers/profile.py:183 users/serializers/profile.py:210 msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:161 users/models/user.py:700 +#: users/forms/profile.py:164 users/models/user.py:700 msgid "Public key" msgstr "公開キー" @@ -5868,7 +5910,7 @@ msgstr "ユーザーパスワード履歴" msgid "Reset password" msgstr "パスワードのリセット" -#: users/notifications.py:85 users/views/profile/reset.py:127 +#: users/notifications.py:85 users/views/profile/reset.py:139 msgid "Reset password success" msgstr "パスワードのリセット成功" @@ -6064,14 +6106,22 @@ msgstr "あなたのssh公開鍵はサイト管理者によってリセットさ msgid "click here to set your password" msgstr "ここをクリックしてパスワードを設定してください" -#: users/templates/users/forgot_password.html:24 +#: users/templates/users/forgot_password.html:38 msgid "Input your email, that will send a mail to your" msgstr "あなたのメールを入力し、それはあなたにメールを送信します" -#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:68 msgid "Submit" msgstr "送信" +#: users/templates/users/forgot_password.html:71 +msgid "Use the phone number to retrieve the password" +msgstr "携帯電話番号を使ってパスワードを探す" + +#: users/templates/users/forgot_password.html:72 +msgid "Use email to retrieve the password" +msgstr "メールアドレスを使ってパスワードを取り戻す" + #: users/templates/users/mfa_setting.html:24 msgid "Enable MFA" msgstr "MFAの有効化" @@ -6218,45 +6268,23 @@ msgstr "OTP無効化成功、ログインページを返す" msgid "Password invalid" msgstr "パスワード無効" -#: users/views/profile/reset.py:40 -msgid "Send reset password message" -msgstr "リセットパスワードメッセージを送信" - -#: users/views/profile/reset.py:41 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"リセットパスワードメールの成功を送信し、メールボックスにログインしてそれに従" -"う" - -#: users/views/profile/reset.py:52 -msgid "Email address invalid, please input again" -msgstr "メールアドレスが無効です。再度入力してください" - -#: users/views/profile/reset.py:58 -msgid "" -"The user is from {}, please go to the corresponding system to change the " -"password" -msgstr "" -"ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" -"さい。" - -#: users/views/profile/reset.py:84 users/views/profile/reset.py:95 +#: users/views/profile/reset.py:96 users/views/profile/reset.py:107 msgid "Token invalid or expired" msgstr "トークンが無効または期限切れ" -#: users/views/profile/reset.py:100 +#: users/views/profile/reset.py:112 msgid "User auth from {}, go there change password" msgstr "ユーザー認証ソース {}, 対応するシステムにパスワードを変更してください" -#: users/views/profile/reset.py:107 +#: users/views/profile/reset.py:119 msgid "* Your password does not meet the requirements" msgstr "* パスワードが要件を満たしていない" -#: users/views/profile/reset.py:113 +#: users/views/profile/reset.py:125 msgid "* The new password cannot be the last {} passwords" msgstr "* 新しいパスワードを最後の {} パスワードにすることはできません" -#: users/views/profile/reset.py:128 +#: users/views/profile/reset.py:140 msgid "Reset password success, return to login page" msgstr "パスワードの成功をリセットし、ログインページに戻る" diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 3eb7ddf92..a9a108f37 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-10-28 09:17+0800\n" +"POT-Creation-Date: 2022-11-04 11:37+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -156,11 +156,13 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 #: assets/models/gathered_user.py:15 audits/models.py:121 #: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 +#: authentication/models.py:260 authentication/serializers/password_mfa.py:25 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:667 -#: users/templates/users/_msg_user_created.html:12 +#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:102 +#: users/models/user.py:667 users/templates/users/_msg_user_created.html:12 +#: users/templates/users/forgot_password.html:51 +#: users/templates/users/forgot_password.html:98 #: xpack/plugins/change_auth_plan/models/asset.py:34 #: xpack/plugins/change_auth_plan/models/asset.py:195 #: xpack/plugins/cloud/serializers/account_attrs.py:26 @@ -832,7 +834,10 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:689 +#: assets/models/cluster.py:22 authentication/serializers/password_mfa.py:29 +#: users/forms/profile.py:104 users/models/user.py:689 +#: users/templates/users/forgot_password.html:59 +#: users/templates/users/forgot_password.html:103 msgid "Phone" msgstr "手机" @@ -1812,6 +1817,28 @@ msgstr "此操作需要验证您的 MFA" msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" +#: authentication/api/password.py:29 terminal/api/session.py:224 +#: users/views/profile/reset.py:74 +msgid "User does not exist: {}" +msgstr "用户不存在: {}" + +#: authentication/api/password.py:29 +msgid "No user matched" +msgstr "没有匹配到用户" + +#: authentication/api/password.py:33 +msgid "" +"The user is from {}, please go to the corresponding system to change the " +"password" +msgstr "用户来自 {} 请去相应系统修改密码" + +#: authentication/api/password.py:59 +#: authentication/templates/authentication/login.html:256 +#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:34 +msgid "Forgot password" +msgstr "忘记密码" + #: authentication/apps.py:7 msgid "Authentication" msgstr "认证" @@ -2221,6 +2248,20 @@ msgstr "过期时间" msgid "Asset or application required" msgstr "资产或应用必填" +#: authentication/serializers/password_mfa.py:25 +#: authentication/serializers/password_mfa.py:29 +#: authentication/serializers/password_mfa.py:33 +#: users/templates/users/forgot_password.html:95 +msgid "The {} cannot be empty" +msgstr "{} 不能为空" + +#: authentication/serializers/password_mfa.py:33 +#: notifications/backends/__init__.py:10 users/forms/profile.py:103 +#: users/models/user.py:671 users/templates/users/forgot_password.html:55 +#: users/templates/users/forgot_password.html:108 +msgid "Email" +msgstr "邮件" + #: authentication/serializers/token.py:79 #: perms/serializers/application/permission.py:20 #: perms/serializers/application/permission.py:41 @@ -2284,7 +2325,6 @@ msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" #: authentication/templates/authentication/_captcha_field.html:15 -#: users/forms/profile.py:103 msgid "Captcha" msgstr "验证码" @@ -2310,6 +2350,7 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_different_city.html:3 #: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3 +#: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 #: jumpserver/conf.py:402 ops/tasks.py:145 ops/tasks.py:148 @@ -2365,6 +2406,21 @@ msgstr "这个链接有效期1小时, 超过时间您可以" msgid "request new one" msgstr "重新申请" +#: authentication/templates/authentication/_msg_reset_password_code.html:12 +#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 +#: users/forms/profile.py:105 users/templates/users/forgot_password.html:63 +msgid "Verify code" +msgstr "验证码" + +#: authentication/templates/authentication/_msg_reset_password_code.html:15 +msgid "" +"Copy the verification code to the Reset Password page to reset the password." +msgstr "将验证码复制到重置密码页面,重置密码。" + +#: authentication/templates/authentication/_msg_reset_password_code.html:18 +msgid "The validity period of the verification code is one minute" +msgstr "验证码有效期为 1 分钟" + #: authentication/templates/authentication/_msg_rest_password_success.html:5 msgid "Your password has just been successfully updated" msgstr "你的密码刚刚成功更新" @@ -2404,12 +2460,6 @@ msgstr "取消" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:256 -#: users/templates/users/forgot_password.html:16 -#: users/templates/users/forgot_password.html:17 -msgid "Forgot password" -msgstr "忘记密码" - #: authentication/templates/authentication/login.html:264 #: templates/_header_bar.html:89 msgid "Login" @@ -2742,15 +2792,15 @@ msgstr "短信服务商不支持:{}" msgid "SMS verification code signature or template invalid" msgstr "短信验证码签名或模版无效" -#: common/sdk/sms/utils.py:15 +#: common/sdk/sms/exceptions.py:8 msgid "The verification code has expired. Please resend it" msgstr "验证码已过期,请重新发送" -#: common/sdk/sms/utils.py:20 +#: common/sdk/sms/exceptions.py:13 msgid "The verification code is incorrect" msgstr "验证码错误" -#: common/sdk/sms/utils.py:25 +#: common/sdk/sms/exceptions.py:18 msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" @@ -2813,11 +2863,6 @@ msgstr "" msgid "Notifications" msgstr "通知" -#: notifications/backends/__init__.py:10 users/forms/profile.py:102 -#: users/models/user.py:671 -msgid "Email" -msgstr "邮件" - #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "站内信" @@ -4678,14 +4723,17 @@ msgstr "" " " #: templates/_mfa_login_field.html:28 +#: users/templates/users/forgot_password.html:65 msgid "Send verification code" msgstr "发送验证码" #: templates/_mfa_login_field.html:106 +#: users/templates/users/forgot_password.html:122 msgid "Wait: " msgstr "等待:" #: templates/_mfa_login_field.html:116 +#: users/templates/users/forgot_password.html:138 msgid "The verification code has been sent" msgstr "验证码已发送" @@ -4745,10 +4793,6 @@ msgstr "会话不存在: {}" msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:224 -msgid "User does not exist: {}" -msgstr "用户不存在: {}" - #: terminal/api/session.py:232 msgid "User does not have permission" msgstr "用户没有权限" @@ -4953,10 +4997,6 @@ msgstr "可以验证会话动作权限" msgid "Creator" msgstr "创建者" -#: terminal/models/sharing.py:26 terminal/models/sharing.py:80 -msgid "Verify code" -msgstr "验证码" - #: terminal/models/sharing.py:31 msgid "Expired time (min)" msgstr "过期时间 (分)" @@ -5678,40 +5718,40 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:109 +#: users/forms/profile.py:112 msgid "Old password" msgstr "原来密码" -#: users/forms/profile.py:119 +#: users/forms/profile.py:122 msgid "Old password error" msgstr "原来密码错误" -#: users/forms/profile.py:129 +#: users/forms/profile.py:132 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms/profile.py:131 +#: users/forms/profile.py:134 msgid "ssh public key" msgstr "SSH公钥" -#: users/forms/profile.py:132 +#: users/forms/profile.py:135 msgid "ssh-rsa AAAA..." msgstr "ssh-rsa AAAA..." -#: users/forms/profile.py:133 +#: users/forms/profile.py:136 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms/profile.py:146 +#: users/forms/profile.py:149 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms/profile.py:150 users/serializers/profile.py:100 +#: users/forms/profile.py:153 users/serializers/profile.py:100 #: users/serializers/profile.py:183 users/serializers/profile.py:210 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:161 users/models/user.py:700 +#: users/forms/profile.py:164 users/models/user.py:700 msgid "Public key" msgstr "SSH公钥" @@ -5782,7 +5822,7 @@ msgstr "用户密码历史" msgid "Reset password" msgstr "重置密码" -#: users/notifications.py:85 users/views/profile/reset.py:127 +#: users/notifications.py:85 users/views/profile/reset.py:139 msgid "Reset password success" msgstr "重置密码成功" @@ -5976,14 +6016,22 @@ msgstr "你的 SSH 密钥已经被管理员重置" msgid "click here to set your password" msgstr "点击这里设置密码" -#: users/templates/users/forgot_password.html:24 +#: users/templates/users/forgot_password.html:38 msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/forgot_password.html:33 +#: users/templates/users/forgot_password.html:68 msgid "Submit" msgstr "提交" +#: users/templates/users/forgot_password.html:71 +msgid "Use the phone number to retrieve the password" +msgstr "使用手机号找回密码" + +#: users/templates/users/forgot_password.html:72 +msgid "Use email to retrieve the password" +msgstr "使用邮箱找回密码" + #: users/templates/users/mfa_setting.html:24 msgid "Enable MFA" msgstr "启用 MFA 多因子认证" @@ -6122,42 +6170,23 @@ msgstr "MFA(OTP) 禁用成功,返回登录页面" msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/profile/reset.py:40 -msgid "Send reset password message" -msgstr "发送重置密码邮件" - -#: users/views/profile/reset.py:41 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" - -#: users/views/profile/reset.py:52 -msgid "Email address invalid, please input again" -msgstr "邮箱地址错误,重新输入" - -#: users/views/profile/reset.py:58 -msgid "" -"The user is from {}, please go to the corresponding system to change the " -"password" -msgstr "用户来自 {} 请去相应系统修改密码" - -#: users/views/profile/reset.py:84 users/views/profile/reset.py:95 +#: users/views/profile/reset.py:96 users/views/profile/reset.py:107 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/profile/reset.py:100 +#: users/views/profile/reset.py:112 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/views/profile/reset.py:107 +#: users/views/profile/reset.py:119 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/views/profile/reset.py:113 +#: users/views/profile/reset.py:125 msgid "* The new password cannot be the last {} passwords" msgstr "* 新密码不能是最近 {} 次的密码" -#: users/views/profile/reset.py:128 +#: users/views/profile/reset.py:140 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" diff --git a/apps/notifications/backends/sms.py b/apps/notifications/backends/sms.py index 860db437d..18836f421 100644 --- a/apps/notifications/backends/sms.py +++ b/apps/notifications/backends/sms.py @@ -1,6 +1,4 @@ -from django.conf import settings - -from common.sdk.sms.alibaba import AlibabaSMS as Client +from common.sdk.sms.endpoint import SMS from .base import BackendBase @@ -9,13 +7,7 @@ class SMS(BackendBase): is_enable_field_in_settings = 'SMS_ENABLED' def __init__(self): - """ - 暂时只对接阿里,之后再扩展 - """ - self.client = Client( - access_key_id=settings.ALIBABA_ACCESS_KEY_ID, - access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET - ) + self.client = SMS() def send_msg(self, users, sign_name: str, template_code: str, template_param: dict): accounts, __, __ = self.get_accounts(users) diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py index 4917f8353..ca9db9e0c 100644 --- a/apps/users/forms/profile.py +++ b/apps/users/forms/profile.py @@ -99,8 +99,11 @@ class UserTokenResetPasswordForm(forms.Form): class UserForgotPasswordForm(forms.Form): - email = forms.EmailField(label=_("Email")) - captcha = CaptchaField(label=_("Captcha")) + username = forms.CharField(label=_("Username")) + email = forms.CharField(label=_("Email"), required=False) + phone = forms.CharField(label=_('Phone'), required=False, max_length=11) + code = forms.CharField(label=_('Verify code'), max_length=6, required=False) + form_type = forms.CharField(widget=forms.HiddenInput({'value': 'email'})) class UserPasswordForm(UserTokenResetPasswordForm): diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index 16c784250..a5770fdcc 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -4,12 +4,29 @@ {% load bootstrap3 %} {% block custom_head_css_js %} {% endblock %} @@ -17,22 +34,111 @@ {% block title %} {% trans 'Forgot password' %}?{% endblock %} {% block content %} - {% if errors %} -

{{ errors }}

- {% endif %} -

+

{% trans 'Input your email, that will send a mail to your' %}

+

+ Enter your phone number and a verification code will be sent to your phone +

{% csrf_token %} - {% bootstrap_field form.email layout="horizontal" %} - {% bootstrap_field form.captcha layout="horizontal" %} - + {% bootstrap_field form.form_type layout="horizontal" %} +
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ +
+ {% if XPACK_ENABLED %} + {% trans 'Use the phone number to retrieve the password' %} + {% trans 'Use email to retrieve the password' %} + {% endif %}
+ {% endblock %} diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index cc14f6d53..bef98789a 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -4,15 +4,13 @@ from __future__ import unicode_literals from django.views.generic import RedirectView from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ -from django.views.generic.base import TemplateView from django.conf import settings from django.urls import reverse_lazy from django.views.generic import FormView -from users.notifications import ResetPasswordSuccessMsg, ResetPasswordMsg +from users.notifications import ResetPasswordSuccessMsg from common.utils import get_object_or_none, FlashMessageUtil -from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.utils.verify_code import SendAndVerifyCodeUtil from ...models import User from ...utils import ( get_password_check_rules, check_password_rules, @@ -34,35 +32,49 @@ class UserForgotPasswordView(FormView): template_name = 'users/forgot_password.html' form_class = forms.UserForgotPasswordForm + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = context['form'] + if getattr(form, 'errors', None): + e = getattr(form, 'errors') + context['errors'] = e + context['form_type'] = 'email' + context['XPACK_ENABLED'] = settings.XPACK_ENABLED + cleaned_data = getattr(form, 'cleaned_data', {}) + for k, v in cleaned_data.items(): + if v: + context[k] = v + return context + @staticmethod - def get_redirect_message_url(): - message_data = { - 'title': _('Send reset password message'), - 'message': _('Send reset password mail success, ' - 'login your mail box and follow it '), - 'redirect_url': reverse('authentication:login'), - } - url = FlashMessageUtil.gen_message_url(message_data) - return url + def get_redirect_url(user): + query_params = '?token=%s' % user.generate_reset_token() + reset_password_url = reverse('authentication:reset-password') + return reset_password_url + query_params def form_valid(self, form): - email = form.cleaned_data['email'] - user = get_object_or_none(User, email=email) + form_type = form.cleaned_data['form_type'] + code = form.cleaned_data['code'] + username = form.cleaned_data['username'] + if settings.XPACK_ENABLED and form_type == 'phone': + backend = 'sms' + target = form.cleaned_data['phone'] + else: + backend = 'email' + target = form.cleaned_data['email'] + try: + sender_util = SendAndVerifyCodeUtil(target, backend=backend) + sender_util.verify(code) + except Exception as e: + form.add_error('code', str(e)) + return super().form_invalid(form) + + user = get_object_or_none(User, **{'username': username, form_type: target}) if not user: - error = _('Email address invalid, please input again') - form.add_error('email', error) - return self.form_invalid(form) + form.add_error('username', _('User does not exist: {}').format(username)) + return super().form_invalid(form) - if not user.is_local: - error = _( - 'The user is from {}, please go to the corresponding system to change the password' - ).format(user.get_source_display()) - form.add_error('email', error) - return self.form_invalid(form) - - ResetPasswordMsg(user).publish_async() - url = self.get_redirect_message_url() - return redirect(url) + return redirect(self.get_redirect_url(user)) class UserResetPasswordView(FormView):