Skip to content

Commit 1c7c46d

Browse files
committed
functional session switcher logic
1 parent 1104344 commit 1c7c46d

File tree

5 files changed

+66
-36
lines changed

5 files changed

+66
-36
lines changed

src/reactpy_django/auth/components.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import contextlib
45
from logging import getLogger
56
from typing import TYPE_CHECKING
67
from uuid import uuid4
@@ -9,7 +10,7 @@
910
from reactpy import component, hooks
1011

1112
from reactpy_django.javascript_components import HttpRequest
12-
from reactpy_django.models import AuthSession
13+
from reactpy_django.models import SwitchSession
1314

1415
if TYPE_CHECKING:
1516
from django.contrib.sessions.backends.base import SessionBase
@@ -25,13 +26,15 @@ def auth_manager():
2526
from reactpy_django import config
2627

2728
switch_sessions, set_switch_sessions = hooks.use_state(False)
28-
uuid = hooks.use_ref(str(uuid4())).current
29+
uuid_ref = hooks.use_ref(str(uuid4()))
30+
uuid = uuid_ref.current
2931
scope = hooks.use_connection().scope
3032

3133
@hooks.use_effect(dependencies=[])
3234
def setup_asgi_scope():
3335
"""Store a trigger function in websocket scope so that ReactPy-Django's hooks can command a session synchronization."""
34-
scope["reactpy-synchronize-session"] = synchronize_session
36+
scope.setdefault("reactpy", {})
37+
scope["reactpy"]["synchronize_session"] = synchronize_session
3538
print("configure_asgi_scope")
3639

3740
@hooks.use_effect(dependencies=[switch_sessions])
@@ -49,16 +52,23 @@ async def synchronize_session():
4952
"""Entrypoint where the server will command the client to switch HTTP sessions
5053
to match the websocket session. This function is stored in the websocket scope so that
5154
ReactPy-Django's hooks can access it."""
52-
print("sync command ", uuid)
55+
print("sync command")
5356
session: SessionBase | None = scope.get("session")
5457
if not session or not session.session_key:
5558
print("sync error")
5659
return
5760

58-
await AuthSession.objects.aget_or_create(uuid=uuid, session_key=session.session_key)
61+
# Delete any sessions currently associated with this UUID
62+
with contextlib.suppress(SwitchSession.DoesNotExist):
63+
obj = await SwitchSession.objects.aget(uuid=uuid)
64+
await obj.adelete()
65+
66+
obj = await SwitchSession.objects.acreate(uuid=uuid, session_key=session.session_key)
67+
await obj.asave()
68+
5969
set_switch_sessions(True)
6070

61-
async def synchronize_sessions_callback(status_code: int, response: str):
71+
async def synchronize_session_callback(status_code: int, response: str):
6272
"""This callback acts as a communication bridge between the client and server, notifying the server
6373
of the client's response to the session switch command."""
6474
print("callback")
@@ -72,13 +82,13 @@ async def synchronize_sessions_callback(status_code: int, response: str):
7282
# If a session cookie was generated, send it to the client
7383
print("render")
7484
if switch_sessions:
75-
print("switching to ", uuid)
85+
print("Rendering HTTP request component with UUID ", uuid)
7686
return HttpRequest(
7787
{
7888
"method": "GET",
7989
"url": reverse("reactpy:switch_session", args=[uuid]),
8090
"body": None,
81-
"callback": synchronize_sessions_callback,
91+
"callback": synchronize_session_callback,
8292
},
8393
)
8494
return None

src/reactpy_django/hooks.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,7 @@ async def login(user: AbstractUser):
427427
await channels_auth.login(scope, user, backend=config.REACTPY_AUTH_BACKEND)
428428
session_save_func = getattr(scope["session"], "asave", scope["session"].save)
429429
await ensure_async(session_save_func)()
430-
431-
# TODO: Pick a different method of triggering a login action
432-
scope["reactpy_login"] = True
430+
await scope["reactpy"]["synchronize_session"]()
433431

434432
async def logout():
435433
await channels_auth.logout(scope)

src/reactpy_django/http/views.py

+24-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
from urllib.parse import parse_qs
33

4-
from django.contrib.auth.models import AnonymousUser
54
from django.core.exceptions import SuspiciousOperation
65
from django.http import FileResponse, HttpRequest, HttpResponse, HttpResponseNotFound
76
from reactpy.config import REACTPY_WEB_MODULES_DIR
@@ -48,35 +47,38 @@ async def view_to_iframe(request: HttpRequest, dotted_path: str) -> HttpResponse
4847
async def switch_session(request: HttpRequest, uuid: str) -> HttpResponse:
4948
"""Switches the client's active session.
5049
51-
Django's authentication design requires HTTP cookies to persist login via cookies.
50+
This view exists because ReactPy is rendered via WebSockets, and browsers do not
51+
allow active WebSocket connections to modify HTTP cookies. Django's authentication
52+
design requires HTTP cookies to persist state changes.
53+
"""
54+
from reactpy_django.models import SwitchSession
5255

53-
This is problematic since ReactPy is rendered via WebSockets, and browsers do not
54-
allow active WebSocket connections to modify HTTP cookies, which necessitates this
55-
view to exist."""
56-
from reactpy_django.models import AuthSession
56+
# Find out what session the client wants to switch
57+
data = await SwitchSession.objects.aget(uuid=uuid)
5758

58-
# TODO: Maybe just relogin the user instead of switching sessions?
59-
60-
# Find out what session we're switching to
61-
auth_session = await AuthSession.objects.aget(uuid=uuid)
62-
63-
# Validate the session
64-
if auth_session.expired:
59+
# CHECK: Session has expired?
60+
if data.expired:
6561
msg = "Session expired."
62+
await data.adelete()
6663
raise SuspiciousOperation(msg)
67-
if not request.session.exists(auth_session.session_key):
68-
msg = "Session does not exist."
64+
65+
# CHECK: Session does not exist?
66+
exists_method = getattr(request.session, "aexists", request.session.exists)
67+
if not await ensure_async(exists_method)(data.session_key):
68+
msg = "Attempting to switch to a session that does not exist."
6969
raise SuspiciousOperation(msg)
7070

71-
# Delete the existing session
72-
flush_method = getattr(request.session, "aflush", request.session.flush)
73-
await ensure_async(flush_method)()
74-
request.user = AnonymousUser()
71+
# CHECK: Client already using the correct session?
72+
if request.session.session_key == data.session_key:
73+
await data.adelete()
74+
return HttpResponse(status=204)
7575

7676
# Switch the client's session
77-
request.session = type(request.session)(auth_session.session_key)
77+
request.session = type(request.session)(session_key=data.session_key)
7878
load_method = getattr(request.session, "aload", request.session.load)
7979
await ensure_async(load_method)()
80-
await auth_session.adelete()
81-
80+
request.session.modified = True
81+
save_method = getattr(request.session, "asave", request.session.save)
82+
await ensure_async(save_method)()
83+
await data.adelete()
8284
return HttpResponse(status=204)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.1.4 on 2024-12-24 22:54
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('reactpy_django', '0007_authsession'),
10+
]
11+
12+
operations = [
13+
migrations.RenameModel(
14+
old_name='AuthSession',
15+
new_name='SwitchSession',
16+
),
17+
]

src/reactpy_django/models.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class ComponentSession(models.Model):
2020
last_accessed = models.DateTimeField(auto_now=True)
2121

2222

23-
class AuthSession(models.Model):
24-
"""A model for storing Django authentication sessions, tied to a UUID.
23+
class SwitchSession(models.Model):
24+
"""A model for stores any relevant data needed to force Django's HTTP session to
25+
match the websocket session.
2526
26-
This is used to switch Django's HTTP session to match the websocket session."""
27+
This data is tied to an arbitrary UUID for security (obfuscation) purposes.
28+
29+
Source code must be written to respect the expiration property of this model."""
2730

2831
# TODO: Add cleanup task for this.
2932
uuid = models.UUIDField(primary_key=True, editable=False, unique=True)

0 commit comments

Comments
 (0)