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
+
+
{% 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):