Skip to content

Add REACTPY_AUTH_BACKEND setting #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Using the following categories, list your changes in this order:
### Added

- Added warning if poor system/cache/database performance is detected.
- Added `REACTPY_AUTH_BACKEND` setting to allow for custom authentication backends.

### Changed

- Using `SessionMiddlewareStack` is now optional.
- Using `AuthMiddlewareStack` is now optional.

## [3.1.0] - 2023-05-06

Expand Down
26 changes: 26 additions & 0 deletions docs/python/configure-asgi-middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Broken load order, only used for linting
from channels.routing import ProtocolTypeRouter, URLRouter

from reactpy_django import REACTPY_WEBSOCKET_PATH


django_asgi_app = ""


# start
from channels.auth import AuthMiddlewareStack # noqa: E402
from channels.sessions import SessionMiddlewareStack # noqa: E402


application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(
URLRouter(
[REACTPY_WEBSOCKET_PATH],
)
)
),
}
)
6 changes: 1 addition & 5 deletions docs/python/configure-asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@
django_asgi_app = get_asgi_application()


from channels.auth import AuthMiddlewareStack # noqa: E402
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
from channels.sessions import SessionMiddlewareStack # noqa: E402

from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402


application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_PATH]))
),
"websocket": URLRouter([REACTPY_WEBSOCKET_PATH]),
}
)
7 changes: 7 additions & 0 deletions docs/python/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@

# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function, or `None`
REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor"

# Dotted path to the Django authentication backend to use for ReactPy components
# This is only needed if:
# 1. You are using `AuthMiddlewareStack` and...
# 2. You are using Django's `AUTHENTICATION_BACKENDS` settings and...
# 3. Your Django user model does not define a `backend` attribute
REACTPY_AUTH_BACKEND = None
2 changes: 1 addition & 1 deletion docs/src/features/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

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

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

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).

Expand Down
24 changes: 21 additions & 3 deletions docs/src/get-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt

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

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.
If you have not enabled ASGI on your **Django project** yet, you will need to

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

=== "settings.py"

```python
{% include "../../python/configure-channels.py" %}
```

Consider reading the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info.

??? note "Configure ReactPy settings (Optional)"

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

```python
```python linenums="0"
{% include "../../python/settings.py" %}
```

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

??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)"

There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to:

1. Access the `User` that is currently logged in
2. Login or logout the current `User`
3. Access Django's `Sesssion` object

In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`.

```python linenums="0"
{% include "../../python/configure-asgi-middleware.py" start="# start" %}
```

??? question "Where is my `asgi.py`?"

If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html).
Expand Down
1 change: 1 addition & 0 deletions requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
channels >=4.0.0
django >=4.1.0
reactpy >=1.0.0, <1.1.0
aiofile >=3.0
dill >=0.3.5
Expand Down
1 change: 0 additions & 1 deletion requirements/test-env.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
django
playwright
twisted
channels[daphne]>=4.0.0
Expand Down
5 changes: 5 additions & 0 deletions src/reactpy_django/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@
)
)
)
REACTPY_AUTH_BACKEND: str | None = getattr(
settings,
"REACTPY_AUTH_BACKEND",
None,
)
34 changes: 27 additions & 7 deletions src/reactpy_django/websocket/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,55 @@ class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""

async def connect(self) -> None:
from django.contrib.auth.models import AbstractBaseUser

"""The browser has connected."""
await super().connect()

user: AbstractBaseUser = self.scope.get("user")
# Authenticate the user, if possible
from reactpy_django.config import REACTPY_AUTH_BACKEND

user: Any = self.scope.get("user")
if user and user.is_authenticated:
try:
await login(self.scope, user)
await database_sync_to_async(self.scope["session"].save)()
await login(self.scope, user, backend=REACTPY_AUTH_BACKEND)
except Exception:
_logger.exception("ReactPy websocket authentication has failed!")
elif user is None:
_logger.warning("ReactPy websocket is missing AuthMiddlewareStack!")
_logger.debug(
"ReactPy websocket is missing AuthMiddlewareStack! "
"Users will not be accessible within `use_scope` or `use_websocket`!"
)

# Save the session, if possible
if self.scope.get("session"):
try:
await database_sync_to_async(self.scope["session"].save)()
except Exception:
_logger.exception("ReactPy has failed to save scope['session']!")
else:
_logger.debug(
"ReactPy websocket is missing SessionMiddlewareStack! "
"Sessions will not be accessible within `use_scope` or `use_websocket`!"
)

# Start allowing component renders
self._reactpy_dispatcher_future = asyncio.ensure_future(
self._run_dispatch_loop()
)

async def disconnect(self, code: int) -> None:
"""The browser has disconnected."""
if self._reactpy_dispatcher_future.done():
await self._reactpy_dispatcher_future
else:
self._reactpy_dispatcher_future.cancel()
await super().disconnect(code)

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

async def _run_dispatch_loop(self):
"""Runs the main loop that performs component rendering tasks."""
from reactpy_django import models
from reactpy_django.config import (
REACTPY_DATABASE,
Expand Down Expand Up @@ -130,7 +150,7 @@ async def _run_dispatch_loop(self):
)
return

# Begin serving the ReactPy component
# Start the ReactPy component rendering loop
try:
await serve_layout(
Layout(ConnectionContext(component_instance, value=connection)),
Expand Down
4 changes: 4 additions & 0 deletions tests/test_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,7 @@
},
},
}


# ReactPy Django Settings
REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend"