Skip to content

Commit a884af3

Browse files
authored
Add REACTPY_AUTH_BACKEND setting (#151)
- Added `REACTPY_AUTH_BACKEND` setting to allow for custom authentication backends. - Refactor the websocket consumer so that using `SessionMiddlewareStack` and/or `AuthMiddlewareStack` is now optional. - Make Django a mandatory project dependency, since we need Django 4.1+
1 parent f3e73aa commit a884af3

File tree

11 files changed

+99
-17
lines changed

11 files changed

+99
-17
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Using the following categories, list your changes in this order:
3737
### Added
3838

3939
- Added warning if poor system/cache/database performance is detected.
40+
- Added `REACTPY_AUTH_BACKEND` setting to allow for custom authentication backends.
41+
42+
### Changed
43+
44+
- Using `SessionMiddlewareStack` is now optional.
45+
- Using `AuthMiddlewareStack` is now optional.
4046

4147
## [3.1.0] - 2023-05-06
4248

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Broken load order, only used for linting
2+
from channels.routing import ProtocolTypeRouter, URLRouter
3+
4+
from reactpy_django import REACTPY_WEBSOCKET_PATH
5+
6+
7+
django_asgi_app = ""
8+
9+
10+
# start
11+
from channels.auth import AuthMiddlewareStack # noqa: E402
12+
from channels.sessions import SessionMiddlewareStack # noqa: E402
13+
14+
15+
application = ProtocolTypeRouter(
16+
{
17+
"http": django_asgi_app,
18+
"websocket": SessionMiddlewareStack(
19+
AuthMiddlewareStack(
20+
URLRouter(
21+
[REACTPY_WEBSOCKET_PATH],
22+
)
23+
)
24+
),
25+
}
26+
)

docs/python/configure-asgi.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
django_asgi_app = get_asgi_application()
1111

1212

13-
from channels.auth import AuthMiddlewareStack # noqa: E402
1413
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
15-
from channels.sessions import SessionMiddlewareStack # noqa: E402
1614

1715
from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402
1816

1917

2018
application = ProtocolTypeRouter(
2119
{
2220
"http": django_asgi_app,
23-
"websocket": SessionMiddlewareStack(
24-
AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_PATH]))
25-
),
21+
"websocket": URLRouter([REACTPY_WEBSOCKET_PATH]),
2622
}
2723
)

docs/python/settings.py

+7
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@
1313

1414
# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function, or `None`
1515
REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor"
16+
17+
# Dotted path to the Django authentication backend to use for ReactPy components
18+
# This is only needed if:
19+
# 1. You are using `AuthMiddlewareStack` and...
20+
# 2. You are using Django's `AUTHENTICATION_BACKENDS` settings and...
21+
# 3. Your Django user model does not define a `backend` attribute
22+
REACTPY_AUTH_BACKEND = None

docs/src/features/decorators.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses).
1010

11-
By default, this decorator checks if the user is logged in, and his/her account has not been deactivated.
11+
By default, this decorator checks if the user is logged in and not deactivated (`is_active`).
1212

1313
This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser).
1414

docs/src/get-started/installation.md

+21-3
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,25 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt
2828

2929
ReactPy-Django requires ASGI Websockets from [Django Channels](https://github.com/django/channels).
3030

31-
If you have not enabled ASGI on your **Django project** yet, you will need to install `channels[daphne]`, add `daphne` to `INSTALLED_APPS`, then set your `ASGI_APPLICATION` variable.
31+
If you have not enabled ASGI on your **Django project** yet, you will need to
3232

33-
Read the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info.
33+
1. Install `channels[daphne]`
34+
2. Add `daphne` to `INSTALLED_APPS`
35+
3. Set your `ASGI_APPLICATION` variable.
3436

3537
=== "settings.py"
3638

3739
```python
3840
{% include "../../python/configure-channels.py" %}
3941
```
4042

43+
Consider reading the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info.
44+
4145
??? note "Configure ReactPy settings (Optional)"
4246

4347
Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy.
4448

45-
```python
49+
```python linenums="0"
4650
{% include "../../python/settings.py" %}
4751
```
4852

@@ -66,6 +70,20 @@ Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`.
6670
{% include "../../python/configure-asgi.py" %}
6771
```
6872

73+
??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)"
74+
75+
There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to:
76+
77+
1. Access the `User` that is currently logged in
78+
2. Login or logout the current `User`
79+
3. Access Django's `Sesssion` object
80+
81+
In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`.
82+
83+
```python linenums="0"
84+
{% include "../../python/configure-asgi-middleware.py" start="# start" %}
85+
```
86+
6987
??? question "Where is my `asgi.py`?"
7088

7189
If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html).

requirements/pkg-deps.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
channels >=4.0.0
2+
django >=4.1.0
23
reactpy >=1.0.0, <1.1.0
34
aiofile >=3.0
45
dill >=0.3.5

requirements/test-env.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
django
21
playwright
32
twisted
43
channels[daphne]>=4.0.0

src/reactpy_django/config.py

+5
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@
5050
)
5151
)
5252
)
53+
REACTPY_AUTH_BACKEND: str | None = getattr(
54+
settings,
55+
"REACTPY_AUTH_BACKEND",
56+
None,
57+
)

src/reactpy_django/websocket/consumer.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,55 @@ class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
2727
"""Communicates with the browser to perform actions on-demand."""
2828

2929
async def connect(self) -> None:
30-
from django.contrib.auth.models import AbstractBaseUser
31-
30+
"""The browser has connected."""
3231
await super().connect()
3332

34-
user: AbstractBaseUser = self.scope.get("user")
33+
# Authenticate the user, if possible
34+
from reactpy_django.config import REACTPY_AUTH_BACKEND
35+
36+
user: Any = self.scope.get("user")
3537
if user and user.is_authenticated:
3638
try:
37-
await login(self.scope, user)
38-
await database_sync_to_async(self.scope["session"].save)()
39+
await login(self.scope, user, backend=REACTPY_AUTH_BACKEND)
3940
except Exception:
4041
_logger.exception("ReactPy websocket authentication has failed!")
4142
elif user is None:
42-
_logger.warning("ReactPy websocket is missing AuthMiddlewareStack!")
43+
_logger.debug(
44+
"ReactPy websocket is missing AuthMiddlewareStack! "
45+
"Users will not be accessible within `use_scope` or `use_websocket`!"
46+
)
47+
48+
# Save the session, if possible
49+
if self.scope.get("session"):
50+
try:
51+
await database_sync_to_async(self.scope["session"].save)()
52+
except Exception:
53+
_logger.exception("ReactPy has failed to save scope['session']!")
54+
else:
55+
_logger.debug(
56+
"ReactPy websocket is missing SessionMiddlewareStack! "
57+
"Sessions will not be accessible within `use_scope` or `use_websocket`!"
58+
)
4359

60+
# Start allowing component renders
4461
self._reactpy_dispatcher_future = asyncio.ensure_future(
4562
self._run_dispatch_loop()
4663
)
4764

4865
async def disconnect(self, code: int) -> None:
66+
"""The browser has disconnected."""
4967
if self._reactpy_dispatcher_future.done():
5068
await self._reactpy_dispatcher_future
5169
else:
5270
self._reactpy_dispatcher_future.cancel()
5371
await super().disconnect(code)
5472

5573
async def receive_json(self, content: Any, **_) -> None:
74+
"""Receive a message from the browser. Typically messages are event signals."""
5675
await self._reactpy_recv_queue.put(content)
5776

5877
async def _run_dispatch_loop(self):
78+
"""Runs the main loop that performs component rendering tasks."""
5979
from reactpy_django import models
6080
from reactpy_django.config import (
6181
REACTPY_DATABASE,
@@ -130,7 +150,7 @@ async def _run_dispatch_loop(self):
130150
)
131151
return
132152

133-
# Begin serving the ReactPy component
153+
# Start the ReactPy component rendering loop
134154
try:
135155
await serve_layout(
136156
Layout(ConnectionContext(component_instance, value=connection)),

tests/test_app/settings.py

+4
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,7 @@
170170
},
171171
},
172172
}
173+
174+
175+
# ReactPy Django Settings
176+
REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend"

0 commit comments

Comments
 (0)