Skip to content
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

Django login and logout functionality #276

Merged
merged 26 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b57da2b
Prototype using cookie setter component
Archmonger Dec 23, 2024
d4ccc5e
misc use_ref cleanup
Archmonger Dec 23, 2024
af14cf2
Prototype using HttpRequest component
Archmonger Dec 23, 2024
1104344
Add plumbing needed to signal session events
Archmonger Dec 24, 2024
1c7c46d
functional session switcher logic
Archmonger Dec 24, 2024
939f642
Allow logouts to re-render the component tree
Archmonger Dec 24, 2024
de30ede
more consistent verbiage
Archmonger Dec 25, 2024
e421cf4
Never re-use a session UUID
Archmonger Dec 25, 2024
7202e57
self review cleanup
Archmonger Dec 25, 2024
833d335
Auto-clean command for auth sync stuff
Archmonger Dec 25, 2024
0207c2f
allow for optional re-rendering on login/logout methods
Archmonger Dec 25, 2024
275ab61
rename `auth_sync` to `auth_token`
Archmonger Dec 25, 2024
56c9b3d
Fix type error
Archmonger Dec 25, 2024
7e3f489
More variable naming consistency
Archmonger Dec 25, 2024
a85d2a3
2nd self review
Archmonger Dec 25, 2024
dfab919
docs and changelog
Archmonger Dec 25, 2024
c6bd166
Add checks for new settings
Archmonger Dec 26, 2024
eb9ed59
Add tests
Archmonger Dec 26, 2024
5341851
TIMEOUT -> MAX_AGE
Archmonger Dec 27, 2024
3e9a1fb
Fix prerender `SynchronousOnlyOperation` bug
Archmonger Dec 28, 2024
8ba4777
Fix type errors
Archmonger Dec 28, 2024
3afad19
Another attempt to fix tests.
Archmonger Dec 28, 2024
fe6fe38
Attempt 3 at fixing tests
Archmonger Dec 29, 2024
91587da
Speed up tests
Archmonger Dec 29, 2024
023df87
self review
Archmonger Dec 29, 2024
623869a
Forgot to set default click delays
Archmonger Dec 29, 2024
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
17 changes: 17 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ jobs:
run: pip install --upgrade pip hatch uv
- name: Run Single DB Tests
run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_single_db -v

python-source-multi-db:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Python Dependencies
run: pip install --upgrade pip hatch uv
- name: Run Multi-DB Tests
run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_multi_db -v

Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ Don't forget to remove deprecated code on each major release!

### Added

- User login/logout features!
- `reactpy_django.hooks.use_auth` to provide **persistent** `login` and `logout` functionality to your components.
- `settings.py:REACTPY_AUTH_TOKEN_MAX_AGE` to control the maximum seconds before ReactPy's login token expires.
- `settings.py:REACTPY_CLEAN_AUTH_TOKENS` to control whether ReactPy should clean up expired authentication tokens during automatic cleanups.
- Automatically convert Django forms to ReactPy forms via the new `reactpy_django.components.django_form` component!
- The ReactPy component tree can now be forcibly re-rendered via the new `reactpy_django.hooks.use_rerender` hook.

### Changed

- Refactoring of internal code to improve maintainability. No changes to public/documented API.
- Refactoring of internal code to improve maintainability. No changes to publicly documented API.

### Fixed

- Fixed bug where pre-rendered components could generate a `SynchronousOnlyOperation` exception if they access a freshly logged out Django user object.

## [5.1.1] - 2024-12-02

Expand Down
23 changes: 23 additions & 0 deletions docs/examples/python/use_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib.auth import get_user_model
from reactpy import component, html

from reactpy_django.hooks import use_auth, use_user


@component
def my_component():
auth = use_auth()
user = use_user()

async def login_user(event):
new_user, _created = await get_user_model().objects.aget_or_create(username="ExampleUser")
await auth.login(new_user)

async def logout_user(event):
await auth.logout()

return html.div(
f"Current User: {user}",
html.button({"onClick": login_user}, "Login"),
html.button({"onClick": logout_user}, "Logout"),
)
15 changes: 15 additions & 0 deletions docs/examples/python/use_rerender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from uuid import uuid4

from reactpy import component, html

from reactpy_django.hooks import use_rerender


@component
def my_component():
rerender = use_rerender()

def on_click():
rerender()

return html.div(f"UUID: {uuid4()}", html.button({"onClick": on_click}, "Rerender"))
3 changes: 3 additions & 0 deletions docs/includes/auth-middleware-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```python linenums="0"
{% include "../examples/python/configure_asgi_middleware.py" start="# start" %}
```
4 changes: 1 addition & 3 deletions docs/src/learn/add-reactpy-to-a-django-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`

In these situations will need to ensure you are using `#!python AuthMiddlewareStack`.

```python linenums="0"
{% include "../../examples/python/configure_asgi_middleware.py" start="# start" %}
```
{% include "../../includes/auth-middleware-stack.md" %}

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

Expand Down
89 changes: 83 additions & 6 deletions docs/src/reference/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,86 @@ Mutation functions can be sync or async.

---

## User Hooks

---

### Use Auth

Provides a `#!python NamedTuple` containing `#!python async login` and `#!python async logout` functions.

This hook utilizes the Django's authentication framework in a way that provides **persistent** login.

=== "components.py"

```python
{% include "../../examples/python/use_auth.py" %}
```

??? example "See Interface"

<font size="4">**Parameters**</font>

`#!python None`

<font size="4">**Returns**</font>

| Type | Description |
| --- | --- |
| `#!python UseAuthTuple` | A named tuple containing `#!python login` and `#!python logout` async functions. |

??? warning "Extra Django configuration required"

Your ReactPy WebSocket must utilize `#!python AuthMiddlewareStack` in order to use this hook.

{% include "../../includes/auth-middleware-stack.md" %}

??? question "Why use this instead of `#!python channels.auth.login`?"

The `#!python channels.auth.*` functions cannot trigger re-renders of your ReactPy components. Additionally, they do not provide persistent authentication when used within ReactPy.

Django's authentication design requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies.

To work around this limitation, when `#!python use_auth().login()` is called within your application, ReactPy performs the following process...

1. The server authenticates the user into the WebSocket session
2. The server generates a temporary login token linked to the WebSocket session
3. The server commands the browser to fetch the login token via HTTP
4. The client performs the HTTP request
5. The server returns the HTTP response, which contains all necessary cookies
6. The client stores these cookies in the browser

This ultimately results in persistent authentication which will be retained even if the browser tab is refreshed.

---

### Use User

Shortcut that returns the WebSocket or HTTP connection's `#!python User`.

=== "components.py"

```python
{% include "../../examples/python/use_user.py" %}
```

??? example "See Interface"

<font size="4">**Parameters**</font>

`#!python None`

<font size="4">**Returns**</font>

| Type | Description |
| --- | --- |
| `#!python AbstractUser` | A Django `#!python User`, which can also be an `#!python AnonymousUser`. |

---

### Use User Data

Store or retrieve a `#!python dict` containing user data specific to the connection's `#!python User`.
Store or retrieve a `#!python dict` containing arbitrary data specific to the connection's `#!python User`.

This hook is useful for storing user-specific data, such as preferences, settings, or any generic key-value pairs.

Expand Down Expand Up @@ -522,7 +599,7 @@ You can expect this hook to provide strings such as `http://example.com`.

Shortcut that returns the root component's `#!python id` from the WebSocket or HTTP connection.

The root ID is a randomly generated `#!python uuid4`. It is notable to mention that it is persistent across the current connection. The `uuid` is reset when the page is refreshed.
The root ID is a randomly generated `#!python uuid4`. It is notable to mention that it is persistent across the current connection. The `uuid` is reset only when the page is refreshed.

This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and/or retain a backlog of messages in case that component is disconnected via `#!python use_channel_layer( ... , group_discard=False)`.

Expand All @@ -546,14 +623,14 @@ This is useful when used in combination with [`#!python use_channel_layer`](#use

---

### Use User
### Use Re-render

Shortcut that returns the WebSocket or HTTP connection's `#!python User`.
Returns a function that can be used to trigger a re-render of the entire component tree.

=== "components.py"

```python
{% include "../../examples/python/use_user.py" %}
{% include "../../examples/python/use_rerender.py" %}
```

??? example "See Interface"
Expand All @@ -566,4 +643,4 @@ Shortcut that returns the WebSocket or HTTP connection's `#!python User`.

| Type | Description |
| --- | --- |
| `#!python AbstractUser` | A Django `#!python User`, which can also be an `#!python AnonymousUser`. |
| `#!python Callable[[], None]` | A function that triggers a re-render of the entire component tree. |
42 changes: 33 additions & 9 deletions docs/src/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ These are ReactPy-Django's default settings values. You can modify these values

</p>

!!! abstract "Note"

The default configuration of ReactPy is suitable for the vast majority of use cases.

You should only consider changing settings when the necessity arises.

---

## General Settings
Expand Down Expand Up @@ -60,13 +54,17 @@ This file path must be valid to Django's [template finder](https://docs.djangopr

---

## Authentication Settings

---

### `#!python REACTPY_AUTH_BACKEND`

**Default:** `#!python "django.contrib.auth.backends.ModelBackend"`

**Example Value(s):** `#!python "example_project.auth.MyModelBackend"`

Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
Dotted path to the Django authentication backend to use for ReactPy components. This is typically needed if:

1. You are using `#!python settings.py:REACTPY_AUTO_RELOGIN=True` and...
2. You are using `#!python AuthMiddlewareStack` and...
Expand All @@ -75,6 +73,22 @@ Dotted path to the Django authentication backend to use for ReactPy components.

---

### `#!python REACTPY_AUTH_TOKEN_MAX_AGE`

**Default:** `#!python 30`

**Example Value(s):** `#!python 5`

Maximum seconds before ReactPy's login token expires.

This setting exists because Django's authentication design requires cookies to retain login status. ReactPy is rendered via WebSockets, and browsers do not allow active WebSocket connections to modify cookies.

To work around this limitation, this setting provides a maximum validity period of a temporary login token. When `#!python reactpy_django.hooks.use_auth().login()` is called within your application, ReactPy will automatically create this temporary login token and command the browser to fetch it via HTTP.

This setting should be a reasonably low value, but still be high enough to account for a combination of client lag, slow internet, and server response time.

---

### `#!python REACTPY_AUTO_RELOGIN`

**Default:** `#!python False`
Expand Down Expand Up @@ -141,9 +155,9 @@ This setting is incompatible with [`daphne`](https://github.com/django/daphne).

**Example Value(s):** `#!python True`

Configures whether to use an async ReactPy rendering queue. When enabled, large renders will no longer block smaller renders from taking place. Additionally, prevents the rendering queue from being blocked on waiting for async effects to startup/shutdown (which is typically a relatively slow operation).
Configures whether to use an async ReactPy rendering queue. When enabled, large renders will no longer block smaller renders from taking place. Additionally, prevents the rendering queue from being blocked on waiting for async effects to startup/shutdown (which is a relatively slow operation).

This setting is currently in early release, and currently no effort is made to de-duplicate renders. For example, if parent and child components are scheduled to render at the same time, both renders will take place even though a single render of the parent component would have been sufficient.
This setting is currently in early release, and currently no effort is made to de-duplicate renders. For example, if parent and child components are scheduled to render at the same time, both renders will take place, even though a single render would have been sufficient.

---

Expand Down Expand Up @@ -270,6 +284,16 @@ Configures whether ReactPy should clean up expired component sessions during aut

---

### `#!python REACTPY_CLEAN_AUTH_TOKENS`

**Default:** `#!python True`

**Example Value(s):** `#!python False`

Configures whether ReactPy should clean up expired authentication tokens during automatic clean up operations.

---

### `#!python REACTPY_CLEAN_USER_DATA`

**Default:** `#!python True`
Expand Down
2 changes: 1 addition & 1 deletion docs/src/reference/template-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ The entire file path provided is loaded directly into the browser, and must have

This template tag configures the current page to be able to run `pyscript`.

You can optionally use this tag to configure the current PyScript environment. For example, you can include a list of Python packages to automatically install within the PyScript environment.
You can optionally use this tag to configure the current PyScript environment, such as adding dependencies.

=== "my_template.html"

Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,24 @@ extra-dependencies = [
"twisted",
"servestatic",
"django-bootstrap5",
"decorator",
"playwright",
]

[tool.hatch.envs.django.scripts]
runserver = [
"cd tests && python manage.py migrate --noinput",
"cd tests && python manage.py runserver",
]
makemigrations = ["cd tests && python manage.py makemigrations"]
clean = ["cd tests && python manage.py clean_reactpy -v 3"]
clean_sessions = ["cd tests && python manage.py clean_reactpy --sessions -v 3"]
clean_auth_tokens = [
"cd tests && python manage.py clean_reactpy --auth-tokens -v 3",
]
clean_user_data = [
"cd tests && python manage.py clean_reactpy --user-data -v 3",
]

#######################################
# >>> Hatch Documentation Scripts <<< #
Expand Down
26 changes: 25 additions & 1 deletion src/js/src/components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DjangoFormProps } from "./types";
import { DjangoFormProps, HttpRequestProps } from "./types";
import React from "react";
import ReactDOM from "react-dom";
/**
Expand Down Expand Up @@ -62,3 +62,27 @@ export function DjangoForm({

return null;
}

export function HttpRequest({ method, url, body, callback }: HttpRequestProps) {
React.useEffect(() => {
fetch(url, {
method: method,
body: body,
})
.then((response) => {
response
.text()
.then((text) => {
callback(response.status, text);
})
.catch(() => {
callback(response.status, "");
});
})
.catch(() => {
callback(520, "");
});
}, []);

return null;
}
2 changes: 1 addition & 1 deletion src/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { DjangoForm, bind } from "./components";
export { HttpRequest, DjangoForm, bind } from "./components";
export { mountComponent } from "./mount";
7 changes: 7 additions & 0 deletions src/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ export interface DjangoFormProps {
onSubmitCallback: (data: Object) => void;
formId: string;
}

export interface HttpRequestProps {
method: string;
url: string;
body: string;
callback: (status: Number, response: string) => void;
}
Empty file.
Loading
Loading