# -*- coding: utf-8 -*-
"""
| This file is part of the web2py Web Framework
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
"""
from gluon._compat import long
from gluon import current
from gluon.storage import Messages, Settings, Storage
from gluon.utils import web2py_uuid
from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, \
    IS_NOT_IN_DB
from pydal.objects import Table, Field, Row
import datetime
from gluon.settings import global_settings

DEFAULT = lambda: None


class AuthAPI(object):
    """
    AuthAPI is a barebones Auth implementation which does not have a concept of
    HTML forms or redirects, emailing or even an URL, you are responsible for
    all that if you use it.
    The main Auth functions such as login, logout, register, profile are designed
    in a Dict In -> Dict Out logic so, for instance, if you set
    registration_requires_verification you are responsible for sending the key to
    the user and even rolling back the transaction if you can't do it.

    NOTES: * It does not support all the callbacks Traditional Auth does yet.
             Some of the callbacks will not be supported.
             Check the method signatures to find out which ones are supported.
           * register_fields and profile_fields settings are ignored for now.

    WARNING: No builtin CSRF protection whatsoever.
    """

    default_settings = {
        'create_user_groups': 'user_%(id)s',
        'email_case_sensitive': False,
        'everybody_group_id': None,
        'expiration': 3600,
        'keep_session_onlogin': True,
        'keep_session_onlogout': False,
        'logging_enabled': True,
        'login_after_registration': False,
        'login_email_validate': True,
        'login_userfield': None,
        'logout_onlogout': None,
        'long_expiration': 3600 * 24 * 30,
        'ondelete': 'CASCADE',
        'password_field': 'password',
        'password_min_length': 4,
        'registration_requires_approval': False,
        'registration_requires_verification': False,
        'renew_session_onlogin': True,
        'renew_session_onlogout': True,
        'table_event_name': 'auth_event',
        'table_group_name': 'auth_group',
        'table_membership_name': 'auth_membership',
        'table_permission_name': 'auth_permission',
        'table_user_name': 'auth_user',
        'use_username': False,
        'username_case_sensitive': True
    }

    default_messages = {
        'add_group_log': 'Group %(group_id)s created',
        'add_membership_log': None,
        'add_permission_log': None,
        'change_password_log': 'User %(id)s Password changed',
        'del_group_log': 'Group %(group_id)s deleted',
        'del_membership_log': None,
        'del_permission_log': None,
        'email_taken': 'This email already has an account',
        'group_description': 'Group uniquely assigned to user %(id)s',
        'has_membership_log': None,
        'has_permission_log': None,
        'invalid_email': 'Invalid email',
        'key_verified': 'Key verified',
        'invalid_login': 'Invalid login',
        'invalid_password': 'Invalid password',
        'invalid_user': 'Invalid user',
        'invalid_key': 'Invalid key',
        'invalid_username': 'Invalid username',
        'logged_in': 'Logged in',
        'logged_out': 'Logged out',
        'login_failed_log': None,
        'login_log': 'User %(id)s Logged-in',
        'logout_log': 'User %(id)s Logged-out',
        'mismatched_password': "Password fields don't match",
        'password_changed': 'Password changed',
        'profile_log': 'User %(id)s Profile updated',
        'profile_updated': 'Profile updated',
        'register_log': 'User %(id)s Registered',
        'registration_pending': 'Registration is pending approval',
        'registration_successful': 'Registration successful',
        'registration_verifying': 'Registration needs verification',
        'username_taken': 'Username already taken',
        'verify_log': 'User %(id)s verified registration key'
    }

    def __init__(self, db=None, hmac_key=None, signature=True):
        self.db = db
        session = current.session
        auth = session.auth
        self.user_groups = auth and auth.user_groups or {}
        now = current.request.now
        # if we have auth info
        #    if not expired it, used it
        #    if expired, clear the session
        # else, only clear auth info in the session
        if auth:
            delta = datetime.timedelta(days=0, seconds=auth.expiration)
            if auth.last_visit and auth.last_visit + delta > now:
                self.user = auth.user
                # this is a trick to speed up sessions to avoid many writes
                if (now - auth.last_visit).seconds > (auth.expiration // 10):
                    auth.last_visit = now
            else:
                self.user = None
                if session.auth:
                    del session.auth
                session.renew(clear_session=True)
        else:
            self.user = None
            if session.auth:
                del session.auth

        settings = self.settings = Settings(self.__class__.default_settings)
        settings.update(
            extra_fields={},
            hmac_key=hmac_key,
        )
        settings.lock_keys = True
        messages = self.messages = Messages(current.T)
        messages.update(self.default_messages)
        messages.lock_keys = True
        if signature is True:
            self.define_signature()
        else:
            self.signature = signature or None

    def __validate(self, value, requires):
        if not isinstance(requires, (list, tuple)):
            requires = [requires]
        for validator in requires:
            (value, error) = validator(value)
            if error:
                return (value, error)
        return (value, None)

    def _get_migrate(self, tablename, migrate=True):

        if type(migrate).__name__ == 'str':
            return (migrate + tablename + '.table')
        elif not migrate:
            return False
        else:
            return True

    def _get_user_id(self):
        """accessor for auth.user_id"""
        return self.user and self.user.id or None

    user_id = property(_get_user_id, doc="user.id or None")

    def table_user(self):
        return self.db[self.settings.table_user_name]

    def table_group(self):
        return self.db[self.settings.table_group_name]

    def table_membership(self):
        return self.db[self.settings.table_membership_name]

    def table_permission(self):
        return self.db[self.settings.table_permission_name]

    def table_event(self):
        return self.db[self.settings.table_event_name]

    def define_signature(self):
        db = self.db
        settings = self.settings
        request = current.request
        T = current.T
        reference_user = 'reference %s' % settings.table_user_name

        def lazy_user(auth=self):
            return auth.user_id

        def represent(id, record=None, s=settings):
            try:
                user = s.table_user(id)
                return '%s %s' % (user.get("first_name", user.get("email")),
                                  user.get("last_name", ''))
            except:
                return id
        ondelete = self.settings.ondelete
        self.signature = Table(
            self.db, 'auth_signature',
            Field('is_active', 'boolean',
                  default=True,
                  readable=False, writable=False,
                  label=T('Is Active')),
            Field('created_on', 'datetime',
                  default=request.now,
                  writable=False, readable=False,
                  label=T('Created On')),
            Field('created_by',
                  reference_user,
                  default=lazy_user, represent=represent,
                  writable=False, readable=False,
                  label=T('Created By'), ondelete=ondelete),
            Field('modified_on', 'datetime',
                  update=request.now, default=request.now,
                  writable=False, readable=False,
                  label=T('Modified On')),
            Field('modified_by',
                  reference_user, represent=represent,
                  default=lazy_user, update=lazy_user,
                  writable=False, readable=False,
                  label=T('Modified By'),  ondelete=ondelete))

    def define_tables(self, username=None, signature=None, migrate=None,
                      fake_migrate=None):
        """
        To be called unless tables are defined manually

        Examples:
            Use as::

                # defines all needed tables and table files
                # 'myprefix_auth_user.table', ...
                auth.define_tables(migrate='myprefix_')

                # defines all needed tables without migration/table files
                auth.define_tables(migrate=False)

        """

        db = self.db
        if migrate is None:
            migrate = db._migrate
        if fake_migrate is None:
            fake_migrate = db._fake_migrate

        settings = self.settings
        if username is None:
            username = settings.use_username
        else:
            settings.use_username = username

        if not self.signature:
            self.define_signature()
        if signature is True:
            signature_list = [self.signature]
        elif not signature:
            signature_list = []
        elif isinstance(signature, Table):
            signature_list = [signature]
        else:
            signature_list = signature
        self._table_signature_list = signature_list  # Should it defined in __init__ first??

        is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty)
        is_crypted = CRYPT(key=settings.hmac_key,
                           min_length=settings.password_min_length)
        is_unique_email = [
            IS_EMAIL(error_message=self.messages.invalid_email),
            IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name,
                         error_message=self.messages.email_taken)]
        if not settings.email_case_sensitive:
            is_unique_email.insert(1, IS_LOWER())
        if settings.table_user_name not in db.tables:
            passfield = settings.password_field
            extra_fields = settings.extra_fields.get(
                settings.table_user_name, []) + signature_list
            # cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables
            if username or settings.cas_provider:
                is_unique_username = \
                    [IS_MATCH('[\w\.\-]+', strict=True,
                              error_message=self.messages.invalid_username),
                     IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name,
                                  error_message=self.messages.username_taken)]
                if not settings.username_case_sensitive:
                    is_unique_username.insert(1, IS_LOWER())
                db.define_table(
                    settings.table_user_name,
                    Field('first_name', length=128, default='',
                          label=self.messages.label_first_name,
                          requires=is_not_empty),
                    Field('last_name', length=128, default='',
                          label=self.messages.label_last_name,
                          requires=is_not_empty),
                    Field('email', length=512, default='',
                          label=self.messages.label_email,
                          requires=is_unique_email),
                    Field('username', length=128, default='',
                          label=self.messages.label_username,
                          requires=is_unique_username),
                    Field(passfield, 'password', length=512,
                          readable=False, label=self.messages.label_password,
                          requires=[is_crypted]),
                    Field('registration_key', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_registration_key),
                    Field('reset_password_key', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_reset_password_key),
                    Field('registration_id', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_registration_id),
                    *extra_fields,
                    **dict(
                        migrate=self._get_migrate(settings.table_user_name,
                                                  migrate),
                        fake_migrate=fake_migrate,
                        format='%(username)s'))
            else:
                db.define_table(
                    settings.table_user_name,
                    Field('first_name', length=128, default='',
                          label=self.messages.label_first_name,
                          requires=is_not_empty),
                    Field('last_name', length=128, default='',
                          label=self.messages.label_last_name,
                          requires=is_not_empty),
                    Field('email', length=512, default='',
                          label=self.messages.label_email,
                          requires=is_unique_email),
                    Field(passfield, 'password', length=512,
                          readable=False, label=self.messages.label_password,
                          requires=[is_crypted]),
                    Field('registration_key', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_registration_key),
                    Field('reset_password_key', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_reset_password_key),
                    Field('registration_id', length=512,
                          writable=False, readable=False, default='',
                          label=self.messages.label_registration_id),
                    *extra_fields,
                    **dict(
                        migrate=self._get_migrate(settings.table_user_name,
                                                  migrate),
                        fake_migrate=fake_migrate,
                        format='%(first_name)s %(last_name)s (%(id)s)'))
        reference_table_user = 'reference %s' % settings.table_user_name
        if settings.table_group_name not in db.tables:
            extra_fields = settings.extra_fields.get(
                settings.table_group_name, []) + signature_list
            db.define_table(
                settings.table_group_name,
                Field('role', length=512, default='',
                      label=self.messages.label_role,
                      requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)),
                Field('description', 'text',
                      label=self.messages.label_description),
                *extra_fields,
                **dict(
                    migrate=self._get_migrate(
                        settings.table_group_name, migrate),
                    fake_migrate=fake_migrate,
                    format='%(role)s (%(id)s)'))
        reference_table_group = 'reference %s' % settings.table_group_name
        if settings.table_membership_name not in db.tables:
            extra_fields = settings.extra_fields.get(
                settings.table_membership_name, []) + signature_list
            db.define_table(
                settings.table_membership_name,
                Field('user_id', reference_table_user,
                      label=self.messages.label_user_id),
                Field('group_id', reference_table_group,
                      label=self.messages.label_group_id),
                *extra_fields,
                **dict(
                    migrate=self._get_migrate(
                        settings.table_membership_name, migrate),
                    fake_migrate=fake_migrate))
        if settings.table_permission_name not in db.tables:
            extra_fields = settings.extra_fields.get(
                settings.table_permission_name, []) + signature_list
            db.define_table(
                settings.table_permission_name,
                Field('group_id', reference_table_group,
                      label=self.messages.label_group_id),
                Field('name', default='default', length=512,
                      label=self.messages.label_name,
                      requires=is_not_empty),
                Field('table_name', length=512,
                      label=self.messages.label_table_name),
                Field('record_id', 'integer', default=0,
                      label=self.messages.label_record_id,
                      requires=IS_INT_IN_RANGE(0, 10 ** 9)),
                *extra_fields,
                **dict(
                    migrate=self._get_migrate(
                        settings.table_permission_name, migrate),
                    fake_migrate=fake_migrate))
        if settings.table_event_name not in db.tables:
            db.define_table(
                settings.table_event_name,
                Field('time_stamp', 'datetime',
                      default=current.request.now,
                      label=self.messages.label_time_stamp),
                Field('client_ip',
                      default=current.request.client,
                      label=self.messages.label_client_ip),
                Field('user_id', reference_table_user, default=None,
                      label=self.messages.label_user_id),
                Field('origin', default='auth', length=512,
                      label=self.messages.label_origin,
                      requires=is_not_empty),
                Field('description', 'text', default='',
                      label=self.messages.label_description,
                      requires=is_not_empty),
                *settings.extra_fields.get(settings.table_event_name, []),
                **dict(
                    migrate=self._get_migrate(
                        settings.table_event_name, migrate),
                    fake_migrate=fake_migrate))

        return self

    def log_event(self, description, vars=None, origin='auth'):
        """
        Examples:
            Use as::

                auth.log_event(description='this happened', origin='auth')

        """
        if not self.settings.logging_enabled or not description:
            return
        elif self.is_logged_in():
            user_id = self.user.id
        else:
            user_id = None  # user unknown
        vars = vars or {}
        # log messages should not be translated
        if type(description).__name__ == 'lazyT':
            description = description.m
        if not user_id or self.table_user()[user_id]:
            self.table_event().insert(
                description=str(description % vars), origin=origin, user_id=user_id)

    def id_group(self, role):
        """
        Returns the group_id of the group specified by the role
        """
        rows = self.db(self.table_group().role == role).select()
        if not rows:
            return None
        return rows[0].id

    def user_group(self, user_id=None):
        """
        Returns the group_id of the group uniquely associated to this user
        i.e. `role=user:[user_id]`
        """
        return self.id_group(self.user_group_role(user_id))

    def user_group_role(self, user_id=None):
        if not self.settings.create_user_groups:
            return None
        if user_id:
            user = self.table_user()[user_id]
        else:
            user = self.user
        return self.settings.create_user_groups % user

    def add_group(self, role, description=''):
        """
        Creates a group associated to a role
        """
        group_id = self.table_group().insert(role=role, description=description)
        self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role))
        return group_id

    def del_group(self, group_id):
        """
        Deletes a group
        """
        self.db(self.table_group().id == group_id).delete()
        self.db(self.table_membership().group_id == group_id).delete()
        self.db(self.table_permission().group_id == group_id).delete()
        if group_id in self.user_groups:
            del self.user_groups[group_id]
        self.log_event(self.messages.del_group_log, dict(group_id=group_id))

    def update_groups(self):
        if not self.user:
            return
        user_groups = self.user_groups = {}
        if current.session.auth:
            current.session.auth.user_groups = self.user_groups
        table_group = self.table_group()
        table_membership = self.table_membership()
        memberships = self.db(
            table_membership.user_id == self.user.id).select()
        for membership in memberships:
            group = table_group(membership.group_id)
            if group:
                user_groups[membership.group_id] = group.role

    def add_membership(self, group_id=None, user_id=None, role=None):
        """
        Gives user_id membership of group_id or role
        if user is None than user_id is that of current logged in user
        """

        group_id = group_id or self.id_group(role)
        try:
            group_id = int(group_id)
        except:
            group_id = self.id_group(group_id)  # interpret group_id as a role
        if not user_id and self.user:
            user_id = self.user.id
        if not group_id:
            raise ValueError('group_id not provided or invalid')
        if not user_id:
            raise ValueError('user_id not provided or invalid')
        membership = self.table_membership()
        db = membership._db
        record = db((membership.user_id == user_id) &
                    (membership.group_id == group_id),
                    ignore_common_filters=True).select().first()
        if record:
            if hasattr(record, 'is_active') and not record.is_active:
                record.update_record(is_active=True)
            return record.id
        else:
            id = membership.insert(group_id=group_id, user_id=user_id)
        if role and user_id == self.user_id:
            self.user_groups[group_id] = role
        else:
            self.update_groups()
        self.log_event(self.messages['add_membership_log'],
                       dict(user_id=user_id, group_id=group_id))
        return id

    def del_membership(self, group_id=None, user_id=None, role=None):
        """
        Revokes membership from group_id to user_id
        if user_id is None than user_id is that of current logged in user
        """

        group_id = group_id or self.id_group(role)
        try:
            group_id = int(group_id)
        except:
            group_id = self.id_group(group_id)  # interpret group_id as a role
        if not user_id and self.user:
            user_id = self.user.id
        membership = self.table_membership()
        self.log_event(self.messages['del_membership_log'],
                       dict(user_id=user_id, group_id=group_id))
        ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete()
        if group_id in self.user_groups and user_id == self.user_id:
            del self.user_groups[group_id]
        return ret

    def has_membership(self, group_id=None, user_id=None, role=None, cached=False):
        """
        Checks if user is member of group_id or role

        NOTE: To avoid database query at each page load that use auth.has_membership, someone can use cached=True.
              If cached is set to True has_membership() check group_id or role only against auth.user_groups variable
              which is populated properly only at login time. This means that if an user membership change during a
              given session the user has to log off and log in again in order to auth.user_groups to be properly
              recreated and reflecting the user membership modification. There is one exception to this log off and
              log in process which is in case that the user change his own membership, in this case auth.user_groups
              can be properly update for the actual connected user because web2py has access to the proper session
              user_groups variable. To make use of this exception someone has to place an "auth.update_groups()"
              instruction in his app code to force auth.user_groups to be updated. As mention this will only work if it
              the user itself that change it membership not if another user, let say an administrator, change someone
              else's membership.
        """
        if not user_id and self.user:
            user_id = self.user.id
        if cached:
            id_role = group_id or role
            r = (user_id and id_role in self.user_groups.values()) or (user_id and id_role in self.user_groups)
        else:
            group_id = group_id or self.id_group(role)
            try:
                group_id = int(group_id)
            except:
                group_id = self.id_group(group_id)  # interpret group_id as a role
            membership = self.table_membership()
            if group_id and user_id and self.db((membership.user_id == user_id) &
                                                (membership.group_id == group_id)).select():
                r = True
            else:
                r = False
        self.log_event(self.messages['has_membership_log'],
                       dict(user_id=user_id, group_id=group_id, check=r))
        return r

    def add_permission(self,
                       group_id,
                       name='any',
                       table_name='',
                       record_id=0,
                       ):
        """
        Gives group_id 'name' access to 'table_name' and 'record_id'
        """

        permission = self.table_permission()
        if group_id == 0:
            group_id = self.user_group()
        record = self.db((permission.group_id == group_id) &
                         (permission.name == name) &
                         (permission.table_name == str(table_name)) &
                         (permission.record_id == long(record_id)),
                         ignore_common_filters=True
                         ).select(limitby=(0, 1), orderby_on_limitby=False).first()
        if record:
            if hasattr(record, 'is_active') and not record.is_active:
                record.update_record(is_active=True)
            id = record.id
        else:
            id = permission.insert(group_id=group_id, name=name,
                                   table_name=str(table_name),
                                   record_id=long(record_id))
        self.log_event(self.messages['add_permission_log'],
                       dict(permission_id=id, group_id=group_id,
                            name=name, table_name=table_name,
                            record_id=record_id))
        return id

    def del_permission(self,
                       group_id,
                       name='any',
                       table_name='',
                       record_id=0,
                       ):
        """
        Revokes group_id 'name' access to 'table_name' and 'record_id'
        """

        permission = self.table_permission()
        self.log_event(self.messages['del_permission_log'],
                       dict(group_id=group_id, name=name,
                            table_name=table_name, record_id=record_id))
        return self.db(permission.group_id ==
                       group_id)(permission.name ==
                                 name)(permission.table_name ==
                                       str(table_name))(permission.record_id ==
                                                        long(record_id)).delete()

    def has_permission(self,
                       name='any',
                       table_name='',
                       record_id=0,
                       user_id=None,
                       group_id=None,
                       ):
        """
        Checks if user_id or current logged in user is member of a group
        that has 'name' permission on 'table_name' and 'record_id'
        if group_id is passed, it checks whether the group has the permission
        """

        if not group_id and self.settings.everybody_group_id and \
                self.has_permission(name, table_name, record_id, user_id=None,
                                    group_id=self.settings.everybody_group_id):
                return True

        if not user_id and not group_id and self.user:
            user_id = self.user.id
        if user_id:
            membership = self.table_membership()
            rows = self.db(membership.user_id == user_id).select(membership.group_id)
            groups = set([row.group_id for row in rows])
            if group_id and group_id not in groups:
                return False
        else:
            groups = set([group_id])
        permission = self.table_permission()
        rows = self.db(permission.name ==
                       name)(permission.table_name ==
                             str(table_name))(permission.record_id ==
                                              record_id).select(permission.group_id)
        groups_required = set([row.group_id for row in rows])
        if record_id:
            rows = self.db(permission.name ==
                           name)(permission.table_name ==
                                 str(table_name))(permission.record_id ==
                                                  0).select(permission.group_id)
            groups_required = groups_required.union(set([row.group_id for row in rows]))
        if groups.intersection(groups_required):
            r = True
        else:
            r = False
        if user_id:
            self.log_event(self.messages['has_permission_log'],
                           dict(user_id=user_id, name=name,
                                table_name=table_name, record_id=record_id))
        return r

    def is_logged_in(self):
        """
        Checks if the user is logged in and returns True/False.
        If so user is in auth.user as well as in session.auth.user
        """
        if self.user:
            return True
        return False

    def _update_session_user(self, user):
        if global_settings.web2py_runtime_gae:
            user = Row(self.table_user()._filter_fields(user, id=True))
            delattr(user, self.settings.password_field)
        else:
            user = Row(user)
            for key in list(user.keys()):
                value = user[key]
                if callable(value) or key == self.settings.password_field:
                    delattr(user, key)
        current.session.auth = Storage(user=user,
                                       last_visit=current.request.now,
                                       expiration=self.settings.expiration,
                                       hmac_key=web2py_uuid())
        return user

    def login_user(self, user):
        """
        Logins the `user = db.auth_user(id)`
        """
        user = self._update_session_user(user)
        if self.settings.renew_session_onlogin:
            current.session.renew(clear_session=not self.settings.keep_session_onlogin)
        self.user = user
        self.update_groups()

    def login(self, log=DEFAULT, **kwargs):
        """
        Login a user

        Keyword Args:
            username/email/name_of_your_username_field (string) - username
            password/name_of_your_passfield (string) - user's password
            remember_me (boolean) - extend the duration of the login to settings.long_expiration
        """
        settings = self.settings
        session = current.session
        table_user = self.table_user()

        if 'username' in table_user.fields or \
                not settings.login_email_validate:
            userfield_validator = IS_NOT_EMPTY(error_message=self.messages.is_empty)
            if not settings.username_case_sensitive:
                userfield_validator = [IS_LOWER(), userfield_validator]
        else:
            userfield_validator = IS_EMAIL(error_message=self.messages.invalid_email)
            if not settings.email_case_sensitive:
                userfield_validator = [IS_LOWER(), userfield_validator]

        passfield = settings.password_field

        if log is DEFAULT:
            log = self.messages['login_log']

        user = None

        # Setup the default field used for the userfield
        if self.settings.login_userfield:
            userfield = self.settings.login_userfield
        else:
            if 'username' in table_user.fields:
                userfield = 'username'
            else:
                userfield = 'email'

        # Get the userfield from kwargs and validate it
        userfield_value = kwargs.get(userfield)
        if userfield_value is None:
            raise KeyError('%s not found in kwargs' % userfield)

        validated, error = self.__validate(userfield_value, userfield_validator)

        if error:
            return {'errors': {userfield: error}, 'message': self.messages.invalid_login, 'user': None}

        # Get the user for this userfield and check it
        user = table_user(**{userfield: validated})

        if user is None:
            return {'errors': {userfield: self.messages.invalid_user},
                    'message': self.messages.invalid_login, 'user': None}

        if (user.registration_key or '').startswith('pending'):
            return {'errors': None, 'message': self.messages.registration_pending, 'user': None}
        elif user.registration_key in ('disabled', 'blocked'):
            return {'errors': None, 'message': self.messages.login_disabled, 'user': None}
        elif (user.registration_key is not None and user.registration_key.strip()):
            return {'errors': None, 'message': self.messages.registration_verifying, 'user': None}

        # Finally verify the password
        passfield = settings.password_field
        password = table_user[passfield].validate(kwargs.get(passfield, ''), None)[0]

        if password == user[passfield]:
            self.login_user(user)
            session.auth.expiration = \
                kwargs.get('remember_me', False) and \
                settings.long_expiration or \
                settings.expiration
            session.auth.remember_me = kwargs.get('remember_me', False)
            self.log_event(log, user)
            return {'errors': None, 'message': self.messages.logged_in,
                    'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
        else:
            self.log_event(self.messages['login_failed_log'], kwargs)
            return {'errors': {passfield: self.messages.invalid_password},
                    'message': self.messages.invalid_login, 'user': None}

    def logout(self, log=DEFAULT, onlogout=DEFAULT, **kwargs):
        """
        Logs out user
        """
        settings = self.settings
        session = current.session

        if onlogout is DEFAULT:
            onlogout = settings.logout_onlogout
        if onlogout:
            onlogout(self.user)
        if log is DEFAULT:
            log = self.messages['logout_log']
        if self.user:
            self.log_event(log, self.user)

        session.auth = None
        self.user = None
        if settings.renew_session_onlogout:
            session.renew(clear_session=not settings.keep_session_onlogout)

        return {'errors': None, 'message': self.messages.logged_out, 'user': None}

    def register(self, log=DEFAULT, **kwargs):
        """
        Register a user.
        """

        table_user = self.table_user()
        settings = self.settings

        if self.is_logged_in():
            raise AssertionError('User trying to register is logged in')

        if log is DEFAULT:
            log = self.messages['register_log']

        if self.settings.login_userfield:
            userfield = self.settings.login_userfield
        elif 'username' in table_user.fields:
            userfield = 'username'
        else:
            userfield = 'email'

        # Ensure the username field is unique.
        unique_validator = IS_NOT_IN_DB(self.db, table_user[userfield])
        userfield_validator = table_user[userfield].requires
        if userfield_validator is None:
            userfield_validator = unique_validator
        elif isinstance(userfield_validator, (list, tuple)):
            if not any([isinstance(validator, IS_NOT_IN_DB) for validator in
                        userfield_validator]):
                if isinstance(userfield_validator, list):
                    userfield_validator.append(unique_validator)
                else:
                    userfield_validator += (unique_validator, )
        elif not isinstance(userfield_validator, IS_NOT_IN_DB):
            userfield_validator = [userfield_validator, unique_validator]
        table_user[userfield].requires = userfield_validator

        passfield = settings.password_field

        try:  # Make sure we have our original minimum length
            table_user[passfield].requires[-1].min_length = settings.password_min_length
        except:
            pass

        key = web2py_uuid()
        if settings.registration_requires_approval:
            key = 'pending-' + key

        table_user.registration_key.default = key

        result = table_user.validate_and_insert(**kwargs)
        if result.errors:
            return {'errors': result.errors.as_dict(), 'message': None, 'user': None}

        user = table_user[result.id]

        message = self.messages.registration_successful

        if settings.create_user_groups:
            d = user.as_dict()
            description = self.messages.group_description % d
            group_id = self.add_group(settings.create_user_groups % d, description)
            self.add_membership(group_id, result.id)

        if self.settings.everybody_group_id:
            self.add_membership(self.settings.everybody_group_id, result)

        if settings.registration_requires_verification:
            d = {k: user[k] for k in table_user.fields if table_user[k].readable}
            d['key'] = key
            if settings.login_after_registration and not settings.registration_requires_approval:
                self.login_user(user)
            return {'errors': None, 'message': None, 'user': d}

        if settings.registration_requires_approval:
            user.update_record(registration_key='pending')
            message = self.messages.registration_pending
        elif settings.login_after_registration:
            user.update_record(registration_key='')
            self.login_user(user)
            message = self.messages.logged_in

        self.log_event(log, user)

        return {'errors': None, 'message': message,
                'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}

    def profile(self, log=DEFAULT, **kwargs):
        """
        Lets the user change his/her profile
        """

        table_user = self.table_user()
        settings = self.settings
        table_user[settings.password_field].writable = False

        if not self.is_logged_in():
            raise AssertionError('User is not logged in')

        if not kwargs:
            user = table_user[self.user.id]
            return {'errors': None, 'message': None,
                    'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}

        result = self.db(table_user.id == self.user.id).validate_and_update(**kwargs)
        user = table_user[self.user.id]

        if result.errors:
            return {'errors': result.errors, 'message': None,
                    'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}

        if log is DEFAULT:
            log = self.messages['profile_log']

        self.log_event(log, user)
        self._update_session_user(user)
        return {'errors': None, 'message': self.messages.profile_updated,
                'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}

    def change_password(self, log=DEFAULT, **kwargs):
        """
        Lets the user change password

        Keyword Args:
            old_password (string) - User's current password
            new_password (string) - User's new password
            new_password2 (string) - Verify the new password
        """
        settings = self.settings
        messages = self.messages

        if not self.is_logged_in():
            raise AssertionError('User is not logged in')

        db = self.db
        table_user = self.table_user()
        s = db(table_user.id == self.user.id)

        request = current.request
        session = current.session
        passfield = settings.password_field

        requires = table_user[passfield].requires
        if not isinstance(requires, (list, tuple)):
            requires = [requires]
        requires = [t for t in requires if isinstance(t, CRYPT)]
        if requires:
            requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes
            requires[0].min_length = 0 # But do not enforce minimum length for the old password

        old_password = kwargs.get('old_password', '')
        new_password = kwargs.get('new_password', '')
        new_password2 = kwargs.get('new_password2', '')

        validator_old = requires
        validator_pass2 = IS_EQUAL_TO(new_password, error_message=messages.mismatched_password)

        old_password, error_old = self.__validate(old_password, validator_old)
        new_password2, error_new2 = self.__validate(new_password2, validator_pass2)

        errors = {}
        if error_old:
            errors['old_password'] = error_old
        if error_new2:
            errors['new_password2'] = error_new2
        if errors:
            return {'errors': errors, 'message': None}

        current_user = s.select(limitby=(0, 1), orderby_on_limitby=False).first()
        if not old_password == current_user[passfield]:
            return {'errors': {'old_password': messages.invalid_password}, 'message': None}
        else:
            d = {passfield: new_password}
            resp = s.validate_and_update(**d)
            if resp.errors:
                return {'errors': {'new_password': resp.errors[passfield]}, 'message': None}
            if log is DEFAULT:
                log = messages['change_password_log']
            self.log_event(log, self.user)
            return {'errors': None, 'message': messages.password_changed}

    def verify_key(self,
                   key=None,
                   ignore_approval=False,
                   log=DEFAULT,
                   ):
        """
        Verify a given registration_key actually exists in the user table.
        Resets the key to empty string '' or 'pending' if
        setttings.registration_requires_approval is true.

        Keyword Args:
            key (string) - User's registration key
        """
        table_user = self.table_user()
        user = table_user(registration_key=key)
        if (user is None) or (key is None):
            return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key}

        if self.settings.registration_requires_approval:
            user.update_record(registration_key='pending')
            result = {'errors': None, 'message': self.messages.registration_pending}
        else:
            user.update_record(registration_key='')
            result = {'errors': None, 'message': self.messages.key_verified}
        # make sure session has same user.registration_key as db record
        if current.session.auth and current.session.auth.user:
            current.session.auth.user.registration_key = user.registration_key
        if log is DEFAULT:
            log = self.messages['verify_log']
        self.log_event(log, user)
        return result
