From 1cadc7cc56c86b63037616cd72eae8ca73ea3539 Mon Sep 17 00:00:00 2001 From: Jhony Lucas Date: Sun, 29 Sep 2024 19:58:08 -0300 Subject: [PATCH 1/5] feat: add GCP email field to Account model and update related functionality --- .../migrations/0018_account_gcp_email.py | 20 ++++++ backend/apps/account/models.py | 1 + backend/apps/account/signals.py | 2 +- backend/apps/account_payment/graphql.py | 63 ++++++++++++++++++- backend/apps/account_payment/webhooks.py | 61 ++++++++++++++++-- 5 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 backend/apps/account/migrations/0018_account_gcp_email.py diff --git a/backend/apps/account/migrations/0018_account_gcp_email.py b/backend/apps/account/migrations/0018_account_gcp_email.py new file mode 100644 index 00000000..bfb4c796 --- /dev/null +++ b/backend/apps/account/migrations/0018_account_gcp_email.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 4.2.16 on 2024-09-29 22:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("account", "0017_account_available_for_research_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="account", + name="gcp_email", + field=models.EmailField( + blank=True, max_length=254, null=True, verbose_name="GCP email" + ), + ), + ] diff --git a/backend/apps/account/models.py b/backend/apps/account/models.py index 52dee4c8..779225cf 100644 --- a/backend/apps/account/models.py +++ b/backend/apps/account/models.py @@ -211,6 +211,7 @@ class Account(BaseModel, AbstractBaseUser, PermissionsMixin): uuid = models.UUIDField(primary_key=False, default=uuid4) email = models.EmailField("Email", unique=True) + gcp_email = models.EmailField("GCP email", null=True, blank=True) # Google Cloud Platform email username = models.CharField("Username", max_length=40, blank=True, null=True, unique=True) first_name = models.CharField("Nome", max_length=40, blank=True) diff --git a/backend/apps/account/signals.py b/backend/apps/account/signals.py index a3d889a7..957d8408 100644 --- a/backend/apps/account/signals.py +++ b/backend/apps/account/signals.py @@ -62,7 +62,7 @@ def create_subscription(user: Account): customer.subscriber = user customer.save() # Add user to Google Group - add_user(user.email) + add_user(user.gcp_email or user.email) @receiver(post_save, sender=Account) diff --git a/backend/apps/account_payment/graphql.py b/backend/apps/account_payment/graphql.py index f69b9ba7..77f98caa 100644 --- a/backend/apps/account_payment/graphql.py +++ b/backend/apps/account_payment/graphql.py @@ -24,7 +24,7 @@ from stripe import SetupIntent from backend.apps.account.models import Account, Subscription -from backend.apps.account_payment.webhooks import add_user, remove_user +from backend.apps.account_payment.webhooks import add_user, is_email_in_group, remove_user from backend.custom.graphql_base import CountableConnection, PlainTextNode if settings.STRIPE_LIVE_MODE: @@ -382,7 +382,7 @@ def mutate(cls, root, info, account_id, subscription_id): subscription = Subscription.objects.get(id=subscription_id) assert admin.id == subscription.admin.id - add_user(account.email) + add_user(account.gcp_email or account.email) subscription.subscribers.add(account) return cls(ok=True) except Exception as e: @@ -408,7 +408,7 @@ def mutate(cls, root, info, account_id, subscription_id): account = Account.objects.get(id=account_id) subscription = Subscription.objects.get(id=subscription_id) assert admin.id == subscription.admin.id - remove_user(account.email) + remove_user(account.gcp_email or account.email) subscription.subscribers.remove(account) return cls(ok=True) except Exception as e: @@ -416,6 +416,61 @@ def mutate(cls, root, info, account_id, subscription_id): return cls(errors=[str(e)]) +class ChangeUserGCPEmail(Mutation): + """Change user GCP email""" + + ok = Boolean() + errors = List(String) + + class Arguments: + email = String(required=True) + + @classmethod + @login_required + def mutate(cls, root, info, email): + try: + user = info.context.user + old_email = user.gcp_email or user.email + + if old_email == email: + return cls(ok=True) + + user.gcp_email = email + user.save() + + if is_email_in_group(old_email): + remove_user(old_email) + + subscription = user.pro_subscription() + if subscription and not is_email_in_group(email): + add_user(email) + + return cls(ok=True) + except Exception as e: + logger.error(e) + return cls(errors=[str(e)]) + + +# Query to check based on a email if the user is in a group +class IsEmailInGoogleGroup(Mutation): + """Check if user is in group""" + + ok = Boolean() + errors = List(String) + + class Arguments: + email = String(required=True) + + @classmethod + @login_required + def mutate(cls, root, info, email): + try: + return cls(ok=is_email_in_group(email)) + except Exception as e: + logger.error(e) + return cls(errors=[str(e)]) + + def get_stripe_promo(promotion_code): """ Helper function to retrieve a Stripe Promotion Code by its code. @@ -441,6 +496,7 @@ def get_stripe_promo(promotion_code): class Query(ObjectType): stripe_price = PlainTextNode.Field(StripePriceNode) all_stripe_price = DjangoFilterConnectionField(StripePriceNode) + is_email_in_google_group = IsEmailInGoogleGroup.Field() class Mutation(ObjectType): @@ -451,6 +507,7 @@ class Mutation(ObjectType): create_stripe_customer_subscription = StripeSubscriptionCustomerCreateMutation.Field() update_stripe_customer_subscription = StripeSubscriptionCustomerDeleteMutation.Field() validate_stripe_coupon = StripeCouponValidationMutation.Field() + change_user_gcp_email = ChangeUserGCPEmail.Field() # Reference diff --git a/backend/apps/account_payment/webhooks.py b/backend/apps/account_payment/webhooks.py index b0f64888..73abf42a 100644 --- a/backend/apps/account_payment/webhooks.py +++ b/backend/apps/account_payment/webhooks.py @@ -10,7 +10,7 @@ from stripe import Customer as StripeCustomer from stripe import Subscription as StripeSubscription -from backend.apps.account.models import Subscription +from backend.apps.account.models import Account, Subscription from backend.custom.client import send_discord_message as send logger = logger.bind(module="payment") @@ -109,6 +109,31 @@ def list_user(group_key: str = None): raise e +def is_email_in_group(email: str, group_key: str = None) -> bool: + """Check if a user is in a Google group.""" + if not group_key: + group_key = settings.GOOGLE_DIRECTORY_GROUP_KEY + if "+" in email and email.index("+") < email.index("@"): + email = email.split("+")[0] + "@" + email.split("@")[1] + + try: + service = get_service() + service.members().get( + groupKey=group_key, + memberKey=email, + ).execute() + return True + except HttpError as e: + if e.resp.status == 404: + return False + else: + logger.error(f"Erro ao verificar o usuário {email} no grupo {group_key}: {e}") + raise e + except Exception as e: + logger.error(f"Erro inesperado ao verificar o usuário {email}: {e}") + raise e + + @webhooks.handler("customer.updated") def update_customer(event: Event, **kwargs): """Propagate customer email update if exists""" @@ -122,21 +147,29 @@ def update_customer(event: Event, **kwargs): def handle_subscription(event: Event): """Handle subscription status""" subscription = get_subscription(event) + account = Account.objects.filter(email=event.customer.email).first() if event.data["object"]["status"] in ["trialing", "active"]: if subscription: logger.info(f"Adicionando a inscrição do cliente {event.customer.email}") subscription.is_active = True subscription.save() + # Add user to google group if subscription exists or not - add_user(event.customer.email) + if account: + add_user(account.gcp_email or account.email) + else: + add_user(event.customer.email) else: if subscription: logger.info(f"Removendo a inscrição do cliente {event.customer.email}") subscription.is_active = False subscription.save() # Remove user from google group if subscription exists or not - remove_user(event.customer.email) + if account: + remove_user(account.gcp_email or account.email) + else: + remove_user(event.customer.email) @webhooks.handler("customer.subscription.updated") @@ -158,28 +191,44 @@ def unsubscribe(event: Event, **kwargs): logger.info(f"Removendo a inscrição do cliente {event.customer.email}") subscription.is_active = False subscription.save() + + account = Account.objects.filter(email=event.customer.email).first() # Remove user from google group if subscription exists or not - remove_user(event.customer.email) + if account: + remove_user(account.gcp_email or account.email) + else: + remove_user(event.customer.email) @webhooks.handler("customer.subscription.paused") def pause_subscription(event: Event, **kwargs): """Pause customer subscription""" + account = Account.objects.filter(email=event.customer.email).first() + if subscription := get_subscription(event): logger.info(f"Pausando a inscrição do cliente {event.customer.email}") subscription.is_active = False subscription.save() - remove_user(event.customer.email) + if account: + remove_user(account.gcp_email or account.email) + else: + remove_user(event.customer.email) @webhooks.handler("customer.subscription.resumed") def resume_subscription(event: Event, **kwargs): """Resume customer subscription""" + account = Account.objects.filter(email=event.customer.email).first() + if subscription := get_subscription(event): logger.info(f"Resumindo a inscrição do cliente {event.customer.email}") subscription.is_active = True subscription.save() - add_user(event.customer.email) + + if account: + add_user(account.gcp_email or account.email) + else: + add_user(event.customer.email) @webhooks.handler("setup_intent.succeeded") From f2e99f0550cbd5e847204b591404a1e82b7109ce Mon Sep 17 00:00:00 2001 From: Jhony Lucas Date: Wed, 2 Oct 2024 09:18:05 -0300 Subject: [PATCH 2/5] fix: handle exceptions when adding/removing users from Google Groups --- backend/apps/account_payment/graphql.py | 10 ++++++++-- backend/apps/account_payment/webhooks.py | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/apps/account_payment/graphql.py b/backend/apps/account_payment/graphql.py index cf8d8402..a147313c 100644 --- a/backend/apps/account_payment/graphql.py +++ b/backend/apps/account_payment/graphql.py @@ -441,11 +441,17 @@ def mutate(cls, root, info, email): user.save() if is_email_in_group(old_email): - remove_user(old_email) + try: + remove_user(old_email) + except Exception: + pass subscription = user.pro_subscription() if subscription and not is_email_in_group(email): - add_user(email) + try: + add_user(email) + except Exception: + pass return cls(ok=True) except Exception as e: diff --git a/backend/apps/account_payment/webhooks.py b/backend/apps/account_payment/webhooks.py index 1a443ffd..3322ef11 100644 --- a/backend/apps/account_payment/webhooks.py +++ b/backend/apps/account_payment/webhooks.py @@ -93,7 +93,7 @@ def remove_user(email: str, group_key: str = None) -> None: if e.resp.status == 404: logger.warning(f"{email} já foi removido do google groups") else: - send(f"Verifique o erro ao remover o usuário do google groups: {e}") + send(f"Verifique o erro ao remover o usuário do google groups '{email}': {e}") logger.error(e) raise e @@ -119,11 +119,17 @@ def is_email_in_group(email: str, group_key: str = None) -> bool: try: service = get_service() - service.members().get( - groupKey=group_key, - memberKey=email, - ).execute() - return True + response = ( + service.members() + .get( + groupKey=group_key, + memberKey=email, + ) + .execute() + ) + + member_email = response.get("email", "").lower() + return member_email == email.lower() except HttpError as e: if e.resp.status == 404: return False From a09709dbde4c6d519fc610a38ef35dfa52258264 Mon Sep 17 00:00:00 2001 From: Jhony Lucas Date: Thu, 3 Oct 2024 21:00:00 -0300 Subject: [PATCH 3/5] fix: normalize email addresses in user management functions and improve error handling --- backend/apps/account_payment/webhooks.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/apps/account_payment/webhooks.py b/backend/apps/account_payment/webhooks.py index 3322ef11..e4d848da 100644 --- a/backend/apps/account_payment/webhooks.py +++ b/backend/apps/account_payment/webhooks.py @@ -87,7 +87,7 @@ def remove_user(email: str, group_key: str = None) -> None: service = get_service() service.members().delete( groupKey=group_key, - memberKey=email, + memberKey=email.lower(), ).execute() except HttpError as e: if e.resp.status == 404: @@ -123,7 +123,7 @@ def is_email_in_group(email: str, group_key: str = None) -> bool: service.members() .get( groupKey=group_key, - memberKey=email, + memberKey=email.lower(), ) .execute() ) @@ -131,11 +131,8 @@ def is_email_in_group(email: str, group_key: str = None) -> bool: member_email = response.get("email", "").lower() return member_email == email.lower() except HttpError as e: - if e.resp.status == 404: - return False - else: - logger.error(f"Erro ao verificar o usuário {email} no grupo {group_key}: {e}") - raise e + logger.error(f"Erro ao verificar o usuário {email} no grupo {group_key}: {e}") + return False except Exception as e: logger.error(f"Erro inesperado ao verificar o usuário {email}: {e}") raise e @@ -173,10 +170,13 @@ def handle_subscription(event: Event): subscription.is_active = False subscription.save() # Remove user from google group if subscription exists or not - if account: - remove_user(account.gcp_email or account.email) - else: - remove_user(event.customer.email) + try: + if account: + remove_user(account.gcp_email or account.email) + else: + remove_user(event.customer.email) + except Exception as e: + logger.error(e) @webhooks.handler("customer.subscription.updated") From d8fa556fc2a57093d468d8a742df6746027ab1e6 Mon Sep 17 00:00:00 2001 From: Jhony Lucas Date: Sat, 5 Oct 2024 22:07:58 -0300 Subject: [PATCH 4/5] fix: change email --- backend/apps/account_payment/graphql.py | 14 ++++++++++---- backend/apps/account_payment/webhooks.py | 7 +++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/apps/account_payment/graphql.py b/backend/apps/account_payment/graphql.py index a147313c..26b6009e 100644 --- a/backend/apps/account_payment/graphql.py +++ b/backend/apps/account_payment/graphql.py @@ -432,8 +432,10 @@ class Arguments: def mutate(cls, root, info, email): try: user = info.context.user - old_email = user.gcp_email or user.email + if user is None: + return cls(ok=False, errors=["User is none"]) + old_email = user.gcp_email or user.email if old_email == email: return cls(ok=True) @@ -446,8 +448,12 @@ def mutate(cls, root, info, email): except Exception: pass - subscription = user.pro_subscription() - if subscription and not is_email_in_group(email): + subscription = user.pro_subscription + + if subscription is None: + return cls(ok=True) + + if subscription.is_active and not is_email_in_group(email): try: add_user(email) except Exception: @@ -456,7 +462,7 @@ def mutate(cls, root, info, email): return cls(ok=True) except Exception as e: logger.error(e) - return cls(errors=[str(e)]) + return cls(ok=False, errors=[str(e)]) # Query to check based on a email if the user is in a group diff --git a/backend/apps/account_payment/webhooks.py b/backend/apps/account_payment/webhooks.py index e4d848da..fb1a29a4 100644 --- a/backend/apps/account_payment/webhooks.py +++ b/backend/apps/account_payment/webhooks.py @@ -128,8 +128,11 @@ def is_email_in_group(email: str, group_key: str = None) -> bool: .execute() ) - member_email = response.get("email", "").lower() - return member_email == email.lower() + member_email = response.get("email") + if not member_email: + return False + + return member_email.lower() == email.lower() except HttpError as e: logger.error(f"Erro ao verificar o usuário {email} no grupo {group_key}: {e}") return False From 3583fd0242c98c01f9ea977634813dd8e8624a43 Mon Sep 17 00:00:00 2001 From: Jhony Lucas Date: Mon, 7 Oct 2024 10:04:58 -0300 Subject: [PATCH 5/5] fix: update email group check logic in ChangeUserGCPEmail mutation --- backend/apps/account_payment/graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/account_payment/graphql.py b/backend/apps/account_payment/graphql.py index 26b6009e..289791a1 100644 --- a/backend/apps/account_payment/graphql.py +++ b/backend/apps/account_payment/graphql.py @@ -453,7 +453,7 @@ def mutate(cls, root, info, email): if subscription is None: return cls(ok=True) - if subscription.is_active and not is_email_in_group(email): + if not is_email_in_group(email): try: add_user(email) except Exception: