Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
a62b43b7c4 | |||
44fda2d94e | |||
bc48198bb1 |
@ -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}"
|
||||
|
@ -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):
|
||||
@ -115,7 +118,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/user")
|
||||
return redirect("/admin/email_search")
|
||||
|
||||
|
||||
class UserAdmin(SLModelView):
|
||||
@ -351,15 +354,40 @@ 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
|
||||
|
||||
ManualSubscription.create(
|
||||
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
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
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()
|
||||
@ -743,13 +779,17 @@ class EmailSearchResult:
|
||||
mailbox: List[Mailbox] = []
|
||||
mailbox_count: int = 0
|
||||
deleted_alias: Optional[DeletedAlias] = None
|
||||
deleted_custom_alias: Optional[DomainDeletedAlias] = None
|
||||
deleted_alias_audit_log: Optional[List[AliasAuditLog]] = None
|
||||
domain_deleted_alias: Optional[DomainDeletedAlias] = None
|
||||
domain_deleted_alias_audit_log: Optional[List[AliasAuditLog]] = None
|
||||
user: Optional[User] = None
|
||||
user_audit_log: Optional[List[UserAuditLog]] = None
|
||||
query: str
|
||||
|
||||
@staticmethod
|
||||
def from_email(email: str) -> EmailSearchResult:
|
||||
output = EmailSearchResult()
|
||||
output.query = email
|
||||
alias = Alias.get_by(email=email)
|
||||
if alias:
|
||||
output.alias = alias
|
||||
@ -768,6 +808,15 @@ class EmailSearchResult:
|
||||
.all()
|
||||
)
|
||||
output.no_match = False
|
||||
|
||||
user_audit_log = (
|
||||
UserAuditLog.filter_by(user_email=email)
|
||||
.order_by(UserAuditLog.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
if user_audit_log:
|
||||
output.user_audit_log = user_audit_log
|
||||
output.no_match = False
|
||||
mailboxes = (
|
||||
Mailbox.filter_by(email=email).order_by(Mailbox.id.desc()).limit(10).all()
|
||||
)
|
||||
@ -778,10 +827,20 @@ class EmailSearchResult:
|
||||
deleted_alias = DeletedAlias.get_by(email=email)
|
||||
if deleted_alias:
|
||||
output.deleted_alias = deleted_alias
|
||||
output.deleted_alias_audit_log = (
|
||||
AliasAuditLog.filter_by(alias_email=deleted_alias.email)
|
||||
.order_by(AliasAuditLog.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
output.no_match = False
|
||||
domain_deleted_alias = DomainDeletedAlias.get_by(email=email)
|
||||
if domain_deleted_alias:
|
||||
output.domain_deleted_alias = domain_deleted_alias
|
||||
output.domain_deleted_alias_audit_log = (
|
||||
AliasAuditLog.filter_by(alias_email=domain_deleted_alias.email)
|
||||
.order_by(AliasAuditLog.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
output.no_match = False
|
||||
return output
|
||||
|
||||
|
@ -419,9 +419,8 @@ def create_contact_route(alias_id):
|
||||
if not data:
|
||||
return jsonify(error="request body cannot be empty"), 400
|
||||
|
||||
alias: Alias = Alias.get(alias_id)
|
||||
|
||||
if alias.user_id != g.user.id:
|
||||
alias: Optional[Alias] = Alias.get_by(id=alias_id, user_id=g.user.id)
|
||||
if not alias:
|
||||
return jsonify(error="Forbidden"), 403
|
||||
|
||||
contact_address = data.get("contact")
|
||||
|
@ -23,6 +23,7 @@ from app.events.auth_event import LoginEvent, RegisterEvent
|
||||
from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import User, ApiKey, SocialAuth, AccountActivation
|
||||
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
||||
from app.utils import sanitize_email, canonicalize_email
|
||||
|
||||
|
||||
@ -187,6 +188,11 @@ def auth_activate():
|
||||
|
||||
LOG.d("activate user %s", user)
|
||||
user.activated = True
|
||||
emit_user_audit_log(
|
||||
user=user,
|
||||
action=UserAuditLogAction.ActivateUser,
|
||||
message=f"User has been activated: {user.email}",
|
||||
)
|
||||
AccountActivation.delete(account_activation.id)
|
||||
Session.commit()
|
||||
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,7 @@ from app.db import Session
|
||||
from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import ActivationCode
|
||||
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
||||
from app.utils import sanitize_next_url
|
||||
|
||||
|
||||
@ -47,6 +48,11 @@ def activate():
|
||||
|
||||
user = activation_code.user
|
||||
user.activated = True
|
||||
emit_user_audit_log(
|
||||
user=user,
|
||||
action=UserAuditLogAction.ActivateUser,
|
||||
message=f"User has been activated: {user.email}",
|
||||
)
|
||||
login_user(user)
|
||||
|
||||
# activation code is to be used only once
|
||||
|
@ -9,6 +9,7 @@ from app.auth.views.login_utils import after_login
|
||||
from app.db import Session
|
||||
from app.extensions import limiter
|
||||
from app.models import ResetPasswordCode
|
||||
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
@ -59,6 +60,11 @@ def reset_password():
|
||||
|
||||
# this can be served to activate user too
|
||||
user.activated = True
|
||||
emit_user_audit_log(
|
||||
user=user,
|
||||
action=UserAuditLogAction.ResetPassword,
|
||||
message="User has reset their password",
|
||||
)
|
||||
|
||||
# remove all reset password codes
|
||||
ResetPasswordCode.filter_by(user_id=user.id).delete()
|
||||
|
@ -309,6 +309,7 @@ JOB_DELETE_DOMAIN = "delete-domain"
|
||||
JOB_SEND_USER_REPORT = "send-user-report"
|
||||
JOB_SEND_PROTON_WELCOME_1 = "proton-welcome-1"
|
||||
JOB_SEND_ALIAS_CREATION_EVENTS = "send-alias-creation-events"
|
||||
JOB_SEND_EVENT_TO_WEBHOOK = "send-event-to-webhook"
|
||||
|
||||
# for pagination
|
||||
PAGE_LIMIT = 20
|
||||
|
@ -16,6 +16,7 @@ from app.utils import sanitize_email
|
||||
class ContactCreateError(Enum):
|
||||
InvalidEmail = "Invalid email"
|
||||
NotAllowed = "Your plan does not allow to create contacts"
|
||||
Unknown = "Unknown error when trying to create contact"
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -87,6 +88,7 @@ def create_contact(
|
||||
return __update_contact_if_needed(contact, name, mail_from)
|
||||
# Create the contact
|
||||
reply_email = generate_reply_email(email, alias)
|
||||
alias_id = alias.id
|
||||
try:
|
||||
flags = Contact.FLAG_PARTNER_CREATED if from_partner else 0
|
||||
contact = Contact.create(
|
||||
@ -114,11 +116,21 @@ def create_contact(
|
||||
LOG.d(
|
||||
f"Created contact {contact} for alias {alias} with email {email} invalid_email={contact.invalid_email}"
|
||||
)
|
||||
return ContactCreateResult(contact, created=True, error=None)
|
||||
except IntegrityError:
|
||||
Session.rollback()
|
||||
LOG.info(
|
||||
f"Contact with email {email} for alias_id {alias.id} already existed, fetching from DB"
|
||||
f"Contact with email {email} for alias_id {alias_id} already existed, fetching from DB"
|
||||
)
|
||||
contact = Contact.get_by(alias_id=alias.id, website_email=email)
|
||||
contact: Optional[Contact] = Contact.get_by(
|
||||
alias_id=alias_id, website_email=email
|
||||
)
|
||||
if contact:
|
||||
return __update_contact_if_needed(contact, name, mail_from)
|
||||
return ContactCreateResult(contact, created=True, error=None)
|
||||
else:
|
||||
LOG.warning(
|
||||
f"Could not find contact with email {email} for alias_id {alias_id} and it should exist"
|
||||
)
|
||||
return ContactCreateResult(
|
||||
None, created=False, error=ContactCreateError.Unknown
|
||||
)
|
||||
|
@ -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:
|
||||
|
70
app/app/jobs/send_event_job.py
Normal file
70
app/app/jobs/send_event_job.py
Normal file
@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
|
||||
from app import config
|
||||
from app.errors import ProtonPartnerNotSetUp
|
||||
from app.events.generated import event_pb2
|
||||
from app.events.generated.event_pb2 import EventContent
|
||||
from app.models import (
|
||||
User,
|
||||
Job,
|
||||
PartnerUser,
|
||||
)
|
||||
from app.proton.utils import get_proton_partner
|
||||
from events.event_sink import EventSink
|
||||
|
||||
|
||||
class SendEventToWebhookJob:
|
||||
def __init__(self, user: User, event: EventContent):
|
||||
self._user: User = user
|
||||
self._event: EventContent = event
|
||||
|
||||
def run(self, sink: EventSink) -> bool:
|
||||
# Check if the current user has a partner_id
|
||||
try:
|
||||
proton_partner_id = get_proton_partner().id
|
||||
except ProtonPartnerNotSetUp:
|
||||
return False
|
||||
|
||||
# It has. Retrieve the information for the PartnerUser
|
||||
partner_user = PartnerUser.get_by(
|
||||
user_id=self._user.id, partner_id=proton_partner_id
|
||||
)
|
||||
if partner_user is None:
|
||||
return True
|
||||
event = event_pb2.Event(
|
||||
user_id=self._user.id,
|
||||
external_user_id=partner_user.external_user_id,
|
||||
partner_id=partner_user.partner_id,
|
||||
content=self._event,
|
||||
)
|
||||
|
||||
serialized = event.SerializeToString()
|
||||
return sink.send_data_to_webhook(serialized)
|
||||
|
||||
@staticmethod
|
||||
def create_from_job(job: Job) -> Optional[SendEventToWebhookJob]:
|
||||
user = User.get(job.payload["user_id"])
|
||||
if not user:
|
||||
return None
|
||||
event_data = base64.b64decode(job.payload["event"])
|
||||
event = event_pb2.EventContent()
|
||||
event.ParseFromString(event_data)
|
||||
|
||||
return SendEventToWebhookJob(user=user, event=event)
|
||||
|
||||
def store_job_in_db(self, run_at: Optional[arrow.Arrow]) -> Job:
|
||||
stub = self._event.SerializeToString()
|
||||
return Job.create(
|
||||
name=config.JOB_SEND_EVENT_TO_WEBHOOK,
|
||||
payload={
|
||||
"user_id": self._user.id,
|
||||
"event": base64.b64encode(stub).decode("utf-8"),
|
||||
},
|
||||
run_at=run_at if run_at is not None else arrow.now(),
|
||||
commit=True,
|
||||
)
|
@ -37,8 +37,9 @@ class OnlyPaidError(MailboxError):
|
||||
|
||||
|
||||
class CannotVerifyError(MailboxError):
|
||||
def __init__(self, msg: str):
|
||||
def __init__(self, msg: str, deleted_activation_code: bool = False):
|
||||
self.msg = msg
|
||||
self.deleted_activation_code = deleted_activation_code
|
||||
|
||||
|
||||
MAX_ACTIVATION_TRIES = 3
|
||||
@ -171,17 +172,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)
|
||||
@ -196,7 +197,10 @@ def verify_mailbox_code(user: User, mailbox_id: int, code: str) -> Mailbox:
|
||||
if activation.tries >= MAX_ACTIVATION_TRIES:
|
||||
LOG.i(f"User {user} failed to verify mailbox {mailbox_id} more than 3 times")
|
||||
clear_activation_codes_for_mailbox(mailbox)
|
||||
raise CannotVerifyError("Invalid activation code. Please request another code.")
|
||||
raise CannotVerifyError(
|
||||
"Invalid activation code. Please request another code.",
|
||||
deleted_activation_code=True,
|
||||
)
|
||||
if activation.created_at < arrow.now().shift(minutes=-15):
|
||||
LOG.i(
|
||||
f"User {user} failed to verify mailbox {mailbox_id} because code is too old"
|
||||
|
@ -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
|
||||
|
||||
@ -616,6 +617,15 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
if "alternative_id" not in kwargs:
|
||||
user.alternative_id = str(uuid.uuid4())
|
||||
|
||||
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
||||
|
||||
trail = ". Created from partner" if from_partner else ""
|
||||
emit_user_audit_log(
|
||||
user=user,
|
||||
action=UserAuditLogAction.CreateUser,
|
||||
message=f"Created user {email}{trail}",
|
||||
)
|
||||
|
||||
# If the user is created from partner, do not notify
|
||||
# nor give a trial
|
||||
if from_partner:
|
||||
@ -3772,6 +3782,7 @@ class SyncEvent(Base, ModelMixin):
|
||||
)
|
||||
|
||||
def mark_as_taken(self, allow_taken_older_than: Optional[Arrow] = None) -> bool:
|
||||
try:
|
||||
taken_condition = ["taken_time IS NULL"]
|
||||
args = {"taken_time": arrow.now().datetime, "sync_event_id": self.id}
|
||||
if allow_taken_older_than:
|
||||
@ -3781,6 +3792,8 @@ class SyncEvent(Base, ModelMixin):
|
||||
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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -4,6 +4,10 @@ from app.models import User, UserAuditLog
|
||||
|
||||
|
||||
class UserAuditLogAction(Enum):
|
||||
CreateUser = "create_user"
|
||||
ActivateUser = "activate_user"
|
||||
ResetPassword = "reset_password"
|
||||
|
||||
Upgrade = "upgrade"
|
||||
SubscriptionExtended = "subscription_extended"
|
||||
SubscriptionCancelled = "subscription_cancelled"
|
||||
|
@ -12,6 +12,10 @@ class EventSink(ABC):
|
||||
def process(self, event: SyncEvent) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def send_data_to_webhook(self, data: bytes) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class HttpEventSink(EventSink):
|
||||
def process(self, event: SyncEvent) -> bool:
|
||||
@ -21,9 +25,16 @@ class HttpEventSink(EventSink):
|
||||
|
||||
LOG.info(f"Sending event {event.id} to {EVENT_WEBHOOK}")
|
||||
|
||||
if self.send_data_to_webhook(event.content):
|
||||
LOG.info(f"Event {event.id} sent successfully to webhook")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def send_data_to_webhook(self, data: bytes) -> bool:
|
||||
res = requests.post(
|
||||
url=EVENT_WEBHOOK,
|
||||
data=event.content,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/x-protobuf"},
|
||||
verify=not EVENT_WEBHOOK_SKIP_VERIFY_SSL,
|
||||
)
|
||||
@ -36,7 +47,6 @@ class HttpEventSink(EventSink):
|
||||
)
|
||||
return False
|
||||
else:
|
||||
LOG.info(f"Event {event.id} sent successfully to webhook")
|
||||
return True
|
||||
|
||||
|
||||
@ -44,3 +54,7 @@ class ConsoleEventSink(EventSink):
|
||||
def process(self, event: SyncEvent) -> bool:
|
||||
LOG.info(f"Handling event {event.id}")
|
||||
return True
|
||||
|
||||
def send_data_to_webhook(self, data: bytes) -> bool:
|
||||
LOG.info(f"Sending {len(data)} bytes to webhook")
|
||||
return True
|
||||
|
@ -18,6 +18,7 @@ from app.events.event_dispatcher import PostgresDispatcher
|
||||
from app.import_utils import handle_batch_import
|
||||
from app.jobs.event_jobs import send_alias_creation_events_for_user
|
||||
from app.jobs.export_user_data_job import ExportUserDataJob
|
||||
from app.jobs.send_event_job import SendEventToWebhookJob
|
||||
from app.log import LOG
|
||||
from app.models import User, Job, BatchImport, Mailbox, CustomDomain, JobState
|
||||
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
||||
@ -300,6 +301,10 @@ def process_job(job: Job):
|
||||
send_alias_creation_events_for_user(
|
||||
user, dispatcher=PostgresDispatcher.get()
|
||||
)
|
||||
elif job.name == config.JOB_SEND_EVENT_TO_WEBHOOK:
|
||||
send_job = SendEventToWebhookJob.create_from_job(job)
|
||||
if send_job:
|
||||
send_job.run()
|
||||
else:
|
||||
LOG.e("Unknown job name %s", job.name)
|
||||
|
||||
|
@ -442,10 +442,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(UserAdmin(User, Session))
|
||||
admin.add_view(AliasAdmin(Alias, Session))
|
||||
admin.add_view(MailboxAdmin(Mailbox, Session))
|
||||
admin.add_view(EmailSearchAdmin(name="Email Search", endpoint="email_search"))
|
||||
admin.add_view(CouponAdmin(Coupon, Session))
|
||||
admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session))
|
||||
admin.add_view(CustomDomainAdmin(CustomDomain, Session))
|
||||
|
@ -8,6 +8,7 @@
|
||||
<tr>
|
||||
<th scope="col">User ID</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Verified</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Paid</th>
|
||||
<th>Subscription</th>
|
||||
@ -20,8 +21,12 @@
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td><a href="?email={{ user.email }}">{{ user.email }}</a></td>
|
||||
{% if user.activated %}
|
||||
<td class="text-success">Activated</td>
|
||||
{% else %}
|
||||
<td class="text-warning">Pending</td>
|
||||
{% endif %}
|
||||
{% if user.disabled %}
|
||||
|
||||
<td class="text-danger">Disabled</td>
|
||||
{% else %}
|
||||
<td class="text-success">Enabled</td>
|
||||
@ -239,6 +244,11 @@
|
||||
{{ show_user(data.user) }}
|
||||
{{ list_mailboxes("Mailboxes for user", helper.mailbox_count(data.user) , helper.mailbox_list(data.user) ) }}
|
||||
{{ list_alias(helper.alias_count(data.user) ,helper.alias_list(data.user)) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.user_audit_log %}
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">Audit log entries for user {{ data.query }}</h3>
|
||||
{{ list_user_audit_log(data.user_audit_log) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -260,6 +270,7 @@
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">Found DeletedAlias {{ data.deleted_alias.email }}</h3>
|
||||
{{ show_deleted_alias(data.deleted_alias) }}
|
||||
{{ list_alias_audit_log(data.deleted_alias_audit_log) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.domain_deleted_alias %}
|
||||
@ -267,6 +278,7 @@
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}</h3>
|
||||
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
|
||||
{{ list_alias_audit_log(data.domain_deleted_alias_audit_log) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -511,6 +511,19 @@ def test_create_contact_route_invalid_alias(flask_client):
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_create_contact_route_non_existing_alias(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=99999999),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": "First Last <first@example.com>"},
|
||||
)
|
||||
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_create_contact_route_free_users(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
|
@ -5,7 +5,7 @@ from app.models import Mailbox
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_create_mailbox(flask_client):
|
||||
def test_create_mailbox_valid(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.post(
|
||||
@ -21,10 +21,34 @@ def test_create_mailbox(flask_client):
|
||||
assert r.json["default"] is False
|
||||
assert r.json["nb_alias"] == 0
|
||||
|
||||
# invalid email address
|
||||
|
||||
def test_create_mailbox_invalid_email(flask_client):
|
||||
login(flask_client)
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={"email": "gmail.com"},
|
||||
json={"email": "gmail.com"}, # not an email address
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "Invalid email"}
|
||||
|
||||
|
||||
def test_create_mailbox_empty_payload(flask_client):
|
||||
login(flask_client)
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "Invalid email"}
|
||||
|
||||
|
||||
def test_create_mailbox_empty_email(flask_client):
|
||||
login(flask_client)
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={"email": ""},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
|
40
app/tests/jobs/test_send_event_to_webhook.py
Normal file
40
app/tests/jobs/test_send_event_to_webhook.py
Normal file
@ -0,0 +1,40 @@
|
||||
import arrow
|
||||
|
||||
from app import config
|
||||
from app.events.generated.event_pb2 import EventContent, AliasDeleted
|
||||
from app.jobs.send_event_job import SendEventToWebhookJob
|
||||
from app.models import PartnerUser
|
||||
from app.proton.utils import get_proton_partner
|
||||
from events.event_sink import ConsoleEventSink
|
||||
from tests.utils import create_new_user, random_token
|
||||
|
||||
|
||||
def test_serialize_and_deserialize_job():
|
||||
user = create_new_user()
|
||||
alias_id = 34
|
||||
alias_email = "a@b.c"
|
||||
event = EventContent(alias_deleted=AliasDeleted(id=alias_id, email=alias_email))
|
||||
run_at = arrow.now().shift(hours=10)
|
||||
db_job = SendEventToWebhookJob(user, event).store_job_in_db(run_at=run_at)
|
||||
assert db_job.run_at == run_at
|
||||
assert db_job.name == config.JOB_SEND_EVENT_TO_WEBHOOK
|
||||
job = SendEventToWebhookJob.create_from_job(db_job)
|
||||
assert job._user.id == user.id
|
||||
assert job._event.alias_deleted.id == alias_id
|
||||
assert job._event.alias_deleted.email == alias_email
|
||||
|
||||
|
||||
def test_send_event_to_webhook():
|
||||
user = create_new_user()
|
||||
PartnerUser.create(
|
||||
user_id=user.id,
|
||||
partner_id=get_proton_partner().id,
|
||||
external_user_id=random_token(10),
|
||||
flush=True,
|
||||
)
|
||||
alias_id = 34
|
||||
alias_email = "a@b.c"
|
||||
event = EventContent(alias_deleted=AliasDeleted(id=alias_id, email=alias_email))
|
||||
job = SendEventToWebhookJob(user, event)
|
||||
sink = ConsoleEventSink()
|
||||
assert job.run(sink)
|
@ -25,15 +25,17 @@ class MockProtonClient(ProtonClient):
|
||||
return self.user
|
||||
|
||||
|
||||
def check_initial_sync_job(user: User):
|
||||
def check_initial_sync_job(user: User, expected: bool):
|
||||
found = False
|
||||
for job in Job.yield_per_query(10).filter_by(
|
||||
name=config.JOB_SEND_ALIAS_CREATION_EVENTS,
|
||||
state=JobState.ready.value,
|
||||
):
|
||||
if job.payload.get("user_id") == user.id:
|
||||
found = True
|
||||
Job.delete(job.id)
|
||||
return
|
||||
assert False
|
||||
break
|
||||
assert expected == found
|
||||
|
||||
|
||||
def test_proton_callback_handler_unexistant_sl_user():
|
||||
@ -69,10 +71,9 @@ def test_proton_callback_handler_unexistant_sl_user():
|
||||
)
|
||||
assert partner_user is not None
|
||||
assert partner_user.external_user_id == external_id
|
||||
check_initial_sync_job(res.user)
|
||||
|
||||
|
||||
def test_proton_callback_handler_existant_sl_user():
|
||||
def test_proton_callback_handler_existing_sl_user():
|
||||
email = random_email()
|
||||
sl_user = User.create(email, commit=True)
|
||||
|
||||
@ -98,7 +99,43 @@ def test_proton_callback_handler_existant_sl_user():
|
||||
sa = PartnerUser.get_by(user_id=sl_user.id, partner_id=get_proton_partner().id)
|
||||
assert sa is not None
|
||||
assert sa.partner_email == user.email
|
||||
check_initial_sync_job(res.user)
|
||||
check_initial_sync_job(res.user, True)
|
||||
|
||||
|
||||
def test_proton_callback_handler_linked_sl_user():
|
||||
email = random_email()
|
||||
external_id = random_string()
|
||||
sl_user = User.create(email, commit=True)
|
||||
PartnerUser.create(
|
||||
user_id=sl_user.id,
|
||||
partner_id=get_proton_partner().id,
|
||||
external_user_id=external_id,
|
||||
partner_email=email,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
user = UserInformation(
|
||||
email=email,
|
||||
name=random_string(),
|
||||
id=external_id,
|
||||
plan=SLPlan(type=SLPlanType.Premium, expiration=Arrow.utcnow().shift(hours=2)),
|
||||
)
|
||||
handler = ProtonCallbackHandler(MockProtonClient(user=user))
|
||||
res = handler.handle_login(get_proton_partner())
|
||||
|
||||
assert res.user is not None
|
||||
assert res.user.id == sl_user.id
|
||||
# Ensure the user is not marked as created from partner
|
||||
assert User.FLAG_CREATED_FROM_PARTNER != (
|
||||
res.user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert res.user.notification is True
|
||||
assert res.user.trial_end is not None
|
||||
|
||||
sa = PartnerUser.get_by(user_id=sl_user.id, partner_id=get_proton_partner().id)
|
||||
assert sa is not None
|
||||
assert sa.partner_email == user.email
|
||||
check_initial_sync_job(res.user, False)
|
||||
|
||||
|
||||
def test_proton_callback_handler_none_user_login():
|
||||
|
@ -94,10 +94,12 @@ def test_login_case_from_partner():
|
||||
)
|
||||
assert res.user.activated is True
|
||||
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(user_id=res.user.id).all()
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=res.user.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(audit_logs) == 1
|
||||
assert audit_logs[0].user_id == res.user.id
|
||||
assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
|
||||
|
||||
def test_login_case_from_partner_with_uppercase_email():
|
||||
@ -133,7 +135,10 @@ def test_login_case_from_web():
|
||||
assert 0 == (res.user.flags & User.FLAG_CREATED_FROM_PARTNER)
|
||||
assert res.user.activated is True
|
||||
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(user_id=res.user.id).all()
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=res.user.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(audit_logs) == 1
|
||||
assert audit_logs[0].user_id == res.user.id
|
||||
assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
@ -218,7 +223,10 @@ def test_link_account_with_proton_account_same_address(flask_client):
|
||||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(user_id=res.user.id).all()
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=res.user.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(audit_logs) == 1
|
||||
assert audit_logs[0].user_id == res.user.id
|
||||
assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
@ -246,7 +254,10 @@ def test_link_account_with_proton_account_different_address(flask_client):
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(user_id=res.user.id).all()
|
||||
audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=res.user.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(audit_logs) == 1
|
||||
assert audit_logs[0].user_id == res.user.id
|
||||
assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
@ -304,19 +315,19 @@ def test_link_account_with_proton_account_same_address_but_linked_to_other_user(
|
||||
|
||||
# Ensure audit logs for sl_user_1 show the link action
|
||||
sl_user_1_audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=sl_user_1.id
|
||||
user_id=sl_user_1.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(sl_user_1_audit_logs) == 1
|
||||
assert sl_user_1_audit_logs[0].user_id == sl_user_1.id
|
||||
assert sl_user_1_audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
|
||||
# Ensure audit logs for sl_user_2 show the unlink action
|
||||
sl_user_2_audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=sl_user_2.id
|
||||
user_id=sl_user_2.id,
|
||||
action=UserAuditLogAction.UnlinkAccount.value,
|
||||
).all()
|
||||
assert len(sl_user_2_audit_logs) == 1
|
||||
assert sl_user_2_audit_logs[0].user_id == sl_user_2.id
|
||||
assert sl_user_2_audit_logs[0].action == UserAuditLogAction.UnlinkAccount.value
|
||||
|
||||
|
||||
def test_link_account_with_proton_account_different_address_and_linked_to_other_user(
|
||||
@ -356,19 +367,19 @@ def test_link_account_with_proton_account_different_address_and_linked_to_other_
|
||||
|
||||
# Ensure audit logs for sl_user_1 show the link action
|
||||
sl_user_1_audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=sl_user_1.id
|
||||
user_id=sl_user_1.id,
|
||||
action=UserAuditLogAction.LinkAccount.value,
|
||||
).all()
|
||||
assert len(sl_user_1_audit_logs) == 1
|
||||
assert sl_user_1_audit_logs[0].user_id == sl_user_1.id
|
||||
assert sl_user_1_audit_logs[0].action == UserAuditLogAction.LinkAccount.value
|
||||
|
||||
# Ensure audit logs for sl_user_2 show the unlink action
|
||||
sl_user_2_audit_logs: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=sl_user_2.id
|
||||
user_id=sl_user_2.id,
|
||||
action=UserAuditLogAction.UnlinkAccount.value,
|
||||
).all()
|
||||
assert len(sl_user_2_audit_logs) == 1
|
||||
assert sl_user_2_audit_logs[0].user_id == sl_user_2.id
|
||||
assert sl_user_2_audit_logs[0].action == UserAuditLogAction.UnlinkAccount.value
|
||||
|
||||
|
||||
def test_cannot_create_instance_of_base_strategy():
|
||||
|
@ -286,6 +286,15 @@ def test_verify_other_users_mailbox():
|
||||
mailbox_utils.verify_mailbox_code(user, mailbox.id, "9999999")
|
||||
|
||||
|
||||
def test_verify_other_users_already_verified_mailbox():
|
||||
other = create_new_user()
|
||||
mailbox = Mailbox.create(
|
||||
user_id=other.id, email=random_email(), verified=True, commit=True
|
||||
)
|
||||
with pytest.raises(mailbox_utils.MailboxError):
|
||||
mailbox_utils.verify_mailbox_code(user, mailbox.id, "9999999")
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_verify_fail():
|
||||
output = mailbox_utils.create_mailbox(user, random_email())
|
||||
@ -305,10 +314,13 @@ def test_verify_too_may():
|
||||
output = mailbox_utils.create_mailbox(user, random_email())
|
||||
output.activation.tries = mailbox_utils.MAX_ACTIVATION_TRIES
|
||||
Session.commit()
|
||||
with pytest.raises(mailbox_utils.CannotVerifyError):
|
||||
try:
|
||||
mailbox_utils.verify_mailbox_code(
|
||||
user, output.mailbox.id, output.activation.code
|
||||
)
|
||||
assert False
|
||||
except mailbox_utils.CannotVerifyError as e:
|
||||
assert e.deleted_activation_code
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
@ -351,7 +363,9 @@ def test_perform_mailbox_email_change_valid_id_not_new_email():
|
||||
res = mailbox_utils.perform_mailbox_email_change(mb.id)
|
||||
assert res.error == MailboxEmailChangeError.InvalidId
|
||||
assert res.message_category == "error"
|
||||
audit_log_entries = UserAuditLog.filter_by(user_id=user.id).count()
|
||||
audit_log_entries = UserAuditLog.filter_by(
|
||||
user_id=user.id, action=UserAuditLogAction.UpdateMailbox.value
|
||||
).count()
|
||||
assert audit_log_entries == 0
|
||||
|
||||
|
||||
@ -374,7 +388,9 @@ def test_perform_mailbox_email_change_valid_id_email_already_used():
|
||||
res = mailbox_utils.perform_mailbox_email_change(mb_to_change.id)
|
||||
assert res.error == MailboxEmailChangeError.EmailAlreadyUsed
|
||||
assert res.message_category == "error"
|
||||
audit_log_entries = UserAuditLog.filter_by(user_id=user.id).count()
|
||||
audit_log_entries = UserAuditLog.filter_by(
|
||||
user_id=user.id, action=UserAuditLogAction.UpdateMailbox.value
|
||||
).count()
|
||||
assert audit_log_entries == 0
|
||||
|
||||
|
||||
@ -398,6 +414,7 @@ def test_perform_mailbox_email_change_success():
|
||||
assert db_mailbox.email == new_email
|
||||
assert db_mailbox.new_email is None
|
||||
|
||||
audit_log_entries = UserAuditLog.filter_by(user_id=user.id).all()
|
||||
assert len(audit_log_entries) == 1
|
||||
assert audit_log_entries[0].action == UserAuditLogAction.UpdateMailbox.value
|
||||
audit_log_entries = UserAuditLog.filter_by(
|
||||
user_id=user.id, action=UserAuditLogAction.UpdateMailbox.value
|
||||
).count()
|
||||
assert audit_log_entries == 1
|
||||
|
@ -27,7 +27,9 @@ def test_emit_alias_audit_log_for_random_data():
|
||||
commit=True,
|
||||
)
|
||||
|
||||
logs_for_user: List[UserAuditLog] = UserAuditLog.filter_by(user_id=user.id).all()
|
||||
logs_for_user: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=user.id, action=action.value
|
||||
).all()
|
||||
assert len(logs_for_user) == 1
|
||||
assert logs_for_user[0].user_id == user.id
|
||||
assert logs_for_user[0].user_email == user.email
|
||||
@ -41,7 +43,10 @@ def test_emit_audit_log_on_mailbox_creation():
|
||||
user=user, email=random_email(), verified=True
|
||||
)
|
||||
|
||||
logs_for_user: List[UserAuditLog] = UserAuditLog.filter_by(user_id=user.id).all()
|
||||
logs_for_user: List[UserAuditLog] = UserAuditLog.filter_by(
|
||||
user_id=user.id,
|
||||
action=UserAuditLogAction.CreateMailbox.value,
|
||||
).all()
|
||||
assert len(logs_for_user) == 1
|
||||
assert logs_for_user[0].user_id == user.id
|
||||
assert logs_for_user[0].user_email == user.email
|
||||
|
Reference in New Issue
Block a user