Skip to content

Commit

Permalink
more consistent verbiage
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger committed Dec 25, 2024
1 parent 939f642 commit de30ede
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 31 deletions.
52 changes: 29 additions & 23 deletions src/reactpy_django/auth/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import asyncio
import contextlib
from logging import getLogger
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from uuid import uuid4

from django.urls import reverse
from reactpy import component, hooks, html

from reactpy_django.javascript_components import HttpRequest
from reactpy_django.models import SwitchSession
from reactpy_django.models import SynchronizeSession

if TYPE_CHECKING:
from django.contrib.sessions.backends.base import SessionBase
Expand All @@ -19,38 +19,38 @@


@component
def session_manager(child):
def session_manager(child: Any):
"""This component can force the client (browser) to switch HTTP sessions,
making it match the websocket session.
Used to force persistent authentication between Django's websocket and HTTP stack."""
from reactpy_django import config

switch_sessions, set_switch_sessions = hooks.use_state(False)
synchronize_requested, set_synchronize_requested = hooks.use_state(False)
_, set_rerender = hooks.use_state(uuid4)
uuid_ref = hooks.use_ref(str(uuid4()))
uuid = uuid_ref.current
scope = hooks.use_connection().scope

@hooks.use_effect(dependencies=[])
def setup_asgi_scope():
"""Store a trigger function in websocket scope so that ReactPy-Django's hooks can command a session synchronization."""
"""Store trigger functions in the websocket scope so that ReactPy-Django's hooks can command
any relevant actions."""
scope.setdefault("reactpy", {})
scope["reactpy"]["synchronize_session"] = synchronize_session
scope["reactpy"]["rerender"] = rerender

@hooks.use_effect(dependencies=[switch_sessions])
async def synchronize_session_timeout():
"""Ensure that the ASGI scope is available to this component.
This effect will automatically be cancelled if the session is successfully
switched (via dependencies=[switch_sessions])."""
if switch_sessions:
@hooks.use_effect(dependencies=[synchronize_requested])
async def synchronize_session_watchdog():
"""This effect will automatically be cancelled if the session is successfully
switched (via effect dependencies)."""
if synchronize_requested:
await asyncio.sleep(config.REACTPY_AUTH_TIMEOUT + 0.1)
await asyncio.to_thread(
_logger.warning,
f"Client did not switch sessions within {config.REACTPY_AUTH_TIMEOUT} (REACTPY_AUTH_TIMEOUT) seconds.",
)
set_switch_sessions(False)
set_synchronize_requested(False)

async def synchronize_session():
"""Entrypoint where the server will command the client to switch HTTP sessions
Expand All @@ -60,37 +60,43 @@ async def synchronize_session():
if not session or not session.session_key:
return

# Delete any sessions currently associated with this UUID
with contextlib.suppress(SwitchSession.DoesNotExist):
obj = await SwitchSession.objects.aget(uuid=uuid)
# Delete any sessions currently associated with this UUID, which also resets
# the SynchronizeSession validity time.
# This exists to fix scenarios where...
# 1) The developer manually rotates the session key.
# 2) A component tree requests multiple logins back-to-back before they finish.
# 3) A login is requested, but the server failed to respond to the HTTP request.
with contextlib.suppress(SynchronizeSession.DoesNotExist):
obj = await SynchronizeSession.objects.aget(uuid=uuid)
await obj.adelete()

# Begin the process of synchronizing HTTP and websocket sessions
obj = await SwitchSession.objects.acreate(uuid=uuid, session_key=session.session_key)
obj = await SynchronizeSession.objects.acreate(uuid=uuid, session_key=session.session_key)
await obj.asave()
set_switch_sessions(True)
set_synchronize_requested(True)

async def synchronize_session_callback(status_code: int, response: str):
"""This callback acts as a communication bridge, allowing the client to notify the server
of the status of session switch command."""
set_switch_sessions(False)
of the status of session switch."""
set_synchronize_requested(False)
if status_code >= 300 or status_code < 200:
await asyncio.to_thread(
_logger.warning,
f"Client returned unexpected HTTP status code ({status_code}) while trying to sychronize sessions.",
)

async def rerender():
"""Force a rerender of the entire component tree."""
"""Event that can force a rerender of the entire component tree."""
set_rerender(uuid4())

# Switch sessions using a client side HttpRequest component, if needed
# If needed, synchronize sessions by configuring all relevant session cookies.
# This is achieved by commanding the client to perform a HTTP request to our session manager endpoint.
http_request = None
if switch_sessions:
if synchronize_requested:
http_request = HttpRequest(
{
"method": "GET",
"url": reverse("reactpy:switch_session", args=[uuid]),
"url": reverse("reactpy:session_manager", args=[uuid]),
"body": None,
"callback": synchronize_session_callback,
},
Expand Down
2 changes: 1 addition & 1 deletion src/reactpy_django/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def reactpy_warnings(app_configs, **kwargs):
try:
reverse("reactpy:web_modules", kwargs={"file": "example"})
reverse("reactpy:view_to_iframe", kwargs={"dotted_path": "example"})
reverse("reactpy:switch_session", args=[str(uuid4())])
reverse("reactpy:session_manager", args=[str(uuid4())])
except Exception:
warnings.append(
Warning(
Expand Down
4 changes: 2 additions & 2 deletions src/reactpy_django/http/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
),
path(
"session/<uuid:uuid>",
views.switch_session,
name="switch_session",
views.session_manager,
name="session_manager",
),
]
8 changes: 4 additions & 4 deletions src/reactpy_django/http/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,17 @@ async def view_to_iframe(request: HttpRequest, dotted_path: str) -> HttpResponse
return response


async def switch_session(request: HttpRequest, uuid: str) -> HttpResponse:
"""Switches the client's active session.
async def session_manager(request: HttpRequest, uuid: str) -> HttpResponse:
"""Switches the client's active session to match ReactPy.
This view exists because ReactPy is rendered via WebSockets, and browsers do not
allow active WebSocket connections to modify HTTP cookies. Django's authentication
design requires HTTP cookies to persist state changes.
"""
from reactpy_django.models import SwitchSession
from reactpy_django.models import SynchronizeSession

# Find out what session the client wants to switch
data = await SwitchSession.objects.aget(uuid=uuid)
data = await SynchronizeSession.objects.aget(uuid=uuid)

# CHECK: Session has expired?
if data.expired:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2024-12-25 00:18

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('reactpy_django', '0008_rename_authsession_switchsession'),
]

operations = [
migrations.RenameModel(
old_name='SwitchSession',
new_name='SynchronizeSession',
),
]
2 changes: 1 addition & 1 deletion src/reactpy_django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ComponentSession(models.Model):
last_accessed = models.DateTimeField(auto_now=True)


class SwitchSession(models.Model):
class SynchronizeSession(models.Model):
"""A model for stores any relevant data needed to force Django's HTTP session to
match the websocket session.
Expand Down

0 comments on commit de30ede

Please sign in to comment.