Skip to content

Commit 833d335

Browse files
committed
Auto-clean command for auth sync stuff
1 parent 7202e57 commit 833d335

File tree

6 files changed

+67
-32
lines changed

6 files changed

+67
-32
lines changed

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ extra-dependencies = [
148148
"twisted",
149149
"servestatic",
150150
"django-bootstrap5",
151+
"decorator",
152+
"playwright",
151153
]
152154

153155
[tool.hatch.envs.django.scripts]
@@ -156,6 +158,14 @@ runserver = [
156158
"cd tests && python manage.py runserver",
157159
]
158160
makemigrations = ["cd tests && python manage.py makemigrations"]
161+
clean = ["cd tests && python manage.py clean_reactpy -v 3"]
162+
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",
165+
]
166+
clean_user_data = [
167+
"cd tests && python manage.py clean_reactpy --user-data -v 3",
168+
]
159169

160170
#######################################
161171
# >>> Hatch Documentation Scripts <<< #

src/reactpy_django/auth/components.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ def setup_asgi_scope():
4141

4242
@hooks.use_effect(dependencies=[synchronize_requested])
4343
async def synchronize_session_watchdog():
44-
"""This effect will automatically be cancelled if the session is successfully
44+
"""Detected if the client has taken too long to request a session synchronization.
45+
46+
This effect will automatically be cancelled if the session is successfully
4547
switched (via effect dependencies)."""
4648
if synchronize_requested:
47-
await asyncio.sleep(config.REACTPY_AUTH_TIMEOUT + 0.1)
49+
await asyncio.sleep(config.REACTPY_AUTH_SYNC_TIMEOUT + 0.1)
4850
await asyncio.to_thread(
4951
_logger.warning,
50-
f"Client did not switch sessions within {config.REACTPY_AUTH_TIMEOUT} (REACTPY_AUTH_TIMEOUT) seconds.",
52+
f"Client did not switch sessions within {config.REACTPY_AUTH_SYNC_TIMEOUT} (REACTPY_AUTH_SYNC_TIMEOUT) seconds.",
5153
)
5254
set_synchronize_requested(False)
5355

src/reactpy_django/config.py

Lines changed: 7 additions & 2 deletions
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_TIMEOUT: int = getattr(
42+
REACTPY_AUTH_SYNC_TIMEOUT: int = getattr(
4343
settings,
44-
"REACTPY_AUTH_TIMEOUT",
44+
"REACTPY_AUTH_SYNC_TIMEOUT",
4545
30, # Default to 30 seconds
4646
)
4747
REACTPY_CACHE: str = getattr(
@@ -126,6 +126,11 @@
126126
"REACTPY_CLEAN_SESSIONS",
127127
True,
128128
)
129+
REACTPY_CLEAN_AUTH_SYNC: bool = getattr(
130+
settings,
131+
"REACTPY_CLEAN_AUTH_SYNC",
132+
True,
133+
)
129134
REACTPY_CLEAN_USER_DATA: bool = getattr(
130135
settings,
131136
"REACTPY_CLEAN_USER_DATA",
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from logging import getLogger
2-
from typing import Literal
32

43
from django.core.management.base import BaseCommand
54

@@ -9,18 +8,12 @@
98
class Command(BaseCommand):
109
help = "Manually clean ReactPy data. When using this command without args, it will perform all cleaning operations."
1110

12-
def handle(self, **options):
13-
from reactpy_django.tasks import clean
11+
def handle(self, *_args, **options):
12+
from reactpy_django.tasks import CleaningArgs, clean
1413

15-
verbosity = options.get("verbosity", 1)
16-
17-
cleaning_args: set[Literal["all", "sessions", "user_data"]] = set()
18-
if options.get("sessions"):
19-
cleaning_args.add("sessions")
20-
if options.get("user_data"):
21-
cleaning_args.add("user_data")
22-
if not cleaning_args:
23-
cleaning_args = {"all"}
14+
verbosity = options.pop("verbosity", 1)
15+
valid_args: set[CleaningArgs] = {"all", "sessions", "auth_sync", "user_data"}
16+
cleaning_args: set[CleaningArgs] = {arg for arg in options if arg in valid_args and options[arg]} or {"all"}
2417

2518
clean(*cleaning_args, immediate=True, verbosity=verbosity)
2619

@@ -31,10 +24,15 @@ def add_arguments(self, parser):
3124
parser.add_argument(
3225
"--sessions",
3326
action="store_true",
34-
help="Clean session data. This value can be combined with other cleaning options.",
27+
help="Clean component session data. This value can be combined with other cleaning options.",
3528
)
3629
parser.add_argument(
3730
"--user-data",
3831
action="store_true",
3932
help="Clean user data. This value can be combined with other cleaning options.",
4033
)
34+
parser.add_argument(
35+
"--auth-sync",
36+
action="store_true",
37+
help="Clean authentication synchronizer data. This value can be combined with other cleaning options.",
38+
)

src/reactpy_django/models.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ class SynchronizeSession(models.Model):
2828
2929
Source code must be written to respect the expiration property of this model."""
3030

31-
# TODO: Add cleanup task for this.
3231
uuid = models.UUIDField(primary_key=True, editable=False, unique=True)
3332
session_key = models.CharField(max_length=40, editable=False)
3433
created_at = models.DateTimeField(auto_now_add=True, editable=False)
3534

3635
@property
3736
def expired(self) -> bool:
38-
"""Check if the login UUID has expired."""
39-
from reactpy_django.config import REACTPY_AUTH_TIMEOUT
37+
"""Check the client has exceeded the max timeout."""
38+
from reactpy_django.config import REACTPY_AUTH_SYNC_TIMEOUT
4039

41-
return self.created_at < (timezone.now() - timedelta(seconds=REACTPY_AUTH_TIMEOUT))
40+
return self.created_at < (timezone.now() - timedelta(seconds=REACTPY_AUTH_SYNC_TIMEOUT))
4241

4342

4443
class Config(models.Model):

src/reactpy_django/tasks.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
from datetime import datetime, timedelta
5-
from typing import TYPE_CHECKING, Literal
5+
from typing import TYPE_CHECKING, Literal, TypeAlias
66

77
from django.contrib.auth import get_user_model
88
from django.utils import timezone
@@ -13,37 +13,39 @@
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"]
1617

1718

18-
def clean(
19-
*args: Literal["all", "sessions", "user_data"],
20-
immediate: bool = False,
21-
verbosity: int = 1,
22-
):
19+
def clean(*args: CleaningArgs, immediate: bool = False, verbosity: int = 1):
2320
from reactpy_django.config import (
21+
REACTPY_CLEAN_AUTH_SYNC,
2422
REACTPY_CLEAN_SESSIONS,
2523
REACTPY_CLEAN_USER_DATA,
2624
)
2725
from reactpy_django.models import Config
2826

2927
config = Config.load()
30-
if immediate or is_clean_needed(config):
28+
if immediate or clean_is_needed(config):
3129
config.cleaned_at = timezone.now()
3230
config.save()
3331
sessions = REACTPY_CLEAN_SESSIONS
32+
auth_sync = REACTPY_CLEAN_AUTH_SYNC
3433
user_data = REACTPY_CLEAN_USER_DATA
3534

3635
if args:
3736
sessions = any(value in args for value in ("sessions", "all"))
37+
auth_sync = any(value in args for value in ("auth_sync", "all"))
3838
user_data = any(value in args for value in ("user_data", "all"))
3939

4040
if sessions:
41-
clean_sessions(verbosity)
41+
clean_component_sessions(verbosity)
42+
if auth_sync:
43+
clean_auth_synchronizer(verbosity)
4244
if user_data:
4345
clean_user_data(verbosity)
4446

4547

46-
def clean_sessions(verbosity: int = 1):
48+
def clean_component_sessions(verbosity: int = 1):
4749
"""Deletes expired component sessions from the database.
4850
As a performance optimization, this is only run once every REACTPY_SESSION_MAX_AGE seconds.
4951
"""
@@ -67,6 +69,25 @@ def clean_sessions(verbosity: int = 1):
6769
inspect_clean_duration(start_time, "component sessions", verbosity)
6870

6971

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
75+
76+
if verbosity >= 2:
77+
_logger.info("Cleaning ReactPy auth sync data...")
78+
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+
82+
if verbosity >= 2:
83+
_logger.info("Deleting %d expired auth sync objects...", synchronizer_objects.count())
84+
85+
synchronizer_objects.delete()
86+
87+
if DJANGO_DEBUG or verbosity >= 2:
88+
inspect_clean_duration(start_time, "auth sync", verbosity)
89+
90+
7091
def clean_user_data(verbosity: int = 1):
7192
"""Delete any user data that is not associated with an existing `User`.
7293
This is a safety measure to ensure that we don't have any orphaned data in the database.
@@ -101,7 +122,7 @@ def clean_user_data(verbosity: int = 1):
101122
inspect_clean_duration(start_time, "user data", verbosity)
102123

103124

104-
def is_clean_needed(config: Config | None = None) -> bool:
125+
def clean_is_needed(config: Config | None = None) -> bool:
105126
"""Check if a clean is needed. This function avoids unnecessary database reads by caching the
106127
CLEAN_NEEDED_BY date."""
107128
from reactpy_django.config import REACTPY_CLEAN_INTERVAL

0 commit comments

Comments
 (0)