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 "Organizers" %}

+ {% if can_create_organizer %}

{% trans "Create a new organizer" %}

+ {% endif %} diff --git a/app/eventyay/control/views/organizer_views/organizer_view.py b/app/eventyay/control/views/organizer_views/organizer_view.py index 954aeaff64..a03c1e0723 100644 --- a/app/eventyay/control/views/organizer_views/organizer_view.py +++ b/app/eventyay/control/views/organizer_views/organizer_view.py @@ -1,7 +1,7 @@ import logging from django.contrib import messages -from django.core.exceptions import ValidationError +from django.core.exceptions import PermissionDenied, ValidationError from django.core.files import File from django.db import transaction from django.db.models import Max, Min, Prefetch, ProtectedError @@ -33,6 +33,7 @@ ) from eventyay.control.permissions import ( AdministratorPermissionRequiredMixin, + OrganizerCreationPermissionMixin, OrganizerPermissionRequiredMixin, ) from eventyay.control.signals import nav_organizer @@ -52,13 +53,16 @@ logger = logging.getLogger(__name__) -class OrganizerCreate(CreateView): +class OrganizerCreate(OrganizerCreationPermissionMixin, CreateView): model = Organizer form_class = OrganizerForm template_name = 'pretixcontrol/organizers/create.html' context_object_name = 'organizer' def dispatch(self, request, *args, **kwargs): + # Check if user has permission to create organizers + if not self._can_create_organizer(request.user): + raise PermissionDenied(_('You do not have permission to create organizers. Please contact an administrator.')) return super().dispatch(request, *args, **kwargs) @transaction.atomic @@ -362,7 +366,7 @@ def get_object(self, queryset=None) -> Organizer: return self.request.organizer -class OrganizerList(PaginationMixin, ListView): +class OrganizerList(OrganizerCreationPermissionMixin, PaginationMixin, ListView): model = Organizer context_object_name = 'organizers' template_name = 'pretixcontrol/organizers/index.html' @@ -379,6 +383,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['filter_form'] = self.filter_form + ctx['can_create_organizer'] = self._can_create_organizer(self.request.user) return ctx @cached_property diff --git a/app/eventyay/eventyay_common/templates/eventyay_common/organizers/index.html b/app/eventyay/eventyay_common/templates/eventyay_common/organizers/index.html index 9e17c50ca5..ae4780e919 100644 --- a/app/eventyay/eventyay_common/templates/eventyay_common/organizers/index.html +++ b/app/eventyay/eventyay_common/templates/eventyay_common/organizers/index.html @@ -18,7 +18,7 @@

{% translate "Organizers" %}

- {% if staff_session %} + {% if can_create_organizer %}

diff --git a/app/eventyay/eventyay_common/views/organizer.py b/app/eventyay/eventyay_common/views/organizer.py index f3b79fbf03..ebda7ebc47 100644 --- a/app/eventyay/eventyay_common/views/organizer.py +++ b/app/eventyay/eventyay_common/views/organizer.py @@ -13,15 +13,18 @@ from eventyay.base.models import Organizer, Team from eventyay.control.forms.filter import OrganizerFilterForm +from eventyay.control.permissions import ( + OrganizerCreationPermissionMixin, + OrganizerPermissionRequiredMixin, +) from eventyay.control.views import CreateView, PaginationMixin, UpdateView from ...control.forms.organizer_forms import OrganizerForm, OrganizerUpdateForm -from ...control.permissions import OrganizerPermissionRequiredMixin logger = logging.getLogger(__name__) -class OrganizerList(PaginationMixin, ListView): +class OrganizerList(OrganizerCreationPermissionMixin, PaginationMixin, ListView): model = Organizer context_object_name = 'organizers' template_name = 'eventyay_common/organizers/index.html' @@ -37,6 +40,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['filter_form'] = self.filter_form + ctx['can_create_organizer'] = self._can_create_organizer(self.request.user) return ctx @cached_property @@ -44,15 +48,16 @@ def filter_form(self): return OrganizerFilterForm(data=self.request.GET, request=self.request) -class OrganizerCreate(CreateView): +class OrganizerCreate(OrganizerCreationPermissionMixin, CreateView): model = Organizer form_class = OrganizerForm template_name = 'eventyay_common/organizers/create.html' context_object_name = 'organizer' def dispatch(self, request, *args, **kwargs): - if not request.user.has_active_staff_session(self.request.session.session_key): - raise PermissionDenied() + # Check if user has permission to create organizers + if not self._can_create_organizer(request.user): + raise PermissionDenied(_('You do not have permission to create organizers. Please contact an administrator.')) return super().dispatch(request, *args, **kwargs) @transaction.atomic