Example #1
0
def restore_user_session(sio: ServerApp, encrypted_session: bytes,
                         session_id: Optional[int]):
    try:
        decrypted_session: bytes = sio.fernet_encrypt.decrypt(
            encrypted_session)
        session = json.loads(decrypted_session.decode("utf-8"))

        if "discord-access-token" in session:
            user, result = _create_session_with_discord_token(
                sio, session["discord-access-token"])
        else:
            user = User.get_by_id(session["user-id"])
            sio.save_session(session)
            result = _create_client_side_session(sio, user)

        if session_id is not None:
            sio.join_game_session(
                GameSessionMembership.get_by_ids(user.id, session_id))

        return result

    except UserNotAuthorized:
        raise

    except (KeyError, peewee.DoesNotExist, json.JSONDecodeError,
            InvalidTokenError) as e:
        # InvalidTokenError: discord token expired and couldn't renew
        logger().info("Client at %s was unable to restore session: (%s) %s",
                      sio.current_client_ip(), str(type(e)), str(e))
        raise InvalidSession()

    except Exception:
        logger().exception("Error decoding user session")
        raise InvalidSession()
Example #2
0
def _create_client_side_session(sio: ServerApp, user: Optional[User]) -> dict:
    """

    :param user: If the session's user was already retrieved, pass it along to avoid an extra query.
    :return:
    """
    session = sio.get_session()
    encrypted_session = sio.fernet_encrypt.encrypt(
        json.dumps(session).encode("utf-8"))
    if user is None:
        user = User.get_by_id(session["user-id"])
    elif user.id != session["user-id"]:
        raise RuntimeError(f"Provided user does not match the session's user")

    logger().info(
        f"Client at {sio.current_client_ip()} is user {user.name} ({user.id})."
    )

    return {
        "user":
        user.as_json,
        "sessions": [
            membership.session.create_list_entry()
            for membership in GameSessionMembership.select().where(
                GameSessionMembership.user == user)
        ],
        "encoded_session_b85":
        base64.b85encode(encrypted_session),
    }
Example #3
0
def restore_user_session(sio: ServerApp, encrypted_session: bytes,
                         session_id: Optional[int]):
    try:
        decrypted_session: bytes = sio.fernet_encrypt.decrypt(
            encrypted_session)
        session = json.loads(decrypted_session.decode("utf-8"))

        user = User.get_by_id(session["user-id"])

        if "discord-access-token" in session:
            # TODO: test if the discord access token is still valid
            flask.session["DISCORD_OAUTH2_TOKEN"] = session[
                "discord-access-token"]
        sio.get_server().save_session(flask.request.sid, session)

        if session_id is not None:
            sio.join_game_session(
                GameSessionMembership.get_by_ids(user.id, session_id))

        return _create_client_side_session(sio, user)

    except (KeyError, peewee.DoesNotExist, json.JSONDecodeError):
        raise InvalidSession()

    except Exception:
        logger().exception("Error decoding user session")
        raise InvalidSession()
Example #4
0
def _change_password(sio: ServerApp, session: GameSession, password: str):
    _verify_has_admin(sio, session.id, None)

    session.password = _hash_password(password)
    logger().info(f"{_describe_session(session)}: Changing password.")
    session.save()
    _add_audit_entry(sio, session, f"Changed password")
Example #5
0
def _finish_session(sio: ServerApp, session: GameSession):
    _verify_has_admin(sio, session.id, None)
    if session.state != GameSessionState.IN_PROGRESS:
        raise InvalidAction("Session is not in progress")

    session.state = GameSessionState.FINISHED
    logger().info(f"Session {session.id}: Finishing session.")
    session.save()
Example #6
0
def _finish_session(sio: ServerApp, session: GameSession):
    _verify_has_admin(sio, session.id, None)
    if session.state != GameSessionState.IN_PROGRESS:
        raise InvalidAction("Session is not in progress")

    session.state = GameSessionState.FINISHED
    logger().info(f"{_describe_session(session)}: Finishing session.")
    session.save()
    _add_audit_entry(sio, session, f"Finished session")
Example #7
0
def _change_title(sio: ServerApp, session: GameSession, title: str):
    _verify_has_admin(sio, session.id, None)

    old_name = session.name
    session.name = title
    logger().info(
        f"{_describe_session(session)}: Changed name from {old_name}.")
    session.save()
    _add_audit_entry(sio, session, f"Changed name from {old_name} to {title}")
Example #8
0
        def _handler(*args):
            try:
                return {
                    "result": handler(self, *args),
                }
            except BaseNetworkError as error:
                return error.as_json

            except Exception:
                logger().exception(f"Unhandled exception while processing request for message {message}. Args: {args}")
                return ServerError().as_json
Example #9
0
        def _handler(*args):
            try:
                return {
                    "result": handler(self, *args),
                }
            except BaseNetworkError as error:
                return error.as_json

            except Exception:
                logger().exception(
                    "Unexpected exception while processing request")
                return ServerError().as_json
Example #10
0
def _create_row(sio: ServerApp, session: GameSession, preset_json: dict):
    _verify_has_admin(sio, session.id, None)
    _verify_in_setup(session)
    _verify_no_layout_description(session)
    preset = _get_preset(preset_json)

    new_row_id = session.num_rows
    with database.db.atomic():
        logger().info(f"Session {session.id}: Creating row {new_row_id}.")
        GameSessionPreset.create(session=session,
                                 row=new_row_id,
                                 preset=json.dumps(preset.as_json))
Example #11
0
def _emit_game_session_pickups_update(sio: ServerApp,
                                      membership: GameSessionMembership):
    session: GameSession = membership.session

    if session.state == GameSessionState.SETUP:
        raise RuntimeError("Unable to emit pickups during SETUP")

    if membership.is_observer:
        raise RuntimeError("Unable to emit pickups for observers")

    description = session.layout_description
    row_to_member_name = {
        member.row: member.effective_name
        for member in GameSessionMembership.non_observer_members(session)
    }

    resource_database = _get_resource_database(description, membership.row)

    result = []
    actions: List[GameSessionTeamAction] = list(_query_for_actions(membership))
    for action in actions:
        pickup_target = _get_pickup_target(description, action.provider_row,
                                           action.provider_location_index)

        if pickup_target is None:
            logging.error(
                f"Action {action} has a location index with nothing.")
            result.append(None)
        else:
            name = row_to_member_name.get(action.provider_row,
                                          f"Player {action.provider_row + 1}")
            result.append({
                "provider_name":
                name,
                "pickup":
                _base64_encode_pickup(pickup_target.pickup, resource_database),
            })

    logger().info(
        f"{_describe_session(session, membership)} "
        f"notifying {resource_database.game_enum.value} of {len(result)} pickups."
    )

    data = {
        "game": resource_database.game_enum.value,
        "pickups": result,
    }
    flask_socketio.emit("game_session_pickups_update",
                        data,
                        room=f"game-session-{session.id}-{membership.user.id}")
Example #12
0
def game_session_admin_session(sio: ServerApp, session_id: int, action: str,
                               arg):
    action: SessionAdminGlobalAction = SessionAdminGlobalAction(action)
    session: database.GameSession = database.GameSession.get_by_id(session_id)

    if action == SessionAdminGlobalAction.CREATE_ROW:
        _create_row(sio, session, arg)

    elif action == SessionAdminGlobalAction.CHANGE_ROW:
        _change_row(sio, session, arg)

    elif action == SessionAdminGlobalAction.DELETE_ROW:
        _delete_row(sio, session, arg)

    elif action == SessionAdminGlobalAction.UPDATE_LAYOUT_GENERATION:
        _update_layout_generation(sio, session, arg)

    elif action == SessionAdminGlobalAction.CHANGE_LAYOUT_DESCRIPTION:
        _change_layout_description(sio, session, arg)

    elif action == SessionAdminGlobalAction.DOWNLOAD_LAYOUT_DESCRIPTION:
        return _download_layout_description(sio, session)

    elif action == SessionAdminGlobalAction.START_SESSION:
        _start_session(sio, session)

    elif action == SessionAdminGlobalAction.FINISH_SESSION:
        _finish_session(sio, session)

    elif action == SessionAdminGlobalAction.RESET_SESSION:
        _reset_session(sio, session)

    elif action == SessionAdminGlobalAction.CHANGE_PASSWORD:
        _change_password(sio, session, arg)

    elif action == SessionAdminGlobalAction.CHANGE_TITLE:
        _change_title(sio, session, arg)

    elif action == SessionAdminGlobalAction.DUPLICATE_SESSION:
        return _duplicate_session(sio, session, arg)

    elif action == SessionAdminGlobalAction.DELETE_SESSION:
        logger().info(f"{_describe_session(session)}: Deleting session.")
        session.delete_instance(recursive=True)

    elif action == SessionAdminGlobalAction.REQUEST_PERMALINK:
        return _get_permalink(sio, session)

    _emit_session_meta_update(session)
Example #13
0
    def verify_user(self, user_id: int) -> bool:
        r = self.session.get(
            "https://discordapp.com/api/guilds/{}/members/{}".format(
                self.guild_id, user_id))
        try:
            result = r.json()
            if r.ok:
                return self.role_id in result["roles"]
            else:
                logger().warning("Unable to verify user %s: %s", user_id,
                                 r.text)
                return False

        except requests.RequestException as e:
            logger().warning("Unable to verify user %s: %s / %s", user_id,
                             r.text, str(e))
            return True
Example #14
0
        def _handler(*args):
            if with_header_check:
                error_msg = self.check_client_headers()
                if error_msg is not None:
                    return UnsupportedClient(error_msg).as_json

            try:
                return {
                    "result": handler(self, *args),
                }
            except BaseNetworkError as error:
                return error.as_json

            except (Exception, TypeError):
                logger().exception(
                    f"Unhandled exception while processing request for message {message}. Args: {args}"
                )
                return ServerError().as_json
Example #15
0
def _start_session(sio: ServerApp, session: GameSession):
    _verify_has_admin(sio, session.id, None)
    _verify_in_setup(session)
    if session.layout_description_json is None:
        raise InvalidAction("Unable to start session, no game is available.")

    num_players = GameSessionMembership.select().where(
        GameSessionMembership.session == session,
        GameSessionMembership.row != None).count()
    expected_players = session.num_rows
    if num_players != expected_players:
        raise InvalidAction(
            f"Unable to start session, there are {num_players} but expected {expected_players} "
            f"({session.num_rows} x {session.num_teams}).")

    session.state = GameSessionState.IN_PROGRESS
    logger().info(f"Session {session.id}: Starting session.")
    session.save()
Example #16
0
def _update_layout_generation(sio: ServerApp, session: GameSession,
                              active: bool):
    _verify_has_admin(sio, session.id, None)
    _verify_in_setup(session)

    if active:
        if session.generation_in_progress is None:
            session.generation_in_progress = sio.get_current_user()
        else:
            raise InvalidAction(
                f"Generation already in progress by {session.generation_in_progress.name}."
            )
    else:
        session.generation_in_progress = None

    logger().info(
        f"{_describe_session(session)}: Making generation in progress to {session.generation_in_progress}."
    )
    session.save()
Example #17
0
def _delete_row(sio: ServerApp, session: GameSession, row_id: int):
    _verify_has_admin(sio, session.id, None)
    _verify_in_setup(session)
    _verify_no_layout_description(session)

    if session.num_rows < 2:
        raise InvalidAction("Can't delete row when there's only one")

    if row_id != session.num_rows - 1:
        raise InvalidAction(f"Can only delete the last row")

    with database.db.atomic():
        logger().info(f"{_describe_session(session)}: Deleting {row_id}.")
        GameSessionPreset.delete().where(
            GameSessionPreset.session == session,
            GameSessionPreset.row == row_id).execute()
        GameSessionMembership.update(row=None).where(
            GameSessionMembership.session == session.id,
            GameSessionMembership.row == row_id,
        ).execute()
Example #18
0
def _change_row(sio: ServerApp, session: GameSession, arg: Tuple[int, dict]):
    if len(arg) != 2:
        raise InvalidAction("Missing arguments.")
    row_id, preset_json = arg
    _verify_has_admin(sio, session.id, sio.get_current_user().id)
    _verify_in_setup(session)
    _verify_no_layout_description(session)
    preset = _get_preset(preset_json)

    try:
        with database.db.atomic():
            preset_row = GameSessionPreset.get(
                GameSessionPreset.session == session,
                GameSessionPreset.row == row_id)
            preset_row.preset = json.dumps(preset.as_json)
            logger().info(f"Session {session.id}: Changing row {row_id}.")
            preset_row.save()

    except peewee.DoesNotExist:
        raise InvalidAction(f"invalid row: {row_id}")
Example #19
0
def _create_session_with_discord_token(sio: ServerApp,
                                       access_token: str) -> Tuple[User, dict]:
    flask.session["DISCORD_OAUTH2_TOKEN"] = access_token
    discord_user = sio.discord.fetch_user()

    user: User = User.get_or_create(discord_id=discord_user.id,
                                    defaults={"name": discord_user.name})[0]
    if user.name != discord_user.name:
        user.name = discord_user.name
        user.save()

    if sio.enforce_role is not None:
        if not sio.enforce_role.verify_user(discord_user.id):
            logger().info(
                "User %s is not authorized for connecting to the server",
                discord_user.name)
            raise UserNotAuthorized()

    with sio.session() as session:
        session["user-id"] = user.id
        session["discord-access-token"] = access_token

    return user, _create_client_side_session(sio, user)
Example #20
0
def _collect_location(session: GameSession, membership: GameSessionMembership,
                      description: LayoutDescription,
                      pickup_location: int) -> Optional[int]:
    """
    Collects the pickup in the given location. Returns
    :param session:
    :param membership:
    :param description:
    :param pickup_location:
    :return: The rewarded player if some player must be updated of the fact.
    """
    player_row: int = membership.row
    pickup_target = _get_pickup_target(description, player_row,
                                       pickup_location)

    if pickup_target is None:
        logger().info(f"Session {session.id}, Row {membership.row} found item "
                      f"at {pickup_location}. It's an ETM.")
        return None

    if pickup_target.player == membership.row:
        logger().info(
            f"Session {session.id}, Row {membership.row} found item "
            f"at {pickup_location}. It's a {pickup_target.pickup.name} for themselves."
        )
        return None

    try:
        GameSessionTeamAction.create(
            session=session,
            provider_row=membership.row,
            provider_location_index=pickup_location,
            receiver_row=pickup_target.player,
        )
    except peewee.IntegrityError:
        # Already exists and it's for another player, no inventory update needed
        logger().info(
            f"Session {session.id}, Row {membership.row} found item "
            f"at {pickup_location}. It's a {pickup_target.pickup.name} for {pickup_target.player}, "
            f"but it was already collected.")
        return None

    logger().info(
        f"Session {session.id}, Row {membership.row} found item "
        f"at {pickup_location}. It's a {pickup_target.pickup.name} for {pickup_target.player}."
    )
    return pickup_target.player
Example #21
0
def game_session_collect_locations(sio: ServerApp, session_id: int,
                                   pickup_locations: Tuple[int, ...]):
    current_user = sio.get_current_user()
    session: GameSession = database.GameSession.get_by_id(session_id)
    membership = GameSessionMembership.get_by_ids(current_user.id, session_id)

    if session.state != GameSessionState.IN_PROGRESS:
        raise InvalidAction(
            "Unable to collect locations of sessions that aren't in progress")

    if membership.is_observer:
        raise InvalidAction("Observers can't collect locations")

    logger().info(
        f"{_describe_session(session, membership)} found items {pickup_locations}"
    )
    description = session.layout_description

    receiver_players = set()
    for location in pickup_locations:
        receiver_player = _collect_location(session, membership, description,
                                            location)
        if receiver_player is not None:
            receiver_players.add(receiver_player)

    if not receiver_players:
        return

    for receiver_player in receiver_players:
        try:
            receiver_membership = GameSessionMembership.get_by_session_position(
                session, row=receiver_player)
            _emit_game_session_pickups_update(sio, receiver_membership)
        except peewee.DoesNotExist:
            pass
    _emit_session_actions_update(session)
Example #22
0
def game_session_request_pickups(sio: ServerApp, session_id: int):
    current_user = sio.get_current_user()
    your_membership = GameSessionMembership.get_by_ids(current_user.id,
                                                       session_id)
    session: GameSession = your_membership.session

    if session.state == GameSessionState.SETUP:
        logger().info(f"Session {session_id}, Row {your_membership.row} "
                      f"requested pickups, but session is setup.")
        return None

    if your_membership.is_observer:
        logger().info(
            f"Session {session_id}, {current_user.name} requested pickups, but is an observer."
        )
        return None

    description = session.layout_description
    row_to_member_name = {
        member.row: member.effective_name
        for member in GameSessionMembership.non_observer_members(session)
    }

    resource_database = _get_resource_database(description,
                                               your_membership.row)

    result = []
    actions: List[GameSessionTeamAction] = list(
        _query_for_actions(your_membership))
    for action in actions:
        pickup_target = _get_pickup_target(description, action.provider_row,
                                           action.provider_location_index)

        if pickup_target is None:
            logging.error(
                f"Action {action} has a location index with nothing.")
            result.append(None)
        else:
            name = row_to_member_name.get(action.provider_row,
                                          f"Player {action.provider_row + 1}")
            result.append({
                "provider_name":
                name,
                "pickup":
                _base64_encode_pickup(pickup_target.pickup, resource_database),
            })

    logger().info(
        f"Session {session_id}, Row {your_membership.row} "
        f"requested pickups, returning {len(result)} elements for {resource_database.game_enum.value}."
    )

    return {
        "game": resource_database.game_enum.value,
        "pickups": result,
    }
Example #23
0
 def log(msg):
     logger().info(
         f"{_describe_session(session, membership)} found item at {pickup_location}. {msg}"
     )
Example #24
0
def game_session_admin_player(sio: ServerApp, session_id: int, user_id: int,
                              action: str, arg):
    _verify_has_admin(sio, session_id, user_id)
    action: SessionAdminUserAction = SessionAdminUserAction(action)

    session: GameSession = database.GameSession.get_by_id(session_id)
    membership = GameSessionMembership.get_by_ids(user_id, session_id)

    if action == SessionAdminUserAction.KICK:
        _add_audit_entry(
            sio, session, f"Kicked {membership.effective_name}"
            if membership.user != sio.get_current_user() else "Left session")
        membership.delete_instance()
        if not list(session.players):
            session.delete_instance(recursive=True)
            logger().info(
                f"{_describe_session(session)}. Kicking user {user_id} and deleting session."
            )
        else:
            logger().info(
                f"{_describe_session(session)}. Kicking user {user_id}.")

    elif action == SessionAdminUserAction.MOVE:
        offset: int = arg
        if membership.is_observer is None:
            raise InvalidAction("Player is an observer")

        new_row = membership.row + offset
        if new_row < 0:
            raise InvalidAction("New position is negative")
        if new_row >= session.num_rows:
            raise InvalidAction("New position is beyond num of rows")

        team_members = [None] * session.num_rows
        for member in GameSessionMembership.non_observer_members(session):
            team_members[member.row] = member

        while (0 <= new_row <
               session.num_rows) and team_members[new_row] is not None:
            new_row += offset

        if new_row < 0 or new_row >= session.num_rows:
            raise InvalidAction("No empty slots found in this direction")

        with database.db.atomic():
            logger().info(
                f"{_describe_session(session)}, User {user_id}. "
                f"Performing {action}, new row is {new_row}, from {membership.row}."
            )
            membership.row = new_row
            membership.save()

    elif action == SessionAdminUserAction.SWITCH_IS_OBSERVER:
        if membership.is_observer:
            membership.row = _find_empty_row(session)
        else:
            membership.row = None
        logger().info(
            f"{_describe_session(session)}, User {user_id}. Performing {action}, "
            f"new row is {membership.row}.")
        membership.save()

    elif action == SessionAdminUserAction.SWITCH_ADMIN:
        # Must be admin for this
        _verify_has_admin(sio, session_id, None, allow_when_no_admins=True)
        num_admins = GameSessionMembership.select().where(
            GameSessionMembership.session == session_id,
            GameSessionMembership.admin == True).count()

        if membership.admin and num_admins <= 1:
            raise InvalidAction("can't demote the only admin")

        membership.admin = not membership.admin
        _add_audit_entry(
            sio, session,
            f"Made {membership.effective_name} {'' if membership.admin else 'not '}an admin"
        )
        logger().info(
            f"{_describe_session(session)}, User {user_id}. Performing {action}, "
            f"new status is {membership.admin}.")
        membership.save()

    elif action == SessionAdminUserAction.CREATE_PATCHER_FILE:
        player_names = {i: f"Player {i + 1}" for i in range(session.num_rows)}

        for member in GameSessionMembership.non_observer_members(session):
            player_names[member.row] = member.effective_name

        layout_description = session.layout_description
        players_config = PlayersConfiguration(
            player_index=membership.row,
            player_names=player_names,
        )
        preset = layout_description.get_preset(players_config.player_index)
        cosmetic_patches = preset.game.data.layout.cosmetic_patches.from_json(
            arg)

        _add_audit_entry(sio, session,
                         f"Made an ISO for row {membership.row + 1}")

        data_factory = preset.game.patch_data_factory(layout_description,
                                                      players_config,
                                                      cosmetic_patches)
        try:
            return data_factory.create_data()
        except Exception as e:
            logger().exception("Error when creating patch data")
            raise InvalidAction(f"Unable to export game: {e}")

    elif action == SessionAdminUserAction.ABANDON:
        # FIXME
        raise InvalidAction("Abandon is NYI")

    _emit_session_meta_update(session)
Example #25
0
 def log(msg):
     logger().info(
         f"Session {session.id}, Row {membership.row} found item at {pickup_location}. {msg}"
     )
Example #26
0
def game_session_admin_player(sio: ServerApp, session_id: int, user_id: int,
                              action: str, arg):
    _verify_has_admin(sio, session_id, user_id)
    action: SessionAdminUserAction = SessionAdminUserAction(action)

    session: GameSession = database.GameSession.get_by_id(session_id)
    membership = GameSessionMembership.get_by_ids(user_id, session_id)

    if action == SessionAdminUserAction.KICK:
        membership.delete_instance()
        if not list(session.players):
            session.delete_instance(recursive=True)
            logger().info(
                f"Session {session_id}. Kicking user {user_id} and deleting session."
            )
        else:
            logger().info(f"Session {session_id}. Kicking user {user_id}.")

    elif action == SessionAdminUserAction.MOVE:
        offset: int = arg
        if membership.is_observer is None:
            raise InvalidAction("Player is an observer")

        new_row = membership.row + offset
        if new_row < 0:
            raise InvalidAction("New position is negative")
        if new_row >= session.num_rows:
            raise InvalidAction("New position is beyond num of rows")

        team_members = [None] * session.num_rows
        for member in GameSessionMembership.non_observer_members(session):
            team_members[member.row] = member

        while (0 <= new_row <
               session.num_rows) and team_members[new_row] is not None:
            new_row += offset

        if new_row < 0 or new_row >= session.num_rows:
            raise InvalidAction("No empty slots found in this direction")

        with database.db.atomic():
            logger().info(
                f"Session {session_id}, User {user_id}. "
                f"Performing {action}, new row is {new_row}, from {membership.row}."
            )
            membership.row = new_row
            membership.save()

    elif action == SessionAdminUserAction.SWITCH_IS_OBSERVER:
        if membership.is_observer:
            membership.row = _find_empty_row(session)
        else:
            membership.row = None
        logger().info(
            f"Session {session_id}, User {user_id}. Performing {action}, new row is {membership.row}."
        )
        membership.save()

    elif action == SessionAdminUserAction.SWITCH_ADMIN:
        # Must be admin for this
        _verify_has_admin(sio, session_id, None, allow_when_no_admins=True)
        num_admins = GameSessionMembership.select().where(
            GameSessionMembership.session == session_id,
            GameSessionMembership.admin == True).count()

        if membership.admin and num_admins <= 1:
            raise InvalidAction("can't demote the only admin")

        membership.admin = not membership.admin
        logger().info(
            f"Session {session_id}, User {user_id}. Performing {action}, new status is {membership.admin}."
        )
        membership.save()

    elif action == SessionAdminUserAction.CREATE_PATCHER_FILE:
        cosmetic_patches = CosmeticPatches.from_json_dict(arg)
        player_names = {i: f"Player {i + 1}" for i in range(session.num_rows)}

        for member in GameSessionMembership.non_observer_members(session):
            player_names[member.row] = member.effective_name

        players_config = PlayersConfiguration(
            player_index=membership.row,
            player_names=player_names,
        )
        return patcher_file.create_patcher_file(session.layout_description,
                                                players_config,
                                                cosmetic_patches)

    elif action == SessionAdminUserAction.ABANDON:
        # FIXME
        raise InvalidAction("Abandon is NYI")

    _emit_session_update(session)
Example #27
0
def _change_password(sio: ServerApp, session: GameSession, password: str):
    _verify_has_admin(sio, session.id, None)

    session.password = _hash_password(password)
    logger().info(f"Session {session.id}: Changing password.")
    session.save()