diff --git a/app/app/admin_model.py b/app/app/admin_model.py index 6338641..a8b01a2 100644 --- a/app/app/admin_model.py +++ b/app/app/admin_model.py @@ -48,6 +48,7 @@ from app.models import ( CustomDomain, ) from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address +from app.proton.proton_unlink import perform_proton_account_unlink from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction @@ -125,7 +126,7 @@ class SLAdminIndexView(AdminIndexView): if not current_user.is_authenticated or not current_user.is_admin: return redirect(url_for("auth.login", next=request.url)) - return redirect("/admin/email_search") + return redirect(url_for("admin.email_search.index")) class UserAdmin(SLModelView): @@ -917,7 +918,7 @@ class EmailSearchAdmin(BaseView): @expose("/", methods=["GET", "POST"]) def index(self): search = EmailSearchResult() - email = request.args.get("email") + email = request.args.get("query") if email is not None and len(email) > 0: email = email.strip() search = EmailSearchResult.from_request_email(email) @@ -929,6 +930,37 @@ class EmailSearchAdmin(BaseView): helper=EmailSearchHelpers, ) + @expose("/partner_unlink", methods=["POST"]) + def delete_partner_link(self): + user_id = request.form.get("user_id") + if not user_id: + flash("Missing user_id", "error") + return redirect(url_for("admin.email_search.index")) + try: + user_id = int(user_id) + except ValueError: + flash("Missing user_id", "error") + return redirect(url_for("admin.email_search.index", query=user_id)) + user = User.get(user_id) + if user is None: + flash("User not found", "error") + return redirect(url_for("admin.email_search.index", query=user_id)) + external_user_id = perform_proton_account_unlink(user, skip_check=True) + if not external_user_id: + flash("User unlinked", "success") + return redirect(url_for("admin.email_search.index", query=user_id)) + + AdminAuditLog.create( + admin_user_id=user.id, + model=User.__class__.__name__, + model_id=user.id, + action=AuditLogActionEnum.unlink_user.value, + data={"external_user_id": external_user_id}, + ) + Session.commit() + + return redirect(url_for("admin.email_search.index", query=user_id)) + class CustomDomainWithValidationData: def __init__(self, domain: CustomDomain): diff --git a/app/app/api/views/new_custom_alias.py b/app/app/api/views/new_custom_alias.py index 94d48a9..11ce227 100644 --- a/app/app/api/views/new_custom_alias.py +++ b/app/app/api/views/new_custom_alias.py @@ -62,8 +62,17 @@ def new_custom_alias_v2(): if not data: return jsonify(error="request body cannot be empty"), 400 - alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") - signed_suffix = data.get("signed_suffix", "").strip() + alias_prefix = data.get("alias_prefix", "") + if not isinstance(alias_prefix, str) or not alias_prefix: + return jsonify(error="invalid value for alias_prefix"), 400 + + alias_prefix = alias_prefix.strip().lower().replace(" ", "") + signed_suffix = data.get("signed_suffix", "") + if not isinstance(signed_suffix, str) or not signed_suffix: + return jsonify(error="invalid value for signed_suffix"), 400 + + signed_suffix = signed_suffix.strip() + note = data.get("note") alias_prefix = convert_to_id(alias_prefix) diff --git a/app/app/log.py b/app/app/log.py index 2d22081..335e9f9 100644 --- a/app/app/log.py +++ b/app/app/log.py @@ -44,7 +44,7 @@ class RequestIdFilter(logging.Filter): from flask import g, has_request_context request_id = "" - if has_request_context(): + if has_request_context() and hasattr(g, "request_id"): ctx_request_id = getattr(g, "request_id") if ctx_request_id: request_id = f"{ctx_request_id} - " diff --git a/app/app/mailbox_utils.py b/app/app/mailbox_utils.py index 9ed2f02..496583f 100644 --- a/app/app/mailbox_utils.py +++ b/app/app/mailbox_utils.py @@ -247,7 +247,7 @@ def verify_mailbox_code(user: User, mailbox_id: int, code: str) -> Mailbox: message=f"Verify mailbox {mailbox_id} ({mailbox.email})", ) if Mailbox.get_by(email=mailbox.new_email, user_id=user.id): - raise MailboxError("That addres is already in use") + raise MailboxError("That address is already in use") else: LOG.i( @@ -354,6 +354,7 @@ def request_mailbox_email_change( if email_ownership_verified: mailbox.email = new_email mailbox.new_email = None + mailbox.verified = True else: mailbox.new_email = new_email emit_user_audit_log( diff --git a/app/app/models.py b/app/app/models.py index 3032ba5..c657cb9 100644 --- a/app/app/models.py +++ b/app/app/models.py @@ -238,6 +238,7 @@ class AuditLogActionEnum(EnumE): disable_user = 9 enable_user = 10 stop_trial = 11 + unlink_user = 12 class Phase(EnumE): diff --git a/app/app/proton/proton_unlink.py b/app/app/proton/proton_unlink.py index 69f5cb9..7399e97 100644 --- a/app/app/proton/proton_unlink.py +++ b/app/app/proton/proton_unlink.py @@ -13,9 +13,11 @@ def can_unlink_proton_account(user: User) -> bool: return (user.flags & User.FLAG_CREATED_FROM_PARTNER) == 0 -def perform_proton_account_unlink(current_user: User) -> bool: - if not can_unlink_proton_account(current_user): - return False +def perform_proton_account_unlink( + current_user: User, skip_check: bool = False +) -> None | str: + if not skip_check and not can_unlink_proton_account(current_user): + return None proton_partner = get_proton_partner() partner_user = PartnerUser.get_by( user_id=current_user.id, partner_id=proton_partner.id @@ -31,6 +33,7 @@ def perform_proton_account_unlink(current_user: User) -> bool: partner_user.user, EventContent(user_unlinked=UserUnlinked()) ) PartnerUser.delete(partner_user.id) + external_user_id = partner_user.external_user_id Session.commit() agent.record_custom_event("AccountUnlinked", {"partner": proton_partner.name}) - return True + return external_user_id diff --git a/app/server.py b/app/server.py index bc54cf7..b645fee 100644 --- a/app/server.py +++ b/app/server.py @@ -446,10 +446,10 @@ def init_admin(app): admin = Admin(name="SimpleLogin", template_mode="bootstrap4") admin.init_app(app, index_view=SLAdminIndexView()) - admin.add_view(EmailSearchAdmin(name="Email Search", endpoint="email_search")) + admin.add_view(EmailSearchAdmin(name="Email Search", endpoint="admin.email_search")) admin.add_view( CustomDomainSearchAdmin( - name="Custom domain search", endpoint="custom_domain_search" + name="Custom domain search", endpoint="admin.custom_domain_search" ) ) admin.add_view(UserAdmin(User, Session)) diff --git a/app/templates/admin/email_search.html b/app/templates/admin/email_search.html index 72b2674..177d0ed 100644 --- a/app/templates/admin/email_search.html +++ b/app/templates/admin/email_search.html @@ -22,7 +22,7 @@