diff --git a/app/dashboard/templates/dashboard/setting.html b/app/dashboard/templates/dashboard/setting.html index 80ac9d2d..d0e0d537 100644 --- a/app/dashboard/templates/dashboard/setting.html +++ b/app/dashboard/templates/dashboard/setting.html @@ -15,10 +15,21 @@

Profile

-
+
- {{ form.email(class="form-control", value=current_user.email, readonly=True) }} + + {{ form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }} {{ render_field_errors(form.email) }} + + {% if pending_email %} +
+ Pending email change: {{ pending_email }} + Resend + confirmation email + Cancel email + change +
+ {% endif %}
@@ -40,7 +51,7 @@
- +

Change password

diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index 31bd7322..e5845f1f 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -1,5 +1,6 @@ from io import BytesIO +import arrow from flask import render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from flask_wtf import FlaskForm @@ -7,11 +8,19 @@ from flask_wtf.file import FileField from wtforms import StringField, validators from app import s3, email_utils -from app.config import URL +from app.config import URL, EMAIL_DOMAIN from app.dashboard.base import dashboard_bp from app.extensions import db from app.log import LOG -from app.models import PlanEnum, File, ResetPasswordCode +from app.models import ( + PlanEnum, + File, + ResetPasswordCode, + EmailChange, + User, + GenEmail, + DeletedAlias, +) from app.utils import random_string @@ -31,6 +40,12 @@ def setting(): form = SettingForm() promo_form = PromoCodeForm() + email_change = EmailChange.get_by(user_id=current_user.id) + if email_change: + pending_email = email_change.new_email + else: + pending_email = None + if request.method == "POST": if request.form.get("form-name") == "update-profile": if form.validate(): @@ -54,13 +69,43 @@ def setting(): db.session.commit() flash(f"Your profile has been updated", "success") + if form.email.data and form.email.data != current_user.email: + new_email = form.email.data + + # check if this email is not used by other user, or as alias + if ( + User.get_by(email=new_email) + or GenEmail.get_by(email=new_email) + or DeletedAlias.get_by(email=new_email) + ): + flash(f"Email {new_email} already used", "error") + elif new_email.endswith(EMAIL_DOMAIN): + flash( + "You cannot use alias as your personal inbox. Nice try though 😉", + "error", + ) + else: + email_change = EmailChange.create( + user_id=current_user.id, + code=random_string( + 60 + ), # todo: make sure the code is unique + new_email=new_email, + ) + db.session.commit() + send_change_email_confirmation(current_user, email_change) + elif request.form.get("form-name") == "change-password": send_reset_password_email(current_user) return redirect(url_for("dashboard.setting")) return render_template( - "dashboard/setting.html", form=form, PlanEnum=PlanEnum, promo_form=promo_form + "dashboard/setting.html", + form=form, + PlanEnum=PlanEnum, + promo_form=promo_form, + pending_email=pending_email, ) @@ -82,3 +127,49 @@ def send_reset_password_email(user): "You are going to receive an email containing instruction to change your password", "success", ) + + +def send_change_email_confirmation(user: User, email_change: EmailChange): + """ + send confirmation email to the new email address + """ + + link = f"{URL}/auth/change_email?code={email_change.code}" + + email_utils.send_change_email(email_change.new_email, user.email, user.name, link) + + flash("You are going to receive an email to confirm email change", "success") + + +@dashboard_bp.route("/resend_email_change", methods=["GET", "POST"]) +@login_required +def resend_email_change(): + email_change = EmailChange.get_by(user_id=current_user.id) + if email_change: + # extend email change expiration + email_change.expired = arrow.now().shift(hours=12) + db.session.commit() + + send_change_email_confirmation(current_user, email_change) + flash("A confirmation email is on the way, please check your inbox", "success") + return redirect(url_for("dashboard.setting")) + else: + flash( + "You have no pending email change. Redirect back to Setting page", "warning" + ) + return redirect(url_for("dashboard.setting")) + + +@dashboard_bp.route("/cancel_email_change", methods=["GET", "POST"]) +@login_required +def cancel_email_change(): + email_change = EmailChange.get_by(user_id=current_user.id) + if email_change: + EmailChange.delete(email_change.id) + flash("Your email change is cancelled", "success") + return redirect(url_for("dashboard.setting")) + else: + flash( + "You have no pending email change. Redirect back to Setting page", "warning" + ) + return redirect(url_for("dashboard.setting"))