From d8fe59debb015a7cff0fa406eda596ade2d666c3 Mon Sep 17 00:00:00 2001
From: "xiaokong1937@gmail.com" <763691951@qq.com>
Date: Thu, 8 Sep 2016 21:51:44 +0800
Subject: [PATCH 1/2] temp save for issue 8
---
.gitignore | 1 +
apps/users/forms.py | 12 +++
apps/users/models.py | 22 +++---
apps/users/templates/users/first_login.html | 68 ++++++++++++++++
.../templates/users/first_login.old.html | 77 +++++++++++++++++++
apps/users/urls.py | 1 +
apps/users/views.py | 25 +++++-
requirements.txt | 1 +
8 files changed, 192 insertions(+), 15 deletions(-)
create mode 100644 apps/users/templates/users/first_login.html
create mode 100644 apps/users/templates/users/first_login.old.html
diff --git a/.gitignore b/.gitignore
index 658ea27ac..959682445 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.DS_Store
*.pyc
*.pyo
+*.swp
env
env*
dist
diff --git a/apps/users/forms.py b/apps/users/forms.py
index 8eb8ebcf8..c7f2a96e8 100644
--- a/apps/users/forms.py
+++ b/apps/users/forms.py
@@ -18,6 +18,7 @@ class UserLoginForm(AuthenticationForm):
class UserCreateForm(forms.ModelForm):
+
class Meta:
model = User
fields = [
@@ -67,3 +68,14 @@ class UserGroupForm(forms.ModelForm):
help_texts = {
'name': '* required'
}
+
+
+class UserInfoForm(forms.Form):
+ name = forms.CharField(max_length=20)
+ wechat = forms.CharField(max_length=30)
+ phone = forms.CharField(max_length=20)
+ enable_otp = forms.BooleanField()
+
+
+class UserKeyForm(forms.Form):
+ private_key = forms.CharField(max_length=5000, widget=forms.Textarea)
diff --git a/apps/users/models.py b/apps/users/models.py
index f78c6082d..fba66a8d8 100644
--- a/apps/users/models.py
+++ b/apps/users/models.py
@@ -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
-
diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html
new file mode 100644
index 000000000..6d51ca435
--- /dev/null
+++ b/apps/users/templates/users/first_login.html
@@ -0,0 +1,68 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+{% load bootstrap %}
+
+{% block custom_head_css_js %}
+{{ wizard.form.media }}
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+ {% if wizard.steps.prev %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/apps/users/templates/users/first_login.old.html b/apps/users/templates/users/first_login.old.html
new file mode 100644
index 000000000..b6b1cf838
--- /dev/null
+++ b/apps/users/templates/users/first_login.old.html
@@ -0,0 +1,77 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block custom_head_css_js %}
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+ This is basic example of Step
+
+
+
First Step
+
+
+
Hello in Step 1
+
+ This is the first content.
+
+
+
+
+
Second Step
+
+
+
This is step 2
+
+ This content is diferent than the first one.
+
+
+
+
+
Third Step
+
+
+
This is step 3
+
+ This is last content.
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
+
diff --git a/apps/users/urls.py b/apps/users/urls.py
index a89958a2a..179827216 100644
--- a/apps/users/urls.py
+++ b/apps/users/urls.py
@@ -16,6 +16,7 @@ urlpatterns = [
name='reset-password-success'),
url(r'^user$', views.UserListView.as_view(), name='user-list'),
url(r'^user/(?P[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[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[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'),
diff --git a/apps/users/views.py b/apps/users/views.py
index bc967cb1e..c34d5fcf8 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -7,9 +7,10 @@ import logging
from django.conf import settings
from django.contrib.auth import login as auth_login, logout as auth_logout
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
+from django.shortcuts import get_object_or_404, reverse, redirect, render
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.urls import reverse_lazy
@@ -21,10 +22,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 +52,9 @@ class UserLoginView(FormView):
return redirect(self.get_success_url())
def get_success_url(self):
+ if self.request.user.is_first_login:
+ return '/firstlogin'
+
return self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, reverse('index')))
@@ -292,3 +298,18 @@ class UserResetPasswordView(TemplateView):
user.reset_password(password)
return HttpResponseRedirect(reverse('users:reset-password-success'))
+
+
+class UserFirstLoginView(SessionWizardView):
+ template_name = 'users/first_login.html'
+ form_list = [UserInfoForm, UserKeyForm]
+ file_storage = default_storage
+
+ def done(self, form_list, form_dict, **kwargs):
+ print form_list
+ 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
diff --git a/requirements.txt b/requirements.txt
index 1b3476a53..ffc458d20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
From a7e3f9c465383e77da2829d3c447675371a35d4a Mon Sep 17 00:00:00 2001
From: "xiaokong1937@gmail.com" <763691951@qq.com>
Date: Sat, 10 Sep 2016 13:16:58 +0800
Subject: [PATCH 2/2] #8 user first login view
---
apps/jumpserver/settings.py | 3 +-
apps/users/forms.py | 19 +++--
apps/users/templates/users/first_login.html | 56 ++++++++------
.../templates/users/first_login.old.html | 77 -------------------
apps/users/utils.py | 59 +++++++++++++-
apps/users/views.py | 33 +++++++-
6 files changed, 134 insertions(+), 113 deletions(-)
delete mode 100644 apps/users/templates/users/first_login.old.html
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index 0e8487b2a..409cc2d03 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -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/
diff --git a/apps/users/forms.py b/apps/users/forms.py
index c7f2a96e8..a24565f89 100644
--- a/apps/users/forms.py
+++ b/apps/users/forms.py
@@ -71,11 +71,20 @@ class UserGroupForm(forms.ModelForm):
class UserInfoForm(forms.Form):
- name = forms.CharField(max_length=20)
- wechat = forms.CharField(max_length=30)
- phone = forms.CharField(max_length=20)
- enable_otp = forms.BooleanField()
+ 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)
+ 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
diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html
index 6d51ca435..915a1f425 100644
--- a/apps/users/templates/users/first_login.html
+++ b/apps/users/templates/users/first_login.html
@@ -13,7 +13,7 @@
+ {{ 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 %}
- {% if wizard.steps.prev %}
-
-
- {% endif %}
-
+
@@ -66,3 +65,16 @@
{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/templates/users/first_login.old.html b/apps/users/templates/users/first_login.old.html
deleted file mode 100644
index b6b1cf838..000000000
--- a/apps/users/templates/users/first_login.old.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends 'base.html' %}
-{% load static %}
-{% load i18n %}
-
-{% block custom_head_css_js %}
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
-
- This is basic example of Step
-
-
-
First Step
-
-
-
Hello in Step 1
-
- This is the first content.
-
-
-
-
-
Second Step
-
-
-
This is step 2
-
- This content is diferent than the first one.
-
-
-
-
-
Third Step
-
-
-
This is step 3
-
- This is last content.
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-{% block custom_foot_js %}
-
-{% endblock %}
-
diff --git a/apps/users/utils.py b/apps/users/utils.py
index 7b3222c29..9e9a34321 100644
--- a/apps/users/utils.py
+++ b/apps/users/utils.py
@@ -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, ) 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()])
diff --git a/apps/users/views.py b/apps/users/views.py
index c34d5fcf8..4bc044b12 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -6,11 +6,12 @@ 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, render
+from django.shortcuts import get_object_or_404, reverse, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.urls import reverse_lazy
@@ -53,7 +54,7 @@ class UserLoginView(FormView):
def get_success_url(self):
if self.request.user.is_first_login:
- return '/firstlogin'
+ return reverse('users:user-first-login')
return self.request.POST.get(
self.redirect_field_name,
@@ -300,16 +301,40 @@ class UserResetPasswordView(TemplateView):
return HttpResponseRedirect(reverse('users:reset-password-success'))
-class UserFirstLoginView(SessionWizardView):
+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):
- print form_list
+ 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)