add alias transfer
This commit is contained in:
parent
1f7366c07c
commit
9e486fc2c0
|
@ -171,6 +171,9 @@ FLASK_SECRET = os.environ["FLASK_SECRET"]
|
||||||
SESSION_COOKIE_NAME = "slapp"
|
SESSION_COOKIE_NAME = "slapp"
|
||||||
MAILBOX_SECRET = FLASK_SECRET + "mailbox"
|
MAILBOX_SECRET = FLASK_SECRET + "mailbox"
|
||||||
CUSTOM_ALIAS_SECRET = FLASK_SECRET + "custom_alias"
|
CUSTOM_ALIAS_SECRET = FLASK_SECRET + "custom_alias"
|
||||||
|
ALIAS_TRANSFER_SECRET = os.environ.get("ALIAS_TRANSFER_SECRET") or (
|
||||||
|
FLASK_SECRET + "alias_transfer"
|
||||||
|
)
|
||||||
|
|
||||||
# AWS
|
# AWS
|
||||||
AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3"
|
AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3"
|
||||||
|
|
|
@ -25,4 +25,5 @@ from .views import (
|
||||||
contact_detail,
|
contact_detail,
|
||||||
setup_done,
|
setup_done,
|
||||||
batch_import,
|
batch_import,
|
||||||
|
alias_transfer,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends 'default.html' %}
|
||||||
|
|
||||||
|
{% set active_page = "dashboard" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Receive {{ alias.email }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="h3">Receive {{ alias.email }}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You are invited to become the owner of the alias <b>{{ alias.email }}</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please choose the mailbox(es) that owns this alias 👇
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" class="mt-2">
|
||||||
|
<select data-width="100%"
|
||||||
|
class="mailbox-select" multiple name="mailbox_ids">
|
||||||
|
{% for mailbox in mailboxes %}
|
||||||
|
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}
|
||||||
|
selected {% endif %}>
|
||||||
|
{{ mailbox.email }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn btn-success mt-2">Confirm</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
$('.mailbox-select').multipleSelect();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
43
app/dashboard/templates/dashboard/alias_transfer_send.html
Normal file
43
app/dashboard/templates/dashboard/alias_transfer_send.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends 'default.html' %}
|
||||||
|
|
||||||
|
{% set active_page = "dashboard" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Send {{ alias.email }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="h3">Transfer {{ alias.email }}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This page allows you to transfer {{ alias.email }} to another person so they can use it to receive and send
|
||||||
|
emails.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In order to transfer ownership,
|
||||||
|
please send the following URL 👇 to the other person.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<em data-toggle="tooltip"
|
||||||
|
title="Click to copy"
|
||||||
|
class="clipboard"
|
||||||
|
data-clipboard-text="{{ alias_transfer_url }}">
|
||||||
|
{{ alias_transfer_url }}
|
||||||
|
</em>
|
||||||
|
|
||||||
|
<p class="mt-5">
|
||||||
|
This person can then confirm the reception and become the owner of the alias.
|
||||||
|
</p>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
After the confirmation, you can no longer use this alias.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -416,20 +416,28 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-3">
|
<div>
|
||||||
<div class="col">
|
<div class="btn-group float-right" role="group" aria-label="Basic example">
|
||||||
|
<a href="{{ url_for('dashboard.alias_transfer_send_route', alias_id=alias.id) }}" class="btn btn-sm btn-link">
|
||||||
|
Transfer
|
||||||
|
<i class="ml-0 dropdown-icon fe fe-share-2 text-primary"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="form-name" value="delete-email">
|
<input type="hidden" name="form-name" value="delete-email">
|
||||||
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
||||||
|
|
||||||
<span class="delete-email btn btn-link btn-sm float-right text-danger">
|
<span class="delete-email btn btn-link btn-sm float-right text-danger">
|
||||||
Delete <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
Delete <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- END Collapse section -->
|
<!-- END Collapse section -->
|
||||||
</div>
|
</div>
|
||||||
|
|
140
app/dashboard/views/alias_transfer.py
Normal file
140
app/dashboard/views/alias_transfer.py
Normal file
|
@ -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/<int:alias_id>/", 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,
|
||||||
|
)
|
41
tests/dashboard/test_alias_transfer.py
Normal file
41
tests/dashboard/test_alias_transfer.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue