✨(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):
|
def update(self, instance, validated_data):
|
||||||
"""Not implemented as this is a validation-only serializer."""
|
"""Not implemented as this is a validation-only serializer."""
|
||||||
raise NotImplementedError("StartRecordingSerializer is validation-only")
|
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,
|
LobbyParticipantNotFound,
|
||||||
LobbyService,
|
LobbyService,
|
||||||
)
|
)
|
||||||
|
from core.services.room_creation import RoomCreation
|
||||||
|
|
||||||
from . import permissions, serializers
|
from . import permissions, serializers
|
||||||
|
|
||||||
@@ -186,6 +187,12 @@ class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
|
|||||||
scope = "request_entry"
|
scope = "request_entry"
|
||||||
|
|
||||||
|
|
||||||
|
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
|
||||||
|
"""Throttle Anonymous user requesting room generation callback"""
|
||||||
|
|
||||||
|
scope = "creation_callback"
|
||||||
|
|
||||||
|
|
||||||
class RoomViewSet(
|
class RoomViewSet(
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
@@ -268,6 +275,9 @@ class RoomViewSet(
|
|||||||
role=models.RoleChoices.OWNER,
|
role=models.RoleChoices.OWNER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if callback_id := self.request.data.get("callback_id"):
|
||||||
|
RoomCreation().persist_callback_state(callback_id, room)
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
@@ -460,6 +470,31 @@ class RoomViewSet(
|
|||||||
{"status": "error", "message": str(e)}, status=status_code
|
{"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:
|
class ResourceAccessListModelMixin:
|
||||||
"""List mixin for resource access API."""
|
"""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.
|
Test rooms API endpoints in the Meet core app: create.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=W0621,W0613
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
@@ -11,6 +14,15 @@ from ...models import Room
|
|||||||
pytestmark = pytest.mark.django_db
|
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():
|
def test_api_rooms_create_anonymous():
|
||||||
"""Anonymous users should not be allowed to create rooms."""
|
"""Anonymous users should not be allowed to create rooms."""
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
@@ -26,7 +38,7 @@ def test_api_rooms_create_anonymous():
|
|||||||
assert Room.objects.exists() is False
|
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
|
Authenticated users should be able to create rooms and should automatically be declared
|
||||||
as owner of the newly created room.
|
as owner of the newly created room.
|
||||||
@@ -49,6 +61,33 @@ def test_api_rooms_create_authenticated():
|
|||||||
assert room.slug == "my-room"
|
assert room.slug == "my-room"
|
||||||
assert room.accesses.filter(role="owner", user=user).exists() is True
|
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():
|
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_name="REQUEST_ENTRY_THROTTLE_RATES",
|
||||||
environ_prefix=None,
|
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,
|
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
|
# pylint: disable=invalid-name
|
||||||
@property
|
@property
|
||||||
def ENVIRONMENT(self):
|
def ENVIRONMENT(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user