hyperflask 0.2.4__tar.gz → 0.3.1__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.4 → hyperflask-0.3.1}/PKG-INFO +10 -12
- {hyperflask-0.2.4 → hyperflask-0.3.1}/README.md +3 -4
- {hyperflask-0.2.4 → hyperflask-0.3.1}/pyproject.toml +8 -9
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/__init__.py +2 -1
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/app.py +64 -14
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/cli.py +22 -9
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/components/core.py +21 -5
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/factory.py +4 -4
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/forms/widgets.py +1 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/layouts/web.html +7 -2
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/model.py +24 -1
- hyperflask-0.3.1/src/hyperflask/security.py +81 -0
- hyperflask-0.3.1/src/hyperflask/static/alpine-csp.js +5 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/alpine.js +1 -1
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/app.js +1 -1
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/htmx-exts.js +32 -14
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/forms.html +1 -1
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/layout.html +1 -1
- hyperflask-0.3.1/src/hyperflask/ui/metadata.html +4 -0
- hyperflask-0.3.1/src/hyperflask/ui/skeleton.html +33 -0
- hyperflask-0.3.1/src/hyperflask/ui/sse.html +22 -0
- hyperflask-0.2.4/src/hyperflask/ui/sse.html +0 -14
- hyperflask-0.2.4/src/hyperflask/utils/models.py +0 -5
- hyperflask-0.2.4/src/hyperflask/utils/request.py +0 -25
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/components/__init__.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/components/alpine.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/components/jsx.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/components/webcomponent.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/layouts/email.mjml +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/page_helpers.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/app.css +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/flash.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/htmx.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/page_actions.py +0 -0
- {hyperflask-0.2.4 → hyperflask-0.3.1}/src/hyperflask/utils/tokens.py +0 -0
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperflask
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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:
|
|
30
|
-
Requires-Dist: jinja-layout~=0.4
|
|
29
|
+
Requires-Dist: jinja-layout~=0.5
|
|
31
30
|
Requires-Dist: jinja-wtforms~=0.2
|
|
32
31
|
Requires-Dist: periodiq~=0.13
|
|
33
32
|
Requires-Dist: python-slugify~=8.0.4
|
|
@@ -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.1"
|
|
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
|
-
"
|
|
36
|
-
"jinja-layout ~=0.4",
|
|
35
|
+
"jinja-layout ~=0.5",
|
|
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
|
|
@@ -31,10 +31,9 @@ from .utils.page_actions import page_action_url
|
|
|
31
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
|
|
|
@@ -73,14 +72,24 @@ class Hyperflask(Flask):
|
|
|
73
72
|
"SESSION_COOKIE_SAMESITE": "Lax",
|
|
74
73
|
# Hyperflask specific
|
|
75
74
|
"LAYOUT": "layouts/default.html",
|
|
75
|
+
"ALPINE": False,
|
|
76
76
|
"STATIC_MODE": static_mode,
|
|
77
|
-
"
|
|
78
|
-
"HTMX_EXT": [
|
|
77
|
+
"ASSETS_INCLUDE_ALPINE": False,
|
|
78
|
+
"HTMX_EXT": [],
|
|
79
79
|
"HTMX_BOOST_SITE": False,
|
|
80
80
|
"MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
|
|
81
81
|
"MARKDOWN_SANITIZER_CONFIG": {},
|
|
82
82
|
"FLASH_TOAST_OOB": True,
|
|
83
|
-
"FLASH_TOAST_REMOVE_AFTER": None
|
|
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
|
|
84
93
|
})
|
|
85
94
|
if config:
|
|
86
95
|
self.config.update(config)
|
|
@@ -93,13 +102,22 @@ class Hyperflask(Flask):
|
|
|
93
102
|
self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
94
103
|
self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
95
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
|
+
|
|
96
114
|
#self.otel = Observability(self)
|
|
97
115
|
|
|
98
116
|
self.jinja_env.add_extension(LayoutExtension)
|
|
99
117
|
self.jinja_env.add_extension(WtformExtension)
|
|
100
118
|
self.jinja_env.add_extension(MarkdownExtension)
|
|
101
119
|
self.jinja_env.filters.update(markdown=jinja_markdown, sanitize=sanitize_html, nl2br=nl2br)
|
|
102
|
-
self.jinja_env.globals.update(page_action_url=page_action_url, app=self)
|
|
120
|
+
self.jinja_env.globals.update(page_action_url=page_action_url, app=self, csp_nonce=csp_nonce)
|
|
103
121
|
self.jinja_env.form_base_cls = Form
|
|
104
122
|
self.jinja_env.default_layout = self.config["LAYOUT"]
|
|
105
123
|
self.forms = self.jinja_env.forms
|
|
@@ -130,7 +148,7 @@ class Hyperflask(Flask):
|
|
|
130
148
|
Geolocation(self)
|
|
131
149
|
Files(self)
|
|
132
150
|
MailTemplates(self, template_folder=emails_folder)
|
|
133
|
-
file_routes = FileRoutes(self, pages_folder=pages_folder,
|
|
151
|
+
file_routes = FileRoutes(self, pages_folder=pages_folder, module_view_class=HyperModuleView)
|
|
134
152
|
self.page_helper = file_routes.page_helper
|
|
135
153
|
self.collections = Collections(self)
|
|
136
154
|
self.sse = MercureSSE(self)
|
|
@@ -153,11 +171,13 @@ class Hyperflask(Flask):
|
|
|
153
171
|
})
|
|
154
172
|
self.assets.bundle(
|
|
155
173
|
{"@hyperflask": ["app.js"],
|
|
156
|
-
"@hyperflask/alpine": ["alpine.js"]},
|
|
174
|
+
"@hyperflask/alpine": ["alpine-csp.js" if not self.config["CSP_UNSAFE_EVAL"] else "alpine.js"]},
|
|
157
175
|
from_package="hyperflask",
|
|
158
176
|
assets_folder="static"
|
|
159
177
|
)
|
|
160
178
|
self.assets.include("@hyperflask", 0)
|
|
179
|
+
if self.config["ALPINE"]:
|
|
180
|
+
self.assets.include("@hyperflask/alpine", 0)
|
|
161
181
|
|
|
162
182
|
self.extensions['mail_templates'].jinja_env.add_extension(LayoutExtension)
|
|
163
183
|
self.extensions['mail_templates'].jinja_env.default_layout = "layout.mjml"
|
|
@@ -179,6 +199,7 @@ class Hyperflask(Flask):
|
|
|
179
199
|
for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
|
|
180
200
|
self.forms.register(self.macros.resolve_template(macro), macro)
|
|
181
201
|
|
|
202
|
+
self.after_request(respond_with_security_headers)
|
|
182
203
|
if self.config["FLASH_TOAST_OOB"]:
|
|
183
204
|
self.after_request(respond_with_flash_messages)
|
|
184
205
|
|
|
@@ -189,10 +210,39 @@ class Hyperflask(Flask):
|
|
|
189
210
|
self.db.Model = Model
|
|
190
211
|
self.db.File = SQLFileType
|
|
191
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
|
+
|
|
192
237
|
def relative_import_name(self, name):
|
|
193
238
|
return f"{self.import_name}.{name}" if self.import_name != "__main__" else name
|
|
194
239
|
|
|
195
240
|
|
|
241
|
+
class HyperModuleView(ModuleView):
|
|
242
|
+
def _render_template(self):
|
|
243
|
+
return render_template(page.template)
|
|
244
|
+
|
|
245
|
+
|
|
196
246
|
def static(func):
|
|
197
247
|
pass
|
|
198
248
|
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
import multiprocessing
|
|
8
8
|
import gunicorn.app.base
|
|
9
9
|
from .factory import load_config, create_app
|
|
10
|
+
from .security import generate_csp_policy
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def _create_app():
|
|
@@ -22,19 +23,26 @@ cli.add_command(shell_command)
|
|
|
22
23
|
cli.add_command(routes_command)
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
@
|
|
26
|
+
@cli.command("init")
|
|
27
|
+
def init_command():
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cli.command("freeze")
|
|
26
32
|
def freeze_command():
|
|
27
33
|
current_app.freezer.freeze()
|
|
28
34
|
|
|
29
|
-
cli.add_command(freeze_command)
|
|
30
|
-
|
|
31
35
|
|
|
32
|
-
@
|
|
36
|
+
@cli.command("build")
|
|
33
37
|
@click.pass_context
|
|
34
38
|
def build_command(ctx):
|
|
35
39
|
ctx.invoke(assets_cli.build)
|
|
40
|
+
ctx.invoke(freeze_command)
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
|
|
43
|
+
@cli.command("csp-header")
|
|
44
|
+
def csp_header_command():
|
|
45
|
+
click.echo(generate_csp_policy())
|
|
38
46
|
|
|
39
47
|
|
|
40
48
|
@click.command("serve")
|
|
@@ -69,8 +77,9 @@ cli.add_command(serve_command)
|
|
|
69
77
|
@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
|
|
70
78
|
@click.option("--port", "-p", default=5000, help="The port to bind to.")
|
|
71
79
|
@click.option('--concurrency')
|
|
80
|
+
@click.option("--extend-procfile")
|
|
72
81
|
@click.option("--dev", is_flag=True)
|
|
73
|
-
def run_command(processes, host, port, concurrency, dev):
|
|
82
|
+
def run_command(processes, host, port, concurrency, extend_procfile, dev):
|
|
74
83
|
from honcho.command import _procfile, _parse_concurrency
|
|
75
84
|
from honcho.manager import Manager
|
|
76
85
|
from honcho.printer import Printer
|
|
@@ -85,14 +94,18 @@ def run_command(processes, host, port, concurrency, dev):
|
|
|
85
94
|
if os.path.exists(filename):
|
|
86
95
|
_processes = _procfile(filename).processes
|
|
87
96
|
break
|
|
88
|
-
if _processes is None:
|
|
89
|
-
|
|
97
|
+
if _processes is None or extend_procfile:
|
|
98
|
+
if not _processes:
|
|
99
|
+
_processes = {}
|
|
100
|
+
_processes.update({
|
|
90
101
|
"web": [sys.argv[0], "serve", "--host", host, "--port", "$PORT"],
|
|
91
102
|
# "worker": [sys.argv[0], "worker"],
|
|
92
103
|
# "scheduler": [sys.argv[0], "periodiq"]
|
|
93
|
-
}
|
|
104
|
+
})
|
|
94
105
|
if dev:
|
|
95
106
|
_processes["assets"] = [sys.argv[0], "assets", "dev"]
|
|
107
|
+
else:
|
|
108
|
+
_processes["mercurehub"] = [sys.executable, "-m", "flask_mercure_sse.server"]
|
|
96
109
|
|
|
97
110
|
processes = {name: " ".join(_processes[name]) if isinstance(_processes[name], list) else _processes[name]
|
|
98
111
|
for name in _processes if name in _processes}
|
|
@@ -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}")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from .app import Hyperflask
|
|
2
1
|
from flask_configurator import Config
|
|
3
2
|
import importlib
|
|
4
3
|
from importlib.metadata import entry_points
|
|
5
4
|
import logging
|
|
6
5
|
import os
|
|
7
|
-
|
|
6
|
+
from .app import Hyperflask
|
|
7
|
+
from .model import init_db_locked
|
|
8
8
|
|
|
9
9
|
app = None
|
|
10
10
|
db = None
|
|
@@ -68,8 +68,8 @@ def create_app(root_path=None):
|
|
|
68
68
|
try_import(app, "signals")
|
|
69
69
|
try_import(app, "app")
|
|
70
70
|
|
|
71
|
-
if models:
|
|
72
|
-
|
|
71
|
+
if models and app.config.get("INIT_DATABASE", True):
|
|
72
|
+
init_db_locked(app)
|
|
73
73
|
|
|
74
74
|
return app
|
|
75
75
|
|
|
@@ -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
|
|
|
@@ -6,8 +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
|
-
{% block page_title %}
|
|
10
|
-
|
|
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 %}
|
|
11
16
|
{% block head_assets %}{% endblock %}
|
|
12
17
|
{%- endblock %}
|
|
13
18
|
</head>
|
|
@@ -1,8 +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
|
|
8
|
+
import os
|
|
9
|
+
import fcntl
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
File = SQLType("text", save_file, File.from_uri)
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
class ModelResultSet(CompositeResultSet):
|
|
@@ -44,3 +51,19 @@ class Model(BaseModel, abc.ABC):
|
|
|
44
51
|
|
|
45
52
|
props = {prop: self} if prop else {}
|
|
46
53
|
return Markup(current_app.macros[macro](**props))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def init_db_locked(app):
|
|
57
|
+
# this is required because multiple app process will start at the same time using the runner
|
|
58
|
+
lockfile = os.path.join(app.root_path, ".initdblock")
|
|
59
|
+
if os.path.exists(lockfile):
|
|
60
|
+
return False
|
|
61
|
+
with open(lockfile, "w") as f:
|
|
62
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
63
|
+
f.write("lock\n")
|
|
64
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
65
|
+
try:
|
|
66
|
+
app.db.init_db()
|
|
67
|
+
finally:
|
|
68
|
+
os.remove(lockfile)
|
|
69
|
+
return True
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@ import "htmx-ext-sse";
|
|
|
3
3
|
import "./htmx-exts";
|
|
4
4
|
import "./app.css";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url'];
|
|
6
|
+
const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url', 'hx-stream'];
|
|
7
7
|
document.body.setAttribute('hx-ext', [document.body.getAttribute('hx-ext'), ...DEFAULT_HTMX_EXTS].filter(Boolean).join(","))
|
|
8
8
|
|
|
9
9
|
window.htmx = htmx;
|