diff --git a/app/app/api/base.py b/app/app/api/base.py index a792e8d..80f57b2 100644 --- a/app/app/api/base.py +++ b/app/app/api/base.py @@ -5,6 +5,7 @@ import arrow from flask import Blueprint, request, jsonify, g from flask_login import current_user +from app import constants from app.db import Session from app.models import ApiKey @@ -18,7 +19,9 @@ def authorize_request() -> Optional[Tuple[str, int]]: api_key = ApiKey.get_by(code=api_code) if not api_key: - if current_user.is_authenticated: + if current_user.is_authenticated and request.headers.get( + constants.HEADER_ALLOW_API_COOKIES + ): g.user = current_user else: return jsonify(error="Wrong api key"), 401 diff --git a/app/app/constants.py b/app/app/constants.py new file mode 100644 index 0000000..b20bc6a --- /dev/null +++ b/app/app/constants.py @@ -0,0 +1 @@ +HEADER_ALLOW_API_COOKIES = "X-Sl-Allowcookies" diff --git a/app/app/mail_sender.py b/app/app/mail_sender.py index 9610b45..9c74952 100644 --- a/app/app/mail_sender.py +++ b/app/app/mail_sender.py @@ -76,7 +76,6 @@ class SendRequest: file_path = os.path.join(config.SAVE_UNSENT_DIR, file_name) self.save_request_to_file(file_path) - @staticmethod def save_request_to_failed_dir(self, prefix: str = "DeliveryRetryFail"): file_name = ( f"{prefix}-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}" diff --git a/app/app/session.py b/app/app/session.py index 481e5ff..93acd3b 100644 --- a/app/app/session.py +++ b/app/app/session.py @@ -87,6 +87,7 @@ class RedisSessionStore(SessionInterface): httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) + samesite = self.get_cookie_samesite(app) val = pickle.dumps(dict(session)) ttl = int(app.permanent_session_lifetime.total_seconds()) # Only 5 minutes for non-authenticated sessions. @@ -109,6 +110,7 @@ class RedisSessionStore(SessionInterface): domain=domain, path=path, secure=secure, + samesite=samesite, ) diff --git a/app/email_handler.py b/app/email_handler.py index 2882e56..7257800 100644 --- a/app/email_handler.py +++ b/app/email_handler.py @@ -235,13 +235,14 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con contact.mail_from = mail_from Session.commit() else: + alias_id = alias.id try: contact_email_for_reply = ( contact_email if is_valid_email(contact_email) else "" ) contact = Contact.create( user_id=alias.user_id, - alias_id=alias.id, + alias_id=alias_id, website_email=contact_email, name=contact_name, mail_from=mail_from, @@ -261,9 +262,11 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con Session.commit() except IntegrityError: - LOG.w(f"Contact with email {contact_email} for alias {alias} already exist") - Session.rollback() - contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) + # No need to manually rollback, as IntegrityError already rolls back + LOG.info( + f"Contact with email {contact_email} for alias_id {alias_id} already existed, fetching from DB" + ) + contact = Contact.get_by(alias_id=alias_id, website_email=contact_email) return contact @@ -662,6 +665,9 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str from_header = get_header_unicode(msg[headers.FROM]) LOG.d("Create or get contact for from_header:%s", from_header) contact = get_or_create_contact(from_header, envelope.mail_from, alias) + alias = ( + contact.alias + ) # In case the Session was closed in the get_or_create we re-fetch the alias reply_to_contact = None if msg[headers.REPLY_TO]: diff --git a/app/server.py b/app/server.py index ecbe00a..04278dc 100644 --- a/app/server.py +++ b/app/server.py @@ -29,7 +29,7 @@ from sentry_sdk.integrations.flask import FlaskIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from werkzeug.middleware.proxy_fix import ProxyFix -from app import paddle_utils, config, paddle_callback +from app import paddle_utils, config, paddle_callback, constants from app.admin_model import ( SLAdminIndexView, UserAdmin, @@ -430,6 +430,7 @@ def jinja2_filter(app): PAGE_LIMIT=PAGE_LIMIT, ZENDESK_ENABLED=ZENDESK_ENABLED, MAX_NB_EMAIL_FREE_PLAN=MAX_NB_EMAIL_FREE_PLAN, + HEADER_ALLOW_API_COOKIES=constants.HEADER_ALLOW_API_COOKIES, ) @@ -702,7 +703,12 @@ def setup_coinbase_commerce(app): def handle_coinbase_event(event) -> bool: - user_id = int(event["data"]["metadata"]["user_id"]) + server_user_id = event["data"]["metadata"]["user_id"] + try: + user_id = int(server_user_id) + except ValueError: + user_id = int(float(server_user_id)) + code = event["data"]["code"] user = User.get(user_id) if not user: diff --git a/app/templates/dashboard/alias_contact_manager.html b/app/templates/dashboard/alias_contact_manager.html index 886327a..1fdc518 100644 --- a/app/templates/dashboard/alias_contact_manager.html +++ b/app/templates/dashboard/alias_contact_manager.html @@ -264,6 +264,7 @@ method: "POST", headers: { "Content-Type": "application/json", + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' } }); diff --git a/app/templates/dashboard/support.html b/app/templates/dashboard/support.html index 81b7a68..d51f697 100644 --- a/app/templates/dashboard/support.html +++ b/app/templates/dashboard/support.html @@ -80,7 +80,10 @@ }, methods: { generateRandomAlias: async function (event) { - let result = await fetch('/api/alias/random/new', {method: 'POST'}); + let result = await fetch('/api/alias/random/new', {method: 'POST', + headers: { + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' + }}); if (result.ok) { let data = await result.json(); this.ticket_email = data.alias; diff --git a/app/templates/footer.html b/app/templates/footer.html index 180b6c7..0aa7c33 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -216,6 +216,7 @@ method: "POST", headers: { "Content-Type": "application/json", + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' } }); @@ -232,6 +233,7 @@ method: "GET", headers: { "Content-Type": "application/json", + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' } }); if (res.ok) { @@ -249,6 +251,7 @@ method: "GET", headers: { "Content-Type": "application/json", + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' } }); if (res.ok) { diff --git a/app/templates/phone/phone_reservation.html b/app/templates/phone/phone_reservation.html index c1bb8b8..cf1ec3e 100644 --- a/app/templates/phone/phone_reservation.html +++ b/app/templates/phone/phone_reservation.html @@ -87,6 +87,7 @@ method: "GET", headers: { "Content-Type": "application/json", + '{{HEADER_ALLOW_API_COOKIES}}': 'allow' } }); if (res.ok) { diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 37b2abb..43ea7ac 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,5 +1,7 @@ import os +from flask import testing + # use the tests/test.env config fle # flake8: noqa: E402 @@ -42,7 +44,16 @@ def flask_app(): yield app -from app import config +from app import config, constants + + +class CustomTestClient(testing.FlaskClient): + def open(self, *args, **kwargs): + if isinstance(args[0], str): + headers = kwargs.pop("headers", {}) + headers.update({constants.HEADER_ALLOW_API_COOKIES: "allow"}) + kwargs["headers"] = headers + return super().open(*args, **kwargs) @pytest.fixture @@ -53,7 +64,9 @@ def flask_client(): # disable rate limit during test config.DISABLE_RATE_LIMIT = True try: + app.test_client_class = CustomTestClient client = app.test_client() + client.environ_base[constants.HEADER_ALLOW_API_COOKIES] = "allow" yield client finally: # disable rate limit again as some tests might enable rate limit