mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
Convert firstboot pages to Django forms
This commit is contained in:
parent
5461694b10
commit
b50a99b1e4
@ -1,16 +1,5 @@
|
|||||||
from urlparse import urlparse
|
"""
|
||||||
import os, cherrypy, re
|
First Boot: Initial Plinth Configuration.
|
||||||
from gettext import gettext as _
|
|
||||||
from plugin_mount import PagePlugin, PluginMount, FormPlugin
|
|
||||||
from modules.auth import require, add_user
|
|
||||||
from forms import Form
|
|
||||||
from withsqlite.withsqlite import sqlite_db
|
|
||||||
import cfg
|
|
||||||
import config
|
|
||||||
from model import User
|
|
||||||
import util
|
|
||||||
|
|
||||||
"""First Boot: Initial Plinth Configuration.
|
|
||||||
|
|
||||||
See docs/design/first-connection.mdwn for details.
|
See docs/design/first-connection.mdwn for details.
|
||||||
|
|
||||||
@ -27,27 +16,76 @@ The Plinth first-connection process has several stages:
|
|||||||
3. The box detects the network's configuration and restarts networking.
|
3. The box detects the network's configuration and restarts networking.
|
||||||
|
|
||||||
4. The user interacts with the box normally.
|
4. The user interacts with the box normally.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
from django import forms
|
||||||
|
from django.core import validators
|
||||||
|
from gettext import gettext as _
|
||||||
|
from plugin_mount import PagePlugin
|
||||||
|
from modules.auth import add_user
|
||||||
|
from withsqlite.withsqlite import sqlite_db
|
||||||
|
import cfg
|
||||||
|
import config
|
||||||
|
import util
|
||||||
|
|
||||||
|
|
||||||
|
## TODO: flesh out these tests values
|
||||||
|
def valid_box_key(value):
|
||||||
|
"""Check whether box key is valid"""
|
||||||
|
del value # Unused
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
box_key = forms.CharField(
|
||||||
|
label=_('Box\'s key (optional)'), required=False,
|
||||||
|
widget=forms.Textarea(), validators=[valid_box_key],
|
||||||
|
help_text=_('Cryptographic keys are used so that Box\'s identity can \
|
||||||
|
proved when talking to you. This key can be auto-generated, but if one \
|
||||||
|
already exists (from a prior FreedomBox, for example), you can paste it \
|
||||||
|
below. This key should not be the same as your key because you are not your \
|
||||||
|
FreedomBox!'))
|
||||||
|
|
||||||
|
|
||||||
class FirstBoot(PagePlugin):
|
class FirstBoot(PagePlugin):
|
||||||
|
"""First boot wizard"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
PagePlugin.__init__(self, *args, **kwargs)
|
PagePlugin.__init__(self, *args, **kwargs)
|
||||||
self.register_page("firstboot") # this is the url this page will hang off of (/firstboot)
|
|
||||||
|
# this is the url this page will hang off of (/firstboot)
|
||||||
|
self.register_page('firstboot')
|
||||||
|
self.register_page('firstboot/state0')
|
||||||
|
self.register_page('firstboot/state1')
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self, *args, **kwargs):
|
def index(self, *args, **kwargs):
|
||||||
return self.state0(*args, **kwargs)
|
return self.state0(*args, **kwargs)
|
||||||
|
|
||||||
## TODO: flesh out these tests values
|
|
||||||
def valid_box_key_p(self, key):
|
|
||||||
return True
|
|
||||||
def generate_box_key(self):
|
def generate_box_key(self):
|
||||||
return "fake key"
|
return "fake key"
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def state0(self, message="", hostname="", box_key="", submitted=False, username="", password="", **kwargs):
|
def state0(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
In this state, we do time config over HTTP, name the box and
|
In this state, we do time config over HTTP, name the box and
|
||||||
server key selection.
|
server key selection.
|
||||||
@ -64,68 +102,70 @@ class FirstBoot(PagePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# FIXME: reject connection attempt if db["state"] >= 5.
|
# FIXME: reject connection attempt if db["state"] >= 5.
|
||||||
|
## Until LDAP is in place, we'll put the box key in the cfg.store_file
|
||||||
|
status = self.get_state0()
|
||||||
|
|
||||||
## Until LDAP is in place, we'll put the box name and key in the cfg.store_file
|
form = None
|
||||||
## Must resist the sick temptation to write an LDAP interface to the sqlite file
|
messages = []
|
||||||
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as db:
|
|
||||||
db['about'] = "This table is for information about this FreedomBox"
|
if kwargs:
|
||||||
if hostname:
|
form = State0Form(kwargs, prefix='firstboot')
|
||||||
if '' == config.valid_hostname(hostname):
|
# pylint: disable-msg=E1101
|
||||||
config.set_hostname(hostname)
|
if form.is_valid():
|
||||||
else:
|
success = self._apply_state0(status, form.cleaned_data,
|
||||||
message += _("Invalid box name: %s") % config.valid_hostname(hostname)
|
messages)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Everything is good, permanently mark and move to page 2
|
||||||
|
with sqlite_db(cfg.store_file, table="firstboot",
|
||||||
|
autocommit=True) as database:
|
||||||
|
database['state'] = 1
|
||||||
|
|
||||||
|
raise cherrypy.InternalRedirect('state1')
|
||||||
|
else:
|
||||||
|
form = State0Form(initial=status, prefix='firstboot')
|
||||||
|
|
||||||
|
return util.render_template(template='firstboot_state0',
|
||||||
|
title=_('First Boot!'), form=form,
|
||||||
|
messages=messages)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state0():
|
||||||
|
"""Return the state for form state 0"""
|
||||||
|
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
|
||||||
|
database:
|
||||||
|
return {'hostname': config.get_hostname(),
|
||||||
|
'box_key': database.get('box_key', None)}
|
||||||
|
|
||||||
|
def _apply_state0(self, old_state, new_state, messages):
|
||||||
|
"""Apply changes in state 0 form"""
|
||||||
|
success = True
|
||||||
|
with sqlite_db(cfg.store_file, table="thisbox", autocommit=True) as \
|
||||||
|
database:
|
||||||
|
database['about'] = 'Information about this FreedomBox'
|
||||||
|
|
||||||
|
if new_state['box_key']:
|
||||||
|
database['box_key'] = new_state['box_key']
|
||||||
|
elif not old_state['box_key']:
|
||||||
|
database['box_key'] = self.generate_box_key()
|
||||||
|
|
||||||
|
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.append(
|
||||||
|
('error', _('User account creation failed: %s') % error))
|
||||||
|
success = False
|
||||||
else:
|
else:
|
||||||
hostname = config.get_hostname()
|
messages.append(('success', _('User account created')))
|
||||||
if box_key:
|
|
||||||
if self.valid_box_key_p(box_key):
|
|
||||||
db['box_key'] = box_key
|
|
||||||
else:
|
|
||||||
message += _("Invalid key!")
|
|
||||||
elif 'box_key' in db and db['box_key']:
|
|
||||||
box_key = _("We already have a key for this box on file.") # TODO: Think this through and handle more gracefully. Seriously.
|
|
||||||
elif submitted and not box_key:
|
|
||||||
box_key = self.generate_box_key()
|
|
||||||
db['box_key'] = box_key
|
|
||||||
if username and password:
|
|
||||||
error = add_user(username, password, 'First user - please change', '', True)
|
|
||||||
if error:
|
|
||||||
message += _("User account creation failed: %s") % error
|
|
||||||
validuser = False
|
|
||||||
else:
|
|
||||||
validuser = True
|
|
||||||
else:
|
|
||||||
validuser = False
|
|
||||||
|
|
||||||
if hostname and box_key and '' == config.valid_hostname(hostname) and self.valid_box_key_p(box_key) and validuser:
|
return success
|
||||||
## Update state to 1 and head there
|
|
||||||
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
|
|
||||||
db['state']=1
|
|
||||||
raise cherrypy.InternalRedirect('state1')
|
|
||||||
|
|
||||||
main = "<p>Welcome. It looks like this FreedomBox isn't set up yet. We'll need to ask you a just few questions to get started.</p>"
|
|
||||||
form = Form(title="Welcome to Your FreedomBox!",
|
|
||||||
action="", # stay at firstboot
|
|
||||||
name="whats_my_name",
|
|
||||||
message=message)
|
|
||||||
form.html("<p>For convenience, your FreedomBox needs a name. It should be something short that doesn't contain spaces or punctuation. 'Willard' would be a good name. 'Freestyle McFreedomBox!!!' would not.</p>")
|
|
||||||
form.text_input('Name your FreedomBox', id="hostname", value=hostname)
|
|
||||||
form.html("<p><strong>Initial user and password.</strong> 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.</p>")
|
|
||||||
form.text_input('Username:', id="username", value=username)
|
|
||||||
form.text_input('Password:', id="password", type='password')
|
|
||||||
form.html("<p>%(box_name)s uses cryptographic keys so it can prove its identity when talking to you. %(box_name)s can make a key for itself, but if one already exists (from a prior FreedomBox, for example), you can paste it below. This key should not be the same as your key because you are not your FreedomBox!</p>" % {'box_name':cfg.box_name})
|
|
||||||
form.text_box("If you want, paste your box's key here.", id="box_key", value=box_key)
|
|
||||||
form.hidden(name="submitted", value="True")
|
|
||||||
form.submit("Box it up!")
|
|
||||||
|
|
||||||
main += form.render()
|
|
||||||
return util.render_template(
|
|
||||||
template="base",
|
|
||||||
title=_("First Boot!"),
|
|
||||||
main=main,
|
|
||||||
sidebar_right=sidebar_right)
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def state1(self, message=None):
|
def state1():
|
||||||
"""
|
"""
|
||||||
State 1 is when we have a box name and key. In this state,
|
State 1 is when we have a box name and key. In this state,
|
||||||
our task is to provide a certificate and maybe to guide the
|
our task is to provide a certificate and maybe to guide the
|
||||||
@ -134,31 +174,11 @@ class FirstBoot(PagePlugin):
|
|||||||
|
|
||||||
TODO: HTTPS failure in State 2 should returns to state 1.
|
TODO: HTTPS failure in State 2 should returns to state 1.
|
||||||
"""
|
"""
|
||||||
main = """
|
|
||||||
<p>Welcome screen not completely implemented yet. Press <a
|
|
||||||
href="../router">continue</a> to see the rest of the web interface.</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>TODO: explain all this cert stuff to the user.</li>
|
|
||||||
<li>TODO: add instrux for installing certificate.</li>
|
|
||||||
<li>TODO: add steps for after you have installed certificate.</li>
|
|
||||||
<ul>
|
|
||||||
"""
|
|
||||||
# TODO complete first_boot handling
|
# TODO complete first_boot handling
|
||||||
# Make sure the user is not stuck on a dead end for now.
|
# Make sure the user is not stuck on a dead end for now.
|
||||||
with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
|
with sqlite_db(cfg.store_file, table='firstboot', autocommit=True) as \
|
||||||
db['state']=5
|
database:
|
||||||
|
database['state'] = 5
|
||||||
|
|
||||||
# TODO: Update state to 2 and head there
|
return util.render_template(template='firstboot_state1',
|
||||||
# with sqlite_db(cfg.store_file, table="firstboot", autocommit=True) as db:
|
title=_('Installing the Certificate'))
|
||||||
# db['state']=1
|
|
||||||
# # TODO: switch to HTTPS
|
|
||||||
# raise cherrypy.InternalRedirect('state1')
|
|
||||||
|
|
||||||
return util.render_template(
|
|
||||||
template="base",
|
|
||||||
title=_("Installing the Certificate"),
|
|
||||||
main=main,
|
|
||||||
sidebar_right=sidebar_right)
|
|
||||||
|
|
||||||
sidebar_right=_("""<strong>Getting Help</strong><p>We've done our best to make your FreedomBox easy to use. If you have questions during setup, there are a few places to turn for help. TODO: add links to such help.</p>""")
|
|
||||||
|
|||||||
@ -37,6 +37,11 @@ from plugin_mount import PagePlugin
|
|||||||
import util
|
import util
|
||||||
|
|
||||||
|
|
||||||
|
def get_hostname():
|
||||||
|
"""Return the hostname"""
|
||||||
|
return socket.gethostname()
|
||||||
|
|
||||||
|
|
||||||
class TrimmedCharField(forms.CharField):
|
class TrimmedCharField(forms.CharField):
|
||||||
"""Trim the contents of a CharField"""
|
"""Trim the contents of a CharField"""
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
@ -122,7 +127,7 @@ class Configuration(PagePlugin):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_status():
|
def get_status():
|
||||||
"""Return the current status"""
|
"""Return the current status"""
|
||||||
return {'hostname': socket.gethostname(),
|
return {'hostname': get_hostname(),
|
||||||
'time_zone': util.slurp('/etc/timezone').rstrip()}
|
'time_zone': util.slurp('/etc/timezone').rstrip()}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
28
modules/installed/templates/firstboot_sidebar.html
Normal file
28
modules/installed/templates/firstboot_sidebar.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% comment %}
|
||||||
|
#
|
||||||
|
# This file is part of Plinth.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<div class="well sidebar-nav">
|
||||||
|
|
||||||
|
<h3>Getting Help</h3>
|
||||||
|
|
||||||
|
<p>We've done our best to make your FreedomBox easy to use. If
|
||||||
|
you have questions during setup, there are a few places to turn
|
||||||
|
for help. TODO: add links to such help.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
59
modules/installed/templates/firstboot_state0.html
Normal file
59
modules/installed/templates/firstboot_state0.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
#
|
||||||
|
# This file is part of Plinth.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block main_block %}
|
||||||
|
|
||||||
|
<h2>Welcome to Your FreedomBox!</h2>
|
||||||
|
|
||||||
|
{% for severity, message in messages %}
|
||||||
|
<div class='alert alert-{{ severity }}'>{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<p>Welcome. It looks like this FreedomBox isn't set up yet. We'll
|
||||||
|
need to ask you a just few questions to get started.</p>
|
||||||
|
|
||||||
|
<form class="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% include 'bootstrapform/field.html' with field=form.hostname %}
|
||||||
|
|
||||||
|
<p><strong>Initial user and password.</strong> 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.</p>
|
||||||
|
|
||||||
|
{% include 'bootstrapform/field.html' with field=form.username %}
|
||||||
|
{% include 'bootstrapform/field.html' with field=form.password %}
|
||||||
|
|
||||||
|
{% include 'bootstrapform/field.html' with field=form.box_key %}
|
||||||
|
|
||||||
|
<input type="submit" class="btn-primary" value="Box it up!"/>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block sidebar_right_block %}
|
||||||
|
|
||||||
|
{% include "firstboot_sidebar.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
41
modules/installed/templates/firstboot_state1.html
Normal file
41
modules/installed/templates/firstboot_state1.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
#
|
||||||
|
# This file is part of Plinth.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block main_block %}
|
||||||
|
|
||||||
|
<p>Welcome screen not completely implemented yet. Press <a
|
||||||
|
href="{{basehref }}/router">continue</a> to see the rest of the
|
||||||
|
web interface.</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>TODO: explain all this cert stuff to the user.</li>
|
||||||
|
<li>TODO: add instrux for installing certificate.</li>
|
||||||
|
<li>TODO: add steps for after you have installed certificate.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block sidebar_right_block %}
|
||||||
|
|
||||||
|
{% include "firstboot_sidebar.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user