hyperflask 0.2.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hyperflask-0.2.3 → hyperflask-0.3.0}/PKG-INFO +9 -11
- {hyperflask-0.2.3 → hyperflask-0.3.0}/README.md +3 -4
- {hyperflask-0.2.3 → hyperflask-0.3.0}/pyproject.toml +7 -8
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/__init__.py +2 -1
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/app.py +71 -16
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/cli.py +17 -4
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/core.py +21 -5
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/factory.py +1 -1
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/widgets.py +1 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/layouts/email.mjml +3 -3
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/layouts/web.html +8 -6
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/model.py +6 -1
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/page_helpers.py +3 -11
- hyperflask-0.3.0/src/hyperflask/security.py +81 -0
- hyperflask-0.3.0/src/hyperflask/static/alpine-csp.js +5 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/alpine.js +1 -1
- hyperflask-0.3.0/src/hyperflask/static/app.js +14 -0
- hyperflask-0.3.0/src/hyperflask/static/htmx-exts.js +125 -0
- hyperflask-0.3.0/src/hyperflask/ui/flash.html +17 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/forms.html +1 -1
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/layout.html +1 -1
- hyperflask-0.3.0/src/hyperflask/ui/metadata.html +4 -0
- hyperflask-0.3.0/src/hyperflask/ui/skeleton.html +33 -0
- hyperflask-0.3.0/src/hyperflask/ui/sse.html +22 -0
- hyperflask-0.3.0/src/hyperflask/utils/htmx.py +64 -0
- hyperflask-0.2.3/src/hyperflask/reactivity.py +0 -52
- hyperflask-0.2.3/src/hyperflask/static/app.js +0 -47
- hyperflask-0.2.3/src/hyperflask/static/reactive.js +0 -39
- hyperflask-0.2.3/src/hyperflask/ui/flash.html +0 -8
- hyperflask-0.2.3/src/hyperflask/ui/sse.html +0 -6
- hyperflask-0.2.3/src/hyperflask/utils/htmx.py +0 -27
- hyperflask-0.2.3/src/hyperflask/utils/models.py +0 -5
- hyperflask-0.2.3/src/hyperflask/utils/request.py +0 -25
- hyperflask-0.2.3/src/hyperflask/utils/s3.py +0 -4
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/__init__.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/alpine.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/jsx.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/components/webcomponent.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/app.css +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/page_actions.py +0 -0
- {hyperflask-0.2.3 → hyperflask-0.3.0}/src/hyperflask/utils/tokens.py +0 -0
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperflask
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Full stack web framework
|
|
5
5
|
Author: Maxime Bouroumeau-Fuseau
|
|
6
6
|
Author-email: Maxime Bouroumeau-Fuseau <maxime.bouroumeau@gmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Requires-Dist: flask>=3.0.0,<4.0.0
|
|
9
|
-
Requires-Dist: flask-assets-pipeline~=0.
|
|
9
|
+
Requires-Dist: flask-assets-pipeline~=0.7
|
|
10
10
|
Requires-Dist: flask-babel-hyper~=5.1
|
|
11
11
|
Requires-Dist: flask-collections~=0.2
|
|
12
12
|
Requires-Dist: flask-configurator~=0.1
|
|
13
13
|
Requires-Dist: flask-debugtoolbar~=0.16
|
|
14
14
|
Requires-Dist: flask-dramatiq~=0.6
|
|
15
|
-
Requires-Dist: flask-file-routes~=0.
|
|
15
|
+
Requires-Dist: flask-file-routes~=0.3
|
|
16
16
|
Requires-Dist: flask-files~=0.2
|
|
17
17
|
Requires-Dist: flask-geo~=0.1
|
|
18
18
|
Requires-Dist: flask-mailman-templates~=0.1
|
|
19
|
-
Requires-Dist: flask-mercure-sse~=0.
|
|
20
|
-
Requires-Dist: flask-sqlorm~=0.
|
|
19
|
+
Requires-Dist: flask-mercure-sse~=0.3
|
|
20
|
+
Requires-Dist: flask-sqlorm~=0.3
|
|
21
21
|
Requires-Dist: flask-super-macros~=0.1
|
|
22
|
-
Requires-Dist: flask-
|
|
22
|
+
Requires-Dist: flask-suspense~=0.1
|
|
23
23
|
Requires-Dist: flask-wtf~=1.2
|
|
24
24
|
Requires-Dist: frozen-flask~=1.0
|
|
25
25
|
Requires-Dist: gunicorn~=23.0
|
|
26
26
|
Requires-Dist: honcho~=2.0
|
|
27
27
|
Requires-Dist: html-sanitizer~=2.6
|
|
28
28
|
Requires-Dist: htmx-flask~=0.1
|
|
29
|
-
Requires-Dist: jinja2-fragments~=1.9
|
|
30
29
|
Requires-Dist: jinja-layout~=0.4
|
|
31
30
|
Requires-Dist: jinja-wtforms~=0.2
|
|
32
31
|
Requires-Dist: periodiq~=0.13
|
|
@@ -108,7 +107,7 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
108
107
|
| [Flask-Mercure-SSE](https://github.com/hyperflask/flask-mercure-sse) | Push events via server-sent events using the [Mercure](https://mercure.rocks) protocol
|
|
109
108
|
| [Flask-SQLORM](https://github.com/hyperflask/flask-sqlorm) | Flask integration of [sqlorm](https://github.com/hyperflask/sqlorm)
|
|
110
109
|
| [Flask-Super-Macros](https://github.com/hyperflask/flask-super-macros) | Better macro management for Jinja
|
|
111
|
-
| [Flask-
|
|
110
|
+
| [Flask-Suspense](https://github.com/hyperflask/flask-suspense) | Suspense for Flask
|
|
112
111
|
| [Flask-WTF](https://flask-wtf.readthedocs.io) | [WTForms](https://wtforms.readthedocs.io) integration
|
|
113
112
|
|
|
114
113
|
## Using without Hyperflask-Start
|
|
@@ -116,6 +115,5 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
116
115
|
1. Create your project directory: `mkdir example-project && cd example-project`
|
|
117
116
|
2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
|
|
118
117
|
3. `pip install hyperflask`
|
|
119
|
-
4.
|
|
120
|
-
5.
|
|
121
|
-
6. Start a development server using `hyperflask dev`
|
|
118
|
+
4. Run `hyperflask init .`
|
|
119
|
+
5. Start a development server using `hyperflask dev`
|
|
@@ -70,7 +70,7 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
70
70
|
| [Flask-Mercure-SSE](https://github.com/hyperflask/flask-mercure-sse) | Push events via server-sent events using the [Mercure](https://mercure.rocks) protocol
|
|
71
71
|
| [Flask-SQLORM](https://github.com/hyperflask/flask-sqlorm) | Flask integration of [sqlorm](https://github.com/hyperflask/sqlorm)
|
|
72
72
|
| [Flask-Super-Macros](https://github.com/hyperflask/flask-super-macros) | Better macro management for Jinja
|
|
73
|
-
| [Flask-
|
|
73
|
+
| [Flask-Suspense](https://github.com/hyperflask/flask-suspense) | Suspense for Flask
|
|
74
74
|
| [Flask-WTF](https://flask-wtf.readthedocs.io) | [WTForms](https://wtforms.readthedocs.io) integration
|
|
75
75
|
|
|
76
76
|
## Using without Hyperflask-Start
|
|
@@ -78,6 +78,5 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
78
78
|
1. Create your project directory: `mkdir example-project && cd example-project`
|
|
79
79
|
2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
|
|
80
80
|
3. `pip install hyperflask`
|
|
81
|
-
4.
|
|
82
|
-
5.
|
|
83
|
-
6. Start a development server using `hyperflask dev`
|
|
81
|
+
4. Run `hyperflask init .`
|
|
82
|
+
5. Start a development server using `hyperflask dev`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperflask"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Full stack web framework"
|
|
5
5
|
authors = [
|
|
6
6
|
{"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
|
|
@@ -10,21 +10,21 @@ license = "MIT"
|
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"flask>=3.0.0,<4.0.0",
|
|
13
|
-
"flask-assets-pipeline ~=0.
|
|
13
|
+
"flask-assets-pipeline ~=0.7",
|
|
14
14
|
"flask-babel-hyper ~=5.1",
|
|
15
15
|
"flask-collections ~=0.2",
|
|
16
16
|
"flask-configurator ~=0.1",
|
|
17
17
|
"flask-debugtoolbar ~=0.16",
|
|
18
18
|
"flask-dramatiq ~=0.6",
|
|
19
|
-
"flask-file-routes ~=0.
|
|
19
|
+
"flask-file-routes ~=0.3",
|
|
20
20
|
"flask-files ~=0.2",
|
|
21
21
|
"flask-geo ~=0.1",
|
|
22
22
|
"flask-mailman-templates ~=0.1",
|
|
23
|
-
"flask-mercure-sse ~=0.
|
|
23
|
+
"flask-mercure-sse ~=0.3",
|
|
24
24
|
#"flask-observability ~=0.1"
|
|
25
|
-
"flask-sqlorm ~=0.
|
|
25
|
+
"flask-sqlorm ~=0.3",
|
|
26
26
|
"flask-super-macros ~=0.1",
|
|
27
|
-
"flask-
|
|
27
|
+
"flask-suspense ~=0.1",
|
|
28
28
|
"flask-wtf ~=1.2",
|
|
29
29
|
"frozen-flask ~=1.0",
|
|
30
30
|
#"dramatiq-sqlite ~=0.1"
|
|
@@ -32,12 +32,11 @@ dependencies = [
|
|
|
32
32
|
"honcho ~=2.0",
|
|
33
33
|
"html-sanitizer ~=2.6",
|
|
34
34
|
"htmx-flask ~=0.1",
|
|
35
|
-
"jinja2-fragments ~=1.9",
|
|
36
35
|
"jinja-layout ~=0.4",
|
|
37
36
|
"jinja-wtforms ~=0.2",
|
|
38
37
|
"periodiq ~=0.13",
|
|
39
38
|
"python-slugify ~=8.0.4",
|
|
40
|
-
"redis ~=6.4"
|
|
39
|
+
"redis ~=6.4",
|
|
41
40
|
]
|
|
42
41
|
|
|
43
42
|
[tool.uv.sources]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from flask import *
|
|
2
2
|
from .app import Hyperflask
|
|
3
3
|
from .utils.htmx import htmx_redirect, htmx_oob
|
|
4
|
+
from .security import csp_nonce
|
|
4
5
|
|
|
5
6
|
# useful exports from hyperflask managed extensions
|
|
6
7
|
from flask_babel import gettext, _, pgettext, _p, ngettext, _n, npgettext, _np, lazy_gettext, _lazy
|
|
@@ -8,9 +9,9 @@ from flask_file_routes import page
|
|
|
8
9
|
from flask_files import save_file, validate_file
|
|
9
10
|
from flask_mailman_templates import send_mail
|
|
10
11
|
from flask_mercure_sse import publish_signal, mercure_publish
|
|
12
|
+
from flask_suspense import render_template, stream_template, defer
|
|
11
13
|
|
|
12
14
|
# useful exports from 3rd party libs
|
|
13
15
|
from htmx_flask import request, make_response
|
|
14
|
-
from jinja2_fragments.flask import render_block
|
|
15
16
|
from slugify import slugify
|
|
16
17
|
from blinker import signal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# flask
|
|
2
2
|
from flask import Flask
|
|
3
|
-
from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader
|
|
3
|
+
from jinja2 import ChoiceLoader, FileSystemLoader, PrefixLoader, TemplateNotFound
|
|
4
4
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
5
5
|
# hyperflask extensions
|
|
6
6
|
from jinja_layout import LayoutExtension
|
|
@@ -8,7 +8,7 @@ from jinja_wtforms import WtformExtension
|
|
|
8
8
|
from flask_assets_pipeline import AssetsPipeline
|
|
9
9
|
from flask_super_macros import SuperMacros
|
|
10
10
|
from flask_collections import Collections
|
|
11
|
-
from flask_file_routes import FileRoutes
|
|
11
|
+
from flask_file_routes import FileRoutes, ModuleView, page
|
|
12
12
|
from flask_configurator import Config
|
|
13
13
|
from flask_babel import Babel, _, _p, _n, _np, lazy_gettext
|
|
14
14
|
from flask_geo import Geolocation
|
|
@@ -16,10 +16,10 @@ from flask_files import Files
|
|
|
16
16
|
from flask_sqlorm import FlaskSQLORM
|
|
17
17
|
from flask_mercure_sse import MercureSSE
|
|
18
18
|
from flask_mailman_templates import MailTemplates
|
|
19
|
+
from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_before_render_blocks
|
|
19
20
|
#from flask_observability import Observability
|
|
20
21
|
# 3rd party extensions
|
|
21
22
|
from flask_wtf.csrf import CSRFProtect
|
|
22
|
-
from flask_talisman import Talisman
|
|
23
23
|
from flask_frozen import Freezer
|
|
24
24
|
from htmx_flask import Htmx
|
|
25
25
|
from flask_mailman import Mail
|
|
@@ -28,13 +28,12 @@ from flask_debugtoolbar import DebugToolbarExtension
|
|
|
28
28
|
# from hyperflask
|
|
29
29
|
from .forms import Form
|
|
30
30
|
from .utils.page_actions import page_action_url
|
|
31
|
-
from .utils.htmx import htmx_oob
|
|
31
|
+
from .utils.htmx import htmx_oob, respond_with_flash_messages
|
|
32
32
|
from .utils.markdown import jinja_markdown, MarkdownExtension
|
|
33
33
|
from .utils.html import sanitize_html, nl2br
|
|
34
|
-
from .
|
|
35
|
-
from .
|
|
36
|
-
from .
|
|
37
|
-
from .model import Model
|
|
34
|
+
from .components import register_components
|
|
35
|
+
from .model import Model, File as SQLFileType
|
|
36
|
+
from .security import respond_with_security_headers, csp_nonce
|
|
38
37
|
from . import page_helpers
|
|
39
38
|
# others
|
|
40
39
|
from jinja_super_macros.registry import FileLoader
|
|
@@ -54,7 +53,7 @@ map_jinja_call_node_to_func(lazy_gettext, "lazy_gettext")
|
|
|
54
53
|
class Hyperflask(Flask):
|
|
55
54
|
config_class = Config
|
|
56
55
|
|
|
57
|
-
def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts",
|
|
56
|
+
def __init__(self, *args, static_mode="hybrid", instrument=False, layouts_folder="layouts",
|
|
58
57
|
emails_folder="emails", forms_folder="forms", assets_folder="assets", pages_folder="pages", migrations_folder="migrations",
|
|
59
58
|
config=None, config_filename="config.yml", database_uri="sqlite://:memory:", proxy_fix=True, **kwargs):
|
|
60
59
|
|
|
@@ -69,14 +68,28 @@ class Hyperflask(Flask):
|
|
|
69
68
|
"OTEL_INSTRUMENT": instrument,
|
|
70
69
|
"DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
|
|
71
70
|
"DEBUG_TB_INTERCEPT_REDIRECTS": False,
|
|
71
|
+
"SESSION_COOKIE_HTTPONLY": True,
|
|
72
|
+
"SESSION_COOKIE_SAMESITE": "Lax",
|
|
72
73
|
# Hyperflask specific
|
|
73
74
|
"LAYOUT": "layouts/default.html",
|
|
75
|
+
"ALPINE": False,
|
|
74
76
|
"STATIC_MODE": static_mode,
|
|
75
|
-
"
|
|
76
|
-
"HTMX_EXT": [
|
|
77
|
+
"ASSETS_INCLUDE_ALPINE": False,
|
|
78
|
+
"HTMX_EXT": [],
|
|
77
79
|
"HTMX_BOOST_SITE": False,
|
|
78
80
|
"MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
|
|
79
|
-
"MARKDOWN_SANITIZER_CONFIG": {}
|
|
81
|
+
"MARKDOWN_SANITIZER_CONFIG": {},
|
|
82
|
+
"FLASH_TOAST_OOB": True,
|
|
83
|
+
"FLASH_TOAST_REMOVE_AFTER": None,
|
|
84
|
+
"SERVER_SECURED": False,
|
|
85
|
+
"CSP_HEADER": True,
|
|
86
|
+
"CSP_SAFE_SRC": ["'self'"],
|
|
87
|
+
"CSP_UNSAFE_EVAL": True,
|
|
88
|
+
"CSP_UNSAFE_INLINE": True,
|
|
89
|
+
"CSP_FRAME_ANCESTORS": True,
|
|
90
|
+
"CSP_FRAME_ANCESTORS_SAFE_ENDPOINTS": [],
|
|
91
|
+
"REFERRER_POLICY": "strict-origin-when-cross-origin",
|
|
92
|
+
"HSTS_HEADER": False
|
|
80
93
|
})
|
|
81
94
|
if config:
|
|
82
95
|
self.config.update(config)
|
|
@@ -89,12 +102,22 @@ class Hyperflask(Flask):
|
|
|
89
102
|
self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
90
103
|
self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
91
104
|
|
|
105
|
+
if self.config['SERVER_SECURED']:
|
|
106
|
+
self.config.update({
|
|
107
|
+
'FORCE_URL_SCHEME': 'https',
|
|
108
|
+
'PREFERRED_URL_SCHEME': 'https',
|
|
109
|
+
'SESSION_COOKIE_SECURE': True,
|
|
110
|
+
'REMEMBER_COOKIE_SECURE': True
|
|
111
|
+
})
|
|
112
|
+
self.config.setdefault("HSTS_HEADER", "max-age=31556926; includeSubDomains") # 1 year
|
|
113
|
+
|
|
92
114
|
#self.otel = Observability(self)
|
|
93
115
|
|
|
94
116
|
self.jinja_env.add_extension(LayoutExtension)
|
|
95
117
|
self.jinja_env.add_extension(WtformExtension)
|
|
96
118
|
self.jinja_env.add_extension(MarkdownExtension)
|
|
97
119
|
self.jinja_env.filters.update(markdown=jinja_markdown, sanitize=sanitize_html, nl2br=nl2br)
|
|
120
|
+
self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce)
|
|
98
121
|
self.jinja_env.form_base_cls = Form
|
|
99
122
|
self.jinja_env.default_layout = self.config["LAYOUT"]
|
|
100
123
|
self.forms = self.jinja_env.forms
|
|
@@ -125,7 +148,7 @@ class Hyperflask(Flask):
|
|
|
125
148
|
Geolocation(self)
|
|
126
149
|
Files(self)
|
|
127
150
|
MailTemplates(self, template_folder=emails_folder)
|
|
128
|
-
file_routes = FileRoutes(self, pages_folder=pages_folder,
|
|
151
|
+
file_routes = FileRoutes(self, pages_folder=pages_folder, module_view_class=HyperModuleView)
|
|
129
152
|
self.page_helper = file_routes.page_helper
|
|
130
153
|
self.collections = Collections(self)
|
|
131
154
|
self.sse = MercureSSE(self)
|
|
@@ -148,12 +171,13 @@ class Hyperflask(Flask):
|
|
|
148
171
|
})
|
|
149
172
|
self.assets.bundle(
|
|
150
173
|
{"@hyperflask": ["app.js"],
|
|
151
|
-
"@hyperflask/
|
|
152
|
-
"@hyperflask/alpine": ["alpine.js"]},
|
|
174
|
+
"@hyperflask/alpine": ["alpine-csp.js" if not self.config["CSP_UNSAFE_EVAL"] else "alpine.js"]},
|
|
153
175
|
from_package="hyperflask",
|
|
154
176
|
assets_folder="static"
|
|
155
177
|
)
|
|
156
178
|
self.assets.include("@hyperflask", 0)
|
|
179
|
+
if self.config["ALPINE"]:
|
|
180
|
+
self.assets.include("@hyperflask/alpine", 0)
|
|
157
181
|
|
|
158
182
|
self.extensions['mail_templates'].jinja_env.add_extension(LayoutExtension)
|
|
159
183
|
self.extensions['mail_templates'].jinja_env.default_layout = "layout.mjml"
|
|
@@ -175,7 +199,9 @@ class Hyperflask(Flask):
|
|
|
175
199
|
for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
|
|
176
200
|
self.forms.register(self.macros.resolve_template(macro), macro)
|
|
177
201
|
|
|
178
|
-
self.
|
|
202
|
+
self.after_request(respond_with_security_headers)
|
|
203
|
+
if self.config["FLASH_TOAST_OOB"]:
|
|
204
|
+
self.after_request(respond_with_flash_messages)
|
|
179
205
|
|
|
180
206
|
self.collections.register_freezer_generator(self.freezer)
|
|
181
207
|
|
|
@@ -184,10 +210,39 @@ class Hyperflask(Flask):
|
|
|
184
210
|
self.db.Model = Model
|
|
185
211
|
self.db.File = SQLFileType
|
|
186
212
|
|
|
213
|
+
Suspense(self, nonce_getter="csp_nonce()")
|
|
214
|
+
|
|
215
|
+
@suspense_before_render_template.connect_via(self, weak=False)
|
|
216
|
+
def on_suspense_before_render_template(sender, **kwargs):
|
|
217
|
+
csp_nonce() # we will need a nonce but the first call will be after the template started rendering
|
|
218
|
+
|
|
219
|
+
@suspense_before_render_blocks.connect_via(self, weak=False)
|
|
220
|
+
def on_suspense_before_render_blocks(sender, **kwargs):
|
|
221
|
+
return f'<script nonce="{csp_nonce()}">htmx.bootstrap()</script>'
|
|
222
|
+
|
|
223
|
+
@self.errorhandler(404)
|
|
224
|
+
def not_found(error):
|
|
225
|
+
try:
|
|
226
|
+
return render_template("404.html"), 404
|
|
227
|
+
except TemplateNotFound:
|
|
228
|
+
return self.make_response(("404 Not Found", 404))
|
|
229
|
+
|
|
230
|
+
@self.errorhandler(500)
|
|
231
|
+
def internal_server_error(error):
|
|
232
|
+
try:
|
|
233
|
+
return render_template("500.html"), 500
|
|
234
|
+
except TemplateNotFound:
|
|
235
|
+
return self.make_response(("500 Internal Server Error", 500))
|
|
236
|
+
|
|
187
237
|
def relative_import_name(self, name):
|
|
188
238
|
return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
|
|
189
239
|
|
|
190
240
|
|
|
241
|
+
class HyperModuleView(ModuleView):
|
|
242
|
+
def _render_template(self):
|
|
243
|
+
return render_template(page.template)
|
|
244
|
+
|
|
245
|
+
|
|
191
246
|
def static(func):
|
|
192
247
|
pass
|
|
193
248
|
|
|
@@ -33,6 +33,7 @@ cli.add_command(freeze_command)
|
|
|
33
33
|
@click.pass_context
|
|
34
34
|
def build_command(ctx):
|
|
35
35
|
ctx.invoke(assets_cli.build)
|
|
36
|
+
ctx.invoke(freeze_command)
|
|
36
37
|
|
|
37
38
|
cli.add_command(build_command)
|
|
38
39
|
|
|
@@ -69,8 +70,9 @@ cli.add_command(serve_command)
|
|
|
69
70
|
@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
|
|
70
71
|
@click.option("--port", "-p", default=5000, help="The port to bind to.")
|
|
71
72
|
@click.option('--concurrency')
|
|
73
|
+
@click.option("--extend-procfile")
|
|
72
74
|
@click.option("--dev", is_flag=True)
|
|
73
|
-
def run_command(processes, host, port, concurrency, dev):
|
|
75
|
+
def run_command(processes, host, port, concurrency, extend_procfile, dev):
|
|
74
76
|
from honcho.command import _procfile, _parse_concurrency
|
|
75
77
|
from honcho.manager import Manager
|
|
76
78
|
from honcho.printer import Printer
|
|
@@ -85,14 +87,18 @@ def run_command(processes, host, port, concurrency, dev):
|
|
|
85
87
|
if os.path.exists(filename):
|
|
86
88
|
_processes = _procfile(filename).processes
|
|
87
89
|
break
|
|
88
|
-
if _processes is None:
|
|
89
|
-
|
|
90
|
+
if _processes is None or extend_procfile:
|
|
91
|
+
if not _processes:
|
|
92
|
+
_processes = {}
|
|
93
|
+
_processes.update({
|
|
90
94
|
"web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
|
|
91
95
|
# "worker": [sys.argv[0], "worker"],
|
|
92
96
|
# "scheduler": [sys.argv[0], "periodiq"]
|
|
93
|
-
}
|
|
97
|
+
})
|
|
94
98
|
if dev:
|
|
95
99
|
_processes["assets"] = [sys.argv[0], "assets", "dev"]
|
|
100
|
+
else:
|
|
101
|
+
_processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
|
|
96
102
|
|
|
97
103
|
processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
|
|
98
104
|
for name in _processes if name in _processes}
|
|
@@ -124,6 +130,13 @@ dev_command.params = list(run_command.params)
|
|
|
124
130
|
cli.add_command(dev_command)
|
|
125
131
|
|
|
126
132
|
|
|
133
|
+
@click.command("init")
|
|
134
|
+
def init_command():
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
cli.add_command(init_command)
|
|
138
|
+
|
|
139
|
+
|
|
127
140
|
def main():
|
|
128
141
|
cli.main()
|
|
129
142
|
|
|
@@ -2,6 +2,7 @@ from markupsafe import Markup
|
|
|
2
2
|
from flask import request, current_app
|
|
3
3
|
from flask.views import http_method_funcs
|
|
4
4
|
from flask_file_routes import page
|
|
5
|
+
from flask_suspense import render_template
|
|
5
6
|
from jinjapy import extract_frontmatter
|
|
6
7
|
import importlib
|
|
7
8
|
import re
|
|
@@ -55,19 +56,34 @@ class ComponentAdapter(BaseComponentAdapter):
|
|
|
55
56
|
return props
|
|
56
57
|
if not props:
|
|
57
58
|
props = {}
|
|
58
|
-
|
|
59
|
+
ctx = self.get_render_context(None, **props)
|
|
60
|
+
return render_template(self.template, **ctx)
|
|
59
61
|
|
|
60
62
|
if methods:
|
|
61
63
|
app.add_url_rule(url, self.name, view_func=view_func, methods=methods)
|
|
62
64
|
|
|
63
|
-
def
|
|
64
|
-
tpl = current_app.jinja_env.get_template(self.template)
|
|
65
|
+
def get_render_context(self, caller, *args, **kwargs):
|
|
65
66
|
ctx = kwargs.pop('_ctx', {})
|
|
66
|
-
ctx.update(dict(args=args, kwargs=kwargs, props=kwargs, caller=caller, children=caller))
|
|
67
|
+
ctx.update(dict(args=args, kwargs=kwargs, props=PropsWrapper(kwargs), caller=caller, children=caller))
|
|
67
68
|
if self.module_name:
|
|
68
69
|
module = importlib.import_module(self.module_name)
|
|
69
70
|
if module and "render" in dir(module):
|
|
70
71
|
_ctx = module.render(*args, **kwargs)
|
|
71
72
|
if _ctx:
|
|
72
73
|
ctx.update(_ctx)
|
|
73
|
-
return
|
|
74
|
+
return ctx
|
|
75
|
+
|
|
76
|
+
def render(self, caller, *args, **kwargs):
|
|
77
|
+
tpl = current_app.jinja_env.get_template(self.template)
|
|
78
|
+
ctx = self.get_render_context(caller, *args, **kwargs)
|
|
79
|
+
return Markup(tpl.render(**ctx)) # do not use render_template() so we don't trigger flask signals
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PropsWrapper:
|
|
83
|
+
def __init__(self, props):
|
|
84
|
+
self._props = props
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name):
|
|
87
|
+
if name in self._props:
|
|
88
|
+
return self._props[name]
|
|
89
|
+
raise Exception(f"No such prop: {name}")
|
|
@@ -5,6 +5,7 @@ def create_widget_class(basecls, cssclass="input"):
|
|
|
5
5
|
class Input(basecls):
|
|
6
6
|
def __call__(self, field, **kwargs):
|
|
7
7
|
extend_class(kwargs, cssclass)
|
|
8
|
+
kwargs = {k.replace('_', '-'): v for k, v in kwargs.items() if v is not None}
|
|
8
9
|
return super().__call__(field, **kwargs)
|
|
9
10
|
return Input
|
|
10
11
|
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
</mj-style>
|
|
22
22
|
{% endblock %}
|
|
23
23
|
</mj-head>
|
|
24
|
-
<mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#
|
|
25
|
-
<mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#
|
|
24
|
+
<mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#ebebebff")}}" width="600px"{% endblock %}>
|
|
25
|
+
<mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#d1d1d1ff")}}" padding-bottom="0">
|
|
26
26
|
<mj-column width="100%">
|
|
27
|
-
<mj-text color="#
|
|
27
|
+
<mj-text color="#474747ff" font-weight="bold" align="center" font-size="16px" letter-spacing="1px">
|
|
28
28
|
{% block brand %}
|
|
29
29
|
{{config.get("SITE_TITLE", "")}}
|
|
30
30
|
{% endblock %}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
<meta charset="utf-8">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
8
|
{% endblock %}
|
|
9
|
-
{%
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
{%
|
|
13
|
-
|
|
9
|
+
{% block page_title %}
|
|
10
|
+
<title>{% if site_title is defined %}{{site_title}}{% else %}{% if page_title is defined %}{{ page_title }}{% if config.get("SITE_TITLE") %} | {% endif %}{% endif %}{% if config.get("SITE_TITLE") %}{{config["SITE_TITLE"]}}{% endif %}{% endif %}</title>
|
|
11
|
+
{% endblock %}
|
|
12
|
+
{% if metadata is defined %}
|
|
13
|
+
<{PageMetadata metadata=metadata}/>
|
|
14
|
+
{% endif %}
|
|
15
|
+
{% asset_tags nonce=csp_nonce %}
|
|
14
16
|
{% block head_assets %}{% endblock %}
|
|
15
17
|
{%- endblock %}
|
|
16
18
|
</head>
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
{% block footer %}{% endblock %}
|
|
26
28
|
{% block flash_messages %}
|
|
27
29
|
{% autoescape false %} {# Jinja bug #}
|
|
28
|
-
<{
|
|
30
|
+
<{FlashMessagesToast}/>
|
|
29
31
|
{% endautoescape %}
|
|
30
32
|
{% endblock %}
|
|
31
33
|
{% block body_end %}{% endblock %}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
from flask_sqlorm import Model as BaseModel
|
|
2
1
|
from flask import current_app
|
|
2
|
+
from flask_sqlorm import Model as BaseModel
|
|
3
|
+
from flask_files import save_file, File
|
|
3
4
|
from sqlorm.resultset import CompositeResultSet
|
|
5
|
+
from sqlorm import SQLType
|
|
4
6
|
from markupsafe import Markup
|
|
5
7
|
import abc
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
File = SQLType("text", save_file, File.from_uri)
|
|
11
|
+
|
|
12
|
+
|
|
8
13
|
class ModelResultSet(CompositeResultSet):
|
|
9
14
|
def __str__(self):
|
|
10
15
|
return Markup("".join([str(item) for item in self]))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from flask_file_routes import DEFAULT_HELPERS
|
|
2
|
-
from flask import
|
|
2
|
+
from flask import current_app
|
|
3
3
|
from .utils.page_actions import page_action
|
|
4
4
|
|
|
5
5
|
|
|
@@ -18,19 +18,11 @@ def form(page):
|
|
|
18
18
|
return form_helper
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def stream(page):
|
|
22
|
-
g.__page_stream__ = True
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
21
|
def action(page):
|
|
27
|
-
|
|
28
|
-
return page_action(func, name)
|
|
29
|
-
return decorator
|
|
22
|
+
return page_action
|
|
30
23
|
|
|
31
24
|
|
|
32
25
|
DEFAULT_HELPERS.update(csrf_protect=csrf_protect,
|
|
33
26
|
forms=forms,
|
|
34
27
|
form=form,
|
|
35
|
-
|
|
36
|
-
action=action)
|
|
28
|
+
action=action)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from flask import request, current_app, g
|
|
2
|
+
from urllib.parse import urlparse, urljoin
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_safe_redirect_url(target):
|
|
7
|
+
host_url = urlparse(request.host_url)
|
|
8
|
+
redirect_url = urlparse(urljoin(request.host_url, target))
|
|
9
|
+
return (
|
|
10
|
+
redirect_url.scheme in ("http", "https")
|
|
11
|
+
and host_url.netloc == redirect_url.netloc
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def respond_with_security_headers(response):
|
|
16
|
+
if current_app.config["CSP_HEADER"] is not False:
|
|
17
|
+
response.headers['X-XSS-Protection'] = '1; mode=block'
|
|
18
|
+
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
19
|
+
response.headers['Content-Security-Policy'] = generate_csp_policy()
|
|
20
|
+
|
|
21
|
+
if current_app.config['HSTS_HEADER']:
|
|
22
|
+
response.headers['Strict-Transport-Security'] = current_app.config['HSTS_HEADER']
|
|
23
|
+
|
|
24
|
+
if current_app.config['REFERRER_POLICY']:
|
|
25
|
+
response.headers['Referrer-Policy'] = current_app.config['REFERRER_POLICY']
|
|
26
|
+
|
|
27
|
+
return response
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_csp_policy():
|
|
31
|
+
policy = current_app.config["CSP_HEADER"]
|
|
32
|
+
if policy is True:
|
|
33
|
+
policy = {
|
|
34
|
+
"default-src": "'safe-src'",
|
|
35
|
+
"manifest-src": "'self'",
|
|
36
|
+
"connect-src": 'https: wss:' if current_app.config['SERVER_SECURED'] else 'http: ws:',
|
|
37
|
+
"img-src": "* data: blob:",
|
|
38
|
+
"media-src": "* data: blob:",
|
|
39
|
+
"frame-src": "* data: blob:",
|
|
40
|
+
"object-src": "* data: blob:"
|
|
41
|
+
}
|
|
42
|
+
policy.setdefault("script-src", policy["default-src"])
|
|
43
|
+
policy.setdefault("style-src", policy["default-src"])
|
|
44
|
+
|
|
45
|
+
if not current_app.config['CSP_FRAME_ANCESTORS']:
|
|
46
|
+
policy["frame-ancestors"] = "'none'"
|
|
47
|
+
elif isinstance(current_app.config['CSP_FRAME_ANCESTORS'], (list, tuple)):
|
|
48
|
+
frame_ancestors = list(current_app.config['CSP_FRAME_ANCESTORS'])
|
|
49
|
+
add_frame_ancestors = True
|
|
50
|
+
for endpoint in current_app.config['CSP_FRAME_ANCESTORS_SAFE_ENDPOINTS']:
|
|
51
|
+
ancestors = []
|
|
52
|
+
if isinstance(endpoint, (list, tuple)):
|
|
53
|
+
endpoint, ancestors = endpoint
|
|
54
|
+
if request.endpoint == endpoint:
|
|
55
|
+
if ancestors:
|
|
56
|
+
frame_ancestors.extend(ancestors)
|
|
57
|
+
else:
|
|
58
|
+
add_frame_ancestors = False
|
|
59
|
+
break
|
|
60
|
+
if add_frame_ancestors:
|
|
61
|
+
policy["frame-ancestors"] = ' '.join(set(frame_ancestors))
|
|
62
|
+
|
|
63
|
+
if current_app.config.get('CSP_UNSAFE_EVAL'):
|
|
64
|
+
policy["script-src"] += " 'unsafe-eval'"
|
|
65
|
+
|
|
66
|
+
unsafe_inline = current_app.debug or current_app.config.get('CSP_UNSAFE_INLINE')
|
|
67
|
+
if unsafe_inline:
|
|
68
|
+
policy["script-src"] += " 'unsafe-inline'"
|
|
69
|
+
policy["style-src"] += " 'unsafe-inline'"
|
|
70
|
+
elif 'csp_nonce' in g:
|
|
71
|
+
policy["style-src"] += f" 'nonce-{g.csp_nonce}'"
|
|
72
|
+
policy["script-src"] += f" 'nonce-{g.csp_nonce}'"
|
|
73
|
+
|
|
74
|
+
safe_srcs = " ".join(set(current_app.config['CSP_SAFE_SRC']))
|
|
75
|
+
return "; ".join(("%s %s" % (k, v.replace("'safe-src'", safe_srcs))).strip() for k, v in policy.items()) + ";"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def csp_nonce():
|
|
79
|
+
if 'csp_nonce' not in g:
|
|
80
|
+
g.csp_nonce = secrets.token_urlsafe(16)[:16]
|
|
81
|
+
return g.csp_nonce
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import htmx from "htmx.org";
|
|
2
|
+
import "htmx-ext-sse";
|
|
3
|
+
import "./htmx-exts";
|
|
4
|
+
import "./app.css";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url', 'hx-stream'];
|
|
7
|
+
document.body.setAttribute('hx-ext', [document.body.getAttribute('hx-ext'), ...DEFAULT_HTMX_EXTS].filter(Boolean).join(","))
|
|
8
|
+
|
|
9
|
+
window.htmx = htmx;
|
|
10
|
+
export { htmx };
|
|
11
|
+
|
|
12
|
+
if (window.Alpine) {
|
|
13
|
+
window.Alpine.start()
|
|
14
|
+
}
|