diff --git a/CHANGELOG.md b/CHANGELOG.md index 8470f92c..042e9c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - ♿️(frontend) make carousel image decorative #871 - ♿️(frontend) reactions are now vocalized and configurable #849 - ♿️(frontend) improve background effect announcements #879 +- 🐛(brevo) use django-lasuite for marketing management #885 ### Fixed diff --git a/docs/installation/kubernetes.md b/docs/installation/kubernetes.md index 62f4aeb8..0dbde1ba 100644 --- a/docs/installation/kubernetes.md +++ b/docs/installation/kubernetes.md @@ -248,122 +248,119 @@ You can use LaSuite Meet on https://meet.127.0.0.1.nip.io from the local device. These are the environmental options available on meet backend. -| Option | Description | default | -|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DATA_DIR | Data directory location | /data | -| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] | -| DJANGO_SECRET_KEY | Secret key used for Django security | | -| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] | -| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false | -| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 | -| DB_NAME | Name of the database | meet | -| DB_USER | User used to connect to database | dinum | -| DB_PASSWORD | Password used to connect to the database | pass | -| DB_HOST | Hostname of the database | localhost | -| DB_PORT | Port to connect to database | 5432 | -| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage | -| AWS_S3_ENDPOINT_URL | S3 host endpoint | | -| AWS_S3_ACCESS_KEY_ID | S3 access key | | -| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | | -| AWS_S3_REGION_NAME | S3 region | | -| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage | -| DJANGO_LANGUAGE_CODE | Default language | en-us | -| REDIS_URL | Redis endpoint | redis://redis:6379/1 | -| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) | -| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute | -| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute | -| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false | -| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] | -| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `` tag with this URL as href is added to the `
` of the frontend app | | -| FRONTEND_ANALYTICS | Analytics information | {} | -| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} | -| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} | -| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false | -| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | -| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | -| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | -| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | -| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | -| DJANGO_EMAIL_HOST | Host of the email server | | -| DJANGO_EMAIL_HOST_USER | User to connect to the email server | | -| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | | -| DJANGO_EMAIL_PORT | Port to connect to the email server | | -| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false | -| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false | -| DJANGO_EMAIL_FROM | Email from account | from@example.com | -| EMAIL_BRAND_NAME | Email branding name | | -| EMAIL_SUPPORT_EMAIL | Support email address | | -| EMAIL_LOGO_IMG | Email logo image | | -| EMAIL_DOMAIN | Email domain | | -| EMAIL_APP_BASE_URL | Email app base URL | | -| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | -| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] | -| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] | -| SENTRY_DSN | Sentry server DSN | | -| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 | -| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} | -| OIDC_CREATE_USER | Create OIDC user if not exists | true | -| OIDC_VERIFY_SSL | Verify SSL for OIDC | true | -| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false | -| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 | -| OIDC_RP_CLIENT_ID | OIDC client ID | meet | -| OIDC_RP_CLIENT_SECRET | OIDC client secret | | -| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | | -| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | | -| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | | -| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | | -| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO | -| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | | -| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} | -| OIDC_RP_SCOPES | OIDC scopes | openid email | -| OIDC_USE_NONCE | Use nonce for OIDC | true | -| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false | -| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] | -| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true | -| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo | -| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] | -| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name | -| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] | -| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False | -| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 | -| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 | -| LOGIN_REDIRECT_URL | Login redirect URL | | -| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | | -| LOGOUT_REDIRECT_URL | URL to redirect to on logout | | -| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true | -| LIVEKIT_API_KEY | LiveKit API key | | -| LIVEKIT_API_SECRET | LiveKit API secret | | -| LIVEKIT_API_URL | LiveKit API URL | | -| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true | -| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false | -| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false | -| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public | -| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true | -| RECORDING_ENABLE | Record meeting option | false | -| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings | -| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} | -| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser | -| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true | -| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false | -| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | | -| RECORDING_EXPIRATION_DAYS | Recording expiration in days | | -| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | | -| SCREEN_RECORDING_BASE_URL | Screen recording base URL | | -| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | | -| SUMMARY_SERVICE_API_TOKEN | API token for summary service | | -| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false | -| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService | -| BREVO_API_KEY | Brevo API key for marketing emails | | -| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] | -| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} | -| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 | -| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby | -| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 | -| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 | -| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) | -| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting | -| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId | -| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) | -| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false | -| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 | -| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 | +| Option | Description | default | +|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DATA_DIR | Data directory location | /data | +| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] | +| DJANGO_SECRET_KEY | Secret key used for Django security | | +| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] | +| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false | +| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 | +| DB_NAME | Name of the database | meet | +| DB_USER | User used to connect to database | dinum | +| DB_PASSWORD | Password used to connect to the database | pass | +| DB_HOST | Hostname of the database | localhost | +| DB_PORT | Port to connect to database | 5432 | +| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage | +| AWS_S3_ENDPOINT_URL | S3 host endpoint | | +| AWS_S3_ACCESS_KEY_ID | S3 access key | | +| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | | +| AWS_S3_REGION_NAME | S3 region | | +| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage | +| DJANGO_LANGUAGE_CODE | Default language | en-us | +| REDIS_URL | Redis endpoint | redis://redis:6379/1 | +| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) | +| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute | +| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute | +| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false | +| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] | +| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `` tag with this URL as href is added to the `` of the frontend app | | +| FRONTEND_ANALYTICS | Analytics information | {} | +| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} | +| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} | +| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false | +| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true | +| FRONTEND_FEEDBACK | Frontend feedback configuration | {} | +| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false | +| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false | +| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend | +| DJANGO_EMAIL_HOST | Host of the email server | | +| DJANGO_EMAIL_HOST_USER | User to connect to the email server | | +| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | | +| DJANGO_EMAIL_PORT | Port to connect to the email server | | +| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false | +| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false | +| DJANGO_EMAIL_FROM | Email from account | from@example.com | +| EMAIL_BRAND_NAME | Email branding name | | +| EMAIL_SUPPORT_EMAIL | Support email address | | +| EMAIL_LOGO_IMG | Email logo image | | +| EMAIL_DOMAIN | Email domain | | +| EMAIL_APP_BASE_URL | Email app base URL | | +| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false | +| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] | +| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] | +| SENTRY_DSN | Sentry server DSN | | +| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 | +| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} | +| OIDC_CREATE_USER | Create OIDC user if not exists | true | +| OIDC_VERIFY_SSL | Verify SSL for OIDC | true | +| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false | +| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 | +| OIDC_RP_CLIENT_ID | OIDC client ID | meet | +| OIDC_RP_CLIENT_SECRET | OIDC client secret | | +| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | | +| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | | +| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | | +| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | | +| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO | +| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | | +| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} | +| OIDC_RP_SCOPES | OIDC scopes | openid email | +| OIDC_USE_NONCE | Use nonce for OIDC | true | +| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false | +| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] | +| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true | +| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo | +| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] | +| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name | +| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] | +| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False | +| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 | +| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 | +| LOGIN_REDIRECT_URL | Login redirect URL | | +| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | | +| LOGOUT_REDIRECT_URL | URL to redirect to on logout | | +| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true | +| LIVEKIT_API_KEY | LiveKit API key | | +| LIVEKIT_API_SECRET | LiveKit API secret | | +| LIVEKIT_API_URL | LiveKit API URL | | +| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true | +| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false | +| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false | +| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public | +| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true | +| RECORDING_ENABLE | Record meeting option | false | +| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings | +| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} | +| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser | +| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true | +| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false | +| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | | +| RECORDING_EXPIRATION_DAYS | Recording expiration in days | | +| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | | +| SCREEN_RECORDING_BASE_URL | Screen recording base URL | | +| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | | +| SUMMARY_SERVICE_API_TOKEN | API token for summary service | | +| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false | +| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend | +| LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} | +| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby | +| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 | +| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 | +| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) | +| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting | +| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId | +| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) | +| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false | +| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 | +| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 | diff --git a/src/backend/core/authentication/backends.py b/src/backend/core/authentication/backends.py index 64a962c9..109a68bb 100644 --- a/src/backend/core/authentication/backends.py +++ b/src/backend/core/authentication/backends.py @@ -1,21 +1,14 @@ """Authentication Backends for the Meet core app.""" -import contextlib - from django.conf import settings -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation -from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import SuspiciousOperation +from lasuite.marketing.tasks import create_or_update_contact from lasuite.oidc_login.backends import ( OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend, ) from core.models import User -from core.services.marketing import ( - ContactCreationError, - ContactData, - get_marketing_service, -) class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): @@ -61,25 +54,11 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): @staticmethod def signup_to_marketing_email(email): - """Pragmatic approach to newsletter signup during authentication flow. - - Details: - 1. Uses a very short timeout (1s) to prevent blocking the auth process - 2. Silently fails if the marketing service is down/slow to prioritize user experience - 3. Trade-off: May miss some signups but ensures auth flow remains fast - - Note: For a more robust solution, consider using Async task processing (Celery/Django-Q) - """ - with contextlib.suppress( - ContactCreationError, ImproperlyConfigured, ImportError - ): - marketing_service = get_marketing_service() - contact_data = ContactData( - email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]} - ) - marketing_service.create_contact( - contact_data, timeout=settings.BREVO_API_TIMEOUT - ) + """Pragmatic approach to newsletter signup during authentication flow.""" + create_or_update_contact.delay( + email=email, + attributes={"VISIO_SOURCE": ["SIGNIN"]}, + ) def get_existing_user(self, sub, email): """Fetch existing user by sub or email.""" diff --git a/src/backend/core/services/marketing.py b/src/backend/core/services/marketing.py deleted file mode 100644 index 554e740c..00000000 --- a/src/backend/core/services/marketing.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Marketing service in charge of pushing data for marketing automation.""" - -import logging -from dataclasses import dataclass -from functools import lru_cache -from typing import Dict, List, Optional, Protocol - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.utils.module_loading import import_string - -import brevo_python -import urllib3 - -logger = logging.getLogger(__name__) - - -class ContactCreationError(Exception): - """Raised when the contact creation fails.""" - - -@dataclass -class ContactData: - """Contact data for marketing service integration.""" - - email: str - attributes: Optional[Dict[str, str]] = None - list_ids: Optional[List[int]] = None - update_enabled: bool = True - - -class MarketingServiceProtocol(Protocol): - """Interface for marketing automation service integrations.""" - - def create_contact( - self, contact_data: ContactData, timeout: Optional[int] = None - ) -> dict: - """Create or update a contact. - - Args: - contact_data: Contact information and attributes - timeout: API request timeout in seconds - - Returns: - dict: Service response - - Raises: - ContactCreationError: If contact creation fails - """ - - -class BrevoMarketingService: - """Brevo marketing automation integration. - - Handles: - - Contact management and segmentation - - Marketing campaigns and automation - - Email communications - - Configuration via Django settings: - - BREVO_API_KEY: API authentication - - BREVO_API_CONTACT_LIST_IDS: Default contact lists - - BREVO_API_CONTACT_ATTRIBUTES: Default contact attributes - """ - - def __init__(self): - """Initialize Brevo (ex-sendinblue) marketing service.""" - - if not settings.BREVO_API_KEY: - raise ImproperlyConfigured("Brevo API key is required") - - configuration = brevo_python.Configuration() - configuration.api_key["api-key"] = settings.BREVO_API_KEY - - self._api_client = brevo_python.ApiClient(configuration) - - def create_contact(self, contact_data: ContactData, timeout=None) -> dict: - """Create or update a Brevo contact. - - Args: - contact_data: Contact information and attributes - timeout: API request timeout in seconds - - Returns: - dict: Brevo API response - - Raises: - ContactCreationError: If contact creation fails - ImproperlyConfigured: If required settings are missing - - Note: - Contact attributes must be pre-configured in Brevo. - Changes to attributes can impact existing workflows. - """ - - if not settings.BREVO_API_CONTACT_LIST_IDS: - raise ImproperlyConfigured( - "Default Brevo List IDs must be configured in settings." - ) - - contact_api = brevo_python.ContactsApi(self._api_client) - - attributes = { - **settings.BREVO_API_CONTACT_ATTRIBUTES, - **(contact_data.attributes or {}), - } - - list_ids = (contact_data.list_ids or []) + settings.BREVO_API_CONTACT_LIST_IDS - - contact = brevo_python.CreateContact( - email=contact_data.email, - attributes=attributes, - list_ids=list_ids, - update_enabled=contact_data.update_enabled, - ) - - api_configurations = {} - - if timeout is not None: - api_configurations["_request_timeout"] = timeout - - try: - response = contact_api.create_contact(contact, **api_configurations) - except ( - brevo_python.rest.ApiException, - urllib3.exceptions.ReadTimeoutError, - ) as err: - logger.warning("Failed to create contact in Brevo", exc_info=True) - raise ContactCreationError("Failed to create contact in Brevo") from err - - return response - - -@lru_cache(maxsize=1) -def get_marketing_service() -> MarketingServiceProtocol: - """Return cached instance of configured marketing service.""" - marketing_service_cls = import_string(settings.MARKETING_SERVICE_CLASS) - return marketing_service_cls() diff --git a/src/backend/core/tests/authentication/test_backends.py b/src/backend/core/tests/authentication/test_backends.py index 4e6d649d..9ebc4bbe 100644 --- a/src/backend/core/tests/authentication/test_backends.py +++ b/src/backend/core/tests/authentication/test_backends.py @@ -2,14 +2,14 @@ from unittest import mock -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation +from django.core.exceptions import SuspiciousOperation import pytest +from lasuite.marketing.tasks import create_or_update_contact from core import models from core.authentication.backends import OIDCAuthenticationBackend from core.factories import UserFactory -from core.services import marketing pytestmark = pytest.mark.django_db @@ -498,8 +498,8 @@ def test_marketing_signup_existing_user( mock_signup.assert_not_called() -@mock.patch("core.authentication.backends.get_marketing_service") -def test_signup_to_marketing_email_success(mock_marketing): +@mock.patch.object(create_or_update_contact, "delay") +def test_signup_to_marketing_email_success(mock_create_or_update_contact): """Test successful marketing signup.""" email = "test@example.com" @@ -508,46 +508,6 @@ def test_signup_to_marketing_email_success(mock_marketing): OIDCAuthenticationBackend.signup_to_marketing_email(email) # Verify service interaction - mock_service = mock_marketing.return_value - mock_service.create_contact.assert_called_once() - - -@pytest.mark.parametrize( - "error", - [ - ImportError, - ImproperlyConfigured, - ], -) -@mock.patch("core.authentication.backends.get_marketing_service") -def test_marketing_signup_handles_service_initialization_errors( - mock_marketing, error, settings -): - """Tests errors that occur when trying to get/initialize the marketing service.""" - settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True - - mock_marketing.side_effect = error - - # Should not raise any exception - OIDCAuthenticationBackend.signup_to_marketing_email("test@example.com") - - -@pytest.mark.parametrize( - "error", - [ - marketing.ContactCreationError, - ImproperlyConfigured, - ImportError, - ], -) -@mock.patch("core.authentication.backends.get_marketing_service") -def test_marketing_signup_handles_contact_creation_errors( - mock_marketing, error, settings -): - """Tests errors that occur during the contact creation process.""" - - settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL = True - mock_marketing.return_value.create_contact.side_effect = error - - # Should not raise any exception - OIDCAuthenticationBackend.signup_to_marketing_email("test@example.com") + mock_create_or_update_contact.assert_called_once_with( + email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]} + ) diff --git a/src/backend/core/tests/services/test_marketing.py b/src/backend/core/tests/services/test_marketing.py deleted file mode 100644 index 2b0686d1..00000000 --- a/src/backend/core/tests/services/test_marketing.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -Test marketing services. -""" - -# pylint: disable=W0621,W0613 - -from unittest import mock - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - -import brevo_python -import pytest -import urllib3 - -from core.services.marketing import ( - BrevoMarketingService, - ContactCreationError, - ContactData, - get_marketing_service, -) - - -def test_init_missing_api_key(settings): - """Test initialization with missing API key.""" - settings.BREVO_API_KEY = None - with pytest.raises(ImproperlyConfigured, match="Brevo API key is required"): - BrevoMarketingService() - - -def test_create_contact_missing_list_ids(settings): - """Test contact creation with missing list IDs.""" - - settings.BREVO_API_KEY = "test-api-key" - settings.BREVO_API_CONTACT_LIST_IDS = None - settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"} - - valid_contact_data = ContactData( - email="test@example.com", - attributes={"first_name": "Test"}, - list_ids=[1, 2], - update_enabled=True, - ) - - brevo_service = BrevoMarketingService() - - with pytest.raises( - ImproperlyConfigured, match="Default Brevo List IDs must be configured" - ): - brevo_service.create_contact(valid_contact_data) - - -@mock.patch("brevo_python.ContactsApi") -def test_create_contact_success(mock_contact_api): - """Test successful contact creation.""" - - mock_api = mock_contact_api.return_value - - settings.BREVO_API_KEY = "test-api-key" - settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4] - settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"} - - valid_contact_data = ContactData( - email="test@example.com", - attributes={"first_name": "Test"}, - list_ids=[1, 2], - update_enabled=True, - ) - - brevo_service = BrevoMarketingService() - - mock_api.create_contact.return_value = {"id": "test-id"} - response = brevo_service.create_contact(valid_contact_data) - - assert response == {"id": "test-id"} - - mock_api.create_contact.assert_called_once() - contact_arg = mock_api.create_contact.call_args[0][0] - assert contact_arg.email == "test@example.com" - assert contact_arg.attributes == { - **settings.BREVO_API_CONTACT_ATTRIBUTES, - **valid_contact_data.attributes, - } - assert set(contact_arg.list_ids) == {1, 2, 3, 4} - assert contact_arg.update_enabled is True - - -@mock.patch("brevo_python.ContactsApi") -def test_create_contact_with_timeout(mock_contact_api): - """Test contact creation with timeout.""" - - mock_api = mock_contact_api.return_value - - settings.BREVO_API_KEY = "test-api-key" - settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4] - settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"} - - valid_contact_data = ContactData( - email="test@example.com", - attributes={"first_name": "Test"}, - list_ids=[1, 2], - update_enabled=True, - ) - - brevo_service = BrevoMarketingService() - brevo_service.create_contact(valid_contact_data, timeout=30) - - mock_api.create_contact.assert_called_once() - assert mock_api.create_contact.call_args[1]["_request_timeout"] == 30 - - -@mock.patch("brevo_python.ContactsApi") -def test_create_contact_api_error(mock_contact_api): - """Test contact creation API error handling.""" - - mock_api = mock_contact_api.return_value - - settings.BREVO_API_KEY = "test-api-key" - settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4] - settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"} - - valid_contact_data = ContactData( - email="test@example.com", - attributes={"first_name": "Test"}, - list_ids=[1, 2], - update_enabled=True, - ) - - brevo_service = BrevoMarketingService() - - mock_api.create_contact.side_effect = brevo_python.rest.ApiException() - - with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"): - brevo_service.create_contact(valid_contact_data) - - -@mock.patch("brevo_python.ContactsApi") -def test_create_contact_timeout_error(mock_contact_api): - """Test contact creation timeout error handling.""" - - mock_api = mock_contact_api.return_value - settings.BREVO_API_KEY = "test-api-key" - settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4] - settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"} - - valid_contact_data = ContactData( - email="test@example.com", - attributes={"first_name": "Test"}, - list_ids=[1, 2], - update_enabled=True, - ) - - brevo_service = BrevoMarketingService() - - mock_api.create_contact.side_effect = urllib3.exceptions.ReadTimeoutError( - pool=mock.Mock(), - url="https://api.brevo.com/v3/endpoint", - message="HTTPSConnectionPool(host='api.brevo.com', port=443): Read timed out.", - ) - - with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"): - brevo_service.create_contact(valid_contact_data) - - -@pytest.fixture -def clear_marketing_cache(): - """Clear marketing service cache between tests.""" - get_marketing_service.cache_clear() - yield - get_marketing_service.cache_clear() - - -def test_get_marketing_service_caching(clear_marketing_cache): - """Test marketing service caching behavior.""" - settings.BREVO_API_KEY = "test-api-key" - settings.MARKETING_SERVICE_CLASS = "core.services.marketing.BrevoMarketingService" - - service1 = get_marketing_service() - service2 = get_marketing_service() - - assert service1 is service2 - assert isinstance(service1, BrevoMarketingService) - - -def test_get_marketing_service_invalid_class(clear_marketing_cache): - """Test handling of invalid service class.""" - settings.MARKETING_SERVICE_CLASS = "invalid.service.path" - - with pytest.raises(ImportError): - get_marketing_service() - - -@mock.patch("core.services.marketing.import_string") -def test_service_instantiation_called_once(mock_import_string, clear_marketing_cache): - """Test service class is instantiated only once.""" - - settings.BREVO_API_KEY = "test-api-key" - settings.MARKETING_SERVICE_CLASS = "core.services.marketing.BrevoMarketingService" - get_marketing_service.cache_clear() - - mock_service_cls = mock.Mock() - mock_service_instance = mock.Mock() - mock_service_cls.return_value = mock_service_instance - mock_import_string.return_value = mock_service_cls - - service1 = get_marketing_service() - service2 = get_marketing_service() - - mock_import_string.assert_called_once_with(settings.MARKETING_SERVICE_CLASS) - mock_service_cls.assert_called_once() - assert service1 is service2 - assert service1 is mock_service_instance diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index dd6cca0f..693578c2 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -245,6 +245,8 @@ class Base(Configuration): "django.contrib.staticfiles", # OIDC third party "mozilla_django_oidc", + # LaSuite common + "lasuite.marketing", ] # Cache @@ -644,24 +646,18 @@ class Base(Configuration): environ_name="SIGNUP_NEW_USER_TO_MARKETING_EMAIL", environ_prefix=None, ) - MARKETING_SERVICE_CLASS = values.Value( - "core.services.marketing.BrevoMarketingService", - environ_name="MARKETING_SERVICE_CLASS", - environ_prefix=None, - ) - BREVO_API_KEY = SecretFileValue( - None, environ_name="BREVO_API_KEY", environ_prefix=None - ) - BREVO_API_CONTACT_LIST_IDS = values.ListValue( - [], - environ_name="BREVO_API_CONTACT_LIST_IDS", - environ_prefix=None, - converter=lambda x: int(x), # pylint: disable=unnecessary-lambda - ) - BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True}) - BREVO_API_TIMEOUT = values.PositiveIntegerValue( - 1, environ_name="BREVO_API_TIMEOUT", environ_prefix=None - ) + LASUITE_MARKETING = { + "BACKEND": values.Value( + "lasuite.marketing.backends.dummy.DummyBackend", + environ_name="LASUITE_MARKETING_BACKEND", + environ_prefix=None, + ), + "PARAMETERS": values.DictValue( + default={}, + environ_name="LASUITE_MARKETING_PARAMETERS", + environ_prefix=None, + ), + } # Lobby configurations LOBBY_KEY_PREFIX = values.Value( diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index e5be9560..4d8f85be 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "django-configurations==2.5.1", "django-cors-headers==4.9.0", "django-countries==8.0.0", - "django-lasuite[all]==0.0.19", + "django-lasuite[all]==0.0.22", "django-parler==2.3", "redis==5.2.1", "django-redis==6.0.0", diff --git a/src/helm/env.d/dev/values.meet.yaml.gotmpl b/src/helm/env.d/dev/values.meet.yaml.gotmpl index 7348b255..01dbd52e 100644 --- a/src/helm/env.d/dev/values.meet.yaml.gotmpl +++ b/src/helm/env.d/dev/values.meet.yaml.gotmpl @@ -9,11 +9,6 @@ secrets: field: password podVariable: OIDC_RP_CLIENT_SECRET clusterSecretStore: bitwarden-login-meet - - name: brevoApiKey - itemId: 99107889-6124-4436-97cc-a5193f28443f - field: password - podVariable: BREVO_API_KEY - clusterSecretStore: bitwarden-login-meet image: repository: localhost:5001/meet-backend pullPolicy: Always @@ -87,11 +82,6 @@ backend: SUMMARY_SERVICE_API_TOKEN: password RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording SIGNUP_NEW_USER_TO_MARKETING_EMAIL: True - BREVO_API_KEY: - secretKeyRef: - name: backend - key: BREVO_API_KEY - BREVO_API_CONTACT_LIST_IDS: 8 ROOM_TELEPHONY_ENABLED: True SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem