Skip to content

Commit 275ab61

Browse files
committed
rename auth_sync to auth_token
1 parent 0207c2f commit 275ab61

File tree

8 files changed

+83
-60
lines changed

8 files changed

+83
-60
lines changed

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ runserver = [
160160
makemigrations = ["cd tests && python manage.py makemigrations"]
161161
clean = ["cd tests && python manage.py clean_reactpy -v 3"]
162162
clean_sessions = ["cd tests && python manage.py clean_reactpy --sessions -v 3"]
163-
clean_auth_sync = [
164-
"cd tests && python manage.py clean_reactpy --auth-sync -v 3",
163+
clean_auth_tokens = [
164+
"cd tests && python manage.py clean_reactpy --auth-tokens -v 3",
165165
]
166166
clean_user_data = [
167167
"cd tests && python manage.py clean_reactpy --user-data -v 3",

src/reactpy_django/auth/components.py

+17-18
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from uuid import uuid4
88

99
from django.urls import reverse
10-
from reactpy import component, hooks, html
10+
from reactpy import component, hooks
1111

1212
from reactpy_django.javascript_components import HttpRequest
13-
from reactpy_django.models import SynchronizeSession
13+
from reactpy_django.models import AuthToken
1414

1515
if TYPE_CHECKING:
1616
from django.contrib.sessions.backends.base import SessionBase
@@ -39,13 +39,13 @@ async def rerender():
3939
@component
4040
def session_manager():
4141
"""This component can force the client (browser) to switch HTTP sessions,
42-
making it match the websocket session.
42+
making it match the websocket session, by using a authentication token.
4343
4444
Used to force persistent authentication between Django's websocket and HTTP stack."""
4545
from reactpy_django import config
4646

4747
synchronize_requested, set_synchronize_requested = hooks.use_state(False)
48-
uuid = hooks.use_ref("")
48+
token = hooks.use_ref("")
4949
scope = hooks.use_connection().scope
5050

5151
@hooks.use_effect(dependencies=[])
@@ -61,33 +61,32 @@ async def synchronize_session_watchdog():
6161
This effect will automatically be cancelled if the session is successfully
6262
switched (via effect dependencies)."""
6363
if synchronize_requested:
64-
await asyncio.sleep(config.REACTPY_AUTH_SYNC_TIMEOUT + 0.1)
64+
await asyncio.sleep(config.REACTPY_AUTH_TOKEN_TIMEOUT + 0.1)
6565
await asyncio.to_thread(
6666
_logger.warning,
67-
f"Client did not switch sessions within {config.REACTPY_AUTH_SYNC_TIMEOUT} (REACTPY_AUTH_SYNC_TIMEOUT) seconds.",
67+
f"Client did not switch sessions within {config.REACTPY_AUTH_TOKEN_TIMEOUT} (REACTPY_AUTH_TOKEN_TIMEOUT) seconds.",
6868
)
6969
set_synchronize_requested(False)
7070

7171
async def synchronize_session():
72-
"""Event that can command the client to switch HTTP sessions (to match the websocket sessions)."""
72+
"""Event that can command the client to switch HTTP sessions (to match the websocket session)."""
7373
session: SessionBase | None = scope.get("session")
7474
if not session or not session.session_key:
7575
return
7676

77-
# Delete any sessions currently associated with the previous UUID.
78-
# This exists to fix scenarios where...
79-
# 1) Login is called multiple times before the first one is completed.
80-
# 2) Login was called, but the server failed to respond to the HTTP request.
81-
if uuid.current:
82-
with contextlib.suppress(SynchronizeSession.DoesNotExist):
83-
obj = await SynchronizeSession.objects.aget(uuid=uuid.current)
77+
# Delete previous token to resolve race conditions where...
78+
# 1. Login was called multiple times before the first one is completed.
79+
# 2. Login was called, but the server failed to respond to the HTTP request.
80+
if token.current:
81+
with contextlib.suppress(AuthToken.DoesNotExist):
82+
obj = await AuthToken.objects.aget(value=token.current)
8483
await obj.adelete()
8584

86-
# Create a fresh UUID
87-
uuid.set_current(str(uuid4()))
85+
# Create a fresh token
86+
token.set_current(str(uuid4()))
8887

8988
# Begin the process of synchronizing HTTP and websocket sessions
90-
obj = await SynchronizeSession.objects.acreate(uuid=uuid.current, session_key=session.session_key)
89+
obj = await AuthToken.objects.acreate(value=token.current, session_key=session.session_key)
9190
await obj.asave()
9291
set_synchronize_requested(True)
9392

@@ -107,7 +106,7 @@ async def synchronize_session_callback(status_code: int, response: str):
107106
return HttpRequest(
108107
{
109108
"method": "GET",
110-
"url": reverse("reactpy:session_manager", args=[uuid.current]),
109+
"url": reverse("reactpy:session_manager", args=[token.current]),
111110
"body": None,
112111
"callback": synchronize_session_callback,
113112
},

src/reactpy_django/config.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
"REACTPY_SESSION_MAX_AGE",
4040
259200, # Default to 3 days
4141
)
42-
REACTPY_AUTH_SYNC_TIMEOUT: int = getattr(
42+
REACTPY_AUTH_TOKEN_TIMEOUT: int = getattr(
4343
settings,
44-
"REACTPY_AUTH_SYNC_TIMEOUT",
44+
"REACTPY_AUTH_TOKEN_TIMEOUT",
4545
30, # Default to 30 seconds
4646
)
4747
REACTPY_CACHE: str = getattr(
@@ -126,9 +126,9 @@
126126
"REACTPY_CLEAN_SESSIONS",
127127
True,
128128
)
129-
REACTPY_CLEAN_AUTH_SYNC: bool = getattr(
129+
REACTPY_CLEAN_AUTH_TOKENS: bool = getattr(
130130
settings,
131-
"REACTPY_CLEAN_AUTH_SYNC",
131+
"REACTPY_CLEAN_AUTH_TOKENS",
132132
True,
133133
)
134134
REACTPY_CLEAN_USER_DATA: bool = getattr(

src/reactpy_django/http/views.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,37 @@ async def session_manager(request: HttpRequest, uuid: str) -> HttpResponse:
4848
"""Switches the client's active session to match ReactPy.
4949
5050
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
51+
allow active WebSocket connections to modify cookies. Django's authentication
5252
design requires HTTP cookies to persist state changes.
5353
"""
54-
from reactpy_django.models import SynchronizeSession
54+
from reactpy_django.models import AuthToken
5555

56-
# Find out what session the client wants to switch
57-
data = await SynchronizeSession.objects.aget(uuid=uuid)
56+
# Find out what session the client wants to switch to
57+
token = await AuthToken.objects.aget(value=uuid)
5858

59-
# CHECK: Session has expired?
60-
if data.expired:
59+
# CHECK: Token has expired?
60+
if token.expired:
6161
msg = "Session expired."
62-
await data.adelete()
62+
await token.adelete()
6363
raise SuspiciousOperation(msg)
6464

65-
# CHECK: Session does not exist?
65+
# CHECK: Token does not exist?
6666
exists_method = getattr(request.session, "aexists", request.session.exists)
67-
if not await ensure_async(exists_method)(data.session_key):
67+
if not await ensure_async(exists_method)(token.session_key):
6868
msg = "Attempting to switch to a session that does not exist."
6969
raise SuspiciousOperation(msg)
7070

71-
# CHECK: Client already using the correct session?
72-
if request.session.session_key == data.session_key:
73-
await data.adelete()
71+
# CHECK: Client already using the correct session key?
72+
if request.session.session_key == token.session_key:
73+
await token.adelete()
7474
return HttpResponse(status=204)
7575

7676
# Switch the client's session
77-
request.session = type(request.session)(session_key=data.session_key)
77+
request.session = type(request.session)(session_key=token.session_key)
7878
load_method = getattr(request.session, "aload", request.session.load)
7979
await ensure_async(load_method)()
8080
request.session.modified = True
8181
save_method = getattr(request.session, "asave", request.session.save)
8282
await ensure_async(save_method)()
83-
await data.adelete()
83+
await token.adelete()
8484
return HttpResponse(status=204)

src/reactpy_django/management/commands/clean_reactpy.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def handle(self, *_args, **options):
1212
from reactpy_django.tasks import CleaningArgs, clean
1313

1414
verbosity = options.pop("verbosity", 1)
15-
valid_args: set[CleaningArgs] = {"all", "sessions", "auth_sync", "user_data"}
15+
valid_args: set[CleaningArgs] = {"all", "sessions", "auth_tokens", "user_data"}
1616
cleaning_args: set[CleaningArgs] = {arg for arg in options if arg in valid_args and options[arg]} or {"all"}
1717

1818
clean(*cleaning_args, immediate=True, verbosity=verbosity)
@@ -32,7 +32,7 @@ def add_arguments(self, parser):
3232
help="Clean user data. This value can be combined with other cleaning options.",
3333
)
3434
parser.add_argument(
35-
"--auth-sync",
35+
"--auth-tokens",
3636
action="store_true",
37-
help="Clean authentication synchronizer data. This value can be combined with other cleaning options.",
37+
help="Clean authentication tokens. This value can be combined with other cleaning options.",
3838
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.1.4 on 2024-12-25 05:52
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('reactpy_django', '0009_rename_switchsession_synchronizesession'),
10+
]
11+
12+
operations = [
13+
migrations.RenameModel(
14+
old_name='SynchronizeSession',
15+
new_name='AuthToken',
16+
),
17+
migrations.RenameField(
18+
model_name='authtoken',
19+
old_name='uuid',
20+
new_name='value',
21+
),
22+
]

src/reactpy_django/models.py

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

2222

23-
class SynchronizeSession(models.Model):
23+
class AuthToken(models.Model):
2424
"""A model that contains any relevant data needed to force Django's HTTP session to
2525
match the websocket session.
2626
27-
This data is tied to an arbitrary UUID for security (obfuscation) purposes.
27+
The session key is tied to an arbitrary UUID token for security (obfuscation) purposes.
2828
2929
Source code must be written to respect the expiration property of this model."""
3030

31-
uuid = models.UUIDField(primary_key=True, editable=False, unique=True)
31+
value = models.UUIDField(primary_key=True, editable=False, unique=True)
3232
session_key = models.CharField(max_length=40, editable=False)
3333
created_at = models.DateTimeField(auto_now_add=True, editable=False)
3434

3535
@property
3636
def expired(self) -> bool:
3737
"""Check the client has exceeded the max timeout."""
38-
from reactpy_django.config import REACTPY_AUTH_SYNC_TIMEOUT
38+
from reactpy_django.config import REACTPY_AUTH_TOKEN_TIMEOUT
3939

40-
return self.created_at < (timezone.now() - timedelta(seconds=REACTPY_AUTH_SYNC_TIMEOUT))
40+
return self.created_at < (timezone.now() - timedelta(seconds=REACTPY_AUTH_TOKEN_TIMEOUT))
4141

4242

4343
class Config(models.Model):

src/reactpy_django/tasks.py

+16-14
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
from reactpy_django.models import Config
1414

1515
CLEAN_NEEDED_BY: datetime = datetime(year=1, month=1, day=1, tzinfo=timezone.now().tzinfo)
16-
CleaningArgs: TypeAlias = Literal["all", "sessions", "auth_sync", "user_data"]
16+
CleaningArgs: TypeAlias = Literal["all", "sessions", "auth_tokens", "user_data"]
1717

1818

1919
def clean(*args: CleaningArgs, immediate: bool = False, verbosity: int = 1):
2020
from reactpy_django.config import (
21-
REACTPY_CLEAN_AUTH_SYNC,
21+
REACTPY_CLEAN_AUTH_TOKENS,
2222
REACTPY_CLEAN_SESSIONS,
2323
REACTPY_CLEAN_USER_DATA,
2424
)
@@ -28,19 +28,21 @@ def clean(*args: CleaningArgs, immediate: bool = False, verbosity: int = 1):
2828
if immediate or clean_is_needed(config):
2929
config.cleaned_at = timezone.now()
3030
config.save()
31+
32+
# If no args are provided, use the default settings.
3133
sessions = REACTPY_CLEAN_SESSIONS
32-
auth_sync = REACTPY_CLEAN_AUTH_SYNC
34+
auth_tokens = REACTPY_CLEAN_AUTH_TOKENS
3335
user_data = REACTPY_CLEAN_USER_DATA
3436

3537
if args:
3638
sessions = any(value in args for value in ("sessions", "all"))
37-
auth_sync = any(value in args for value in ("auth_sync", "all"))
39+
auth_tokens = any(value in args for value in ("auth_tokens", "all"))
3840
user_data = any(value in args for value in ("user_data", "all"))
3941

4042
if sessions:
4143
clean_component_sessions(verbosity)
42-
if auth_sync:
43-
clean_auth_synchronizer(verbosity)
44+
if auth_tokens:
45+
clean_auth_tokens(verbosity)
4446
if user_data:
4547
clean_user_data(verbosity)
4648

@@ -69,23 +71,23 @@ def clean_component_sessions(verbosity: int = 1):
6971
inspect_clean_duration(start_time, "component sessions", verbosity)
7072

7173

72-
def clean_auth_synchronizer(verbosity: int = 1):
73-
from reactpy_django.config import DJANGO_DEBUG, REACTPY_AUTH_SYNC_TIMEOUT
74-
from reactpy_django.models import SynchronizeSession
74+
def clean_auth_tokens(verbosity: int = 1):
75+
from reactpy_django.config import DJANGO_DEBUG, REACTPY_AUTH_TOKEN_TIMEOUT
76+
from reactpy_django.models import AuthToken
7577

7678
if verbosity >= 2:
77-
_logger.info("Cleaning ReactPy auth sync data...")
79+
_logger.info("Cleaning ReactPy auth tokens...")
7880
start_time = timezone.now()
79-
expiration_date = timezone.now() - timedelta(seconds=REACTPY_AUTH_SYNC_TIMEOUT)
80-
synchronizer_objects = SynchronizeSession.objects.filter(created_at__lte=expiration_date)
81+
expiration_date = timezone.now() - timedelta(seconds=REACTPY_AUTH_TOKEN_TIMEOUT)
82+
synchronizer_objects = AuthToken.objects.filter(created_at__lte=expiration_date)
8183

8284
if verbosity >= 2:
83-
_logger.info("Deleting %d expired auth sync objects...", synchronizer_objects.count())
85+
_logger.info("Deleting %d expired auth token objects...", synchronizer_objects.count())
8486

8587
synchronizer_objects.delete()
8688

8789
if DJANGO_DEBUG or verbosity >= 2:
88-
inspect_clean_duration(start_time, "auth sync", verbosity)
90+
inspect_clean_duration(start_time, "auth tokens", verbosity)
8991

9092

9193
def clean_user_data(verbosity: int = 1):

0 commit comments

Comments
 (0)