From 9e486fc2c02b981d97f675a1ee6f2fd525083ee6 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 17 Feb 2021 12:56:28 +0100 Subject: [PATCH] add alias transfer --- app/config.py | 3 + app/dashboard/__init__.py | 1 + .../dashboard/alias_transfer_receive.html | 46 ++++++ .../dashboard/alias_transfer_send.html | 43 ++++++ app/dashboard/templates/dashboard/index.html | 18 ++- app/dashboard/views/alias_transfer.py | 140 ++++++++++++++++++ tests/dashboard/test_alias_transfer.py | 41 +++++ 7 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 app/dashboard/templates/dashboard/alias_transfer_receive.html create mode 100644 app/dashboard/templates/dashboard/alias_transfer_send.html create mode 100644 app/dashboard/views/alias_transfer.py create mode 100644 tests/dashboard/test_alias_transfer.py diff --git a/app/config.py b/app/config.py index 731a923a..67d44418 100644 --- a/app/config.py +++ b/app/config.py @@ -171,6 +171,9 @@ FLASK_SECRET = os.environ["FLASK_SECRET"] SESSION_COOKIE_NAME = "slapp" MAILBOX_SECRET = FLASK_SECRET + "mailbox" CUSTOM_ALIAS_SECRET = FLASK_SECRET + "custom_alias" +ALIAS_TRANSFER_SECRET = os.environ.get("ALIAS_TRANSFER_SECRET") or ( + FLASK_SECRET + "alias_transfer" +) # AWS AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3" diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py index 8363aa5d..3852ef88 100644 --- a/app/dashboard/__init__.py +++ b/app/dashboard/__init__.py @@ -25,4 +25,5 @@ from .views import ( contact_detail, setup_done, batch_import, + alias_transfer, ) diff --git a/app/dashboard/templates/dashboard/alias_transfer_receive.html b/app/dashboard/templates/dashboard/alias_transfer_receive.html new file mode 100644 index 00000000..9a6d6c6a --- /dev/null +++ b/app/dashboard/templates/dashboard/alias_transfer_receive.html @@ -0,0 +1,46 @@ +{% extends 'default.html' %} + +{% set active_page = "dashboard" %} + +{% block title %} + Receive {{ alias.email }} +{% endblock %} + +{% block default_content %} +
+
+

Receive {{ alias.email }}

+ +

+ You are invited to become the owner of the alias {{ alias.email }} +

+ +

+ Please choose the mailbox(es) that owns this alias 👇 +

+ +
+ + + +
+ + +
+
+ +{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/app/dashboard/templates/dashboard/alias_transfer_send.html b/app/dashboard/templates/dashboard/alias_transfer_send.html new file mode 100644 index 00000000..fa24b13d --- /dev/null +++ b/app/dashboard/templates/dashboard/alias_transfer_send.html @@ -0,0 +1,43 @@ +{% extends 'default.html' %} + +{% set active_page = "dashboard" %} + +{% block title %} + Send {{ alias.email }} +{% endblock %} + +{% block default_content %} +
+
+

Transfer {{ alias.email }}

+ +

+ This page allows you to transfer {{ alias.email }} to another person so they can use it to receive and send + emails. +

+ +

+ In order to transfer ownership, + please send the following URL 👇 to the other person. +

+ + + {{ alias_transfer_url }} + + +

+ This person can then confirm the reception and become the owner of the alias. +

+
+ After the confirmation, you can no longer use this alias. +
+ + +
+
+ +{% endblock %} + diff --git a/app/dashboard/templates/dashboard/index.html b/app/dashboard/templates/dashboard/index.html index 837187ac..62aae8d5 100644 --- a/app/dashboard/templates/dashboard/index.html +++ b/app/dashboard/templates/dashboard/index.html @@ -416,20 +416,28 @@ -
-
+
+
+ + Transfer + + + +
- Delete    - + Delete    +
-
+
+ +
diff --git a/app/dashboard/views/alias_transfer.py b/app/dashboard/views/alias_transfer.py new file mode 100644 index 00000000..5eba6a9d --- /dev/null +++ b/app/dashboard/views/alias_transfer.py @@ -0,0 +1,140 @@ +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required, current_user +from itsdangerous import Signer + +from app.config import ALIAS_TRANSFER_SECRET +from app.config import URL +from app.dashboard.base import dashboard_bp +from app.extensions import db +from app.log import LOG +from app.models import ( + Alias, + Contact, + AliasUsedOn, + AliasMailbox, + User, + ClientUser, +) +from app.models import Mailbox + + +def transfer(alias, new_user, new_mailboxes: [Mailbox]): + # cannot transfer alias which is used for receiving newsletter + if User.get_by(newsletter_alias_id=alias.id): + raise Exception("Cannot transfer alias that's used to receive newsletter") + + # update user_id + db.session.query(Contact).filter(Contact.alias_id == alias.id).update( + {"user_id": new_user.id} + ) + + db.session.query(AliasUsedOn).filter(AliasUsedOn.alias_id == alias.id).update( + {"user_id": new_user.id} + ) + + db.session.query(ClientUser).filter(ClientUser.alias_id == alias.id).update( + {"user_id": new_user.id} + ) + + # remove existing mailboxes from the alias + db.session.query(AliasMailbox).filter(AliasMailbox.alias_id == alias.id).delete() + + # set mailboxes + alias.mailbox_id = new_mailboxes.pop().id + for mb in new_mailboxes: + AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id) + + # alias has never been transferred before + if not alias.original_owner_id: + alias.original_owner_id = alias.user_id + + # now the alias belongs to the new user + alias.user_id = new_user.id + + db.session.commit() + + +@dashboard_bp.route("/alias_transfer/send//", methods=["GET", "POST"]) +@login_required +def alias_transfer_send_route(alias_id): + alias = Alias.get(alias_id) + if not alias or alias.user_id != current_user.id: + flash("You cannot see this page", "warning") + return redirect(url_for("dashboard.index")) + + if current_user.newsletter_alias_id == alias.id: + flash( + "This alias is currently used for receiving the newsletter and cannot be transferred", + "error", + ) + return redirect(url_for("dashboard.index")) + + s = Signer(ALIAS_TRANSFER_SECRET) + alias_id_signed = s.sign(str(alias.id)).decode() + + alias_transfer_url = ( + URL + "/dashboard/alias_transfer/receive" + f"?alias_id={alias_id_signed}" + ) + + return render_template( + "dashboard/alias_transfer_send.html", + alias=alias, + alias_transfer_url=alias_transfer_url, + ) + + +@dashboard_bp.route("/alias_transfer/receive", methods=["GET", "POST"]) +@login_required +def alias_transfer_receive_route(): + """ + URL has ?alias_id=signed_alias_id + """ + s = Signer(ALIAS_TRANSFER_SECRET) + signed_alias_id = request.args.get("alias_id") + + try: + alias_id = int(s.unsign(signed_alias_id)) + except Exception: + flash("Invalid link", "error") + return redirect(url_for("dashboard.index")) + else: + alias = Alias.get(alias_id) + + # alias already belongs to this user + if alias.user_id == current_user.id: + flash("You already own this alias", "warning") + return redirect(url_for("dashboard.index")) + + mailboxes = current_user.mailboxes() + + if request.method == "POST": + mailbox_ids = request.form.getlist("mailbox_ids") + # check if mailbox is not tempered with + mailboxes = [] + for mailbox_id in mailbox_ids: + mailbox = Mailbox.get(mailbox_id) + if ( + not mailbox + or mailbox.user_id != current_user.id + or not mailbox.verified + ): + flash("Something went wrong, please retry", "warning") + return redirect(request.url) + mailboxes.append(mailbox) + + if not mailboxes: + flash("You must select at least 1 mailbox", "warning") + return redirect(request.url) + + LOG.d( + "transfer alias from %s to %s with %s", alias.user, current_user, mailboxes + ) + transfer(alias, current_user, mailboxes) + flash(f"You are now owner of {alias.email}", "success") + return redirect(url_for("dashboard.index", highlight_alias_id=alias.id)) + + return render_template( + "dashboard/alias_transfer_receive.html", + alias=alias, + mailboxes=mailboxes, + ) diff --git a/tests/dashboard/test_alias_transfer.py b/tests/dashboard/test_alias_transfer.py new file mode 100644 index 00000000..b6f8d3f6 --- /dev/null +++ b/tests/dashboard/test_alias_transfer.py @@ -0,0 +1,41 @@ +from flask import url_for + +from app.dashboard.views import alias_transfer +from app.extensions import db +from app.models import ( + Alias, + Contact, + Mailbox, + User, + AliasMailbox, +) +from tests.utils import login + + +def test_alias_transfer(flask_client): + user = login(flask_client) + mb = Mailbox.create(user_id=user.id, email="mb@gmail.com", commit=True) + + alias = Alias.create_new_random(user) + db.session.commit() + + AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id, commit=True) + + new_user = User.create( + email="hey@example.com", + password="password", + activated=True, + commit=True, + ) + + Mailbox.create( + user_id=new_user.id, email="hey2@example.com", verified=True, commit=True + ) + + alias_transfer.transfer(alias, new_user, new_user.mailboxes()) + + # refresh from db + alias = Alias.get(alias.id) + assert alias.user == new_user + assert set(alias.mailboxes) == set(new_user.mailboxes()) + assert len(alias.mailboxes) == 2