From 6bd5de6549bcdf7c2a1b6d968ed58432a2c465d7 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Tue, 10 Dec 2024 20:04:32 +0700 Subject: [PATCH 01/14] Add UI for generating sso keys --- src/pretix/control/forms/global_settings.py | 12 +++ src/pretix/control/navigation.py | 11 +-- .../templates/pretixcontrol/global_sso.html | 68 ++++++++++++++ src/pretix/control/urls.py | 4 +- src/pretix/control/views/global_settings.py | 92 ++++++++++++++++++- 5 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/global_sso.html diff --git a/src/pretix/control/forms/global_settings.py b/src/pretix/control/forms/global_settings.py index 5ca40c5fc..fe31a93a6 100644 --- a/src/pretix/control/forms/global_settings.py +++ b/src/pretix/control/forms/global_settings.py @@ -183,3 +183,15 @@ class UpdateSettingsForm(SettingsForm): def __init__(self, *args, **kwargs): self.obj = GlobalSettingsObject() super().__init__(*args, obj=self.obj, **kwargs) + + +class SSOConfigForm(SettingsForm): + redirect_url = forms.URLField( + required=True, + label=_("Redirect URL"), + help_text=_("e.g. {sample}").format(sample="https://app-test.eventyay.com/talk/") + ) + + def __init__(self, *args, **kwargs): + self.obj = GlobalSettingsObject() + super().__init__(*args, obj=self.obj, **kwargs) diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index 4d131c4d4..389357a53 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -519,12 +519,6 @@ def get_admin_navigation(request): 'active': 'organizers' in url.url_name, 'icon': 'group', }, - { - 'label': _('Task management'), - 'url': reverse('control:admin.task_management'), - 'active': 'task_management' in url.url_name, - 'icon': 'tasks', - }, { 'label': _('Users'), 'url': reverse('control:admin.users'), @@ -559,6 +553,11 @@ def get_admin_navigation(request): 'url': reverse('control:admin.global.update'), 'active': (url.url_name == 'admin.global.update'), }, + { + 'label': _('Generate keys for SSO'), + 'url': reverse('control:admin.global.sso'), + 'active': (url.url_name == 'admin.global.sso'), + }, ] }, ] diff --git a/src/pretix/control/templates/pretixcontrol/global_sso.html b/src/pretix/control/templates/pretixcontrol/global_sso.html new file mode 100644 index 000000000..40092124b --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/global_sso.html @@ -0,0 +1,68 @@ +{% extends "pretixcontrol/admin/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load rich_text %} + + +{% block title %}{% trans "Generate keys for SSO" %}{% endblock %} +{% block content %} +

{% trans "Generate keys for SSO" %}

+ {{ global_settings.banner_message_detail|rich_text }} + {% block inner %} +
+ {% csrf_token %} + {% bootstrap_form_errors form %} + {% bootstrap_form form layout='control' %} +
+ +
+
+ {% endblock %} + + {% if result.error_message %} +

Error:

+
{{ result.error_message }}
+ {% elif result.success_message %} +

{{ result.success_message }}

+
+
+ + +
+
+ + +
+
+ {% endif %} + + {% if oauth_applications %} +

OAuth Applications

+ + {% endif %} + +{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 4c0592e78..dab49b024 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -333,7 +333,6 @@ url(r'^$', admin.AdminDashboard.as_view(), name='admin.dashboard'), url(r'^organizers/$', admin.OrganizerList.as_view(), name='admin.organizers'), url(r'^events/$', admin.AdminEventList.as_view(), name='admin.events'), - url(r'^task_management', admin.TaskList.as_view(), name='admin.task_management'), url(r'^sudo/(?P\d+)/$', user.EditStaffSession.as_view(), name='admin.user.sudo.edit'), url(r'^sudo/sessions/$', user.StaffSessionList.as_view(), name='admin.user.sudo.list'), url(r'^users/$', users.UserListView.as_view(), name='admin.users'), @@ -347,7 +346,8 @@ url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='admin.global.settings'), url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='admin.global.update'), url(r'^global/message/$', global_settings.MessageView.as_view(), name='admin.global.message'), - + url(r'^global/sso/$', global_settings.SSOView.as_view(), name='admin.global.sso'), + url(r'^global/sso/(?P\d+)/delete/$', global_settings.DeleteOAuthApplicationView.as_view(), name='admin.global.sso.delete'), ])), url(r'^event/(?P[^/]+)/$', RedirectView.as_view(pattern_name='control:organizer'), name='event.organizerredirect'), ] diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index 62c157e86..3a8830999 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -1,20 +1,28 @@ +import logging +import secrets + from django.contrib import messages from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, reverse +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import View -from django.views.generic import FormView, TemplateView +from django.views.generic import FormView, TemplateView, DeleteView +from django.db import IntegrityError +from django.core.exceptions import ValidationError, ObjectDoesNotExist from pretix.base.models import LogEntry, OrderPayment, OrderRefund +from pretix.api.models import OAuthApplication from pretix.base.services.update_check import check_result_table, update_check from pretix.base.settings import GlobalSettingsObject from pretix.control.forms.global_settings import ( - GlobalSettingsForm, UpdateSettingsForm, + GlobalSettingsForm, UpdateSettingsForm, SSOConfigForm ) from pretix.control.permissions import ( AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, ) +logger = logging.getLogger(__name__) class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView): template_name = 'pretixcontrol/global_settings.html' @@ -33,6 +41,86 @@ def get_success_url(self): return reverse('control:admin.global.settings') +class SSOView(AdministratorPermissionRequiredMixin, FormView): + template_name = 'pretixcontrol/global_sso.html' + form_class = SSOConfigForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + oauth_applications = OAuthApplication.objects.all() + context['oauth_applications'] = oauth_applications + return context + + def post(self, request, *args, **kwargs): + form = self.get_form() + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) + + def form_valid(self, form): + url = form.cleaned_data['redirect_url'] + + try: + result = self.create_oauth_application(url) + except IntegrityError as e: + logger.error("Error while creating OAuth2 application: %s", e) + return {"error_message": f"Database integrity error: {str(e)}"} + except ValidationError as e: + logger.error("Error while creating OAuth2 application: %s", e) + return {"error_message": f"Validation error: {e.message_dict}"} + except ObjectDoesNotExist: + logger.error("Error while creating OAuth2 application: %s", e) + return {"error_message": "The object you are trying to access does not exist."} + except ValueError as e: + logger.error("Error while creating OAuth2 application: %s", e) + return {"error_message": f"Value error: {str(e)}"} + except Exception as e: + logger.error("Error while creating OAuth2 application: %s", e) + return {"error_message": f"An unexpected error occurred: {str(e)}"} + + return self.render_to_response(self.get_context_data(form=form, result=result)) + + def form_invalid(self, form): + messages.error(self.request, _('Your changes have not been saved, see below for errors.')) + return super().form_invalid(form) + + def get_success_url(self): + return reverse('control:admin.global.sso') + + def create_oauth_application(self, redirect_uris): + # Check if the application already exists based on redirect_uri + if OAuthApplication.objects.filter(redirect_uris=redirect_uris).exists(): + application = OAuthApplication.objects.filter(redirect_uris=redirect_uris).first() + return { + "success_message": "OAuth2 Application with this redirect URI already exists", + "client_id": application.client_id, + "client_secret": application.client_secret + } + else: + # Create the OAuth2 Application + application = OAuthApplication( + name="Talk SSO Client", + client_type=OAuthApplication.CLIENT_CONFIDENTIAL, + authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE, + redirect_uris=redirect_uris, + user=None, # Set a specific user if you want this to be user-specific, else keep it None + client_id=secrets.token_urlsafe(32), + client_secret=secrets.token_urlsafe(64), + hash_client_secret=False, + skip_authorization=True, + ) + application.save() + + return { + "success_message": "Successfully created OAuth2 Application", + "client_id": application.client_id, + "client_secret": application.client_secret + } + + +class DeleteOAuthApplicationView(DeleteView): + model = OAuthApplication + success_url = reverse_lazy('control:admin.global.sso') + + class UpdateCheckView(StaffMemberRequiredMixin, FormView): template_name = 'pretixcontrol/global_update.html' form_class = UpdateSettingsForm From b557e6d1dc698b989dfa71c809c1eddfa0e8eebd Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Tue, 10 Dec 2024 20:08:48 +0700 Subject: [PATCH 02/14] Check isort and flake8 --- src/pretix/control/views/global_settings.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index 3a8830999..ff28ebf9d 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -2,21 +2,21 @@ import secrets from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import IntegrityError from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, reverse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views import View -from django.views.generic import FormView, TemplateView, DeleteView -from django.db import IntegrityError -from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.views.generic import DeleteView, FormView, TemplateView -from pretix.base.models import LogEntry, OrderPayment, OrderRefund from pretix.api.models import OAuthApplication +from pretix.base.models import LogEntry, OrderPayment, OrderRefund from pretix.base.services.update_check import check_result_table, update_check from pretix.base.settings import GlobalSettingsObject from pretix.control.forms.global_settings import ( - GlobalSettingsForm, UpdateSettingsForm, SSOConfigForm + GlobalSettingsForm, SSOConfigForm, UpdateSettingsForm, ) from pretix.control.permissions import ( AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, @@ -24,6 +24,7 @@ logger = logging.getLogger(__name__) + class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView): template_name = 'pretixcontrol/global_settings.html' form_class = GlobalSettingsForm @@ -66,7 +67,7 @@ def form_valid(self, form): except ValidationError as e: logger.error("Error while creating OAuth2 application: %s", e) return {"error_message": f"Validation error: {e.message_dict}"} - except ObjectDoesNotExist: + except ObjectDoesNotExist as e: logger.error("Error while creating OAuth2 application: %s", e) return {"error_message": "The object you are trying to access does not exist."} except ValueError as e: @@ -113,7 +114,7 @@ def create_oauth_application(self, redirect_uris): "success_message": "Successfully created OAuth2 Application", "client_id": application.client_id, "client_secret": application.client_secret - } + } class DeleteOAuthApplicationView(DeleteView): From 7cbf72f81b87c00beec5915b0e40612655f7b754 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Thu, 12 Dec 2024 20:46:25 +0700 Subject: [PATCH 03/14] Add sign-in button of MediaWiki, Google, Github --- pyproject.toml | 3 +- src/pretix/control/views/global_settings.py | 2 +- src/pretix/plugins/socialauth/__init__.py | 0 src/pretix/plugins/socialauth/adapter.py | 11 +++++ src/pretix/plugins/socialauth/apps.py | 19 ++++++++ src/pretix/plugins/socialauth/backends.py | 51 +++++++++++++++++++++ src/pretix/plugins/socialauth/urls.py | 7 +++ src/pretix/plugins/socialauth/views.py | 26 +++++++++++ src/pretix/settings.py | 45 +++++++++++++++++- src/pretix/urls.py | 3 +- 10 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 src/pretix/plugins/socialauth/__init__.py create mode 100644 src/pretix/plugins/socialauth/adapter.py create mode 100644 src/pretix/plugins/socialauth/apps.py create mode 100644 src/pretix/plugins/socialauth/backends.py create mode 100644 src/pretix/plugins/socialauth/urls.py create mode 100644 src/pretix/plugins/socialauth/views.py diff --git a/pyproject.toml b/pyproject.toml index be7ce4810..1d7164b14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,8 @@ dependencies = [ 'exhibitors @ git+https://github.com/fossasia/eventyay-tickets-exhibitors.git@master', 'pyvat==1.3.18', 'django_celery_beat==2.7.0', - 'cron-descriptor==1.4.5' + 'cron-descriptor==1.4.5', + 'django-allauth[socialaccount]==65.3.0' ] [project.optional-dependencies] diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index ff28ebf9d..a82a04920 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -117,7 +117,7 @@ def create_oauth_application(self, redirect_uris): } -class DeleteOAuthApplicationView(DeleteView): +class DeleteOAuthApplicationView(AdministratorPermissionRequiredMixin, DeleteView): model = OAuthApplication success_url = reverse_lazy('control:admin.global.sso') diff --git a/src/pretix/plugins/socialauth/__init__.py b/src/pretix/plugins/socialauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/plugins/socialauth/adapter.py b/src/pretix/plugins/socialauth/adapter.py new file mode 100644 index 000000000..ee43b3456 --- /dev/null +++ b/src/pretix/plugins/socialauth/adapter.py @@ -0,0 +1,11 @@ +from allauth.core.exceptions import ImmediateHttpResponse +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.http import HttpResponseRedirect +from django.urls import reverse + + +class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): + def on_authentication_error( + self, request, provider, error=None, exception=None, extra_context=None + ): + raise ImmediateHttpResponse(HttpResponseRedirect(reverse("control:index"))) diff --git a/src/pretix/plugins/socialauth/apps.py b/src/pretix/plugins/socialauth/apps.py new file mode 100644 index 000000000..9b6a27fca --- /dev/null +++ b/src/pretix/plugins/socialauth/apps.py @@ -0,0 +1,19 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + +from pretix import __version__ as version + + +class SocialAuthApp(AppConfig): + name = 'pretix.plugins.socialauth' + verbose_name = _("SocialAuth") + + class PretixPluginMeta: + name = _("SocialAuth") + author = _("the pretix team") + version = version + featured = True + description = _("This plugin allows you to login via social networks") + + +default_app_config = 'pretix.plugins.socialauth.PaypalApp' diff --git a/src/pretix/plugins/socialauth/backends.py b/src/pretix/plugins/socialauth/backends.py new file mode 100644 index 000000000..8541e4199 --- /dev/null +++ b/src/pretix/plugins/socialauth/backends.py @@ -0,0 +1,51 @@ +from allauth.socialaccount.adapter import get_adapter + +from pretix.base.auth import BaseAuthBackend +from pretix.helpers.urls import build_absolute_uri + +adapter = get_adapter() + + +class MediaWikiBackend(BaseAuthBackend): + identifier = 'mediawiki' + + @property + def verbose_name(self): + return "Login with MediaWiki" + + def authentication_url(self, request): + return ( + adapter.get_provider(request, 'mediawiki').get_login_url(request) + + "?next=" + + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") + ) + + +class GoogleBackend(BaseAuthBackend): + identifier = 'google' + + @property + def verbose_name(self): + return "Login with Google" + + def authentication_url(self, request): + return ( + adapter.get_provider(request, 'google').get_login_url(request) + + "?next=" + + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") + ) + + +class GithubBackend(BaseAuthBackend): + identifier = 'github' + + @property + def verbose_name(self): + return "Login with Github" + + def authentication_url(self, request): + return ( + adapter.get_provider(request, 'github').get_login_url(request) + + "?next=" + + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") + ) diff --git a/src/pretix/plugins/socialauth/urls.py b/src/pretix/plugins/socialauth/urls.py new file mode 100644 index 000000000..34fbb86c8 --- /dev/null +++ b/src/pretix/plugins/socialauth/urls.py @@ -0,0 +1,7 @@ +from django.urls import re_path as url + +from . import views + +urlpatterns = [ + url(r'^oauth_return$', views.oauth_return, name='mediawiki.oauth.return') +] diff --git a/src/pretix/plugins/socialauth/views.py b/src/pretix/plugins/socialauth/views.py new file mode 100644 index 000000000..42cc23537 --- /dev/null +++ b/src/pretix/plugins/socialauth/views.py @@ -0,0 +1,26 @@ +from django.conf import settings + +from pretix.base.models import User +from pretix.control.views.auth import process_login_and_set_cookie + + +def oauth_return(request): + user = User.objects.filter(email=request.user.email).first() + if not user: + locale = ( + request.LANGUAGE_CODE + if hasattr(request, 'LANGUAGE_CODE') + else settings.LANGUAGE_CODE + ) + timezone = ( + request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE + ) + user = User.objects.create( + email=request.user.email, + locale=locale, + timezone=timezone, + auth_backend='native', + password='', + ) + + return process_login_and_set_cookie(request, user, False) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 638c00eaf..2d2b557e6 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -292,6 +292,7 @@ 'compressor', 'bootstrap3', 'djangoformsetjs', + 'pretix.plugins.socialauth', 'pretix.plugins.banktransfer', 'pretix.plugins.paypal', 'pretix.plugins.ticketoutputpdf', @@ -315,7 +316,13 @@ 'oauth2_provider', 'phonenumber_field', 'pretix.eventyay_common', - 'django_celery_beat' + 'django_celery_beat', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + 'allauth.socialaccount.providers.github', + 'allauth.socialaccount.providers.mediawiki', ] if db_backend == 'postgresql': @@ -390,12 +397,14 @@ 'pretix.base.middleware.SecurityMiddleware', 'pretix.presale.middleware.EventMiddleware', 'pretix.api.middleware.ApiScopeMiddleware', + 'allauth.account.middleware.AccountMiddleware', ] # Configure the authentication backends AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'oauth2_provider.backends.OAuth2Backend', # Required for OAuth2 authentication + 'allauth.account.auth_backends.AuthenticationBackend' ) @@ -559,6 +568,7 @@ 'pretix.control.context.contextprocessor', 'pretix.presale.context.contextprocessor', 'pretix.eventyay_common.context.contextprocessor', + 'django.template.context_processors.request', ], 'loaders': template_loaders }, @@ -800,3 +810,36 @@ HAS_GEOIP = True GEOIP_PATH = config.get('geoip', 'path') GEOIP_COUNTRY = config.get('geoip', 'filename_country', fallback='GeoLite2-Country.mmdb') + +# Django allauth settings for social login +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_AUTHENTICATION_METHOD = 'email' + +SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True +SOCIALACCOUNT_EMAIL_AUTHENTICATION = True +SOCIALACCOUNT_ADAPTER = "pretix.plugins.socialauth.adapter.CustomSocialAccountAdapter" +SOCIALACCOUNT_EMAIL_REQUIRED = True +SOCIALACCOUNT_QUERY_EMAIL = True +SOCIALACCOUNT_LOGIN_ON_GET = True +SOCIALACCOUNT_PROVIDERS = { + "mediawiki": { + "APP": { + "client_id": config.get("social", "mediawiki_client_id", fallback=""), + "secret": config.get("social", "mediawiki_client_secret", fallback=""), + }, + }, + "google": { + "APP": { + "client_id": config.get("social", "google_client_id", fallback=""), + "secret": config.get("social", "google_client_secret", fallback=""), + }, + }, + "github": { + "APP": { + "client_id": config.get("social", "github_client_id", fallback=""), + "secret": config.get("social", "github_client_secret", fallback=""), + }, + } +} diff --git a/src/pretix/urls.py b/src/pretix/urls.py index b5a690ee4..befeab1aa 100644 --- a/src/pretix/urls.py +++ b/src/pretix/urls.py @@ -21,7 +21,8 @@ url(r'^csp_report/$', csp.csp_report, name='csp.report'), url(r'^js_helpers/states/$', js_helpers.states, name='js_helpers.states'), url(r'^api/v1/', include(('pretix.api.urls', 'pretixapi'), namespace='api-v1')), - url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version') + url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'), + url('accounts/', include('allauth.urls')), ] control_patterns = [ From 5b9862568cbb8a27a481c3fb758224fadaeae2c1 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Thu, 12 Dec 2024 21:22:06 +0700 Subject: [PATCH 04/14] Add back url --- src/pretix/control/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index f3039fb49..2ed3799a5 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -333,6 +333,7 @@ url(r'^$', admin.AdminDashboard.as_view(), name='admin.dashboard'), url(r'^organizers/$', admin.OrganizerList.as_view(), name='admin.organizers'), url(r'^events/$', admin.AdminEventList.as_view(), name='admin.events'), + url(r'^task_management', admin.TaskList.as_view(), name='admin.task_management'), url(r'^sudo/(?P\d+)/$', user.EditStaffSession.as_view(), name='admin.user.sudo.edit'), url(r'^sudo/sessions/$', user.StaffSessionList.as_view(), name='admin.user.sudo.list'), url(r'^users/$', users.UserListView.as_view(), name='admin.users'), From d6f3ffd428d288ff8d60c12789acf62fb2e41297 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 13 Dec 2024 09:42:06 +0700 Subject: [PATCH 05/14] Refactor with sourcery --- src/pretix/control/views/global_settings.py | 82 ++++++++++----------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index a82a04920..3ab76207c 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -16,10 +16,13 @@ from pretix.base.services.update_check import check_result_table, update_check from pretix.base.settings import GlobalSettingsObject from pretix.control.forms.global_settings import ( - GlobalSettingsForm, SSOConfigForm, UpdateSettingsForm, + GlobalSettingsForm, + SSOConfigForm, + UpdateSettingsForm, ) from pretix.control.permissions import ( - AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, + AdministratorPermissionRequiredMixin, + StaffMemberRequiredMixin, ) logger = logging.getLogger(__name__) @@ -35,7 +38,9 @@ def form_valid(self, form): return super().form_valid(form) def form_invalid(self, form): - messages.error(self.request, _('Your changes have not been saved, see below for errors.')) + messages.error( + self.request, _('Your changes have not been saved, see below for errors.') + ) return super().form_invalid(form) def get_success_url(self): @@ -61,59 +66,46 @@ def form_valid(self, form): try: result = self.create_oauth_application(url) - except IntegrityError as e: - logger.error("Error while creating OAuth2 application: %s", e) - return {"error_message": f"Database integrity error: {str(e)}"} - except ValidationError as e: - logger.error("Error while creating OAuth2 application: %s", e) - return {"error_message": f"Validation error: {e.message_dict}"} - except ObjectDoesNotExist as e: - logger.error("Error while creating OAuth2 application: %s", e) - return {"error_message": "The object you are trying to access does not exist."} - except ValueError as e: - logger.error("Error while creating OAuth2 application: %s", e) - return {"error_message": f"Value error: {str(e)}"} - except Exception as e: - logger.error("Error while creating OAuth2 application: %s", e) - return {"error_message": f"An unexpected error occurred: {str(e)}"} + except (IntegrityError, ValidationError, ObjectDoesNotExist, Exception) as e: + error_type = type(e).__name__ + error_message = str(e) + logger.error(f"Error while creating OAuth2 application: {error_type} - {error_message}") + return self.render_to_response({"error_message": f"{error_type}: {error_message}"}) return self.render_to_response(self.get_context_data(form=form, result=result)) def form_invalid(self, form): - messages.error(self.request, _('Your changes have not been saved, see below for errors.')) + messages.error( + self.request, _('Your changes have not been saved, see below for errors.') + ) return super().form_invalid(form) def get_success_url(self): return reverse('control:admin.global.sso') def create_oauth_application(self, redirect_uris): - # Check if the application already exists based on redirect_uri - if OAuthApplication.objects.filter(redirect_uris=redirect_uris).exists(): - application = OAuthApplication.objects.filter(redirect_uris=redirect_uris).first() - return { - "success_message": "OAuth2 Application with this redirect URI already exists", - "client_id": application.client_id, - "client_secret": application.client_secret - } - else: - # Create the OAuth2 Application - application = OAuthApplication( - name="Talk SSO Client", - client_type=OAuthApplication.CLIENT_CONFIDENTIAL, - authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE, - redirect_uris=redirect_uris, - user=None, # Set a specific user if you want this to be user-specific, else keep it None - client_id=secrets.token_urlsafe(32), - client_secret=secrets.token_urlsafe(64), - hash_client_secret=False, - skip_authorization=True, - ) - application.save() + application, created = OAuthApplication.objects.get_or_create( + redirect_uris=redirect_uris, + defaults={ + 'name': "Talk SSO Client", + 'client_type': OAuthApplication.CLIENT_CONFIDENTIAL, + 'authorization_grant_type': OAuthApplication.GRANT_AUTHORIZATION_CODE, + 'user': None, + 'client_id': secrets.token_urlsafe(32), + 'client_secret': secrets.token_urlsafe(64), + 'hash_client_secret': False, + 'skip_authorization': True, + }, + ) return { - "success_message": "Successfully created OAuth2 Application", + "success_message": ( + "Successfully created OAuth2 Application" + if created + else "OAuth2 Application with this redirect URI already exists" + ), "client_id": application.client_id, - "client_secret": application.client_secret + "client_secret": application.client_secret, } @@ -138,7 +130,9 @@ def form_valid(self, form): return super().form_valid(form) def form_invalid(self, form): - messages.error(self.request, _('Your changes have not been saved, see below for errors.')) + messages.error( + self.request, _('Your changes have not been saved, see below for errors.') + ) return super().form_invalid(form) def get_context_data(self, **kwargs): From 8b765b677005851d4e6967f5ecd9fe8e6eb0d056 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 13 Dec 2024 09:53:07 +0700 Subject: [PATCH 06/14] Add name url --- src/pretix/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/urls.py b/src/pretix/urls.py index 38d2a9c02..eb259ea72 100644 --- a/src/pretix/urls.py +++ b/src/pretix/urls.py @@ -23,7 +23,7 @@ url(r'^js_helpers/states/$', js_helpers.states, name='js_helpers.states'), url(r'^api/v1/', include(('pretix.api.urls', 'pretixapi'), namespace='api-v1')), url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'), - url('accounts/', include('allauth.urls')), + url(r'^accounts/', include('allauth.urls'), name='allauth'), ] control_patterns = [ From d07600b2c40aaa14cab6a46eb7f1832fc1f41546 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 13 Dec 2024 11:13:29 +0700 Subject: [PATCH 07/14] Fix test_urls and minor refactor --- src/pretix/control/views/global_settings.py | 7 ++-- src/pretix/plugins/socialauth/views.py | 38 ++++++++++++--------- src/pretix/urls.py | 2 +- src/tests/base/test_urls.py | 12 ++++--- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index 3ab76207c..eae7048d2 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -16,13 +16,10 @@ from pretix.base.services.update_check import check_result_table, update_check from pretix.base.settings import GlobalSettingsObject from pretix.control.forms.global_settings import ( - GlobalSettingsForm, - SSOConfigForm, - UpdateSettingsForm, + GlobalSettingsForm, SSOConfigForm, UpdateSettingsForm, ) from pretix.control.permissions import ( - AdministratorPermissionRequiredMixin, - StaffMemberRequiredMixin, + AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin, ) logger = logging.getLogger(__name__) diff --git a/src/pretix/plugins/socialauth/views.py b/src/pretix/plugins/socialauth/views.py index 42cc23537..303593770 100644 --- a/src/pretix/plugins/socialauth/views.py +++ b/src/pretix/plugins/socialauth/views.py @@ -1,26 +1,30 @@ +import logging + from django.conf import settings +from django.contrib import messages +from django.shortcuts import redirect from pretix.base.models import User from pretix.control.views.auth import process_login_and_set_cookie +logger = logging.getLogger(__name__) + def oauth_return(request): - user = User.objects.filter(email=request.user.email).first() - if not user: - locale = ( - request.LANGUAGE_CODE - if hasattr(request, 'LANGUAGE_CODE') - else settings.LANGUAGE_CODE - ) - timezone = ( - request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE - ) - user = User.objects.create( + try: + user, _ = User.objects.get_or_create( email=request.user.email, - locale=locale, - timezone=timezone, - auth_backend='native', - password='', + defaults={ + 'locale': getattr(request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE), + 'timezone': getattr(request, 'timezone', settings.TIME_ZONE), + 'auth_backend': 'native', + 'password': '', + }, ) - - return process_login_and_set_cookie(request, user, False) + return process_login_and_set_cookie(request, user, False) + except AttributeError: + messages.error( + request, _('Your changes have not been saved, see below for errors.') + ) + logger.error('Error while authorizing: user has no email address.') + return redirect('control:auth.login') diff --git a/src/pretix/urls.py b/src/pretix/urls.py index eb259ea72..8ba92dfa2 100644 --- a/src/pretix/urls.py +++ b/src/pretix/urls.py @@ -23,7 +23,7 @@ url(r'^js_helpers/states/$', js_helpers.states, name='js_helpers.states'), url(r'^api/v1/', include(('pretix.api.urls', 'pretixapi'), namespace='api-v1')), url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'), - url(r'^accounts/', include('allauth.urls'), name='allauth'), + url(r'^accounts/', include('allauth.urls')), ] control_patterns = [ diff --git a/src/tests/base/test_urls.py b/src/tests/base/test_urls.py index 3633e71a7..630f36bfa 100644 --- a/src/tests/base/test_urls.py +++ b/src/tests/base/test_urls.py @@ -14,6 +14,7 @@ class URLTestCase(TestCase): def test_url_names(self): urlconf = import_module(settings.ROOT_URLCONF) nameless = self.find_nameless_urls(urlconf) + print(nameless) message = "URL regexes missing names: %s" % " ".join([n.regex.pattern for n in nameless]) self.assertIs(len(nameless), 0, message) @@ -21,11 +22,14 @@ def find_nameless_urls(self, conf): nameless = [] patterns = self.get_patterns(conf) for u in patterns: - if self.has_patterns(u): + # Ignore social urls from django-allauth + # Since it does not support namespace + if 'social' in str(u): + continue + elif self.has_patterns(u): nameless.extend(self.find_nameless_urls(u)) - else: - if u.name is None: - nameless.append(u) + elif u.name is None: + nameless.append(u) return nameless def get_patterns(self, conf): From 78741be6bfe1f27aa05469dab7e3261197a974e0 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 13 Dec 2024 11:15:08 +0700 Subject: [PATCH 08/14] Remove print --- src/tests/base/test_urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/base/test_urls.py b/src/tests/base/test_urls.py index 630f36bfa..ca9890a9e 100644 --- a/src/tests/base/test_urls.py +++ b/src/tests/base/test_urls.py @@ -14,7 +14,6 @@ class URLTestCase(TestCase): def test_url_names(self): urlconf = import_module(settings.ROOT_URLCONF) nameless = self.find_nameless_urls(urlconf) - print(nameless) message = "URL regexes missing names: %s" % " ".join([n.regex.pattern for n in nameless]) self.assertIs(len(nameless), 0, message) From 051d9fbf883da57610ba4ca28152d9202a9c4384 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 13 Dec 2024 11:17:49 +0700 Subject: [PATCH 09/14] Change message log --- src/pretix/plugins/socialauth/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/plugins/socialauth/views.py b/src/pretix/plugins/socialauth/views.py index 303593770..acb7129a8 100644 --- a/src/pretix/plugins/socialauth/views.py +++ b/src/pretix/plugins/socialauth/views.py @@ -24,7 +24,7 @@ def oauth_return(request): return process_login_and_set_cookie(request, user, False) except AttributeError: messages.error( - request, _('Your changes have not been saved, see below for errors.') + request, _('Error while authorizing: no email address available.') ) logger.error('Error while authorizing: user has no email address.') return redirect('control:auth.login') From df85a7bfd804947bec640493e297865831573dae Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Mon, 16 Dec 2024 10:32:05 +0700 Subject: [PATCH 10/14] Resolve review conversations --- src/pretix/control/views/global_settings.py | 8 ++--- src/pretix/plugins/socialauth/backends.py | 35 ++++++++++++--------- src/pretix/plugins/socialauth/urls.py | 2 +- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index eae7048d2..10d14ee85 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -54,19 +54,15 @@ def get_context_data(self, **kwargs): context['oauth_applications'] = oauth_applications return context - def post(self, request, *args, **kwargs): - form = self.get_form() - return self.form_valid(form) if form.is_valid() else self.form_invalid(form) - def form_valid(self, form): url = form.cleaned_data['redirect_url'] try: result = self.create_oauth_application(url) - except (IntegrityError, ValidationError, ObjectDoesNotExist, Exception) as e: + except (IntegrityError, ValidationError, ObjectDoesNotExist) as e: error_type = type(e).__name__ error_message = str(e) - logger.error(f"Error while creating OAuth2 application: {error_type} - {error_message}") + logger.error('Error while creating OAuth2 application: %s - %s', error_type, error_message) return self.render_to_response({"error_message": f"{error_type}: {error_message}"}) return self.render_to_response(self.get_context_data(form=form, result=result)) diff --git a/src/pretix/plugins/socialauth/backends.py b/src/pretix/plugins/socialauth/backends.py index 8541e4199..b0dad08ae 100644 --- a/src/pretix/plugins/socialauth/backends.py +++ b/src/pretix/plugins/socialauth/backends.py @@ -1,3 +1,5 @@ +from urllib.parse import urlencode, urlparse, urlunparse + from allauth.socialaccount.adapter import get_adapter from pretix.base.auth import BaseAuthBackend @@ -14,11 +16,12 @@ def verbose_name(self): return "Login with MediaWiki" def authentication_url(self, request): - return ( - adapter.get_provider(request, 'mediawiki').get_login_url(request) - + "?next=" - + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") - ) + base_url = adapter.get_provider(request, 'mediawiki').get_login_url(request) + query_params = { + "next": build_absolute_uri("plugins:socialauth:social.oauth.return") + } + parsed_url = urlparse(base_url) + return urlunparse(parsed_url._replace(query=urlencode(query_params))) class GoogleBackend(BaseAuthBackend): @@ -29,11 +32,12 @@ def verbose_name(self): return "Login with Google" def authentication_url(self, request): - return ( - adapter.get_provider(request, 'google').get_login_url(request) - + "?next=" - + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") - ) + base_url = adapter.get_provider(request, 'google').get_login_url(request) + query_params = { + "next": build_absolute_uri("plugins:socialauth:social.oauth.return") + } + parsed_url = urlparse(base_url) + return urlunparse(parsed_url._replace(query=urlencode(query_params))) class GithubBackend(BaseAuthBackend): @@ -44,8 +48,9 @@ def verbose_name(self): return "Login with Github" def authentication_url(self, request): - return ( - adapter.get_provider(request, 'github').get_login_url(request) - + "?next=" - + build_absolute_uri("plugins:socialauth:mediawiki.oauth.return") - ) + base_url = adapter.get_provider(request, 'github').get_login_url(request) + query_params = { + "next": build_absolute_uri("plugins:socialauth:social.oauth.return") + } + parsed_url = urlparse(base_url) + return urlunparse(parsed_url._replace(query=urlencode(query_params))) diff --git a/src/pretix/plugins/socialauth/urls.py b/src/pretix/plugins/socialauth/urls.py index 34fbb86c8..b4caeb902 100644 --- a/src/pretix/plugins/socialauth/urls.py +++ b/src/pretix/plugins/socialauth/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ - url(r'^oauth_return$', views.oauth_return, name='mediawiki.oauth.return') + url(r'^oauth_return$', views.oauth_return, name='social.oauth.return') ] From 25d0eed1115d1cf7da5479677382eef823dcb070 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Mon, 16 Dec 2024 10:40:32 +0700 Subject: [PATCH 11/14] Refactor code --- src/pretix/plugins/socialauth/backends.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pretix/plugins/socialauth/backends.py b/src/pretix/plugins/socialauth/backends.py index b0dad08ae..3681522a5 100644 --- a/src/pretix/plugins/socialauth/backends.py +++ b/src/pretix/plugins/socialauth/backends.py @@ -20,8 +20,10 @@ def authentication_url(self, request): query_params = { "next": build_absolute_uri("plugins:socialauth:social.oauth.return") } + parsed_url = urlparse(base_url) - return urlunparse(parsed_url._replace(query=urlencode(query_params))) + updated_url = parsed_url._replace(query=urlencode(query_params)) + return urlunparse(updated_url) class GoogleBackend(BaseAuthBackend): @@ -36,8 +38,10 @@ def authentication_url(self, request): query_params = { "next": build_absolute_uri("plugins:socialauth:social.oauth.return") } + parsed_url = urlparse(base_url) - return urlunparse(parsed_url._replace(query=urlencode(query_params))) + updated_url = parsed_url._replace(query=urlencode(query_params)) + return urlunparse(updated_url) class GithubBackend(BaseAuthBackend): @@ -52,5 +56,7 @@ def authentication_url(self, request): query_params = { "next": build_absolute_uri("plugins:socialauth:social.oauth.return") } + parsed_url = urlparse(base_url) - return urlunparse(parsed_url._replace(query=urlencode(query_params))) + updated_url = parsed_url._replace(query=urlencode(query_params)) + return urlunparse(updated_url) From e557a112eee290d2738c7be4ef7e52f26e3fcd5b Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Mon, 16 Dec 2024 10:57:56 +0700 Subject: [PATCH 12/14] Remove blank --- src/pretix/plugins/socialauth/backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/plugins/socialauth/backends.py b/src/pretix/plugins/socialauth/backends.py index 3681522a5..956eeccff 100644 --- a/src/pretix/plugins/socialauth/backends.py +++ b/src/pretix/plugins/socialauth/backends.py @@ -20,7 +20,7 @@ def authentication_url(self, request): query_params = { "next": build_absolute_uri("plugins:socialauth:social.oauth.return") } - + parsed_url = urlparse(base_url) updated_url = parsed_url._replace(query=urlencode(query_params)) return urlunparse(updated_url) From 4397b50c24f9ee98dcb7b5d12d167efe9ac8ad4f Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Mon, 16 Dec 2024 15:43:27 +0700 Subject: [PATCH 13/14] Change quote type --- src/pretix/control/views/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index 10d14ee85..d7e5f09d4 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -63,7 +63,7 @@ def form_valid(self, form): error_type = type(e).__name__ error_message = str(e) logger.error('Error while creating OAuth2 application: %s - %s', error_type, error_message) - return self.render_to_response({"error_message": f"{error_type}: {error_message}"}) + return self.render_to_response({'error_message': f'{error_type}: {error_message}'}) return self.render_to_response(self.get_context_data(form=form, result=result)) From 9022d6bf5320761ef1f5934c65c852c6e6045016 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Tue, 17 Dec 2024 10:11:14 +0700 Subject: [PATCH 14/14] Remove str() usage --- src/pretix/control/views/global_settings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pretix/control/views/global_settings.py b/src/pretix/control/views/global_settings.py index d7e5f09d4..38618f724 100644 --- a/src/pretix/control/views/global_settings.py +++ b/src/pretix/control/views/global_settings.py @@ -61,9 +61,8 @@ def form_valid(self, form): result = self.create_oauth_application(url) except (IntegrityError, ValidationError, ObjectDoesNotExist) as e: error_type = type(e).__name__ - error_message = str(e) - logger.error('Error while creating OAuth2 application: %s - %s', error_type, error_message) - return self.render_to_response({'error_message': f'{error_type}: {error_message}'}) + logger.error('Error while creating OAuth2 application: %s - %s', error_type, e) + return self.render_to_response({'error_message': f'{error_type}: {e}'}) return self.render_to_response(self.get_context_data(form=form, result=result))