|
| 1 | +from enum import StrEnum |
| 2 | +import logging |
1 | 3 | import datetime as dt |
2 | | -from urllib.parse import unquote, urlparse |
| 4 | +from http import HTTPStatus |
| 5 | +from urllib.parse import unquote, urlparse, urljoin |
| 6 | +from typing import TypeVar |
3 | 7 |
|
4 | 8 | import jwt |
5 | 9 | import requests |
|
23 | 27 | PermissionRequired, |
24 | 28 | SocialMediaCardMixin, |
25 | 29 | ) |
26 | | -from eventyay.base.models import TalkSlot |
| 30 | +from eventyay.base.models import Event, TalkSlot, User |
27 | 31 | from eventyay.submission.forms import FeedbackForm |
28 | 32 | from eventyay.base.models import Submission, SubmissionStates |
29 | 33 |
|
30 | 34 |
|
| 35 | +logger = logging.getLogger(__name__) |
| 36 | + |
| 37 | + |
| 38 | +class TicketCheckResult(StrEnum): |
| 39 | + HAS_TICKET = 'has_ticket' |
| 40 | + MISCONFIGURED = 'missing_configuration' |
| 41 | + NO_TICKET = 'no_ticket' |
| 42 | + |
| 43 | + |
| 44 | +class VideoJoinError(StrEnum): |
| 45 | + # The string value looks diffrent from the enum name |
| 46 | + # because other code may depend on this string value. |
| 47 | + NOT_ALLOWED = 'user_not_allowed' |
| 48 | + MISCONFIGURED = 'missing_configuration' |
| 49 | + |
| 50 | + |
31 | 51 | class TalkMixin(PermissionRequired): |
32 | 52 | permission_required = 'base.view_public_submission' |
33 | 53 |
|
@@ -253,82 +273,117 @@ class OnlineVideoJoin(EventPermissionRequired, View): |
253 | 273 | permission_required = 'base.view_schedule' |
254 | 274 |
|
255 | 275 | def get(self, request, *args, **kwargs): |
256 | | - # First check video is configured or not |
257 | | - if ( |
258 | | - 'eventyay_venueless' not in request.event.plugin_list |
259 | | - or not request.event.venueless_settings |
260 | | - or not request.event.venueless_settings.join_url |
261 | | - or not request.event.venueless_settings.secret |
262 | | - or not request.event.venueless_settings.issuer |
263 | | - or not request.event.venueless_settings.audience |
264 | | - ): |
265 | | - return HttpResponse(status=403, content='missing_configuration') |
266 | | - |
267 | 276 | if not request.user.is_authenticated: |
268 | | - # redirect to login page if user not logged in yet |
269 | | - return HttpResponse(status=403, content='user_not_allowed') |
270 | | - |
271 | | - # prepare event data to check from ticket |
272 | | - if 'ticket_link' not in request.event.display_settings: |
273 | | - return HttpResponse(status=403, content='missing_configuration') |
274 | | - |
275 | | - base_url, organizer, event = self.retrieve_info_from_url(request.event.display_settings['ticket_link']) |
276 | | - |
277 | | - if not organizer or not event or not base_url: |
278 | | - return HttpResponse(status=403, content='missing_configuration') |
279 | | - |
280 | | - check_payload = {'user_email': request.user.email} |
281 | | - |
282 | | - # call to ticket to check if user order ticket yet or not |
283 | | - response = requests.post(f'{base_url}/api/v1/{organizer}/{event}/ticket-check', json=check_payload) |
284 | | - |
285 | | - if response.status_code != 200: |
286 | | - return HttpResponse(status=403, content='user_not_allowed') |
287 | | - |
288 | | - else: |
289 | | - # Redirect user to online event |
290 | | - iat = dt.datetime.utcnow() |
291 | | - exp = iat + dt.timedelta(days=30) |
292 | | - profile = { |
293 | | - 'display_name': request.user.name, |
294 | | - 'fields': { |
295 | | - 'eventyay_id': request.user.code, |
296 | | - }, |
297 | | - } |
298 | | - if request.user.avatar_url: |
299 | | - profile['profile_picture'] = request.user.get_avatar_url(request.event) |
300 | | - |
301 | | - payload = { |
302 | | - 'iss': request.event.venueless_settings.issuer, |
303 | | - 'aud': request.event.venueless_settings.audience, |
304 | | - 'exp': exp, |
305 | | - 'iat': iat, |
306 | | - 'uid': encode_email(request.user.email), |
307 | | - 'profile': profile, |
308 | | - 'traits': list( |
309 | | - { |
310 | | - f'eventyay-video-event-{request.event.slug}', |
311 | | - } |
312 | | - ), |
313 | | - } |
314 | | - token = jwt.encode(payload, request.event.venueless_settings.secret, algorithm='HS256') |
315 | | - |
316 | | - return JsonResponse( |
| 277 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.NOT_ALLOWED) |
| 278 | + |
| 279 | + # First check video is configured or not |
| 280 | + if 'pretalx_venueless' not in request.event.plugin_list: |
| 281 | + logger.info('pretalx_venueless plugin is not enabled.') |
| 282 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 283 | + event = request.event |
| 284 | + logger.info('To check settings for event %s', event) |
| 285 | + if not (venueless_settings := event.venueless_settings): |
| 286 | + logger.info('venueless settings is missing.') |
| 287 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 288 | + if not venueless_settings.join_url: |
| 289 | + logger.info('venueless_settings.join_url is missing.') |
| 290 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 291 | + if not venueless_settings.secret: |
| 292 | + logger.info('venueless_settings.secret is missing.') |
| 293 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 294 | + if not venueless_settings.issuer: |
| 295 | + logger.info('venueless_settings.issuer is missing.') |
| 296 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 297 | + if not venueless_settings.audience: |
| 298 | + logger.info('venueless_settings.audience is missing.') |
| 299 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 300 | + |
| 301 | + # If the logged-in user does not have "orga.view_schedule" permission, we check |
| 302 | + # if he/she owns a ticket. |
| 303 | + if not request.user.has_perm('agenda.view_schedule', event): |
| 304 | + res = check_user_owning_ticket(request.user, event) |
| 305 | + if res == TicketCheckResult.NO_TICKET: |
| 306 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.NOT_ALLOWED) |
| 307 | + if res == TicketCheckResult.MISCONFIGURED: |
| 308 | + return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED) |
| 309 | + |
| 310 | + # Redirect user to online event |
| 311 | + iat = dt.datetime.now(dt.UTC) |
| 312 | + exp = iat + dt.timedelta(days=30) |
| 313 | + profile = { |
| 314 | + "display_name": request.user.name, |
| 315 | + "fields": { |
| 316 | + "pretalx_id": request.user.code, |
| 317 | + }, |
| 318 | + } |
| 319 | + if request.user.avatar_url: |
| 320 | + profile["profile_picture"] = request.user.get_avatar_url(request.event) |
| 321 | + |
| 322 | + payload = { |
| 323 | + "iss": venueless_settings.issuer, |
| 324 | + "aud": venueless_settings.audience, |
| 325 | + "exp": exp, |
| 326 | + "iat": iat, |
| 327 | + "uid": encode_email(request.user.email), |
| 328 | + "profile": profile, |
| 329 | + "traits": list( |
317 | 330 | { |
318 | | - 'redirect_url': '{}/#token={}'.format(request.event.venueless_settings.join_url, token).replace( |
319 | | - '//#', '/#' |
320 | | - ) |
321 | | - }, |
322 | | - status=200, |
323 | | - ) |
| 331 | + f"eventyay-video-event-{request.event.slug}", |
| 332 | + } |
| 333 | + ), |
| 334 | + } |
| 335 | + token = jwt.encode( |
| 336 | + payload, venueless_settings.secret, algorithm="HS256" |
| 337 | + ) |
| 338 | + redirect_url = urljoin(venueless_settings.join_url, f'#token={token}') |
| 339 | + logger.info('Redirect URL to Video: %s', redirect_url) |
| 340 | + return JsonResponse( |
| 341 | + { |
| 342 | + 'redirect_url': redirect_url |
| 343 | + }, |
| 344 | + status=HTTPStatus.OK, |
| 345 | + ) |
| 346 | + |
324 | 347 |
|
325 | | - def retrieve_info_from_url(self, url): |
326 | | - parsed_url = urlparse(url) |
327 | | - ticket_host = settings.EVENTYAY_TICKET_BASE_PATH |
328 | | - path = parsed_url.path |
329 | | - parts = path.strip('/').split('/') |
330 | | - if len(parts) >= 2: |
331 | | - organizer, event = parts[-2:] |
332 | | - return ticket_host, unquote(organizer), unquote(event) |
333 | | - else: |
334 | | - return ticket_host, None, None |
| 348 | +_T = TypeVar('_T', str, None) |
| 349 | +# We use TypeVar because the 2nd and 3rd items must be both `str` or both `None` at the same time. |
| 350 | +# The annotation `tuple[str, str | None, str | None]` doesn't satisfy this requirement. |
| 351 | +def extract_event_info_from_url(url: str) -> tuple[str, _T, _T]: |
| 352 | + parsed_url = urlparse(url) |
| 353 | + ticket_host = settings.EVENTYAY_TICKET_BASE_PATH |
| 354 | + path = parsed_url.path |
| 355 | + parts = path.strip("/").split("/") |
| 356 | + if len(parts) >= 2: |
| 357 | + organizer, event = parts[-2:] |
| 358 | + return ticket_host, unquote(organizer), unquote(event) |
| 359 | + return ticket_host, None, None |
| 360 | + |
| 361 | + |
| 362 | +def check_user_owning_ticket(user: User, event: Event) -> TicketCheckResult: |
| 363 | + """ |
| 364 | + Call eventyay-ticket API to check if user owns ticket for this event. |
| 365 | +
|
| 366 | + # NOTE: It doesn't work with the Docker setup for development, because we use fake domain then, |
| 367 | + and inside the container, the fake domain points to the container itself, not the host. |
| 368 | + """ |
| 369 | + if 'ticket_link' not in event.display_settings: |
| 370 | + logger.info('display_settings[ticket_link] is missing.') |
| 371 | + return TicketCheckResult.MISCONFIGURED |
| 372 | + base_url, organizer, event = extract_event_info_from_url( |
| 373 | + event.display_settings['ticket_link'] |
| 374 | + ) |
| 375 | + if not organizer or not event or not base_url: |
| 376 | + logger.info('display_settings[ticket_link] is not valid.') |
| 377 | + return TicketCheckResult.MISCONFIGURED |
| 378 | + check_payload = {'user_email': user.email} |
| 379 | + # call to ticket to check if user order ticket yet or not |
| 380 | + api_url = urljoin(base_url, f'api/v1/{organizer}/{event}/ticket-check') |
| 381 | + logger.info('To call API %s', api_url) |
| 382 | + # In development, we disable the SSL verification. |
| 383 | + response = requests.post(api_url, json=check_payload, verify=(not settings.DEBUG)) |
| 384 | + |
| 385 | + if response.status_code != HTTPStatus.OK: |
| 386 | + logger.debug('Response from eventyay-ticket: %s', response.text) |
| 387 | + logger.info('user is not allowed to join online event.') |
| 388 | + return TicketCheckResult.NO_TICKET |
| 389 | + return TicketCheckResult.HAS_TICKET |
0 commit comments