diff --git a/app/app/account_linking.py b/app/app/account_linking.py index ef7c0bd..02160e6 100644 --- a/app/app/account_linking.py +++ b/app/app/account_linking.py @@ -112,6 +112,7 @@ def ensure_partner_user_exists_for_user( partner_email=link_request.email, external_user_id=link_request.external_user_id, ) + Session.commit() LOG.i( f"Created new partner_user for partner:{partner.id} user:{sl_user.id} external_user_id:{link_request.external_user_id}. PartnerUser.id is {res.id}" diff --git a/app/app/admin_model.py b/app/app/admin_model.py index 192ef9c..5d6bf66 100644 --- a/app/app/admin_model.py +++ b/app/app/admin_model.py @@ -16,6 +16,8 @@ from flask_admin.contrib import sqla from flask_login import current_user from app.db import Session +from app.events.event_dispatcher import EventDispatcher +from app.events.generated.event_pb2 import EventContent, UserPlanChanged from app.models import ( User, ManualSubscription, @@ -39,6 +41,7 @@ from app.models import ( UserAuditLog, ) from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address +from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction def _admin_action_formatter(view, context, model, name): @@ -351,17 +354,42 @@ def manual_upgrade(way: str, ids: [int], is_giveaway: bool): manual_sub.end_at = manual_sub.end_at.shift(years=1) else: manual_sub.end_at = arrow.now().shift(years=1, days=1) + emit_user_audit_log( + user=user, + action=UserAuditLogAction.Upgrade, + message=f"Admin {current_user.email} extended manual subscription to user {user.email}", + ) + EventDispatcher.send_event( + user=user, + content=EventContent( + user_plan_change=UserPlanChanged( + plan_end_time=manual_sub.end_at.timestamp + ) + ), + ) flash(f"Subscription extended to {manual_sub.end_at.humanize()}", "success") - continue + else: + emit_user_audit_log( + user=user, + action=UserAuditLogAction.Upgrade, + message=f"Admin {current_user.email} created manual subscription to user {user.email}", + ) + manual_sub = ManualSubscription.create( + user_id=user.id, + end_at=arrow.now().shift(years=1, days=1), + comment=way, + is_giveaway=is_giveaway, + ) + EventDispatcher.send_event( + user=user, + content=EventContent( + user_plan_change=UserPlanChanged( + plan_end_time=manual_sub.end_at.timestamp + ) + ), + ) - ManualSubscription.create( - user_id=user.id, - end_at=arrow.now().shift(years=1, days=1), - comment=way, - is_giveaway=is_giveaway, - ) - - flash(f"New {way} manual subscription for {user} is created", "success") + flash(f"New {way} manual subscription for {user} is created", "success") Session.commit() @@ -453,14 +481,7 @@ class ManualSubscriptionAdmin(SLModelView): "Extend 1 year more?", ) def extend_1y(self, ids): - for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)): - ms.end_at = ms.end_at.shift(years=1) - flash(f"Extend subscription for 1 year for {ms.user}", "success") - AdminAuditLog.extend_subscription( - current_user.id, ms.user.id, ms.end_at, "1 year" - ) - - Session.commit() + self.__extend_manual_subscription(ids, msg="1 year", years=1) @action( "extend_1m", @@ -468,11 +489,26 @@ class ManualSubscriptionAdmin(SLModelView): "Extend 1 month more?", ) def extend_1m(self, ids): + self.__extend_manual_subscription(ids, msg="1 month", months=1) + + def __extend_manual_subscription(self, ids: List[int], msg: str, **kwargs): for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)): - ms.end_at = ms.end_at.shift(months=1) - flash(f"Extend subscription for 1 month for {ms.user}", "success") + sub: ManualSubscription = ms + sub.end_at = sub.end_at.shift(**kwargs) + flash(f"Extend subscription for {msg} for {sub.user}", "success") + emit_user_audit_log( + user=sub.user, + action=UserAuditLogAction.Upgrade, + message=f"Admin {current_user.email} extended manual subscription for {msg} for {sub.user}", + ) AdminAuditLog.extend_subscription( - current_user.id, ms.user.id, ms.end_at, "1 month" + current_user.id, sub.user.id, sub.end_at, msg + ) + EventDispatcher.send_event( + user=sub.user, + content=EventContent( + user_plan_change=UserPlanChanged(plan_end_time=sub.end_at.timestamp) + ), ) Session.commit() diff --git a/app/app/api/views/mailbox.py b/app/app/api/views/mailbox.py index 10bba2c..a8787af 100644 --- a/app/app/api/views/mailbox.py +++ b/app/app/api/views/mailbox.py @@ -38,7 +38,11 @@ def create_mailbox(): the new mailbox dict """ user = g.user - mailbox_email = sanitize_email(request.get_json().get("email")) + email = request.get_json().get("email") + if not email: + return jsonify(error="Invalid email"), 400 + + mailbox_email = sanitize_email(email) try: new_mailbox = mailbox_utils.create_mailbox(user, mailbox_email).mailbox diff --git a/app/app/dashboard/views/mailbox.py b/app/app/dashboard/views/mailbox.py index e712471..ee436c5 100644 --- a/app/app/dashboard/views/mailbox.py +++ b/app/app/dashboard/views/mailbox.py @@ -121,10 +121,16 @@ def mailbox_route(): @login_required def mailbox_verify(): mailbox_id = request.args.get("mailbox_id") + if not mailbox_id: + LOG.i("Missing mailbox_id") + flash("You followed an invalid link", "error") + return redirect(url_for("dashboard.mailbox_route")) + code = request.args.get("code") if not code: # Old way return verify_with_signed_secret(mailbox_id) + try: mailbox = mailbox_utils.verify_mailbox_code(current_user, mailbox_id, code) except mailbox_utils.MailboxError as e: diff --git a/app/app/mailbox_utils.py b/app/app/mailbox_utils.py index 4218267..1949956 100644 --- a/app/app/mailbox_utils.py +++ b/app/app/mailbox_utils.py @@ -171,17 +171,17 @@ def verify_mailbox_code(user: User, mailbox_id: int, code: str) -> Mailbox: f"User {user} failed to verify mailbox {mailbox_id} because it does not exist" ) raise MailboxError("Invalid mailbox") + if mailbox.user_id != user.id: + LOG.i( + f"User {user} failed to verify mailbox {mailbox_id} because it's owned by another user" + ) + raise MailboxError("Invalid mailbox") if mailbox.verified: LOG.i( f"User {user} failed to verify mailbox {mailbox_id} because it's already verified" ) clear_activation_codes_for_mailbox(mailbox) return mailbox - if mailbox.user_id != user.id: - LOG.i( - f"User {user} failed to verify mailbox {mailbox_id} because it's owned by another user" - ) - raise MailboxError("Invalid mailbox") activation = ( MailboxActivation.filter(MailboxActivation.mailbox_id == mailbox_id) diff --git a/app/app/models.py b/app/app/models.py index 83140c7..702e875 100644 --- a/app/app/models.py +++ b/app/app/models.py @@ -24,6 +24,7 @@ from sqlalchemy import text, desc, CheckConstraint, Index, Column from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import deferred +from sqlalchemy.orm.exc import ObjectDeletedError from sqlalchemy.sql import and_ from sqlalchemy_utils import ArrowType @@ -3781,15 +3782,18 @@ class SyncEvent(Base, ModelMixin): ) def mark_as_taken(self, allow_taken_older_than: Optional[Arrow] = None) -> bool: - taken_condition = ["taken_time IS NULL"] - args = {"taken_time": arrow.now().datetime, "sync_event_id": self.id} - if allow_taken_older_than: - taken_condition.append("taken_time < :taken_older_than") - args["taken_older_than"] = allow_taken_older_than.datetime - sql_taken_condition = "({})".format(" OR ".join(taken_condition)) - sql = f"UPDATE sync_event SET taken_time = :taken_time WHERE id = :sync_event_id AND {sql_taken_condition}" - res = Session.execute(sql, args) - Session.commit() + try: + taken_condition = ["taken_time IS NULL"] + args = {"taken_time": arrow.now().datetime, "sync_event_id": self.id} + if allow_taken_older_than: + taken_condition.append("taken_time < :taken_older_than") + args["taken_older_than"] = allow_taken_older_than.datetime + sql_taken_condition = "({})".format(" OR ".join(taken_condition)) + sql = f"UPDATE sync_event SET taken_time = :taken_time WHERE id = :sync_event_id AND {sql_taken_condition}" + res = Session.execute(sql, args) + Session.commit() + except ObjectDeletedError: + return False return res.rowcount > 0 diff --git a/app/app/partner_user_utils.py b/app/app/partner_user_utils.py index c25f0b0..ba665f0 100644 --- a/app/app/partner_user_utils.py +++ b/app/app/partner_user_utils.py @@ -1,8 +1,10 @@ from typing import Optional +import arrow from arrow import Arrow -from app.models import PartnerUser, PartnerSubscription, User +from app import config +from app.models import PartnerUser, PartnerSubscription, User, Job from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction @@ -15,6 +17,11 @@ def create_partner_user( partner_email=partner_email, external_user_id=external_user_id, ) + Job.create( + name=config.JOB_SEND_ALIAS_CREATION_EVENTS, + payload={"user_id": user.id}, + run_at=arrow.now(), + ) emit_user_audit_log( user=user, action=UserAuditLogAction.LinkAccount, diff --git a/app/app/proton/proton_callback_handler.py b/app/app/proton/proton_callback_handler.py index f726d48..53c8076 100644 --- a/app/app/proton/proton_callback_handler.py +++ b/app/app/proton/proton_callback_handler.py @@ -2,11 +2,9 @@ from dataclasses import dataclass from enum import Enum from flask import url_for from typing import Optional -import arrow -from app import config from app.errors import LinkException -from app.models import User, Partner, Job +from app.models import User, Partner from app.proton.proton_client import ProtonClient, ProtonUser from app.account_linking import ( process_login_case, @@ -43,21 +41,12 @@ class ProtonCallbackHandler: def __init__(self, proton_client: ProtonClient): self.proton_client = proton_client - def _initial_alias_sync(self, user: User): - Job.create( - name=config.JOB_SEND_ALIAS_CREATION_EVENTS, - payload={"user_id": user.id}, - run_at=arrow.now(), - commit=True, - ) - def handle_login(self, partner: Partner) -> ProtonCallbackResult: try: user = self.__get_partner_user() if user is None: return generate_account_not_allowed_to_log_in() res = process_login_case(user, partner) - self._initial_alias_sync(res.user) return ProtonCallbackResult( redirect_to_login=False, flash_message=None, @@ -86,7 +75,6 @@ class ProtonCallbackHandler: if user is None: return generate_account_not_allowed_to_log_in() res = process_link_case(user, current_user, partner) - self._initial_alias_sync(res.user) return ProtonCallbackResult( redirect_to_login=False, flash_message="Account successfully linked", diff --git a/app/templates/admin/email_search.html b/app/templates/admin/email_search.html index b462796..e42d9e3 100644 --- a/app/templates/admin/email_search.html +++ b/app/templates/admin/email_search.html @@ -8,6 +8,7 @@