Net-ng/kansha

View on GitHub
kansha/authentication/database/forms.py

Summary

Maintainability
F
1 wk
Test Coverage
# -*- coding:utf-8 -*-
#--
# Copyright (c) 2012-2014 Net-ng.
# All rights reserved.
#
# This software is licensed under the BSD License, as described in
# the file LICENSE.txt, which you should have received as part of
# this distribution.
#--

import time
import hashlib
import textwrap
from datetime import datetime, timedelta

import webob
from nagare.i18n import _
from nagare import (presentation, editor, component, security, log, database)

from . import captcha
from kansha import validator
from kansha.user import usermanager
from kansha.user.models import DataToken
from kansha.services.authentication_repository import Authentication


UserConfirmationTimeout = timedelta(hours=12)


class Header(object):

    def __init__(self, app_title, app_banner, theme):
        self.app_title = app_title
        self.app_banner = app_banner
        self.theme = theme


@presentation.render_for(Header)
def render_Header(self, h, comp, *args):
    """Head renderer"""

    h.head << h.head.title(self.app_banner)
    h.head << h.head.meta(
        name='viewport', content='width=device-width, initial-scale=1.0')

    h.head.css_url('css/knacss.css')
    h.head.css_url('css/themes/fonts.css?v=2c')
    h.head.css_url('css/themes/kansha.css')
    h.head.css_url('css/themes/login.css?v=2c')
    h.head.css_url('css/themes/%s/kansha.css?v=2c' % self.theme)
    h.head.css_url('css/themes/%s/login.css?v=2c' % self.theme)

    with h.div(class_='header'):
        with h.a(href=h.request.application_url):
            h << h.div(class_='logo')
        h << h.h1(self.app_banner)
    return h.root


@presentation.render_for(Header, 'hide')
def render_Header_hide(self, h, comp, *args):
    h.head.css('hide_logins', u'''.login {display:none;}''')
    return h.root


def redirect_to(url):
    raise webob.exc.HTTPSeeOther(location=url)


class Login(Authentication):

    CONFIG_SPEC = {
        'activated': 'boolean(default=True)',
        'moderator': 'string(default="")',
        'identicons': 'boolean(default=False)',
        'default_username': 'string(default="")',
        'default_password': 'string(default="")'
    }

    def __init__(self, app_title, app_banner, theme, assets_manager_service, services_service):
        self.assets_manager = assets_manager_service
        self._error_message = ''
        self.registration_task = services_service(RegistrationTask, app_title, app_banner,
                                                  theme, moderator=self.config['moderator'],
                                                  identicons=self.config['identicons'])
        self.default_username = self.config['default_username']
        self.default_password = self.config['default_password']
        self.pwd_reset = services_service(PasswordResetTask, app_title, app_banner, theme)
        self.content = component.Component()

    @property
    def alt_title(self):
        'Return a unicode to overwrite default login page title, or None.'
        return None or getattr(self.content(), 'alt_title', None)

    @property
    def error_message(self):
        return self._error_message or getattr(self.content(), 'error_message', u'')

    @error_message.setter
    def error_message(self, value):
        self._error_message = value
        if self.content():
            setattr(self.content(), 'error_message', u'')

    def log_in(self, comp):
        u = security.get_user()
        # If user is not local user remove user from security
        if u and not u.is_local:
            security.set_user(None)
            u = None
        if u is not None:
            database.session.flush()
            comp.answer(u)
        else:
            self._error_message = _('Login failed')


@presentation.render_for(Login)
def render_Login(self, h, comp, *args):
    if self.content() is not None:
        return self.content
    else:
        return comp.render(h, 'form')


@presentation.render_for(Login, 'form')
def render_Login_form(self, h, comp, *args):
    with h.div(class_='login databaseLogin'):
        with h.form:
            # if self.error_message:
            #     h << h.br << h.div(self.error_message, class_="error")
            h << h.input(type='text', name='__ac_name', id='username',
                         value=self.default_username, placeholder=_('Enter username'))
            h << h.input(type='password', name='__ac_password', id="password",
                         value=self.default_password, placeholder=_('Enter password'))
            h << h.a(_('Forgot password?')).action(self.content.call, self.pwd_reset)
            with h.div(class_='actions'):
                h << h.input(type='submit', value=_(u'Sign in'), class_='btn btn-primary').action(self.log_in, comp)
                with h.div:
                    h << _('No account yet? ')
                    h << h.a(_('Sign up')).action(self.content.call, self.registration_task)
    return h.root


class RegistrationForm(editor.Editor):

    """Registration form for creating a new (unconfirmed) user"""

    def __init__(self, app_title, app_banner, theme):
        self.username = editor.Property('').validate(self.validate_username)
        self.email = editor.Property('').validate(self.validate_email)
        self.fullname = editor.Property('').validate(validator.validate_non_empty_string)
        self.password = editor.Property('').validate(validator.validate_password)
        self.password_repeat = editor.Property('').validate(validator.validate_password)
        self.init_captcha_image()
        self.captcha_text = editor.Property('').validate(self.validate_captcha)
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.user_manager = usermanager.UserManager()
        self.error_message = u''
        self.alt_title = _(u'Sign up')

    def init_captcha_image(self):
        self.captcha_image = component.Component(captcha.Captcha())
        self.captcha_date = datetime.now()

    def validate_username(self, value):
        value = validator.validate_identifier(value)
        # check that this user name does not exist
        u = usermanager.UserManager.get_by_username(value)
        if u:
            raise ValueError(_("Username %s is not available. Please choose another one.")
                             % value)
        return value

    def validate_email(self, value):
        validator.validate_email(value)
        u = usermanager.UserManager.get_by_email(value)
        if u:
            raise ValueError(_(u"This email address (%s) is already registered.")
                             % value)
        return value

    def validate_captcha(self, value):
        # validate the text
        if not value or (value.lower() != self.captcha_image().text.lower()):
            self.init_captcha_image()
            raise ValueError(_("Invalid captcha"))

        # check the expiration date
        captcha_expiration = timedelta(minutes=10)
        if (datetime.now() - self.captcha_date) > captcha_expiration:
            self.init_captcha_image()
            raise ValueError(_("Entered too late"))
        return value

    def validate_passwords_match(self):
        if self.password.value != self.password_repeat.value:
            self.password_repeat.error = _("The two passwords don't match")

    def commit(self):

        properties = ('username', 'fullname', 'email', 'password',
                      'password_repeat', 'captcha_text')
        if not self.is_validated(properties):
            self.captcha_image.becomes(captcha.Captcha())
            self.error_message = _(u'Unable to process. Check your input below.')
            return None

        # register the user in the database
        u = self.user_manager.create_user(username=self.username.value,
                                          password=self.password.value,
                                          fullname=self.fullname.value,
                                          email=self.email.value)
        return u

    def on_ok(self, comp, application_url):
        u = self.commit()
        if u:
            comp.answer((u.username, application_url))


@presentation.render_for(RegistrationForm)
def render_RegistrationForm(self, h, comp, *args):
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        # autocomplete="off": do not store the password
        with h.form(autocomplete="off").post_action(self.validate_passwords_match):
            with h.div(class_='fields'):
                fields = (
                    (_('Username'), 'username', 'text', self.username),
                    (_('Password'),
                     'password', 'password', self.password),
                    (_('Password (repeat)'), 'password-repeat',
                     'password', self.password_repeat),
                    (_('Email address'), 'email', 'text', self.email),
                    (_('Fullname'), 'fullname', 'text', self.fullname)
                )
                for label, css_class, input_type, property in fields:
                    with h.div(class_='%s-field field' % css_class):
                        id_ = h.generate_id("field")
                        with h.label(for_=id_):
                            h << label
                        h << h.input(id=id_,
                                     type=input_type,
                                     value=property()).action(property).error(property.error)

                with h.div(class_='captcha-field field'):
                    id_ = h.generate_id("field")
                    with h.label(for_=id_):
                        h << _("Enter the captcha text")
                    h << h.input(id=id_,
                                 type='text').action(self.captcha_text).error(self.captcha_text.error)
                h << comp.render(h.AsyncRenderer(), 'captcha')
            with h.div(class_='actions'):
                h << h.input(type='submit',
                             value=_("Create new account"),
                             class_="btn btn-primary").action(self.on_ok, comp, h.request.application_url)

            h << _("Already have an account? ") << h.a(
                _("Log in")).action(comp.answer, (None, None))

    return h.root


@presentation.render_for(RegistrationForm, 'captcha')
def render_RegistrationForm_captcha(self, h, comp, *args):
    with h.div(id='captchaImage'):
        h << self.captcha_image
        h << h.a(id="captchaRefresh", title=_("refresh captcha")).action(self.init_captcha_image)
    return h.root
# ----------------------------------------------------------


class EmailRegistrationForm(editor.Editor):

    """Registration form for completing the email of a new external (and unconfirmed) user"""

    def __init__(self, app_title, app_banner, theme, username):
        self.email = editor.Property('').validate(validator.validate_email)
        self.email_repeat = editor.Property('').validate(validator.validate_email)
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.error_message = u''
        self.username = username
        self.app_title = app_title

    def validate_emails_match(self):
        if self.email.value != self.email_repeat.value:
            self.email_repeat.error = _("The two emails don't match")

    def commit(self):

        properties = ('email', 'email_repeat')
        if not self.is_validated(properties):
            self.error_message = _(u'Unable to process. Check your input below.')
            return None

        u = usermanager.UserManager.get_by_username(self.username)
        if u:
            u.set_email_to_confirm(self.email.value)
            return self.username

        self.error_message = _(u'Something went wrong: user does not exist! Please contact the administrator of this site.')
        return None

    def on_ok(self, comp, application_url):
        username = self.commit()
        if username:
            comp.answer((username, application_url))


@presentation.render_for(EmailRegistrationForm)
def render_RegistrationForm(self, h, comp, *args):
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        # autocomplete="off": do not store the password
        with h.form(autocomplete="off").post_action(self.validate_emails_match):
            h << h.p(_(u'Your profile is missing an email address. You have to enter a valid email address to open an account on %s') % self.app_title)
            with h.div(class_='fields'):
                fields = (
                    (_('Email address'), 'email', 'text', self.email),
                    (_('Email address (repeat)'), 'email', 'text', self.email_repeat),
                )
                for label, css_class, input_type, property in fields:
                    with h.div(class_='%s-field field' % css_class):
                        id_ = h.generate_id("field")
                        with h.label(for_=id_):
                            h << label
                        h << h.input(id=id_,
                                     type=input_type,
                                     value=property()).action(property).error(property.error)

            with h.div(class_='actions'):
                h << h.input(type='submit',
                             value=_("Create new account"),
                             class_="btn btn-primary").action(
                                self.on_ok, comp, h.request.application_url)

            h << _("Already have an account? ") << h.a(
                _("Log in")).action(comp.answer, (None, None))

    return h.root

# -----------------------------------------------------------------------


class RegistrationConfirmation(object):

    """Confirm a registration by sending a confirmation email, then acknowledge the success/failure
    of the operation"""

    def __init__(self, app_title, app_banner, theme):
        self.app_title = app_title
        self.header = component.Component(Header(app_title, app_banner, theme))


@presentation.render_for(RegistrationConfirmation, model='success')
def render_registration_confirmation_success(self, h, comp, *args):
    """Renders an registration acknowledgment message"""
    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Sign up'))

        with h.div(class_='container'):
            with h.h3:
                h << _("Registration successful!")

            with h.p:
                h << _("""You are now a registered user of %s. You can login
                on the home page of the application.""") % self.app_title

            with h.form:
                with h.div(class_='actions'):
                    h << h.input(type='submit',
                                 class_="btn btn-primary",
                                 value=_("Ok")).action(comp.answer, h.request.application_url)

    return h.root


@presentation.render_for(RegistrationConfirmation, model='failure')
def render_registration_confirmation_failure(self, h, comp, *args):
    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Sign up'))
            h << h.small(_("Registration failure!"), class_='error')
        with h.div(class_='container'):
            with h.p:
                h << _("""Registration failure! The token received is either expired or invalid. Please register again.""")

            with h.form:
                with h.div(class_='actions'):
                    h << h.input(type='submit', class_="btn btn-primary",
                                 value=_("Ok")).action(comp.answer, h.request.application_url)

    return h.root


# ----------------------------------------------------------

class PasswordEditor(editor.Editor):

    """Password editor, so that users can edit their password"""

    def __init__(self, app_title, app_banner, theme, get_user, check_old_password=True):
        self._get_user = get_user
        self.check_old_password = check_old_password
        self.old_password = editor.Property(
            '').validate(self.validate_old_password)
        self.password = editor.Property(
            '').validate(validator.validate_password)
        self.password_repeat = editor.Property(
            '').validate(validator.validate_password)
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.error_message = u''

    def validate_old_password(self, value):
        validator.validate_non_empty_string(value)
        if not self.user.check_password(value):
            raise ValueError(_("Invalid password"))
        return value

    def validate_passwords_match(self):
        if self.password.value != self.password_repeat.value:
            self.password_repeat.error = _("The two passwords don't match")

    @property
    def user(self):
        return self._get_user()

    def commit(self):
        properties = ['password', 'password_repeat']
        if self.check_old_password:
            properties.insert(0, 'old_password')

        if not self.is_validated(properties):
            self.error_message = _(u'Invalid input!')
            return False

        # change the user's password in the database
        self.user.change_password(self.password.value)

        # change the current user credentials if applicable
        current_user = security.get_user()
        if current_user and (current_user.entity is self.user):
            current_user.update_password(self.password.value)

        return True


@presentation.render_for(PasswordEditor)
def render_password_editor(self, h, comp, *args):
    def commit():
        if self.commit():
            comp.answer(True)

    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Change password'))
            if self.error_message:
                h << h.small(self.error_message, class_='error')
                self.error_message = u''
        with h.div(class_='container'):

            # autocomplete="off" prevent IE & Firefox from remembering the
            # passwords
            with h.form(autocomplete="off").post_action(self.validate_passwords_match):
                with h.div(class_='fields'):
                    fields = [
                        (_('Password'),
                            'password', 'password', self.password),
                        (_('Password (repeat)'), 'password-repeat',
                            'password', self.password_repeat)
                    ]
                    old_password_field = (_('Old Password'),
                                          'old-password', 'password', self.old_password)
                    if self.check_old_password:
                        fields.insert(0, old_password_field)

                    for label, css_class, input_type, property in fields:
                        with h.div(class_='%s-field field' % css_class):
                            id = h.generate_id("field")
                            with h.label(for_=id):
                                h << label
                            h << h.input(id=id,
                                         type=input_type,
                                         value=property()).action(property).error(property.error)

                with h.div(class_='actions'):
                    h << h.input(type='submit',
                                 value=_("Change password"),
                                 class_='btn btn-primary').action(commit)
                    h << u' '
                    h << h.input(type='submit',
                                 value=_("Cancel"),
                                 class_='btn').action(comp.answer)

        return h.root


# ----------------------------------------------------------

class PasswordResetForm(editor.Editor):

    """Password reset form, ask the user email"""

    def __init__(self, app_title, app_banner, theme, get_user_by_username):
        self._get_user_by_username = get_user_by_username
        self.username = editor.Property('').validate(self.validate_username)
        self.email = editor.Property('').validate(validator.validate_email)
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.error_message = u''

    def validate_username(self, value):
        value = validator.validate_identifier(value)

        user = self._get_user_by_username(value)
        if not user:
            raise ValueError(
                _("This username is not registered on our database"))
        if user.source != 'application':
            raise ValueError(
                _("You can not change your password since you log in by an external service."))
        return value

    def validate_email_match_user_email(self):
        if self.username.error or self.email.error:  # there are errors already
            return

        user = self._get_user_by_username(self.username.value)
        if self.email() != user.email:
            self.email.error = _(
                "This email address does not match the user's email address")

    def commit(self):
        properties = ('username', 'email')
        if not self.is_validated(properties):
            self.error_message = _(u'Invalid input!')
            return None

        return self._get_user_by_username(self.username.value)


@presentation.render_for(PasswordResetForm)
def render_password_reset_form(self, h, comp, *args):
    application_url = h.request.application_url
    def commit():
        user = self.commit()
        if user:
            comp.answer((user.username, application_url))
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):

        with h.p:
            h << _("""Please enter your username and your email address and you'll receive an email that contains a link to reset your password.""")

        with h.form.post_action(self.validate_email_match_user_email):
            with h.div(class_='fields'):
                fields = (
                    (_('Username'), 'username', 'text', self.username),
                    (_('Email address'), 'email', 'text', self.email),
                )
                for label, css_class, input_type, property in fields:
                    with h.div(class_='%s-field field' % css_class):
                        id = h.generate_id("field")
                        with h.label(for_=id):
                            h << label
                        h << h.input(id=id,
                                     type=input_type,
                                     value=property()).action(property).error(property.error)

            with h.div(class_='actions'):
                h << h.input(type='submit',
                             value=_("Reset password"),
                             class_='btn btn-primary').action(commit)

            h << (_("Remember your password?"), u' ', h.a(_("Log in")).action(comp.answer, (None, None)))

    return h.root


# ----------------------------------------------------------

class PasswordResetConfirmation(object):

    """Confirm a password reset by sending a confirmation email, then acknowledge the
    success/failure of the operation"""

    def __init__(self, app_title, app_banner, theme, get_user, confirmation_base_url):
        self.app_title = app_title
        self._get_user = get_user
        self.confirmation_base_url = confirmation_base_url
        self.token_generator = TokenGenerator(self._get_user().username, u'reset_password')
        self.header = component.Component(Header(self.app_title, app_banner, theme))
        self.alt_title = _(u'Reset password')

    @property
    def user(self):
        return self._get_user()

    @property
    def confirmation_url(self):
        token = self.token_generator.create_token().token
        return '/'.join((self.confirmation_base_url, 'reset', self._get_user().username, token))

    def confirm_password_reset(self, token):
        return self.token_generator.check_token(token)

    def reset_token(self, token):
        return self.token_generator.reset_token(token)

    def send_email(self, mail_sender):
        subst = dict(fullname=self.user.fullname,
                     app_title=self.app_title,
                     confirmation_url=self.confirmation_url)

        message = _("""
        Hello %(fullname)s,

        We received a request to reset your password. If you are at the origin of this
        request, please click on this confirmation link: %(confirmation_url)s. Otherwise,
        feel free to ignore this email.

        See you soon on %(app_title)s.
        """) % subst

        return mail_sender.send(_("%(app_title)s: Password Reset") % subst,
                                [self.user.email],
                                textwrap.dedent(message).strip())


@presentation.render_for(PasswordResetConfirmation, model='success')
def render_password_reset_confirmation_failure(self, h, comp, *args):
    """Renders a password change acknowledgment message"""
    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Change password'))

        with h.div(class_='container'):
            with h.h3:
                h << _("Password change successful!")

            with h.p:
                h << _("""Your password have been changed successfully.""")

            with h.form:
                with h.div(class_='actions'):
                    h << h.input(type='submit',
                                 class_='btn btn-primary',
                                 value=_("Ok")).action(comp.answer)
    return h.root


@presentation.render_for(PasswordResetConfirmation, model='email')
def render_password_reset_confirmation_mail_sent(self, h, comp, *args):
    """Renders a password change acknowledgment message"""
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        with h.h3:
            h << _("Email sent!")

        with h.p:
            h << _("""An email has been sent!""")

        with h.form:
            with h.div(class_='actions'):
                h << h.input(type='submit',
                             class_='btn btn-primary',
                             value=_("Ok")).action(comp.answer)
    return h.root


@presentation.render_for(PasswordResetConfirmation, model='send_email_failed')
def render_registration_confirmation_mail_error(self, h, comp, *args):
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        with h.h3:
            h << _("SMTP Error!")

        with h.p:
            h << _("""We were unable to send the confirmation email. Please contact the site administrator.""")

        with h.form:
            with h.div(class_='actions'):
                h << h.input(type='submit',
                             class_='btn btn-primary',
                             value=_("Ok")).action(comp.answer)

    return h.root


@presentation.render_for(PasswordResetConfirmation, model='failure')
def render_password_reset_confirmation_failure(self, h, comp, *args):
    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Change password'))
            h << h.small(_("Password reset failure!"), class_='error')
        with h.div(class_='container'):
            with h.p:
                h << _("""Password reset failure! Please try again.""")

            with h.form:
                with h.div(class_='actions'):
                    h << h.input(type='submit',
                                 class_='btn btn-primary',
                                 value=_("Ok")).action(comp.answer)

    return h.root


# ----------------------------------------------------------

class EmailConfirmation(object):

    """Confirm a user email address by sending an email to the user with a confirmation link."""

    def __init__(self, app_title, app_banner, theme, get_user, confirmation_base_url='', moderator=''):
        self.app_title = app_title
        self._get_user = get_user
        self.moderator = moderator
        self.confirmation_base_url = confirmation_base_url
        self.token_generator = TokenGenerator(self._get_user().username, 'email_confirmation')
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.alt_title = _('Sign up')

    @property
    def user(self):
        return self._get_user()

    def confirm_email_address(self, token):
        # too late, the user has been removed from the database
        if not self.user:
            return False

        # valid token?
        if not self.token_generator.check_token(token):
            return False

        self.user.confirm_email()

        return True

    def reset_token(self, token):
        return self.token_generator.reset_token(token)

    @property
    def confirmation_url(self):
        token = self.token_generator.create_token().token
        return '/'.join((self.confirmation_base_url, token))

    def send_email(self, mail_sender):
        '''
        If the email address ``true_dest`` is given, send the mail to
        that person instead of self.user.
        '''

        subst = dict(fullname=self.user.fullname,
                     login=self.user.username,
                     email=self.user.email_to_confirm,
                     app_title=self.app_title,
                     confirmation_url=self.confirmation_url)

        if self.moderator:
            message = _("""
            Hello moderator,

            %(fullname)s is willing to register an account (%(login)s) on the %(app_title)s application.

            Her (his) email address is %(email)s. Please verify it before accepting this account creation.

            In order to confirm that email address and accept the user's registration,
            please click on this confirmation link: %(confirmation_url)s.

            If you don't accept this request, feel free to ignore this email.
            Nothing will change into the user's pending account until your click
            on the confirmation link.

            Once confirmed, the user can login with her (his) login "%(login)s" and
            the password she (he) provided.
            """) % subst
            subject = _("%(app_title)s: Confirm user registration") % subst
        else:
            message = _("""
            Hello %(fullname)s,

            In order to confirm your email address in the %(app_title)s application,
            please click on this confirmation link: %(confirmation_url)s.

            If you are not at the origin of this request, feel free to ignore this
            email. Nothing will change into your account until your click on the
            confirmation link.

            Once confirmed, you can login with your login "%(login)s" and the password you just provided.

            See you soon on %(app_title)s.
            """) % subst
            subject = _("%(app_title)s: Confirm your email address") % subst

        return mail_sender.send(subject,
                                [self.moderator if self.moderator else self.user.email_to_confirm],
                                textwrap.dedent(message).strip())


@presentation.render_for(EmailConfirmation)
def render_registration_confirmation(self, h, comp, *args):
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        with h.h3:
            h << _("Registration request sent") if self.moderator else _("Confirm your email address!")

        with h.p:
            if self.moderator:
                h << _("""An email has been sent to the moderator. You will be contacted when
                your account is activated.""")
            else:
                h << _("""An email has been sent to your email address. You have to click on the
                confirmation link in this email in order to confirm your email address in the
                application.""")

        with h.form:
            with h.div(class_='actions'):
                h << h.input(type='submit',
                             class_='btn btn-primary',
                             value=_("Ok")).action(comp.answer)

    return h.root


@presentation.render_for(EmailConfirmation, 'send_email_failed')
def render_registration_confirmation_error(self, h, comp, *args):
    h << self.header.render(h, 'hide')
    with h.div(class_='regForm'):
        with h.h3:
            h << _("SMTP Error!")

        with h.p:
            h << _("""We were unable to send the confirmation email. Please contact the site administrator.""")

        with h.form:
            with h.div(class_='actions'):
                h << h.input(type='submit',
                             class_='btn btn-primary',
                             value=_("Ok")).action(comp.answer)

    return h.root


# ----------------------------------------------------------

class EmailInvitation(object):

    """Send invitation email to the user with a confirmation link."""

    def __init__(self, app_title, app_banner, theme, email, host, board, confirmation_base_url):
        """ Initialization method

        In:
         - ``email`` -- email of the guest
         - ``home`` -- host (DataUser instance)
         - ``board`` -- target of invitation (DataBoard instance)
         - ``confirmation_base_url`` -- base url for confirmation link
        """
        self.app_title = app_title
        self.email = email
        self.confirmation_base_url = confirmation_base_url
        self.host = host
        self.board = board
        self.header = component.Component(Header(app_title, app_banner, theme))
        self.token_generator = TokenGenerator(email,
                                              u'invite board %s' % board.id, expiration_delay=timedelta(days=2))

    @property
    def confirmation_url(self):
        who = usermanager.UserManager.get_by_email(self.email)
        if who is None:
            token = self.token_generator.create_token()
            self.board.pending.append(token)
        else:
            self.board.add_member(who)
        return u'/'.join((self.confirmation_base_url, self.board.url))

    def send_email(self, mail_sender):
        subst = dict(app_title=self.app_title,
                     board_title=self.board.title,
                     email=self.email,
                     host_name=self.host.fullname,
                     host_email=self.host.email,
                     confirmation_url=self.confirmation_url)

        message = _("""
        Hello,

        You were invited by %(host_name)s (%(host_email)s) to the board "%(board_title)s".

        To view the board, click on this link : %(confirmation_url)s.

        If you don't already have an account, register with email %(email)s and you will automatically join the board.

        See you soon on %(app_title)s.
        """) % subst

        return mail_sender.send(_('%(app_title)s: Invitation to the board "%(board_title)s"') % subst,
                                [self.email],
                                textwrap.dedent(message).strip())

# ----------------------------------------------------------


class RegistrationTask(component.Task):

    """A task that handles the user registration process"""

    def __init__(self, app_title, app_banner, theme, mail_sender_service, assets_manager_service,
                 moderator='', identicons=False, username=''):
        '''
        Register a new user (`username` not provided)
        or register email for an existing unconfirmed user (`username` provided).
        If `moderator` is set to an email address, all confirmation requests are sent there instead of user's email.
        '''
        self.app_title = app_title
        self.app_banner = app_banner
        self.theme = theme
        self.mail_sender = mail_sender_service
        self.assets_manager = assets_manager_service
        self.moderator = moderator
        self.identicons = identicons
        self.state = None  # task state, initialized by a URL rule
        self.user_manager = usermanager.UserManager()
        self.username = username
        self.alt_title = _(u'Sign up')

    def _create_email_confirmation(self, username, confirmation_base_url=''):
        confirmation_url = '/'.join((confirmation_base_url, 'register', username))
        get_user = lambda: usermanager.UserManager.get_by_username(username)
        return EmailConfirmation(self.app_title, self.app_banner, self.theme, get_user, confirmation_url, self.moderator)

    def go(self, comp):
        if not self.state:
            # step 1:
            # - ask the user to fill a registration form
            # - send him a confirmation email
            if self.username:
                username, application_url = comp.call(
                    EmailRegistrationForm(
                        self.app_title,
                        self.app_banner,
                        self.theme,
                        self.username
                    )
                )
            else:
                username, application_url = comp.call(RegistrationForm(
                    self.app_title, self.app_banner, self.theme))
            if username:
                if self.identicons:
                    appuser = self.user_manager.get_app_user(username)
                    appuser.reset_avatar(self.assets_manager)
                confirmation = self._create_email_confirmation(username, application_url)
                if confirmation.send_email(self.mail_sender):
                    comp.call(confirmation)
                else:
                    comp.call(confirmation, model='send_email_failed')
        else:
            # step 2: the user confirms his email by clicking on the
            # confirmation link
            # - check the token (to avoid cheating)
            # - show a confirmation screen
            username, token = self.state

            confirmation = self._create_email_confirmation(username)
            if confirmation.confirm_email_address(token):
                log.debug(_("Registration successful for user %s") % username)
                base_url = comp.call(RegistrationConfirmation(self.app_title, self.app_banner, self.theme), model='success')
            else:
                log.debug(_("Registration failure for user %s") % username)
                base_url = comp.call(RegistrationConfirmation(self.app_title, self.app_banner, self.theme), model='failure')

            redirect_to(base_url)

# ----------------------------------------------------------


class TokenGenerator(object):

    """Generate or check tokens"""

    def __init__(self, username, action, expiration_delay=UserConfirmationTimeout):
        """Create a token generator"""
        self.action = action
        self.username = username
        self.expiration_delay = expiration_delay

    def create_token(self):
        """Create a time-stamped token"""

        token = unicode(
            hashlib.sha512(str(time.time()) + self.username).hexdigest())

        DataToken.delete_by_username(self.username, self.action)
        # create new one
        token_instance = DataToken.new(token, self.username, self.action)
        return token_instance

    def get_tokens(self):
        query = DataToken.query.filter_by(username=self.username)
        query = query.filter(DataToken.action.like(self.action + '%'))
        return query

    def check_token(self, token):
        """Check that the token is valid"""
        t = DataToken.get(token)
        return t and datetime.now() <= (t.date + self.expiration_delay)

    def reset_token(self, token):
        DataToken.get(token).delete()

# ----------------------------------------------------------


class PasswordResetTask(component.Task):

    """A task that handles the password reset process"""

    def __init__(self, app_title, app_banner, theme, mail_sender_service):
        """Be careful! The confirmation URL *should* be rooted"""
        self.app_title = app_title
        self.app_banner = app_banner
        self.theme = theme
        self.mail_sender = mail_sender_service
        self.state = None  # task state, initialized by a URL rule
        self.user_manager = usermanager.UserManager()
        self.alt_title = _(u'Reset password')

    def _get_user(self, username):
        return usermanager.UserManager.get_by_username(username)

    def _create_password_reset_confirmation(self, username, confirmation_base_url):
        return PasswordResetConfirmation(self.app_title, self.app_banner, self.theme,
                                         lambda: self._get_user(username),
                                         confirmation_base_url)

    def go(self, comp):
        if not self.state:
            # step 1:
            # - ask the user email
            # - send him a confirmation email
            username, application_url = comp.call(PasswordResetForm(self.app_title,
                                                   self.app_banner,
                                                   self.theme,
                                                   self._get_user))
            if username:
                confirmation = self._create_password_reset_confirmation(
                    username, application_url)
                if confirmation.send_email(self.mail_sender):
                    comp.call(confirmation, model='email')
                else:
                    comp.call(confirmation, model='send_email_failed')
                redirect_to(application_url)
        else:
            # step 2: the user clicked on the confirmation link on his email
            # - check the token (to avoid cheating)
            # - ask for the new password
            username, token, application_url = self.state

            confirmation = self._create_password_reset_confirmation(username, application_url)
            if security.get_user() is None and confirmation.confirm_password_reset(token):
                log.debug(_("Resetting the password for user %s") % username)
                ret = comp.call(PasswordEditor(self.app_title, self.app_banner, self.theme,
                                lambda username=username: self._get_user(username),
                                check_old_password=False))
                if ret:
                    confirmation.reset_token(token)
                    comp.call(confirmation, model='success')

            else:
                log.debug(_("Password reset failure for user %s") % username)
                comp.call(confirmation, model='failure')

            redirect_to(application_url)


@presentation.init_for(PasswordResetTask, "len(url) == 2")
def init_password_reset_task(self, url, comp, http_method, request):
    self.state = (url[0], url[1], request.application_url)


# ----------------------------------------------------------

class ChangeEmailConfirmation(object):

    def __init__(self, app_title, app_banner, theme, redirect_url='/'):
        self.redirect_url = redirect_url
        self.header = component.Component(Header(app_title, app_banner, theme))


@presentation.render_for(ChangeEmailConfirmation, 'success')
@presentation.render_for(ChangeEmailConfirmation, 'failure')
def render_change_email_confirmation_success(self, h, comp, model):
    """Renders an email change acknowledgment message"""
    with h.body(class_='body-login'):
        h << self.header
        with h.div(class_='title'):
            h << h.h2(_(u'Change email'))

        with h.div(class_='container'):
            with h.h3:
                if model == 'success':
                    h << _("Email change successful!")
                else:
                    h << _("Email change failure!")

            with h.p:
                if model == 'success':
                    h << _("""Your email have been changed successfully.""")
                else:
                    h << _("""Email change failure! Please retry.""")

            with h.form:
                with h.div(class_='actions'):
                    h << h.input(type='submit',
                                 class_='btn btn-primary',
                                 value=_("Ok")).action(lambda: redirect_to(self.redirect_url))

    return h.root