###############################################################################
#
# Crossbar.io Master
# Copyright (c) typedef int GmbH. Licensed under EUPLv1.2.
#
###############################################################################

import os
import uuid
from datetime import datetime

import humanize
import zlmdb
from autobahn import util
from autobahn.twisted.wamp import ApplicationSession
from autobahn.wamp.exception import ApplicationError
from cfxdb.globalschema import GlobalSchema
from cfxdb.user import ActivationStatus, ActivationToken, ActivationType, User, UserRole

from crossbar._util import hl, hlid, hltype
from crossbar.master.node.messenger import Messenger

__all__ = ("Authenticator", "Principal")


class Principal(object):
    """ """

    def __init__(self, realm, authid, role, extra):
        """ """
        self.realm = realm
        self.authid = authid
        self.role = role
        self.extra = extra

    @staticmethod
    def parse(obj):
        realm = obj.get("realm", None)
        authid = obj.get("authid", None)
        role = obj.get("role", None)
        extra = obj.get("extra", None)
        return Principal(realm, authid, role, extra)


class Node(object):
    def __init__(self, node_id, heartbeat, heartbeat_time, status="online"):
        self.node_id = node_id
        self.heartbeat = heartbeat
        self.heartbeat_time = heartbeat_time
        self.status = status

    def marshal(self):
        return {
            "id": self.node_id,
            "heartbeat": self.heartbeat,
            "timestamp": self.heartbeat_time,
            "status": self.status,
        }

    @staticmethod
    def parse(data):
        assert isinstance(data, dict)

        node_id = data.get("node_id", None)
        heartbeat = data.get("heartbeat", None)
        heartbeat_time = data.get("heartbeat_time", None)
        status = data.get("status", None)

        # FIXME: check above

        node = Node(node_id, heartbeat, heartbeat_time, status)
        return node


class Authenticator(ApplicationSession):
    """
    Central CFC dynamic authenticator. This component is responsible for all
    frontend WAMP connections to CFC, for both CF nodes, CFC UI and user CFC scripts.
    """

    GLOBAL_USER_REALM = "com.crossbario.fabric"
    """
    Global users realm on Crossbar.io.
    """

    GLOBAL_USER_REALM_USER_ROLE = "user"
    """
    The WAMP authrole regular users get on the Crossbar.io domain (global) users
    realm. A role different from this only makes sense for Crossbar.io admins.
    """

    MREALM_USER_ROLES = ["guest", "developer", "operator", "admin", "owner"]
    """
    All permissible roles a user can take on a management realm. This is a fixed
    set hardwired into Crossbar.io!
    """

    MREALM_CREATOR_DEFAULT_ROLES = ["owner"]
    """
    THe set of roles a user creating a new management realm gets by default.
    """

    MREALM_NODE_ROLE = "node"
    """
    The (fixed) role a user node gets when joining the management realm it is
    paired to.
    """

    ERROR_AUTH_INVALID_PARAMETERS = "fabric.auth-failed.invalid-parameters"
    ERROR_AUTH_INVALID_PARAMETERS_MSG = "Invalid parameters in authentication: {}"

    ERROR_AUTH_PENDING_ACT = "fabric.auth-failed.pending-activation"
    ERROR_AUTH_PENDING_ACT_MSG = (
        "There is a pending activation (from {} ago) - please check your email inbox, or request a new code"
    )

    ERROR_AUTH_NO_PENDING_ACT = "fabric.auth-failed.no-pending-activation"
    ERROR_AUTH_NO_PENDING_ACT_MSG = (
        "There is no (pending) activation for this user/pubkey, but an activation code was provided"
    )

    ERROR_AUTH_INVALID_ACT_CODE = "fabric.auth-failed.invalid-activation-code"
    ERROR_AUTH_INVALID_ACT_CODE_MSG = "This activation code is invalid: {}"

    ERROR_AUTH_NODE_UNPAIRED = "fabric.auth-failed.node-unpaired"
    ERROR_AUTH_NODE_UNPAIRED_MSG = "This node is unpaired. Please pair the node with management realm first."

    ERROR_AUTH_NODE_ALREADY_CONNECTED = "fabric.auth-failed.node-already-connected"
    ERROR_AUTH_NODE_ALREADY_CONNECTED_MSG = "A node with this pubkey/node_id/authid is already connected."

    ERROR_AUTH_EMAIL_FAILURE = "fabric.auth-failed.email-failure"

    ERROR_AUTH_NEW_USER = "fabric.auth-failed.new-user-auth-code-sent"
    ERROR_AUTH_NEW_USER_MSG = "We have sent an authentication code to {email}."

    ERROR_AUTH_REGISTERED_USER = "fabric.auth-failed.registered-user-auth-code-sent"
    ERROR_AUTH_REGISTERED_USER_MSG = "We have sent an authentication code to {email}."

    def __init__(self, config):
        ApplicationSession.__init__(self, config)
        self._connected_nodes = {}

        # public keys of superusers
        self._superusers = []
        if "CROSSBAR_FABRIC_SUPERUSER" in os.environ:
            superuser_keyfile = os.environ["CROSSBAR_FABRIC_SUPERUSER"].strip()
            if superuser_keyfile:
                pubkey_file = os.path.abspath(os.environ["CROSSBAR_FABRIC_SUPERUSER"])
                if not os.path.exists(pubkey_file):
                    raise Exception(
                        "superuser public key file {} (set from CROSSBAR_FABRIC_SUPERUSER env var) does not exist".format(
                            pubkey_file
                        )
                    )
                with open(pubkey_file, "r") as f:
                    data = f.read()
                    pubkey_hex = None
                    for line in data.splitlines():
                        if line.startswith("public-key-ed25519"):
                            pubkey_hex = line.split(":")[1].strip()
                            break
                    if pubkey_hex is None:
                        raise Exception(
                            "no public key line found in super user public key file {}".format(pubkey_file)
                        )

                    self._superusers.append(pubkey_hex)
                    self.log.info(
                        hl(
                            "SUPERUSER public key {} loaded from {}".format(pubkey_hex, pubkey_file),
                            color="green",
                            bold=True,
                        )
                    )

    async def onJoin(self, details):
        """
        Authenticator has joined the global realm ("com.crossbario.fabric").

        :param details: Session details.
        :type details: :class:`autobahn.wamp.types.SessionDetails`
        """
        # create database and attach tables to database slots
        #
        cbdir = self.config.extra["cbdir"]
        config = self.config.extra.get("database", {})

        dbpath = config.get("path", ".db-controller")
        assert isinstance(dbpath, str)
        dbpath = os.path.join(cbdir, dbpath)

        maxsize = config.get("maxsize", 128 * 2**20)
        assert isinstance(maxsize, int)
        # allow maxsize 128kiB to 128GiB
        assert maxsize >= 128 * 1024 and maxsize <= 128 * 2**30

        self.log.debug(
            "{msg} [realm={realm}, session={session}, authid={authid}, authrole={authrole}, dbpath={dbpath}, maxsize={maxsize}]",
            realm=hlid(details.realm),
            session=details.session,
            authid=details.authid,
            authrole=hlid(details.authrole),
            msg=hl("Crossbar.io main authenticator starting ..", bold=True, color="green"),
            dbpath=hlid(dbpath),
            maxsize=hlid(maxsize),
        )

        # self._db = zlmdb.Database(dbpath=dbpath, maxsize=maxsize, readonly=False, sync=True, context=self)
        self._db = zlmdb.Database.open(dbpath=dbpath, maxsize=maxsize, readonly=False, sync=True, context=self)
        self._db.__enter__()
        self._schema = GlobalSchema.attach(self._db)

        # Mailgun access key
        #
        access_key = None
        if (
            "mailgun" in self.config.extra
            and "access_key" in self.config.extra["mailgun"]
            and self.config.extra["mailgun"]["access_key"]
        ):
            access_key = self.config.extra["mailgun"]["access_key"]
        else:
            if "MAILGUN_KEY" in os.environ:
                access_key = os.environ["MAILGUN_KEY"]
            else:
                self.log.warn("Mailgun access key unconfigured (not in config, and no env var MAILGUN_KEY set)")

        # Mailgun mail submit URL
        # eg https://api.mailgun.net/v3/mailing.crossbar.io/messages
        #
        submit_url = None
        if (
            "mailgun" in self.config.extra
            and "submit_url" in self.config.extra["mailgun"]
            and self.config.extra["mailgun"]["submit_url"]
        ):
            submit_url = self.config.extra["mailgun"]["submit_url"]
        else:
            if "MAILGUN_URL" in os.environ:
                submit_url = os.environ["MAILGUN_URL"]
            else:
                self.log.warn("Mailgun submit URL unconfigured (not in config, and no env var MAILGUN_URL set)")

        # Quick hack - the above code doesn't work properly
        access_key = os.environ.get("MAILGUN_KEY", access_key)
        submit_url = os.environ.get("MAILGUN_URL", submit_url)

        # create Email sender. if submit_url or access_key was None, then the Messenger
        # will not send out mails, but log them at level "warn"!
        #
        self._messenger = Messenger(submit_url, access_key)

        # register our dynamic authenticator
        try:
            await self.register(self._authenticate, "com.crossbario.fabric.authenticate")
        except Exception:
            self.log.failure("failed to register dynamic authenticator: {log_failure}")
            raise Exception("fatal: failed to register dynamic authenticator")

        # set service session to our authenticator
        self.config.controller.set_service_session(self, self._realm, self._authrole)

        self.log.info(
            'Ok, {component} ready on realm "{realm}" (session={session}, authid="{authid}", authrole="{authrole}", controller={controller}) {func}',
            component=hl("master node authenticator", color="green", bold=True),
            func=hltype(self.onJoin),
            realm=hlid(self._realm),
            session=hlid(self._session_id),
            authid=hlid(self._authid),
            authrole=hlid(self._authrole),
            controller=self.config.controller,
        )

    async def _authenticate(self, realm, authid, details):
        """
        Main authenticator for Crossbar.io. This authenticates both users and user nodes
        to Crossbar.io by authenticating against information stored in controller database.
        """
        self.log.debug("authenticate({realm}, {authid}, {details})", realm=realm, authid=authid, details=details)

        # proceed according to authmethod
        #
        if "authmethod" not in details:
            msg = 'missing "authmethod" in authentication details (WAMP HELLO message details)'
            raise ApplicationError(
                Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
            )

        authmethod = details["authmethod"]

        if authmethod not in ["cryptosign"]:
            msg = 'authmethod "{}" not permissible'.format(authmethod)
            raise ApplicationError(
                Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
            )

        if authmethod == "cryptosign":
            # extract mandatory public key
            #
            if "authextra" not in details or "pubkey" not in details["authextra"]:
                msg = "missing public key in authextra for authmethod cryptosign"
                raise ApplicationError(
                    Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                    Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
                )
            pubkey = details["authextra"]["pubkey"]

            # check requested authrole
            #
            authrole = details.get("authrole", None)
            if realm is None:
                # this is either a node joining a management realm or a user joining
                # the global user realm. a node must request authrole "node"
                if authrole not in [None, Authenticator.MREALM_NODE_ROLE]:
                    msg = 'invalid requested authrole "{}" for realm "{}"'.format(authrole, realm)
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
                    )

            elif realm == Authenticator.GLOBAL_USER_REALM:
                # this is a user joining the global user realm - authrole is assigned
                # automatically and cannot be requested explicitly
                if authrole not in [None, Authenticator.GLOBAL_USER_REALM_USER_ROLE]:
                    msg = 'invalid requested authrole "{}" for realm "{}"'.format(authrole, realm)
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
                    )
            else:
                # this is a user joining a specific management realm.
                if authrole is not None and authrole not in Authenticator.MREALM_USER_ROLES:
                    msg = 'invalid requested authrole "{}" for realm "{}"'.format(authrole, realm)
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
                    )

            # proceed depending on whether to authenticate a user or a node
            #
            if authrole == Authenticator.MREALM_NODE_ROLE:
                if realm is not None:
                    msg = 'invalid requested realm "{}" for node - nodes MUST NOT request a realm (the realm is auto-assigned based on Crossbar.io configuration)'.format(
                        realm
                    )
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS,
                        Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG.format(msg),
                    )

                # a node is authenticated based on the pubkey only, and realm/authid/authrole/..
                # are set from the configuration stored in controller database purely
                auth = self._auth_node(pubkey)
                return auth

            else:
                # a user may request to join different realms using the same pubkey, hence
                # we forward all info the client supplied
                activation_code = details["authextra"].get("activation_code", None)
                request_new_activation_code = details["authextra"].get("request_new_activation_code", False)
                auth = await self._auth_user(
                    realm, authid, authrole, pubkey, activation_code, request_new_activation_code
                )
                return auth

        # safeguard: should not arrive here
        raise Exception("logic error")

    def _auth_node(self, pubkey):
        """
        Authenticate a Crossbar.io node.

        A user node is purely authenticated based on the node's public key, MUST request
        the authrole="node", and MUST NOT request a specific realm (the management realm
        of a node is automatically assigned by Crossbar.io).
        """
        self.log.debug("authenticating node with pubkey={pubkey}", pubkey=pubkey)

        # lookup the node by its public key (in hex)
        node = None
        with self._db.begin() as txn:
            node_oid = self._schema.idx_nodes_by_pubkey[txn, pubkey]
            if node_oid:
                node = self._schema.nodes[txn, node_oid]

        if node and node.mrealm_oid:
            if node.mrealm_oid not in self._connected_nodes:
                self._connected_nodes[node.mrealm_oid] = {}

            # for nodes, realm + authid MUST be configured, and authextra MAY be configured
            # note that a authenticating node MUST NOT override the configured values for
            # realm, authid, etc  - this is critical. Eg the management realm a node is
            # assigned to must only be chanced under the control of the Crossbar.io
            # backend, since the owner of a management realm pays for the usage the nodes
            # assigned to do produce. And since a node MUST have the realm set (or the whole
            # record is deleted, and the public key "no longer known"), nodes are always
            # authenticated and associated with the correct realm.
            #
            with self._db.begin() as txn:
                mrealm = self._schema.mrealms[txn, node.mrealm_oid]
            if mrealm:
                # use the node name (which can change!)
                assigned_auth_id = node.authid

                # use the node UUID (which never changes)
                # assigned_auth_id = str(node_oid)

                auth = {
                    "pubkey": pubkey,
                    "realm": mrealm.name,
                    "authid": assigned_auth_id,
                    "role": Authenticator.MREALM_NODE_ROLE,
                    "extra": node.authextra,
                    "cache": False,
                }

                self._connected_nodes[node.mrealm_oid][node_oid] = auth

                self.log.info(
                    'authenticated managed node "{authid}" with pubkey "{pubkey}" on management realm "{realm}" (authrole="{authrole}") {func}',
                    pubkey=hlid("0x" + pubkey[:16] + ".."),
                    authid=hlid(auth["authid"]),
                    authrole=hlid(auth["role"]),
                    realm=hlid(auth["realm"]),
                    func=hltype(self._auth_node),
                )
                return auth

        # the node pairing process is completely driven from a logged in user session!
        # so either the node already is paired, or it is not - in which case, it needs
        # to be paired by a user, and there is nothing a node could do about or needs
        # to care, so we plainly bail out here ..
        #
        self.log.info("denied unpaired CF node with pubkey {pubkey}", pubkey=pubkey)
        raise ApplicationError(Authenticator.ERROR_AUTH_NODE_UNPAIRED, Authenticator.ERROR_AUTH_NODE_UNPAIRED_MSG)

    async def _auth_user(
        self, realm, authid, authrole, pubkey, activation_code=None, request_new_activation_code=False
    ):
        """
        Authenticate a Crossbar.io user.

        For a user that is not yet registered, or a user key not yet associated
        with a user, this will raise an ApplicationError signaling the state
        of the registration process.
        """
        self.log.debug(
            "authenticating user for realm={realm}, authid={authid}, authrole={authrole}, pubkey={pubkey}, activation_code={activation_code}, request_new_activation_code={request_new_activation_code}",
            realm=realm,
            authid=authid,
            authrole=authrole,
            pubkey=pubkey,
            activation_code=activation_code,
            request_new_activation_code=request_new_activation_code,
        )

        # we must protect against this!
        if activation_code and not authid:
            raise ApplicationError(
                Authenticator.ERROR_AUTH_INVALID_PARAMETERS, Authenticator.ERROR_AUTH_INVALID_PARAMETERS_MSG
            )

        # get activation for authid/pubkey pair (this will succeed when the user is registered and
        # the pubkey is associated with the user - the 99% case)
        activation = None
        with self._db.begin() as txn:
            oid = self._schema.idx_act_tokens_by_authid_pubkey[txn, authid + pubkey]
            if oid:
                activation = self._schema.activation_tokens[txn, oid]

        # if the activation is still pending, allow to reset it and we will send
        # a new activation code
        if activation and activation.status == ActivationStatus.PENDING and request_new_activation_code:
            with self._db.begin(write=True) as txn:
                oid = self._schema.idx_act_tokens_by_authid_pubkey[txn, authid + pubkey]
                if oid:
                    del self._schema.activation_tokens[txn, oid]
                else:
                    raise Exception("no such activation")
            activation = None

        # get user (if any)
        user = None
        if authid:
            with self._db.begin() as txn:
                oid = self._schema.idx_users_by_email[txn, authid]
                if oid:
                    user = self._schema.users[txn, oid]

        is_new_user = user is None

        self.log.debug(
            "authenticating user={user}, is_new_user={is_new_user}, activation={activation}",
            user=user,
            is_new_user=is_new_user,
            activation=activation,
        )

        if realm is None or realm == "com.crossbario.fabric":
            realm = Authenticator.GLOBAL_USER_REALM
            authrole = Authenticator.GLOBAL_USER_REALM_USER_ROLE
        else:
            if authrole is None:
                authrole = "owner"

        # superusers are treated special ..
        if pubkey in self._superusers:
            auth = {
                "pubkey": pubkey,
                "realm": realm,
                "authid": "superuser",
                "role": authrole,
                "extra": None,
                "cache": False,
            }
            self.log.info(
                hl(
                    "SUPERUSER authenticated (realm={}, authid={}, authrole={})".format(
                        auth["realm"], auth["authid"], auth["role"]
                    ),
                    color="green",
                    bold=True,
                )
            )
            return auth

        # if there is no activation yet, create/store a new one
        elif not activation:
            # check for user provided an activation code, though there is no activation currently
            if activation_code:
                raise ApplicationError(
                    Authenticator.ERROR_AUTH_NO_PENDING_ACT, Authenticator.ERROR_AUTH_NO_PENDING_ACT_MSG
                )

            # ok, create a new activation in the database
            activation = ActivationToken()
            activation.oid = uuid.uuid4()
            activation.atype = ActivationType.REGISTRATION if is_new_user else ActivationType.LOGIN
            activation.created = datetime.utcnow()
            # activation.activated = None
            activation.code = util.generate_activation_code()
            activation.status = ActivationStatus.PENDING
            activation.email = authid
            activation.pubkey = pubkey

            with self._db.begin(write=True) as txn:
                self._schema.activation_tokens[txn, activation.oid] = activation

            if is_new_user:
                # send user message with activation code
                await self._messenger.send_user_registration_mail(authid, activation.code)
                self.log.info(
                    "User registration mail sent to {authid} with activation code {activation_code}",
                    authid=hlid(authid),
                    activation_code=hl(activation.code, color="red", bold=True),
                )

                # deny authentication by raising an error and providing feedback to client
                raise ApplicationError(
                    Authenticator.ERROR_AUTH_NEW_USER,
                    Authenticator.ERROR_AUTH_NEW_USER_MSG.format(email=authid),
                    email=authid,
                )
            else:
                # send user message with activation code
                await self._messenger.send_user_login_mail(authid, activation.code)
                self.log.info(
                    "User login mail sent to {authid} with activation code {activation_code}",
                    authid=hlid(authid),
                    activation_code=hl(activation.code, color="red", bold=True),
                )

                # deny authentication by raising an error and providing feedback to client
                raise ApplicationError(
                    Authenticator.ERROR_AUTH_REGISTERED_USER,
                    Authenticator.ERROR_AUTH_REGISTERED_USER_MSG.format(email=authid),
                    email=authid,
                )
        else:
            self.log.debug("Activation found in database:\n{activation}", activation=activation)

            if activation.status == ActivationStatus.ACTIVE:
                # ok, so the user's public key is known and active .. the 99% case

                # user provided an activation code, though there is no activation currently
                if activation_code:
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_NO_PENDING_ACT, Authenticator.ERROR_AUTH_NO_PENDING_ACT_MSG
                    )

                # .. if the user wants to join the global users realm, allow that,
                # but ignore any authrole that might have been requested
                if realm is None or realm == Authenticator.GLOBAL_USER_REALM:
                    auth = {
                        "pubkey": pubkey,
                        "realm": Authenticator.GLOBAL_USER_REALM,
                        "authid": authid,
                        "role": Authenticator.GLOBAL_USER_REALM_USER_ROLE,
                        "extra": None,
                        "cache": False,
                    }
                    self.log.info(
                        "Found user {authid} with active pubkey, authenticating for global user realm", authid=authid
                    )

                    return auth

                # .. if the user wants to join a specific (management) realm, we need to check more ..
                else:
                    user_roles = None
                    with self._db.begin() as txn:
                        user_oid = self._schema.idx_users_by_email[txn, authid]
                        if user_oid:
                            mrealm_oid = self._schema.idx_mrealms_by_name[txn, realm]
                            if mrealm_oid:
                                user_roles = self._schema.users_mrealm_roles[txn, (user_oid, mrealm_oid)]
                                self.log.info("user roles {user_roles}", user_roles=user_roles)
                            else:
                                self.log.info('no mrealm with name "{realm}"', realm=realm)
                        else:
                            self.log.info('no user for authid "{authid}"', authid=authid)

                    if not user_roles:
                        raise Exception('no realm "{}" or user not permitted'.format(realm))
                    else:
                        self.log.info("user has {roles} roles on realm {realm}", roles=user_roles.roles, realm=realm)

                    if authrole is None:
                        # the user did not request a specific role, so take the first one?
                        # or take the role with the highest privileges? or lowest? FIXME
                        authrole = min(user_roles.roles)  # the minimum is the _highest_ permission (OWNER=1)
                    else:
                        MAP = {
                            "owner": UserRole.OWNER,
                            "admin": UserRole.ADMIN,
                            "user": UserRole.USER,
                            "guest": UserRole.GUEST,
                        }
                        authrole = MAP.get(authrole, None)

                    # the user requested a specific role: check that the role is in the
                    # list of permitted roles the user may take on the management realm
                    if authrole not in user_roles.roles:
                        raise Exception("not authorized for role {}".format(authrole))

                    # map the integer authrole to a string
                    MAP = {
                        UserRole.OWNER: "owner",
                        UserRole.ADMIN: "admin",
                        UserRole.USER: "user",
                        UserRole.GUEST: "guest",
                    }
                    authrole = MAP.get(authrole, None)

                    auth = {
                        "pubkey": pubkey,
                        "realm": realm,
                        "authid": authid,
                        "role": authrole,
                        "extra": None,
                        "cache": False,
                    }
                    self.log.info("auth=\n{auth}", auth=auth)

                    self.log.info(
                        'Authenticated CF user with pubkey {pubkey}.. as authid "{authid}" on realm "{realm}"',
                        authid=authid,
                        realm=realm,
                        pubkey=pubkey[:16],
                    )

                    return auth

            elif activation.status == ActivationStatus.PENDING:
                now = datetime.utcnow()

                passed_secs = (now - activation.created).total_seconds()
                passed_secs_str = humanize.naturaldelta(passed_secs)

                if not activation_code:
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_PENDING_ACT,
                        Authenticator.ERROR_AUTH_PENDING_ACT_MSG.format(passed_secs_str),
                    )

                if activation_code != activation.code:
                    msg = "code does not match pending one"
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE,
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE_MSG.format(msg),
                    )

                # check if the activation is expired (15min). if so, delete it, and bail out
                if passed_secs > (60 * 15):
                    with self._db.begin(write=True) as txn:
                        oid = self._schema.idx_act_tokens_by_authid_pubkey[txn, authid + pubkey]
                        if oid:
                            del self._schema.activation_tokens[txn, oid]
                        else:
                            raise Exception("no such activation")

                    msg = "code created {} ago has expired".format(passed_secs_str)
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE,
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE_MSG.format(msg),
                    )

                # sanitize the stored activation info against what the client provided
                if activation.atype not in [ActivationType.LOGIN, ActivationType.REGISTRATION]:
                    msg = 'activation type "{}" is not for user login/registration.'.format(activation.atype)
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE,
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE_MSG.format(msg),
                    )

                if activation.email != authid:
                    msg = "email associated with activation code does not match authid provided by client."
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE,
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE_MSG.format(msg),
                    )

                if activation.pubkey != pubkey:
                    msg = "pubkey associated with activation code does not match pubkey provided by client."
                    raise ApplicationError(
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE,
                        Authenticator.ERROR_AUTH_INVALID_ACT_CODE_MSG.format(msg),
                    )

                activation.status = ActivationStatus.ACTIVE
                activation.activated = now

                # create user
                if is_new_user:
                    user = User()
                    user.oid = uuid.uuid4()
                    user.email = authid
                    user.registered = datetime.utcnow()

                    with self._db.begin(write=True) as txn:
                        self._schema.users[txn, user.oid] = user

                    self.log.info("New user stored in database:\n{user}", user=user)
                else:
                    self.log.info("User already stored in database:\n{user}", user=user)

                # update user-pubkey activation
                with self._db.begin(write=True) as txn:
                    oid = self._schema.idx_act_tokens_by_authid_pubkey[txn, authid + pubkey]
                    if oid:
                        self._schema.activation_tokens[txn, oid] = activation
                    else:
                        raise Exception("no such activation")

                # immediately auth user on global users realm
                auth = {
                    "pubkey": pubkey,
                    "realm": Authenticator.GLOBAL_USER_REALM,
                    "authid": authid,
                    "role": Authenticator.GLOBAL_USER_REALM_USER_ROLE,
                    "extra": None,
                    "cache": False,
                }

                self.log.info(
                    "found principal for public key {pubkey} of {authid}", pubkey=pubkey, authid=auth["authid"]
                )

                return auth

            else:
                raise Exception("internal error: unprocessed activation status {}".format(activation.status))

    def _send_user_login_mail(self, receiver, activation_code):
        subject = "Crossbar.io: your LOGIN code"
        text = "We have received a login request for your account. Please use this activation code: {}".format(
            activation_code
        )
        return self._messenger.send_message(receiver, subject, text)

    def _send_user_registration_mail(self, receiver, activation_code):
        subject = "Crossbar.io: your REGISTRATION code"
        text = "We have received a registration request for your account. Please use this activation code: {}".format(
            activation_code
        )
        return self._messenger.send_message(receiver, subject, text)
