@@ -25,7 +28,9 @@
{% for message in messages %}
{{ message|safe }}
+
+
{% endfor %}
{% endif %}
diff --git a/apps/users/api.py b/apps/users/api.py
index 1f7e4f792..dbc5b66a8 100644
--- a/apps/users/api.py
+++ b/apps/users/api.py
@@ -153,7 +153,7 @@ class UserOtpAuthApi(APIView):
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
if not check_otp_code(user.otp_secret_key, otp_code):
- return Response({'msg': 'otp认证失败'}, status=401)
+ return Response({'msg': 'MFA认证失败'}, status=401)
token = generate_token(request, user)
self.write_login_log(request, user)
@@ -204,7 +204,7 @@ class UserAuthApi(APIView):
return Response(
{
'code': 101,
- 'msg': '请携带seed值,进行OTP二次认证',
+ 'msg': '请携带seed值,进行MFA二次认证',
'otp_url': reverse('api-users:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
diff --git a/apps/users/forms.py b/apps/users/forms.py
index 5af28756b..f777e0dd7 100644
--- a/apps/users/forms.py
+++ b/apps/users/forms.py
@@ -15,6 +15,14 @@ class UserLoginForm(AuthenticationForm):
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
+
+
+class UserLoginCaptchaForm(AuthenticationForm):
+ username = forms.CharField(label=_('Username'), max_length=100)
+ password = forms.CharField(
+ label=_('Password'), widget=forms.PasswordInput,
+ max_length=128, strip=False
+ )
captcha = CaptchaField()
@@ -27,7 +35,7 @@ class UserCheckPasswordForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form):
- otp_code = forms.CharField(label=_('MFA_code'), max_length=6)
+ otp_code = forms.CharField(label=_('MFA code'), max_length=6)
class UserCreateUpdateForm(forms.ModelForm):
@@ -36,7 +44,10 @@ class UserCreateUpdateForm(forms.ModelForm):
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False, required=False,
)
- role = forms.ChoiceField(choices=role_choices, required=True, initial=User.ROLE_USER, label=_("Role"))
+ role = forms.ChoiceField(
+ choices=role_choices, required=True,
+ initial=User.ROLE_USER, label=_("Role")
+ )
public_key = forms.CharField(
label=_('ssh public key'), max_length=5000, required=False,
widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
@@ -47,7 +58,7 @@ class UserCreateUpdateForm(forms.ModelForm):
model = User
fields = [
'username', 'name', 'email', 'groups', 'wechat',
- 'phone', 'role', 'date_expired', 'comment',
+ 'phone', 'role', 'date_expired', 'comment', 'otp_level'
]
help_texts = {
'username': '* required',
@@ -61,6 +72,7 @@ class UserCreateUpdateForm(forms.ModelForm):
'data-placeholder': _('Join user groups')
}
),
+ 'otp_level': forms.RadioSelect()
}
def clean_public_key(self):
@@ -77,11 +89,15 @@ class UserCreateUpdateForm(forms.ModelForm):
def save(self, commit=True):
password = self.cleaned_data.get('password')
+ otp_level = self.cleaned_data.get('otp_level')
public_key = self.cleaned_data.get('public_key')
user = super().save(commit=commit)
if password:
user.set_password(password)
user.save()
+ if otp_level:
+ user.otp_level = otp_level
+ user.save()
if public_key:
user.public_key = public_key
user.save()
@@ -105,6 +121,39 @@ class UserProfileForm(forms.ModelForm):
UserProfileForm.verbose_name = _("Profile")
+class UserMFAForm(forms.ModelForm):
+
+ mfa_description = _(
+ 'Tip: when enabled, '
+ 'you will enter the MFA binding process the next time you log in. '
+ 'you can also directly bind in '
+ '"personal information -> quick modification -> change MFA Settings"!')
+
+ class Meta:
+ model = User
+ fields = ['otp_level']
+ widgets = {'otp_level': forms.RadioSelect()}
+ help_texts = {
+ 'otp_level': _('* Enable MFA authentication '
+ 'to make the account more secure.'),
+ }
+
+
+UserMFAForm.verbose_name = _("MFA")
+
+
+class UserFirstLoginFinishForm(forms.Form):
+ finish_description = _(
+ 'In order to protect you and your company, '
+ 'please keep your account, '
+ 'password and key sensitive information properly. '
+ '(for example: setting complex password, enabling MFA authentication)'
+ )
+
+
+UserFirstLoginFinishForm.verbose_name = _("Finish")
+
+
class UserPasswordForm(forms.Form):
old_password = forms.CharField(
max_length=128, widget=forms.PasswordInput,
@@ -147,6 +196,7 @@ class UserPasswordForm(forms.Form):
class UserPublicKeyForm(forms.Form):
+ pubkey_description = _('Automatically configure and download the SSH key')
public_key = forms.CharField(
label=_('ssh public key'), max_length=5000, required=False,
widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index d64c038d8..23c30cf77 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -36,23 +36,52 @@ class User(AbstractUser):
(2, _("Force enable")),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
- username = models.CharField(max_length=128, unique=True, verbose_name=_('Username'))
+ username = models.CharField(
+ max_length=128, unique=True, verbose_name=_('Username')
+ )
name = models.CharField(max_length=128, verbose_name=_('Name'))
- email = models.EmailField(max_length=128, unique=True, verbose_name=_('Email'))
- groups = models.ManyToManyField('users.UserGroup', related_name='users', blank=True, verbose_name=_('User group'))
- role = models.CharField(choices=ROLE_CHOICES, default='User', max_length=10, blank=True, verbose_name=_('Role'))
- avatar = models.ImageField(upload_to="avatar", null=True, verbose_name=_('Avatar'))
- wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat'))
- phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_('Phone'))
- otp_level = models.SmallIntegerField(default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('Enable OTP'))
+ email = models.EmailField(
+ max_length=128, unique=True, verbose_name=_('Email')
+ )
+ groups = models.ManyToManyField(
+ 'users.UserGroup', related_name='users',
+ blank=True, verbose_name=_('User group')
+ )
+ role = models.CharField(
+ choices=ROLE_CHOICES, default='User', max_length=10,
+ blank=True, verbose_name=_('Role')
+ )
+ avatar = models.ImageField(
+ upload_to="avatar", null=True, verbose_name=_('Avatar')
+ )
+ wechat = models.CharField(
+ max_length=128, blank=True, verbose_name=_('Wechat')
+ )
+ phone = models.CharField(
+ max_length=20, blank=True, null=True, verbose_name=_('Phone')
+ )
+ otp_level = models.SmallIntegerField(
+ default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('MFA')
+ )
_otp_secret_key = models.CharField(max_length=128, blank=True, null=True)
# Todo: Auto generate key, let user download
- _private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Private key'))
- _public_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Public key'))
- comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment'))
+ _private_key = models.CharField(
+ max_length=5000, blank=True, verbose_name=_('Private key')
+ )
+ _public_key = models.CharField(
+ max_length=5000, blank=True, verbose_name=_('Public key')
+ )
+ comment = models.TextField(
+ max_length=200, blank=True, verbose_name=_('Comment')
+ )
is_first_login = models.BooleanField(default=True)
- date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True, verbose_name=_('Date expired'))
- created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by'))
+ date_expired = models.DateTimeField(
+ default=date_expired_default, blank=True, null=True,
+ verbose_name=_('Date expired')
+ )
+ created_by = models.CharField(
+ max_length=30, default='', verbose_name=_('Created by')
+ )
def __str__(self):
return '{0.name}({0.username})'.format(self)
@@ -213,7 +242,9 @@ class User(AbstractUser):
return user_default
def generate_reset_token(self):
- return signer.sign_t({'reset': str(self.id), 'email': self.email}, expires_in=3600)
+ return signer.sign_t(
+ {'reset': str(self.id), 'email': self.email}, expires_in=3600
+ )
@property
def otp_enabled(self):
diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html
index a6eca32d9..755f5d5c9 100644
--- a/apps/users/templates/users/_base_otp.html
+++ b/apps/users/templates/users/_base_otp.html
@@ -74,13 +74,11 @@
-
+