Skip to content

Commit

Permalink
Add UI to activate login with eventyay ticket (#139)
Browse files Browse the repository at this point in the history
* Add UI to activate login with eventyay ticket
* update format for logging
  • Loading branch information
lcduong authored Jul 29, 2024
1 parent e262fe4 commit a3c018a
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 35 deletions.
9 changes: 4 additions & 5 deletions src/pretalx/cfp/templates/cfp/event/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
<h2>{% translate "Welcome back!" %}</h2>
<p>
{% blocktranslate trimmed %}
You do not need an account to view the event, or submit feedback, or receive
schedule updates. You’ll only need an account if you participate in the
event as speaker or as an organiser.
If you already created a proposal for a different event on this server, you can re-use your account to log in for this event.
{% endblocktranslate %}
</p>
<p>
{% blocktranslate trimmed %}
If you already created a proposal for a different event on this server, you can re-use your account
to log in for this event.
You do not need an account to view the event, submit feedback, or receive schedule updates.
An account is only necessary if you want to create your own personalized schedule,
or if you are participating in the event as a speaker or organizer.
{% endblocktranslate %}
</p>
{% include "common/auth.html" %}
Expand Down
18 changes: 10 additions & 8 deletions src/pretalx/common/templates/common/auth.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ <h4 class="text-center">{% translate "I already have an account" %}</h4>
{% translate "Log in" %}
</button>
{% endif %}
{% socialapp_exists 'eventyay' as eventyay_exists %}
{% if eventyay_exists %}
{% if not no_buttons %}
<div class="text-center">
<a class="btn btn-lg btn-primary btn-block mt-3" href="{% provider_login_url 'eventyay' %}">
{% translate "Login with Eventyay-ticket" %}
</a>
</div>
{% if request.event and request.event.organiser and request.event.organiser.slug %}
{% socialapp_exists request.event.organiser.slug as eventyay_exists %}
{% if eventyay_exists %}
{% if not no_buttons %}
<div class="text-center">
<a class="btn btn-lg btn-primary btn-block mt-3" href="{% provider_login_url request.event.organiser.slug %}">
{% translate "Login with Eventyay-ticket" %}
</a>
</div>
{% endif %}
{% endif %}
{% endif %}
{% if password_reset_link or request.event %}
Expand Down
22 changes: 22 additions & 0 deletions src/pretalx/orga/forms/sso_client_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from allauth.socialaccount.models import SocialApp
from django import forms
from django.conf import settings
from django.contrib.sites.models import Site


class SSOClientForm(forms.ModelForm):
def __init__(self, provider_id, *args, **kwargs):
social_app = SocialApp.objects.filter(provider=provider_id).first()
kwargs["instance"] = social_app
super().__init__(*args, **kwargs)
self.fields['secret'].required = True # Secret is required

class Meta:
model = SocialApp
fields = ["client_id", "secret"]

def save(self, organiser=None):
self.instance.name = organiser
self.instance.provider = organiser
super().save()
self.instance.sites.add(Site.objects.get(pk=settings.SITE_ID))
3 changes: 3 additions & 0 deletions src/pretalx/orga/templates/orga/organiser/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
</span>
</div>
</fieldset>
<fieldset>
{% include "orga/organiser/organiser_sso.html" %}
</fieldset>
{% endif %}
</form>
{% endblock %}
27 changes: 27 additions & 0 deletions src/pretalx/orga/templates/orga/organiser/organiser_sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% load bootstrap4 %}
{% load i18n %}
{% load rules %}

{% block content %}
<form method="post">
<fieldset>
<legend>{% translate "SSO with Eventyay-ticket" %}</legend>
{% csrf_token %}
{% bootstrap_field sso_client_form.client_id layout='event' %}
{% bootstrap_field sso_client_form.secret layout='event' %}
<div class="submit-group panel">
<span></span>
<span>
<button type="submit" class="btn btn-success" name="form" value="sso_client">
<i class="fa fa-check"></i>
{% translate "Save" %}
</button>
<button type="submit" class="btn btn-danger" name="form" value="remove_sso_client">
<i class="fa fa-check"></i>
{% translate "Remove key" %}
</button>
</span>
</div>
</fieldset>
</form>
{% endblock %}
66 changes: 65 additions & 1 deletion src/pretalx/orga/views/organiser.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import logging

from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views.generic import DeleteView, DetailView, TemplateView
from django_context_decorator import context
from allauth.socialaccount.models import SocialApp

from pretalx.common.exceptions import SendMailException
from pretalx.common.mixins.views import PermissionRequired
from pretalx.common.views import CreateOrUpdateView
from pretalx.common.views import CreateOrUpdateView, is_form_bound
from pretalx.event.forms import OrganiserForm, TeamForm, TeamInviteForm
from pretalx.event.models import Organiser, Team, TeamInvite
from pretalx.orga.forms.sso_client_form import SSOClientForm

logger = logging.getLogger(__name__)


class TeamMixin:
Expand Down Expand Up @@ -225,6 +231,64 @@ def get_success_url(self):
messages.success(self.request, _("Saved!"))
return self.request.path

@context
@cached_property
def sso_client_form(self):
organiser = self.kwargs.get("organiser", None)
if self.request.POST.get('form') == 'remove_sso_client':
bind = is_form_bound(self.request, "remove_sso_client")
else:
bind = is_form_bound(self.request, "sso_client")
return SSOClientForm(
provider_id=organiser,
data=self.request.POST if bind else None,
)

def save_sso_client(self, request, *args, **kwargs):
try:
self.sso_client_form.save(organiser=self.kwargs.get("organiser", None))
except Exception as e:
logger.error(
f"Error saving SSO client for organiser {self.kwargs.get('organiser', None)}: {e}")
messages.error(request, _("An error occurred: ") + str(e))
return redirect(self.request.path)
return redirect(self.get_success_url())

def post(self, request, *args, **kwargs):
try:
if self.is_remove_sso_client_request(request):
return self.handle_remove_sso_client(request)
elif self.is_sso_client_request(request):
return self.handle_sso_client(request, *args, **kwargs)
else:
self.set_object()
return super().post(request, *args, **kwargs)
except Exception as e:
messages.error(request, _("An error occurred: ") + str(e))
return redirect(self.request.path)

def is_remove_sso_client_request(self, request):
return is_form_bound(self.request, "remove_sso_client") and request.POST.get(
'form') == 'remove_sso_client'

def handle_remove_sso_client(self, request):
provider_id = self.kwargs.get("organiser")
try:
social_app = SocialApp.objects.get(provider=provider_id)
social_app.delete()
except SocialApp.DoesNotExist:
messages.error(request, _("The key does not exist."))
return redirect(self.request.path)

def is_sso_client_request(self, request):
return is_form_bound(self.request, "sso_client") and request.POST.get(
'form') == 'sso_client' and self.sso_client_form.is_valid()

def handle_sso_client(self, request, *args, **kwargs):
return self.save_sso_client(request, *args, **kwargs)




class OrganiserDelete(PermissionRequired, DeleteView):
template_name = "orga/organiser/delete.html"
Expand Down
6 changes: 4 additions & 2 deletions src/pretalx/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,10 @@ def merge_csp(*options, config=None):
)

# Below is configuration for SSO using eventyay-ticket
EVENTYAY_TICKET_BASE_PATH = os.getenv("EVENTYAY_TICKET_BASE_PATH",
"https://tickets-dev.eventyay.com")

EVENTYAY_TICKET_BASE_PATH = config.get("urls", "eventyay-ticket",
fallback="https://tickets-dev.eventyay.com")

SITE_ID = 1
# for now, customer must verified their email at eventyay-ticket, so this check not required
ACCOUNT_EMAIL_VERIFICATION = 'none'
Expand Down
1 change: 1 addition & 0 deletions src/pretalx/sso_provider/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from allauth.socialaccount.forms import SignupForm


class CustomSignUpForm(SignupForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
26 changes: 17 additions & 9 deletions src/pretalx/sso_provider/providers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import logging
import requests
from urllib.parse import urlencode

from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from allauth.socialaccount.providers.base import AuthAction, ProviderAccount
from allauth.socialaccount.app_settings import QUERY_EMAIL
from allauth.account.models import EmailAddress
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.helpers import render_authentication_error
from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from django.conf import settings
from django.urls import reverse

from .views import EventyayTicketOAuth2Adapter

logger = logging.getLogger(__name__)

class Scope(object):
OPEN_ID = "openid"
Expand All @@ -27,23 +29,29 @@ def get_profile_url(self):
def get_avatar_url(self):
return self.account.extra_data.get("picture")

def to_str(self):
dflt = super(GoogleAccount, self).to_str()
return self.account.extra_data.get("name", dflt)


class EventyayProvider(OAuth2Provider):
id = 'eventyay'
name = 'Eventyay'
account_class = EventYayTicketAccount
oauth2_adapter_class = EventyayTicketOAuth2Adapter

def __init__(self, request, app=None):
if hasattr(request, 'event'):
app = SocialApp.objects.get(provider=request.event.organiser.slug)
self.id = request.event.organiser.slug
elif request.session.get('org') is not None:
app = SocialApp.objects.get(provider=request.session.get('org'))
self.id = request.session.get('org')
super(EventyayProvider, self).__init__(request, app=app)

def get_openid_config(self):
try:
response = requests.get(settings.EVENTYAY_TICKET_SSO_WELL_KNOW_URL
.format(org=self.request.session.get('org')))
response.raise_for_status()
except:
except Exception as e:
logger.error(f"Error when getting openid config: {e}")
raise ImmediateHttpResponse(
render_authentication_error(self.request,
'Error happened when trying get configurations from Eventyay-ticket'))
Expand Down Expand Up @@ -75,7 +83,7 @@ def extract_email_addresses(self, data):
def get_login_url(self, request, **kwargs):
current_event = request.event
request.session['org'] = current_event.organiser.slug
url = reverse(self.id + "_login")
url = reverse("eventyay_login") # Base login url for sso with eventyay-ticker
if kwargs:
url = url + "?" + urlencode(kwargs)
return url
Expand Down
4 changes: 0 additions & 4 deletions src/pretalx/sso_provider/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
from django.urls import path
from .views import oauth2_login, oauth2_callback


from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns

from .providers import EventyayProvider
Expand Down
57 changes: 51 additions & 6 deletions src/pretalx/sso_provider/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import jwt
import requests

from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.socialaccount.providers.oauth2.views import (
OAuth2LoginView, OAuth2CallbackView)
from allauth.socialaccount.internal import jwtkit
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.utils import build_absolute_uri
from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError
from django.urls import NoReverseMatch
from django.urls import reverse


class EventyayTicketOAuth2Adapter(OAuth2Adapter):
provider_id = 'eventyay'

def __init__(self, request):
if request.session.get('org') is not None:
self.provider_id = request.session.get('org')
else:
self.provider_id = 'eventyay'
super().__init__(request)

@property
def access_token_url(self):
Expand All @@ -35,13 +42,51 @@ def complete_login(self, request, app, token, **kwargs):
extra_data = response.json()
return self.get_provider().sociallogin_from_response(request, extra_data)

def get_callback_url(self, request, app):
try:
callback_url = reverse(self.provider_id + "_callback")
except NoReverseMatch as e:
callback_url = reverse('eventyay_callback') # Default call back url
protocol = self.redirect_uri_protocol
return build_absolute_uri(request, callback_url, protocol)


class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):

def get_provider(self, request, provider, client_id=None):
"""Looks up a `provider`, supporting subproviders by looking up by
`provider_id`.
"""
from allauth.socialaccount.providers import registry

try:
provider_class = registry.get_class(provider)
if provider_class is None or provider_class.uses_apps:
app = self.get_app(request, provider=provider, client_id=client_id)
if not provider_class:
provider_class = registry.get_class(app.provider)
if not provider_class:
raise ImproperlyConfigured("unknown provider: %s", app.provider)
return provider_class(request, app=app)
elif provider_class:
assert not provider_class.uses_apps
return provider_class(request, app=None)
else:
raise ImproperlyConfigured("unknown provider: %s", provider)
except ImproperlyConfigured as e:
app = self.get_app(request, provider=provider, client_id=client_id)
if app is not None:
provider_class = registry.get_class('eventyay') # Get default custom provider
return provider_class(request, app=app)
else:
raise ImproperlyConfigured("unknown provider: " + app.provider)

def save_user(self, request, sociallogin, form=None):
try:
user = super().save_user(request, sociallogin, form)
except IntegrityError as e:
# bypass the error if the user with this email created in eventyay-talk before
# bypass the error if the user with this email created in eventyay-talk
# before
pass


Expand Down

0 comments on commit a3c018a

Please sign in to comment.