✨(backend) introduce a creation callback endpoint
Necessary in cross browser context situation, where we need to pass data of a room newly created between two different windows. This happens in Calendar integration.
This commit is contained in:
committed by
aleb_the_flash
parent
1f603ce17b
commit
4e1a4be650
@@ -209,3 +209,17 @@ class ParticipantEntrySerializer(serializers.Serializer):
|
||||
def update(self, instance, validated_data):
|
||||
"""Not implemented as this is a validation-only serializer."""
|
||||
raise NotImplementedError("StartRecordingSerializer is validation-only")
|
||||
|
||||
|
||||
class CreationCallbackSerializer(serializers.Serializer):
|
||||
"""Validate room creation callback data."""
|
||||
|
||||
callback_id = serializers.CharField(required=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Not implemented as this is a validation-only serializer."""
|
||||
raise NotImplementedError("StartRecordingSerializer is validation-only")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Not implemented as this is a validation-only serializer."""
|
||||
raise NotImplementedError("StartRecordingSerializer is validation-only")
|
||||
|
||||
@@ -47,6 +47,7 @@ from core.services.lobby import (
|
||||
LobbyParticipantNotFound,
|
||||
LobbyService,
|
||||
)
|
||||
from core.services.room_creation import RoomCreation
|
||||
|
||||
from . import permissions, serializers
|
||||
|
||||
@@ -186,6 +187,12 @@ class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
scope = "request_entry"
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
scope = "creation_callback"
|
||||
|
||||
|
||||
class RoomViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
@@ -268,6 +275,9 @@ class RoomViewSet(
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
|
||||
if callback_id := self.request.data.get("callback_id"):
|
||||
RoomCreation().persist_callback_state(callback_id, room)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
@@ -460,6 +470,31 @@ class RoomViewSet(
|
||||
{"status": "error", "message": str(e)}, status=status_code
|
||||
)
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=["POST"],
|
||||
url_path="creation-callback",
|
||||
permission_classes=[],
|
||||
throttle_classes=[CreationCallbackAnonRateThrottle],
|
||||
)
|
||||
def creation_callback(self, request):
|
||||
"""Retrieve cached room data via an unauthenticated request with a unique ID.
|
||||
|
||||
Designed for interoperability across iframes, popups, and other contexts,
|
||||
even on the same domain, bypassing browser security restrictions on direct communication.
|
||||
"""
|
||||
|
||||
serializer = serializers.CreationCallbackSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
room = RoomCreation().get_callback_state(
|
||||
callback_id=serializer.validated_data.get("callback_id")
|
||||
)
|
||||
|
||||
return drf_response.Response(
|
||||
{"status": "success", "room": room}, status=drf_status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class ResourceAccessListModelMixin:
|
||||
"""List mixin for resource access API."""
|
||||
|
||||
37
src/backend/core/services/room_creation.py
Normal file
37
src/backend/core/services/room_creation.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Room creation service."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class RoomCreation:
|
||||
"""Room creation related methods"""
|
||||
|
||||
@staticmethod
|
||||
def _get_cache_key(callback_id):
|
||||
"""Generate a standardized cache key for room creation callbacks."""
|
||||
return f"room-creation-callback_{callback_id}"
|
||||
|
||||
def persist_callback_state(self, callback_id: str, room) -> None:
|
||||
"""Store room data in cache using the callback ID as an identifier."""
|
||||
data = {
|
||||
"slug": room.slug,
|
||||
}
|
||||
cache.set(
|
||||
self._get_cache_key(callback_id),
|
||||
data,
|
||||
timeout=settings.ROOM_CREATION_CALLBACK_CACHE_TIMEOUT,
|
||||
)
|
||||
|
||||
def get_callback_state(self, callback_id: str) -> dict:
|
||||
"""Retrieve and clear cached room data for the given callback ID."""
|
||||
|
||||
cache_key = self._get_cache_key(callback_id)
|
||||
data = cache.get(cache_key)
|
||||
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
cache.delete(cache_key)
|
||||
|
||||
return data
|
||||
@@ -2,6 +2,9 @@
|
||||
Test rooms API endpoints in the Meet core app: create.
|
||||
"""
|
||||
|
||||
# pylint: disable=W0621,W0613
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -11,6 +14,15 @@ from ...models import Room
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_cache():
|
||||
"""Provide cache cleanup after each test to maintain test isolation."""
|
||||
yield
|
||||
keys = cache.keys("room-creation-callback_*")
|
||||
if keys:
|
||||
cache.delete(*keys)
|
||||
|
||||
|
||||
def test_api_rooms_create_anonymous():
|
||||
"""Anonymous users should not be allowed to create rooms."""
|
||||
client = APIClient()
|
||||
@@ -26,7 +38,7 @@ def test_api_rooms_create_anonymous():
|
||||
assert Room.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_rooms_create_authenticated():
|
||||
def test_api_rooms_create_authenticated(reset_cache):
|
||||
"""
|
||||
Authenticated users should be able to create rooms and should automatically be declared
|
||||
as owner of the newly created room.
|
||||
@@ -49,6 +61,33 @@ def test_api_rooms_create_authenticated():
|
||||
assert room.slug == "my-room"
|
||||
assert room.accesses.filter(role="owner", user=user).exists() is True
|
||||
|
||||
rooms_data = cache.keys("room-creation-callback_*")
|
||||
assert not rooms_data
|
||||
|
||||
|
||||
def test_api_rooms_create_generation_cache(reset_cache):
|
||||
"""
|
||||
Authenticated users creating a room with a callback ID should have room data stored in cache.
|
||||
"""
|
||||
user = UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/",
|
||||
{"name": "my room", "callback_id": "1234"},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
room = Room.objects.get()
|
||||
assert room.name == "my room"
|
||||
assert room.slug == "my-room"
|
||||
assert room.accesses.filter(role="owner", user=user).exists() is True
|
||||
|
||||
room_data = cache.get("room-creation-callback_1234")
|
||||
assert room_data.get("slug") == "my-room"
|
||||
|
||||
|
||||
def test_api_rooms_create_authenticated_existing_slug():
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Test rooms API endpoints in the Meet core app: creation callback functionality.
|
||||
"""
|
||||
|
||||
# pylint: disable=W0621,W0613
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ...factories import UserFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
# Tests for creation_callback endpoint
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_cache():
|
||||
"""Provide cache cleanup after each test to maintain test isolation."""
|
||||
yield
|
||||
keys = cache.keys("room-creation-callback_*")
|
||||
if keys:
|
||||
cache.delete(*keys)
|
||||
|
||||
|
||||
def test_api_rooms_create_anonymous(reset_cache):
|
||||
"""Anonymous user can retrieve room data once using a valid callback ID."""
|
||||
client = APIClient()
|
||||
cache.set("room-creation-callback_123", {"slug": "my room"})
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/creation-callback/",
|
||||
{
|
||||
"callback_id": "123",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
|
||||
|
||||
# Data should be cleared after retrieval
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/creation-callback/",
|
||||
{
|
||||
"callback_id": "123",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success", "room": {}}
|
||||
|
||||
|
||||
def test_api_rooms_create_empty_cache():
|
||||
"""Valid callback ID return empty room data when nothing is stored in the cache."""
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/creation-callback/",
|
||||
{
|
||||
"callback_id": "123",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success", "room": {}}
|
||||
|
||||
|
||||
def test_api_rooms_create_missing_callback_id():
|
||||
"""Requests without a callback ID properly fail with a 400 status code."""
|
||||
client = APIClient()
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/creation-callback/",
|
||||
{},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_create_authenticated(reset_cache):
|
||||
"""Authenticated users can also successfully retrieve room data using a valid callback ID"""
|
||||
user = UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
cache.set("room-creation-callback_123", {"slug": "my room"})
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/creation-callback/",
|
||||
{
|
||||
"callback_id": "123",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success", "room": {"slug": "my room"}}
|
||||
@@ -272,6 +272,11 @@ class Base(Configuration):
|
||||
environ_name="REQUEST_ENTRY_THROTTLE_RATES",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"creation_callback": values.Value(
|
||||
default="600/minute",
|
||||
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
|
||||
environ_prefix=None,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -528,6 +533,13 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Calendar integrations
|
||||
ROOM_CREATION_CALLBACK_CACHE_TIMEOUT = values.PositiveIntegerValue(
|
||||
600, # 10 minutes
|
||||
environ_name="ROOM_CREATION_CALLBACK_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
Reference in New Issue
Block a user