diff --git a/app/eventyay/control/forms/global_settings.py b/app/eventyay/control/forms/global_settings.py index e940487e30..7188f98341 100644 --- a/app/eventyay/control/forms/global_settings.py +++ b/app/eventyay/control/forms/global_settings.py @@ -278,6 +278,22 @@ def __init__(self, *args, **kwargs): validators=[MinValueValidator(0)], ), ), + ( + 'allow_all_users_create_organizer', + forms.BooleanField( + label=_('All registered users can create organizers'), + help_text=_('If enabled, all registered users will be allowed to create organizers. System admins can always create organizers.'), + required=False, + ), + ), + ( + 'allow_payment_users_create_organizer', + forms.BooleanField( + label=_('All accounts with payment information can create organizers'), + help_text=_('If enabled, users with valid payment information on file will be allowed to create organizers. System admins can always create organizers.'), + required=False, + ), + ), ] ) @@ -321,6 +337,10 @@ def __init__(self, *args, **kwargs): ('maps', _('Maps'), [ 'opencagedata_apikey', 'mapquest_apikey', 'leaflet_tiles', 'leaflet_tiles_attribution', ]), + ('organizers', _('Organizers'), [ + 'allow_all_users_create_organizer', + 'allow_payment_users_create_organizer', + ]), ] diff --git a/app/eventyay/control/permissions.py b/app/eventyay/control/permissions.py index 7196e42bda..bed6e1220f 100644 --- a/app/eventyay/control/permissions.py +++ b/app/eventyay/control/permissions.py @@ -1,10 +1,15 @@ from urllib.parse import quote from django.core.exceptions import PermissionDenied +from django.db.models import Q from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext as _ +from eventyay.base.models import Organizer +from eventyay.base.models.organizer import OrganizerBillingModel +from eventyay.base.settings import GlobalSettingsObject + def current_url(request): if request.GET: @@ -157,3 +162,83 @@ class StaffMemberRequiredMixin: def as_view(cls, **initkwargs): view = super(StaffMemberRequiredMixin, cls).as_view(**initkwargs) return staff_member_required()(view) + + +class OrganizerCreationPermissionMixin: + """ + Mixin to check if a user has permission to create organizers. + Can be used in any view that needs to check organizer creation permissions. + """ + + def _can_create_organizer(self, user): + """ + Check if the user has permission to create an organizer. + + Permission precedence (highest to lowest): + 1. System admins (staff with active session) - always allowed + 2. Default when both settings are None - allow all users (permissive default) + 3. allow_all_users_create_organizer=True - allow all authenticated users + 4. allow_payment_users_create_organizer=True - allow users with payment info + 5. Both False - deny (admin only) + + Note: If allow_all_users=True, it takes precedence over allow_payment_users + (no need to check payment info if all users are already allowed). + + Args: + user: The user to check permissions for + + Returns: + bool: True if user can create organizers, False otherwise + """ + # System admins can always create organizers + if user.has_active_staff_session(self.request.session.session_key): + return True + + # Get global settings + gs = GlobalSettingsObject() + allow_all_users = gs.settings.get('allow_all_users_create_organizer', None, as_type=bool) + allow_payment_users = gs.settings.get('allow_payment_users_create_organizer', None, as_type=bool) + + # If neither option is explicitly set, default to allowing all users (permissive default) + if allow_all_users is None and allow_payment_users is None: + return True + + # If all users are allowed (takes precedence over payment check) + if allow_all_users: + return True + + # If users with payment information are allowed + if allow_payment_users: + return self._user_has_payment_info(user) + + # By default, deny access if settings are explicitly set to False + return False + + def _user_has_payment_info(self, user): + """ + Check if the user has valid payment information on file. + + This checks if any of the user's organizers have billing records with payment method setup. + Checks for: + - stripe_customer_id: Indicates Stripe customer account + - stripe_payment_method_id: Indicates saved payment method + + Args: + user: The user to check payment info for + + Returns: + bool: True if user has payment info, False otherwise + """ + # Get all organizers where the user is a team member + user_organizers = Organizer.objects.filter( + teams__members=user + ).distinct() + + # Single query to check if any billing record has payment info + # Check for either stripe_customer_id OR stripe_payment_method_id + return OrganizerBillingModel.objects.filter( + organizer__in=user_organizers + ).filter( + (Q(stripe_customer_id__isnull=False) & ~Q(stripe_customer_id='')) | + (Q(stripe_payment_method_id__isnull=False) & ~Q(stripe_payment_method_id='')) + ).exists() diff --git a/app/eventyay/control/templates/pretixcontrol/organizers/index.html b/app/eventyay/control/templates/pretixcontrol/organizers/index.html index a9f8747703..dddfd82624 100644 --- a/app/eventyay/control/templates/pretixcontrol/organizers/index.html +++ b/app/eventyay/control/templates/pretixcontrol/organizers/index.html @@ -20,12 +20,14 @@
{% trans "Create a new organizer" %}
+ {% endif %}