Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/eventyay/api/auth/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ class EventTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()

# Always set request.event if event_id is in the URL, even for unauthenticated requests
if "event_id" in request.resolver_match.kwargs:
event_id = request.resolver_match.kwargs["event_id"]
try:
request.event = Event.objects.get(id=int(event_id))
except (ValueError, TypeError, Event.DoesNotExist):
try:
Comment on lines +25 to +28
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implements a try-except pattern that always attempts integer conversion first, causing an exception on every slug-based lookup. Consider checking if event_id is numeric before attempting integer conversion: if event_id.isdigit(): request.event = Event.objects.get(id=int(event_id)) followed by the slug lookup.

Suggested change
try:
request.event = Event.objects.get(id=int(event_id))
except (ValueError, TypeError, Event.DoesNotExist):
try:
if event_id.isdigit():
try:
request.event = Event.objects.get(id=int(event_id))
except Event.DoesNotExist:
request.event = None
else:
try:

Copilot uses AI. Check for mistakes.
request.event = Event.objects.get(slug=event_id)
except Event.DoesNotExist:
request.event = None

if not auth or auth[0].lower() != self.keyword.lower().encode():
return None

Expand All @@ -35,8 +46,9 @@ def authenticate(self, request):
msg = "Invalid token header. Token string should not contain invalid characters."
raise exceptions.AuthenticationFailed(msg)

event_id = request.resolver_match.kwargs["event_id"]
request.event = get_object_or_404(Event, id=event_id)
if not request.event:
raise exceptions.AuthenticationFailed("Event not found.")

return self.authenticate_credentials(token, request.event)

def authenticate_credentials(self, key, event):
Expand Down
32 changes: 32 additions & 0 deletions app/eventyay/api/serializers/speaker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django_scopes import scopes_disabled
from rest_framework.serializers import (
CharField,
SerializerMethodField,
URLField,
ModelSerializer,
)

from eventyay.base.models.profile import SpeakerProfile


class SpeakerSerializer(ModelSerializer):
"""Serializer for Speaker profiles in the API."""

code = CharField(source="user.code", read_only=True)
name = CharField(source="user.name", read_only=True)
biography = CharField(read_only=True)
submissions = SerializerMethodField()

def get_submissions(self, obj):
"""Return list of submission codes for this speaker."""
if not obj.user:
return []
with scopes_disabled():
return list(
obj.user.submissions.filter(event=obj.event)
.values_list('code', flat=True)
)

class Meta:
model = SpeakerProfile
fields = ('code', 'name', 'biography', 'submissions')
10 changes: 6 additions & 4 deletions app/eventyay/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Import views directly from their modules to avoid relying on package attribute access
from .views.rooms import RoomViewSet
from .views.speaker import SpeakerViewSet
from .views.event import (
EventView,
schedule_update,
Expand All @@ -13,17 +14,18 @@
ExportView,
)

orga_router = routers.DefaultRouter(trailing_slash=False)
orga_router = routers.DefaultRouter()

event_router = routers.DefaultRouter(trailing_slash=False)
event_router = routers.DefaultRouter()
event_router.register(r"rooms", RoomViewSet)
event_router.register(r"speakers", SpeakerViewSet, basename="speaker")

router = routers.DefaultRouter(trailing_slash=False)
router = routers.DefaultRouter()
urlpatterns = [
path("events/<str:event_id>/", EventView.as_view(), name="root"),
re_path("events/(?P<event_id>[^/]+)/schedule_update/?$", schedule_update),
re_path("events/(?P<event_id>[^/]+)/delete_user/?$", delete_user),
path("events/<str:event_id>/", include(event_router.urls)),
path("events/<str:event_id>", EventView.as_view(), name="root"),
Comment on lines 26 to +28
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path pattern conflicts with line 27's include(event_router.urls) which already handles paths starting with events/<str:event_id>/. The pattern without a trailing slash will never match because Django's URL resolver will match the more specific pattern first. Either remove this duplicate or ensure proper ordering and trailing slash handling.

Suggested change
path("events/<str:event_id>/", include(event_router.urls)),
path("events/<str:event_id>", EventView.as_view(), name="root"),
path("events/<str:event_id>", EventView.as_view(), name="root"),
path("events/<str:event_id>/", include(event_router.urls)),

Copilot uses AI. Check for mistakes.
path("events/<str:event_id>/theme", EventThemeView.as_view()),
path(
"events/<str:event_id>/favourite-talk/",
Expand Down
45 changes: 45 additions & 0 deletions app/eventyay/api/views/speaker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.utils.functional import cached_property
from django_scopes import scopes_disabled
from rest_framework import mixins, viewsets
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import AllowAny

from eventyay.base.models.profile import SpeakerProfile
from eventyay.api.serializers.speaker import SpeakerSerializer


class LargeResultsSetPagination(PageNumberPagination):
"""Pagination class that supports a 'limit' query parameter."""
page_size = 50
page_size_query_param = 'limit'
max_page_size = 10000
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maximum page size of 10000 is extremely high and could cause performance issues or memory problems when retrieving large datasets. Consider reducing this to a more reasonable limit (e.g., 100-500) to prevent potential DoS through excessive pagination requests.

Suggested change
max_page_size = 10000
max_page_size = 500

Copilot uses AI. Check for mistakes.


class SpeakerViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
"""
API endpoint for listing and retrieving speakers for an event.
"""
serializer_class = SpeakerSerializer
queryset = SpeakerProfile.objects.none()
lookup_field = "user__code__iexact"
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lookup_field uses __iexact lookup which is not a valid field name but a query expression. This will cause Django to look for a field literally named 'user__code__iexact' rather than performing case-insensitive matching. Override get_object() method instead to implement case-insensitive lookup: def get_object(self): return self.get_queryset().get(user__code__iexact=self.kwargs[self.lookup_url_kwarg])

Copilot uses AI. Check for mistakes.
pagination_class = LargeResultsSetPagination
permission_classes = [AllowAny] # Allow public access to speakers list

def get_queryset(self):
"""Return speakers for the current event."""
if not hasattr(self.request, 'event') or not self.request.event:
return self.queryset

# Use scopes_disabled to bypass django_scopes since we're filtering by event explicitly
with scopes_disabled():
# Get all speaker profiles for this event that have a user
queryset = SpeakerProfile.objects.filter(
event=self.request.event,
user__isnull=False
).select_related('user', 'event').order_by('user__fullname')

return queryset
16 changes: 16 additions & 0 deletions app/eventyay/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,24 @@ def instance_name(request):
CORS_ORIGIN_REGEX_WHITELIST = [
r'^http://localhost$',
r'^http://localhost:\d+$',
r'^http://127\.0\.0\.1$',
r'^http://127\.0\.0\.1:\d+$',
]

# Additional CORS settings for API access
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]

# Video-Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
Expand Down
2 changes: 1 addition & 1 deletion app/eventyay/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
url(r'^metrics$', metrics.serve_metrics, name='metrics'),
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(('eventyay.api.urls', 'eventyayapi'), namespace='api-v1')),
url(r'^api/', include(('eventyay.api.urls', 'eventyayapi'))),
# url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version'),
url(r'^accounts/', include('allauth.urls')),
]
Expand Down
4 changes: 4 additions & 0 deletions app/eventyay/features/live/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ async def receive_json(self, content, **kargs):

if not self.user:
if content[0] == "authenticate":
# Check if components are properly initialized
if "user" not in self.components:
await self.send_error("protocol.not_connected", close=True)
return
await self._maybe_refresh(self.event, allowed_age=30)
await self.components["user"].login(content[-1])
else:
Expand Down
5 changes: 3 additions & 2 deletions app/eventyay/webapp/src/lib/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ export function youtubeid(message) {
return helpers.withMessage(message, helpers.regex(/^[0-9A-Za-z_-]{5,}$/))
}
const relative = helpers.regex(/^\/.*$/)
const devurl = helpers.regex(/^http:\/\/localhost.*$/) // vuelidate does not allow localhost
// Allow localhost and local IP addresses (with or without port)
const localurl = helpers.regex(/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?(\/.*)?$/)
export function url(message) {
return helpers.withMessage(message, (value) => (!helpers.req(value) || _url(value) || relative(value) || (ENV_DEVELOPMENT && devurl(value))))
return helpers.withMessage(message, (value) => (!helpers.req(value) || _url(value) || relative(value) || localurl(value)))
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localurl validation now applies in production environments (removed ENV_DEVELOPMENT check). This allows users to enter local URLs like http://localhost or http://127.0.0.1 in production, which is a security concern. Restore the environment check: (ENV_DEVELOPMENT && localurl(value))

Suggested change
return helpers.withMessage(message, (value) => (!helpers.req(value) || _url(value) || relative(value) || localurl(value)))
return helpers.withMessage(message, (value) => (!helpers.req(value) || _url(value) || relative(value) || (ENV_DEVELOPMENT && localurl(value))))

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is has been mentioned in #1122 and will be resolved soon with the addition of HRM functionality in the video webapp

}
export function isJson() {
return helpers.withMessage(({ $response }) => $response?.message, value => {
Expand Down