Skip to content

Commit b57da2b

Browse files
committed
Prototype using cookie setter component
1 parent 464b4e2 commit b57da2b

File tree

5 files changed

+109
-1
lines changed

5 files changed

+109
-1
lines changed

src/js/src/components.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DjangoFormProps } from "./types";
1+
import { DjangoFormProps, SetCookieProps } from "./types";
22
import React from "react";
33
import ReactDOM from "react-dom";
44
/**
@@ -62,3 +62,13 @@ export function DjangoForm({
6262

6363
return null;
6464
}
65+
66+
export function SetCookie({ cookie, completeCallback }: SetCookieProps) {
67+
React.useEffect(() => {
68+
// Set the `sessionid` cookie
69+
document.cookie = cookie;
70+
completeCallback(true);
71+
}, []);
72+
73+
return null;
74+
}

src/js/src/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export interface DjangoFormProps {
2323
onSubmitCallback: (data: Object) => void;
2424
formId: string;
2525
}
26+
27+
export interface SetCookieProps {
28+
cookie: string;
29+
completeCallback: (success: boolean) => void;
30+
}

src/reactpy_django/auth/__init__.py

Whitespace-only changes.

src/reactpy_django/auth/components.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING
5+
6+
from reactpy import component, hooks, web
7+
8+
if TYPE_CHECKING:
9+
from django.contrib.sessions.backends.base import SessionBase
10+
11+
12+
SetCookie = web.export(
13+
web.module_from_file("reactpy-django", file=Path(__file__).parent.parent / "static" / "client.js"),
14+
("SetCookie"),
15+
)
16+
17+
18+
@component
19+
def auth_manager():
20+
session_cookie, set_session_cookie = hooks.use_state("")
21+
scope = hooks.use_connection().scope
22+
23+
@hooks.use_effect(dependencies=None)
24+
async def _session_check():
25+
"""Generate a session cookie if `login` was called in a user's component."""
26+
from django.conf import settings
27+
28+
session: SessionBase | None = scope.get("session")
29+
login_required: bool = scope.get("reactpy-login", False)
30+
if not login_required or not session or not session.session_key:
31+
return
32+
33+
# Begin generating a cookie string
34+
key = session.session_key
35+
domain: str | None = settings.SESSION_COOKIE_DOMAIN
36+
httponly: bool = settings.SESSION_COOKIE_HTTPONLY
37+
name: str = settings.SESSION_COOKIE_NAME
38+
path: str = settings.SESSION_COOKIE_PATH
39+
samesite: str | bool = settings.SESSION_COOKIE_SAMESITE
40+
secure: bool = settings.SESSION_COOKIE_SECURE
41+
new_cookie = f"{name}={key}"
42+
if domain:
43+
new_cookie += f"; Domain={domain}"
44+
if httponly:
45+
new_cookie += "; HttpOnly"
46+
if isinstance(path, str):
47+
new_cookie += f"; Path={path}"
48+
if samesite:
49+
new_cookie += f"; SameSite={samesite}"
50+
if secure:
51+
new_cookie += "; Secure"
52+
if not session.get_expire_at_browser_close():
53+
session_max_age: int = session.get_expiry_age()
54+
session_expiration: str = session.get_expiry_date().strftime("%a, %d-%b-%Y %H:%M:%S GMT")
55+
if session_expiration:
56+
new_cookie += f"; Expires={session_expiration}"
57+
if isinstance(session_max_age, int):
58+
new_cookie += f"; Max-Age={session_max_age}"
59+
60+
# Save the cookie within this component's state so that the client-side component can ingest it
61+
scope.pop("reactpy-login")
62+
if new_cookie != session_cookie:
63+
set_session_cookie(new_cookie)
64+
65+
def on_complete_callback(success: bool):
66+
"""Remove the cookie from server-side memory if it was successfully set.
67+
This will subsequently remove the client-side cookie-setter component from the DOM."""
68+
if success:
69+
set_session_cookie("")
70+
71+
# If a session cookie was generated, send it to the client
72+
if session_cookie:
73+
print("Session Cookie: ", session_cookie)
74+
return SetCookie({"sessionCookie": session_cookie}, on_complete_callback)

src/reactpy_django/hooks.py

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import orjson
1717
from channels import DEFAULT_CHANNEL_LAYER
18+
from channels import auth as channels_auth
1819
from channels.layers import InMemoryChannelLayer, get_channel_layer
1920
from reactpy import use_callback, use_effect, use_memo, use_ref, use_state
2021
from reactpy import use_connection as _use_connection
@@ -416,6 +417,24 @@ def use_root_id() -> str:
416417
return scope["reactpy"]["id"]
417418

418419

420+
def use_auth():
421+
"""Provides the ability to login/logout a user using Django's standard authentication framework."""
422+
from reactpy_django import config
423+
424+
scope = use_scope()
425+
426+
async def login(user: AbstractUser):
427+
await channels_auth.login(scope, user, backend=config.REACTPY_AUTH_BACKEND)
428+
session_save_func = getattr(scope["session"], "asave", getattr(scope["session"], "save"))
429+
await ensure_async(session_save_func)()
430+
scope["reactpy_login"] = True
431+
432+
async def logout():
433+
await channels_auth.logout(scope)
434+
435+
return login, logout
436+
437+
419438
async def _get_user_data(user: AbstractUser, default_data: None | dict, save_default_data: bool) -> dict | None:
420439
"""The mutation function for `use_user_data`"""
421440
from reactpy_django.models import UserDataModel

0 commit comments

Comments
 (0)