Skip to content

Commit e01b216

Browse files
svengtSven Groot
andauthored
Add support for encrypt and clear history (#55)
* Add support for encrypt and clear history * Add TypeError for non-boolean history flags --------- Co-authored-by: Sven Groot <[email protected]>
1 parent 8219af9 commit e01b216

File tree

7 files changed

+139
-0
lines changed

7 files changed

+139
-0
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,46 @@ Inertia Django ships with a custom JsonEncoder at `inertia.utils.InertiaJsonEnco
148148
`DjangoJSONEncoder` with additional logic to handle encoding models and Querysets. If you have other json
149149
encoding logic you'd prefer, you can set a new JsonEncoder via the settings.
150150

151+
### History Encryption
152+
153+
Inertia.js supports [history encryption](https://inertiajs.com/history-encryption) to protect sensitive data in the browser's history state. This is useful when your pages contain sensitive information that shouldn't be stored in plain text in the browser's history. This feature requires HTTPS since it relies on `window.crypto.subtle` which is only available in secure contexts.
154+
155+
You can enable history encryption globally via the `INERTIA_ENCRYPT_HISTORY` setting in your `settings.py`:
156+
157+
```python
158+
INERTIA_ENCRYPT_HISTORY = True
159+
```
160+
161+
For more granular control, you can enable encryption on specific views:
162+
163+
```python
164+
from inertia import encrypt_history, inertia
165+
166+
@inertia('TestComponent')
167+
def encrypt_history_test(request):
168+
encrypt_history(request)
169+
return {}
170+
171+
# If you have INERTIA_ENCRYPT_HISTORY = True but want to disable encryption for specific views:
172+
@inertia('PublicComponent')
173+
def public_view(request):
174+
encrypt_history(request, False) # Explicitly disable encryption for this view
175+
return {}
176+
```
177+
178+
When users log out, you might want to clear the history to ensure no sensitive data can be accessed. You can do this by extending the logout view:
179+
180+
```python
181+
from inertia import clear_history
182+
from django.contrib.auth import views as auth_views
183+
184+
class LogoutView(auth_views.LogoutView):
185+
def dispatch(self, request, *args, **kwargs):
186+
response = super().dispatch(request, *args, **kwargs)
187+
clear_history(request)
188+
return response
189+
```
190+
151191
### SSR
152192

153193
#### Backend
@@ -168,6 +208,7 @@ INERTIA_LAYOUT = 'layout.html' # required and has no default
168208
INERTIA_JSON_ENCODER = CustomJsonEncoder # defaults to inertia.utils.InertiaJsonEncoder
169209
INERTIA_SSR_URL = 'http://localhost:13714' # defaults to http://localhost:13714
170210
INERTIA_SSR_ENABLED = False # defaults to False
211+
INERTIA_ENCRYPT_HISTORY = False # defaults to False
171212
```
172213

173214
## Testing

inertia/http.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import requests
88
from .utils import LazyProp
99

10+
INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history"
11+
INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history"
12+
1013
def render(request, component, props={}, template_data={}):
1114
def is_a_partial_render():
1215
return 'X-Inertia-Partial-Data' in request.headers and request.headers.get('X-Inertia-Partial-Component', '') == component
@@ -53,11 +56,21 @@ def render_ssr():
5356
})
5457

5558
def page_data():
59+
encrypt_history = getattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, settings.INERTIA_ENCRYPT_HISTORY)
60+
if not isinstance(encrypt_history, bool):
61+
raise TypeError(f"Expected boolean for encrypt_history, got {type(encrypt_history).__name__}")
62+
63+
clear_history = request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False)
64+
if not isinstance(clear_history, bool):
65+
raise TypeError(f"Expected boolean for clear_history, got {type(clear_history).__name__}")
66+
5667
return {
5768
'component': component,
5869
'props': build_props(),
5970
'url': request.build_absolute_uri(),
6071
'version': settings.INERTIA_VERSION,
72+
'encryptHistory': encrypt_history,
73+
'clearHistory': clear_history,
6174
}
6275

6376
if 'X-Inertia' in request.headers:
@@ -87,6 +100,12 @@ def location(location):
87100
'X-Inertia-Location': location,
88101
})
89102

103+
def encrypt_history(request, value=True):
104+
setattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, value)
105+
106+
def clear_history(request):
107+
request.session[INERTIA_SESSION_CLEAR_HISTORY] = True
108+
90109
def inertia(component):
91110
def decorator(func):
92111
@wraps(func)

inertia/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class InertiaSettings:
88
INERTIA_JSON_ENCODER = InertiaJsonEncoder
99
INERTIA_SSR_URL = 'http://localhost:13714'
1010
INERTIA_SSR_ENABLED = False
11+
INERTIA_ENCRYPT_HISTORY = False
1112

1213
def __getattribute__(self, name):
1314
try:

inertia/test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def inertia_page(url, component='TestComponent', props={}, template_data={}):
5959
'props': props,
6060
'url': f'http://testserver/{url}/',
6161
'version': settings.INERTIA_VERSION,
62+
'encryptHistory': False,
63+
'clearHistory': False,
6264
}
6365

6466
def inertia_div(*args, **kwargs):

inertia/tests/test_history.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from inertia.http import encrypt_history
2+
from inertia.test import InertiaTestCase
3+
from django.test import override_settings
4+
from django.test.client import RequestFactory
5+
from inertia.tests.testapp.views import empty_test
6+
7+
8+
class MiddlewareTestCase(InertiaTestCase):
9+
def test_encrypt_history_setting(self):
10+
self.client.get('/empty/')
11+
assert self.page()['encryptHistory'] is False
12+
13+
with override_settings(INERTIA_ENCRYPT_HISTORY=True):
14+
self.client.get('/empty/')
15+
assert self.page()['encryptHistory'] is True
16+
17+
def test_encrypt_history(self):
18+
self.client.get('/encrypt-history/')
19+
assert self.page()['encryptHistory'] is True
20+
21+
with override_settings(INERTIA_ENCRYPT_HISTORY=True):
22+
self.client.get('/no-encrypt-history/')
23+
assert self.page()['encryptHistory'] is False
24+
25+
def test_clear_history(self):
26+
self.client.get('/clear-history/')
27+
assert self.page()['clearHistory'] is True
28+
29+
def test_clear_history_redirect(self):
30+
response = self.client.get('/clear-history-redirect/', follow=True)
31+
self.assertRedirects(response, '/empty/')
32+
assert self.page()['clearHistory'] is True
33+
34+
def test_raises_type_error(self):
35+
with self.assertRaisesMessage(TypeError, 'Expected boolean for encrypt_history, got str'):
36+
self.client.get('/encrypt-history-type-error/')
37+
38+
with self.assertRaisesMessage(TypeError, 'Expected boolean for clear_history, got str'):
39+
self.client.get('/clear-history-type-error/')

inertia/tests/testapp/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@
1212
path('share/', views.share_test),
1313
path('inertia-redirect/', views.inertia_redirect_test),
1414
path('external-redirect/', views.external_redirect_test),
15+
path('encrypt-history/', views.encrypt_history_test),
16+
path('no-encrypt-history/', views.encrypt_history_false_test),
17+
path('encrypt-history-type-error/', views.encrypt_history_type_error_test),
18+
path('clear-history/', views.clear_history_test),
19+
path('clear-history-redirect/', views.clear_history_redirect_test),
20+
path('clear-history-type-error/', views.clear_history_type_error_test),
1521
]

inertia/tests/testapp/views.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.shortcuts import redirect
33
from django.utils.decorators import decorator_from_middleware
44
from inertia import inertia, render, lazy, share, location
5+
from inertia.http import INERTIA_SESSION_CLEAR_HISTORY, clear_history, encrypt_history
56

67
class ShareMiddleware:
78
def __init__(self, get_response):
@@ -65,3 +66,33 @@ def share_test(request):
6566
return {
6667
'name': 'Brandon',
6768
}
69+
70+
@inertia('TestComponent')
71+
def encrypt_history_test(request):
72+
encrypt_history(request)
73+
return {}
74+
75+
@inertia('TestComponent')
76+
def encrypt_history_false_test(request):
77+
encrypt_history(request, False)
78+
return {}
79+
80+
@inertia('TestComponent')
81+
def encrypt_history_type_error_test(request):
82+
encrypt_history(request, "foo")
83+
return {}
84+
85+
@inertia('TestComponent')
86+
def clear_history_test(request):
87+
clear_history(request)
88+
return {}
89+
90+
@inertia('TestComponent')
91+
def clear_history_redirect_test(request):
92+
clear_history(request)
93+
return redirect(empty_test)
94+
95+
@inertia('TestComponent')
96+
def clear_history_type_error_test(request):
97+
request.session[INERTIA_SESSION_CLEAR_HISTORY] = "foo"
98+
return {}

0 commit comments

Comments
 (0)