mirror of https://github.com/jumpserver/jumpserver
#8 user first login view
commit
899233338d
|
@ -1,6 +1,7 @@
|
|||
.DS_Store
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
env
|
||||
env*
|
||||
dist
|
||||
|
|
|
@ -108,6 +108,7 @@ TEMPLATES = [
|
|||
# WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||
LOGIN_URL = reverse_lazy('users:login')
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||
|
@ -227,7 +228,7 @@ USE_L10N = True
|
|||
USE_TZ = True
|
||||
|
||||
# I18N translation
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'),]
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'), ]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
|
|
@ -18,6 +18,7 @@ class UserLoginForm(AuthenticationForm):
|
|||
|
||||
|
||||
class UserCreateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
|
@ -67,3 +68,23 @@ class UserGroupForm(forms.ModelForm):
|
|||
help_texts = {
|
||||
'name': '* required'
|
||||
}
|
||||
|
||||
|
||||
class UserInfoForm(forms.Form):
|
||||
name = forms.CharField(max_length=20, label=_('name'))
|
||||
avatar = forms.ImageField(label=_('avatar'), required=False)
|
||||
wechat = forms.CharField(max_length=30, label=_('wechat'), required=False)
|
||||
phone = forms.CharField(max_length=20, label=_('phone'), required=False)
|
||||
enable_otp = forms.BooleanField(required=False, label=_('enable otp'))
|
||||
|
||||
|
||||
class UserKeyForm(forms.Form):
|
||||
private_key = forms.CharField(max_length=5000, widget=forms.Textarea, label=_('private key'))
|
||||
|
||||
def clean_private_key(self):
|
||||
from users.utils import validate_ssh_pk
|
||||
ssh_pk = self.cleaned_data['private_key']
|
||||
checked, reason = validate_ssh_pk(ssh_pk)
|
||||
if not checked:
|
||||
raise forms.ValidationError(_('Not a valid ssh private key.'))
|
||||
return ssh_pk
|
||||
|
|
|
@ -2,19 +2,17 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core import signing
|
||||
from django.db import models, IntegrityError
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.db import IntegrityError
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.core import signing
|
||||
|
||||
from common.utils import encrypt, decrypt
|
||||
|
||||
|
@ -44,16 +42,15 @@ class UserGroup(models.Model):
|
|||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from random import seed, randint, choice
|
||||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
group = cls(name=forgery_py.name.full_name(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by=choice(User.objects.all()).username
|
||||
)
|
||||
)
|
||||
try:
|
||||
group.save()
|
||||
except IntegrityError:
|
||||
|
@ -84,7 +81,7 @@ class User(AbstractUser):
|
|||
_private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key'))
|
||||
_public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key'))
|
||||
comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment'))
|
||||
is_first_login = models.BooleanField(default=False)
|
||||
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'))
|
||||
|
@ -235,7 +232,7 @@ class User(AbstractUser):
|
|||
wechat=forgery_py.internet.user_name(True),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by=choice(cls.objects.all()).username,
|
||||
)
|
||||
)
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
|
@ -264,4 +261,3 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
|
|||
Token.objects.create(user=instance)
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
{{ wizard.form.media }}
|
||||
<link href="{% static 'css/plugins/steps/jquery.steps.css' %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'First Login' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="wizard">
|
||||
<div class="steps clearfix">
|
||||
<ul role="tablist">
|
||||
{% for step in wizard.steps.all %}
|
||||
<li role="tab" class="{% ifequal step wizard.steps.first %}first{% endifequal %} {% ifequal step wizard.steps.current %}current{% else %}disabled{% endifequal %} {% ifequal step wizard.steps.last %}last{% endifequal %}"
|
||||
aria-disabled="false" aria-selected="true">
|
||||
<a href="javascript:void(0)"><span class="number">{% trans 'Step' %} {{ step }}</span></a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content clearfix">
|
||||
<form action="" method="post" class="form col-lg-8 p-m" id="fl_form">
|
||||
{% csrf_token %}
|
||||
{{ wizard.management_form }}
|
||||
{% if wizard.form.forms %}
|
||||
{{ wizard.form.management_form }}
|
||||
{% for form in wizard.form.forms %}
|
||||
{{ form|bootstrap }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ wizard.form|bootstrap }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions clearfix">
|
||||
<ul>
|
||||
{% if wizard.steps.prev %}
|
||||
<li><a class="fl_goto" data-goto="{{ wizard.steps.first }}">{% trans "first step" %}</a></li>
|
||||
<li><a class="fl_goto" name="wizard_goto_step" data-goto="{{ wizard.steps.prev }}">{% trans "prev step" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a id="fl_submit">{% trans "submit" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).on('click', ".fl_goto", function(){
|
||||
var $form = $('#fl_form');
|
||||
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||
$form.submit();
|
||||
return false;
|
||||
}).on('click', '#fl_submit', function(){
|
||||
$('#fl_form').submit();
|
||||
return false;
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -16,6 +16,8 @@ urlpatterns = [
|
|||
name='reset-password-success'),
|
||||
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
||||
url(r'^user/(?P<pk>[0-9]+)$', views.UserDetailView.as_view(), name='user-detail'),
|
||||
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||
url(r'^user/(?P<pk>[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'),
|
||||
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
|
||||
url(r'^user/(?P<pk>[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'),
|
||||
url(r'^user/(?P<pk>[0-9]+)/delete$', views.UserDeleteView.as_view(), name='user-delete'),
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from paramiko.rsakey import RSAKey
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from paramiko.rsakey import RSAKey
|
||||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse
|
||||
from users.models import User
|
||||
|
||||
|
||||
try:
|
||||
|
@ -125,5 +125,56 @@ def send_reset_password_mail(user):
|
|||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def validate_ssh_pk(text):
|
||||
"""
|
||||
Expects a SSH private key as string.
|
||||
Returns a boolean and a error message.
|
||||
If the text is parsed as private key successfully,
|
||||
(True,'') is returned. Otherwise,
|
||||
(False, <message describing the error>) is returned.
|
||||
|
||||
from https://github.com/githubnemo/SSH-private-key-validator/blob/master/validate.py
|
||||
|
||||
"""
|
||||
|
||||
if not text:
|
||||
return False, 'No text given'
|
||||
|
||||
startPattern = re.compile("^-----BEGIN [A-Z]+ PRIVATE KEY-----")
|
||||
optionPattern = re.compile("^.+: .+")
|
||||
contentPattern = re.compile("^([a-zA-Z0-9+/]{64}|[a-zA-Z0-9+/]{1,64}[=]{0,2})$")
|
||||
endPattern = re.compile("^-----END [A-Z]+ PRIVATE KEY-----")
|
||||
|
||||
def contentState(text):
|
||||
for i in range(0, len(text)):
|
||||
line = text[i]
|
||||
|
||||
if endPattern.match(line):
|
||||
if i == len(text) - 1 or len(text[i + 1]) == 0:
|
||||
return True, ''
|
||||
else:
|
||||
return False, 'At end but content coming'
|
||||
|
||||
elif not contentPattern.match(line):
|
||||
return False, 'Wrong string in content section'
|
||||
|
||||
return False, 'No content or missing end line'
|
||||
|
||||
def optionState(text):
|
||||
for i in range(0, len(text)):
|
||||
line = text[i]
|
||||
|
||||
if line[-1:] == '\\':
|
||||
return optionState(text[i + 2:])
|
||||
|
||||
if not optionPattern.match(line):
|
||||
return contentState(text[i + 1:])
|
||||
|
||||
return False, 'Expected option, found nothing'
|
||||
|
||||
def startState(text):
|
||||
if len(text) == 0 or not startPattern.match(text[0]):
|
||||
return False, 'Header is wrong'
|
||||
return optionState(text[1:])
|
||||
|
||||
return startState([n.strip() for n in text.splitlines()])
|
||||
|
|
|
@ -6,7 +6,9 @@ import logging
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, reverse, redirect
|
||||
|
@ -21,10 +23,12 @@ from django.views.generic.list import ListView
|
|||
from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
|
||||
from .models import User, UserGroup
|
||||
from .forms import UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm
|
||||
from .forms import (UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm, UserInfoForm, UserKeyForm)
|
||||
from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail
|
||||
|
||||
|
||||
|
@ -49,6 +53,9 @@ class UserLoginView(FormView):
|
|||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
if self.request.user.is_first_login:
|
||||
return reverse('users:user-first-login')
|
||||
|
||||
return self.request.POST.get(
|
||||
self.redirect_field_name,
|
||||
self.request.GET.get(self.redirect_field_name, reverse('index')))
|
||||
|
@ -292,3 +299,42 @@ class UserResetPasswordView(TemplateView):
|
|||
|
||||
user.reset_password(password)
|
||||
return HttpResponseRedirect(reverse('users:reset-password-success'))
|
||||
|
||||
|
||||
class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
|
||||
template_name = 'users/first_login.html'
|
||||
form_list = [UserInfoForm, UserKeyForm]
|
||||
file_storage = default_storage
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated() and not request.user.is_first_login:
|
||||
return redirect(reverse('index'))
|
||||
return super(UserFirstLoginView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def done(self, form_list, form_dict, **kwargs):
|
||||
user = self.request.user
|
||||
for form in form_list:
|
||||
for field in form:
|
||||
if field.value():
|
||||
setattr(user, field.name, field.value())
|
||||
if field.name == 'enable_otp':
|
||||
user.enable_otp = field.value()
|
||||
user.is_first_login = False
|
||||
user.save()
|
||||
return redirect(reverse('index'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UserFirstLoginView, self).get_context_data(**kwargs)
|
||||
context.update({'app': _('Users'), 'action': _('First Login')})
|
||||
return context
|
||||
|
||||
def get_form_initial(self, step):
|
||||
user = self.request.user
|
||||
if step == '0':
|
||||
return {
|
||||
'name': user.name or user.username,
|
||||
'enable_otp': user.enable_otp or True,
|
||||
'wechat': user.wechat or '',
|
||||
'phone': user.phone or ''
|
||||
}
|
||||
return super(UserFirstLoginView, self).get_form_initial(step)
|
||||
|
|
|
@ -17,3 +17,4 @@ paramiko==2.0.2
|
|||
celery==3.1.23
|
||||
ansible==2.1.1.0
|
||||
django-simple-captcha==0.5.2
|
||||
django-formtools==1.0
|
||||
|
|
Loading…
Reference in New Issue