From 94040e402a6889cd901777aaf0aebfb96c56e42f Mon Sep 17 00:00:00 2001 From: fonfon Date: Tue, 7 Oct 2014 03:41:49 +0200 Subject: [PATCH] refactored first boot module; user is now logged in automatically after an account is created --- plinth/modules/first_boot/__init__.py | 4 - plinth/modules/first_boot/first_boot.py | 125 ------------------ plinth/modules/first_boot/forms.py | 59 +++++++++ plinth/modules/first_boot/middleware.py | 21 ++- plinth/modules/first_boot/static/state0.css | 11 ++ .../templates/firstboot_state0.html | 30 ++--- ...oot_state1.html => firstboot_state10.html} | 4 + plinth/modules/first_boot/urls.py | 10 +- plinth/modules/first_boot/views.py | 42 ++++++ 9 files changed, 145 insertions(+), 161 deletions(-) delete mode 100644 plinth/modules/first_boot/first_boot.py create mode 100644 plinth/modules/first_boot/forms.py create mode 100644 plinth/modules/first_boot/static/state0.css rename plinth/modules/first_boot/templates/{firstboot_state1.html => firstboot_state10.html} (91%) create mode 100644 plinth/modules/first_boot/views.py diff --git a/plinth/modules/first_boot/__init__.py b/plinth/modules/first_boot/__init__.py index b9a2a74f5..558116da9 100644 --- a/plinth/modules/first_boot/__init__.py +++ b/plinth/modules/first_boot/__init__.py @@ -18,7 +18,3 @@ """ Plinth module for first boot wizard """ - -from . import first_boot - -__all__ = ['first_boot'] diff --git a/plinth/modules/first_boot/first_boot.py b/plinth/modules/first_boot/first_boot.py deleted file mode 100644 index f5d823c17..000000000 --- a/plinth/modules/first_boot/first_boot.py +++ /dev/null @@ -1,125 +0,0 @@ -"""First Boot: Initial Plinth Configuration. - -See docs/design/first-connection.mdwn for details. - -The Plinth first-connection process has several stages: - -0. The user connects to Plinth for the first time and is redirected from - the home page to the Hello page. - -1. The user sets the Box's name, the administrator user name and password. - -2. The user interacts with the box normally. - -""" - -from django import forms -from django.contrib import messages -from django.core import validators -from django.core.urlresolvers import reverse -from django.http.response import HttpResponseRedirect -from django.template.response import TemplateResponse -from gettext import gettext as _ - -from plinth.modules.config import config -from plinth.modules.lib.auth import add_user -from plinth import kvstore - - -class State0Form(forms.Form): # pylint: disable-msg=W0232 - """First boot state 0 form""" - - hostname = forms.CharField( - label=_('Name your FreedomBox'), - help_text=_('For convenience, your FreedomBox needs a name. It \ -should be something short that does not contain spaces or punctuation. \ -"Willard" would be a good name while "Freestyle McFreedomBox!!!" would \ -not. It must be alphanumeric, start with an alphabet and must not be greater \ -than 63 characters in length.'), - validators=[ - validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$', - _('Invalid hostname'))]) - - username = forms.CharField(label=_('Username')) - password = forms.CharField(label=_('Password'), - widget=forms.PasswordInput()) - - -def index(request): - """Serve the index first boot page""" - return state0(request) - - -def state0(request): - """In this state, configure hostname and administrator username and - password. - - All the parameters are form inputs. They get passed in when the form is - submitted. This method checks the inputs and if they validate, uses them - to take action. If they do not validate, it displays the form to give the - user a chance to input correct values. It might display an error message. - - """ - try: - if kvstore.get_default('firstboot_state', 0) >= 5: - return HttpResponseRedirect(reverse('index')) - except KeyError: - pass - - # Until LDAP is in place, we'll put the box key in the cfg.store_file - status = get_state0() - - form = None - - if request.method == 'POST': - form = State0Form(request.POST, prefix='firstboot') - # pylint: disable-msg=E1101 - if form.is_valid(): - success = _apply_state0(request, status, form.cleaned_data) - - if success: - # Everything is good, permanently mark and move to page 2 - kvstore.set('firstboot_state', 1) - return HttpResponseRedirect(reverse('first_boot:state1')) - else: - form = State0Form(initial=status, prefix='firstboot') - - return TemplateResponse(request, 'firstboot_state0.html', - {'title': _('First Boot!'), - 'form': form}) - - -def get_state0(): - """Return the state for form state 0""" - return {'hostname': config.get_hostname()} - - -def _apply_state0(request, old_state, new_state): - """Apply changes in state 0 form""" - success = True - - if old_state['hostname'] != new_state['hostname']: - config.set_hostname(new_state['hostname']) - - error = add_user(new_state['username'], new_state['password'], - 'First user, please change', '', True) - if error: - messages.error(request, _('User account creation failed: %s') % error) - success = False - else: - messages.success(request, _('User account created')) - - return success - - -def state1(request): - """State 1 is when we have a hostname and administrator account. Redirect - the user to login page after this. - - """ - # TODO complete first_boot handling - # Make sure the user is not stuck on a dead end for now. - kvstore.set('firstboot_state', 5) - - return TemplateResponse(request, 'firstboot_state1.html', - {'title': _('Setup Complete')}) diff --git a/plinth/modules/first_boot/forms.py b/plinth/modules/first_boot/forms.py new file mode 100644 index 000000000..7fd60ad1a --- /dev/null +++ b/plinth/modules/first_boot/forms.py @@ -0,0 +1,59 @@ +from django import forms +from django.contrib import auth, messages +from django.core import validators +from gettext import gettext as _ + +from plinth.modules.config import config + + +class State0Form(forms.ModelForm): + """Firstboot state 0: Set hostname and create a new user""" + hostname = forms.CharField( + label=_('Name of your FreedomBox'), + help_text=_('For convenience, your FreedomBox needs a name. It \ +should be something short that does not contain spaces or punctuation. \ +"Willard" would be a good name while "Freestyle McFreedomBox!!!" would \ +not. It must be alphanumeric, start with an alphabet and must not be greater \ +than 63 characters in length.'), + validators=[ + validators.RegexValidator(r'^[a-zA-Z][a-zA-Z0-9]{,62}$', + _('Invalid hostname'))]) + + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request') + super(State0Form, self).__init__(*args, **kwargs) + + class Meta: + model = auth.models.User + fields = ("hostname", "username", "password") + widgets = { + 'password': forms.PasswordInput, + } + help_texts = { + 'username': 'Choose a username and password to access this web\ + interface. The password can be changed and other users can\ + be added later.', + } + + def save(self, commit=True): + """Set hostname, create and login the user""" + config.set_hostname(self.cleaned_data['hostname']) + user = super(State0Form, self).save(commit=False) + user.set_password(self.cleaned_data["password"]) + user.is_expert = True + if commit: + user.save() + self.login_user() + return user + + def login_user(self): + """Try to login the user with the credentials he provided""" + try: + user = auth.authenticate(username=self.request.POST['username'], + password=self.request.POST['password']) + auth.login(self.request, user) + except Exception: + pass + else: + msg = _('User account created, you are now logged in') + messages.success(self.request, msg) diff --git a/plinth/modules/first_boot/middleware.py b/plinth/modules/first_boot/middleware.py index 061230907..de8c5bc76 100644 --- a/plinth/modules/first_boot/middleware.py +++ b/plinth/modules/first_boot/middleware.py @@ -35,19 +35,14 @@ class FirstBootMiddleware(object): @staticmethod def process_request(request): - """Handle a request as Django middleware request handler.""" - # Prevent redirecting to first boot wizard in a loop by - # checking if we are already in first boot wizard. - if request.path.startswith(reverse('first_boot:index')): - return - state = kvstore.get_default('firstboot_state', 0) - if not state: - # Permanent redirect causes the browser to cache the redirect, - # preventing the user from navigating to /plinth until the - # browser is restarted. - return HttpResponseRedirect(reverse('first_boot:index')) + firstboot_index_url = reverse('first_boot:index') + user_requests_firstboot = request.path.startswith(firstboot_index_url) - if state < 5: - LOGGER.info('First boot state - %d', state) + # Setup is complete: Forbid accessing firstboot + if state >= 10 and user_requests_firstboot: + return HttpResponseRedirect(reverse('index')) + + # Setup is not complete: Forbid accessing anything but firstboot + if state < 10 and not user_requests_firstboot: return HttpResponseRedirect(reverse('first_boot:state%d' % state)) diff --git a/plinth/modules/first_boot/static/state0.css b/plinth/modules/first_boot/static/state0.css new file mode 100644 index 000000000..d1f95b4ba --- /dev/null +++ b/plinth/modules/first_boot/static/state0.css @@ -0,0 +1,11 @@ +form .control-label { + font-size: large; +} +form .help-block { + font-size: small; +} +/* TODO: remove this customization once we use bootstrap3 */ +.has-error span.help-block { + background-color: red; +} + diff --git a/plinth/modules/first_boot/templates/firstboot_state0.html b/plinth/modules/first_boot/templates/firstboot_state0.html index 00f232ff9..39074bbea 100644 --- a/plinth/modules/first_boot/templates/firstboot_state0.html +++ b/plinth/modules/first_boot/templates/firstboot_state0.html @@ -18,28 +18,24 @@ # {% endcomment %} +{% load static %} {% load bootstrap %} +{% block page_head %} + +{% endblock %} + {% block main_block %}

Welcome to Your FreedomBox!

-

Welcome. It looks like this FreedomBox isn't set up yet. We'll - need to ask you a just few questions to get started.

+

It looks like this FreedomBox isn't set up yet. We + need to ask you a few questions to get started.

+
{% csrf_token %} - - {% include 'bootstrapform/field.html' with field=form.hostname %} - -

Initial user and password. Access to this web - interface is protected by knowing a username and password. - Provide one here to register the initial privileged user. The - password can be changed and other users added later.

- - {% include 'bootstrapform/field.html' with field=form.username %} - {% include 'bootstrapform/field.html' with field=form.password %} - + {{ form|bootstrap }}
@@ -47,7 +43,11 @@ {% endblock %} {% block sidebar_right_block %} - {% include "firstboot_sidebar.html" %} - +{% endblock %} + +{% block page_js %} + {% endblock %} diff --git a/plinth/modules/first_boot/templates/firstboot_state1.html b/plinth/modules/first_boot/templates/firstboot_state10.html similarity index 91% rename from plinth/modules/first_boot/templates/firstboot_state1.html rename to plinth/modules/first_boot/templates/firstboot_state10.html index 2ee0c89a0..333fa87cf 100644 --- a/plinth/modules/first_boot/templates/firstboot_state1.html +++ b/plinth/modules/first_boot/templates/firstboot_state10.html @@ -22,7 +22,11 @@ {% block main_block %} +{% if user.is_authenticated %} +

Have fun with your freedombox!

+{% else %}

Proceed to login.

+{% endif %} {% endblock %} diff --git a/plinth/modules/first_boot/urls.py b/plinth/modules/first_boot/urls.py index d16290dba..17646a4bf 100644 --- a/plinth/modules/first_boot/urls.py +++ b/plinth/modules/first_boot/urls.py @@ -20,11 +20,13 @@ URLs for the First Boot module """ from django.conf.urls import patterns, url +from .views import State0View urlpatterns = patterns( # pylint: disable-msg=C0103 - 'plinth.modules.first_boot.first_boot', - url(r'^firstboot/$', 'index', name='index'), - url(r'^firstboot/state0/$', 'state0', name='state0'), - url(r'^firstboot/state1/$', 'state1', name='state1') + 'plinth.modules.first_boot.views', + # Take care of the firstboot middleware when changing URLs + url(r'^firstboot/$', State0View.as_view(), name='index'), + url(r'^firstboot/state0/$', State0View.as_view(), name='state0'), + url(r'^firstboot/state10/$', 'state10', name='state10'), ) diff --git a/plinth/modules/first_boot/views.py b/plinth/modules/first_boot/views.py new file mode 100644 index 000000000..13db39d5b --- /dev/null +++ b/plinth/modules/first_boot/views.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse_lazy +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.views.generic.edit import CreateView +from gettext import gettext as _ + +from plinth import kvstore +from plinth.modules.config import config +from .forms import State0Form + + +class State0View(CreateView): + """Setup hostname and create user account""" + template_name = 'firstboot_state0.html' + form_class = State0Form + success_url = reverse_lazy('first_boot:state10') + + def get_initial(self): + initial = super(State0View, self).get_initial() + initial['hostname'] = config.get_hostname() + return initial + + def get_form_kwargs(self): + """Make request available to the form (to insert messages)""" + kwargs = super(State0View, self).get_form_kwargs() + kwargs['request'] = self.request + return kwargs + + +def state10(request): + """State 10 is when all firstboot setup is done. + + After viewing this page the firstboot module can't be accessed anymore. + """ + # Make sure that a user exists before finishing firstboot + if User.objects.all(): + kvstore.set('firstboot_state', 10) + + return render_to_response('firstboot_state10.html', + {'title': _('Setup Complete')}, + context_instance=RequestContext(request))