tina4-python 3.8.7__tar.gz → 3.9.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.
- {tina4_python-3.8.7 → tina4_python-3.9.1}/PKG-INFO +1 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/pyproject.toml +1 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/CLAUDE.md +1 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/__init__.py +1 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/auth/__init__.py +3 -3
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/middleware.py +126 -3
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/server.py +2 -2
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/engine.py +20 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue/__init__.py +232 -132
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session/__init__.py +1 -1
- {tina4_python-3.8.7 → tina4_python-3.9.1}/.gitignore +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/README.md +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/Testing.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/events.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/request.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/response.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/router.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1015,7 +1015,7 @@ async def admin_dashboard(request, response):
|
|
|
1015
1015
|
|
|
1016
1016
|
**Rule: Any operation that takes more than ~1 second must use a queue.**
|
|
1017
1017
|
|
|
1018
|
-
Supports:
|
|
1018
|
+
Supports: file (default, zero-config), RabbitMQ, Kafka, MongoDB.
|
|
1019
1019
|
|
|
1020
1020
|
### Producing — enqueue work from a route
|
|
1021
1021
|
|
|
@@ -39,7 +39,7 @@ class Auth:
|
|
|
39
39
|
self.secret = secret or os.environ.get("SECRET", "tina4-default-secret")
|
|
40
40
|
self.algorithm = algorithm
|
|
41
41
|
self.expires_in = expires_in or int(
|
|
42
|
-
os.environ.get("
|
|
42
|
+
os.environ.get("TINA4_TOKEN_LIMIT", "60")
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
# ── JWT ────────────────────────────────────────────────────────
|
|
@@ -184,8 +184,8 @@ class Auth:
|
|
|
184
184
|
|
|
185
185
|
@staticmethod
|
|
186
186
|
def validate_api_key(provided: str) -> bool:
|
|
187
|
-
"""Check a Bearer token against the
|
|
188
|
-
expected = os.environ.get("API_KEY", "")
|
|
187
|
+
"""Check a Bearer token against the TINA4_API_KEY env var (falls back to API_KEY)."""
|
|
188
|
+
expected = os.environ.get("TINA4_API_KEY", os.environ.get("API_KEY", ""))
|
|
189
189
|
if not expected:
|
|
190
190
|
return False
|
|
191
191
|
return hmac.compare_digest(provided, expected)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Tina4 Middleware — CORS
|
|
1
|
+
# Tina4 Middleware — CORS, Rate Limiter, CSRF.
|
|
2
2
|
"""
|
|
3
|
-
Built-in middleware for cross-origin requests and
|
|
3
|
+
Built-in middleware for cross-origin requests, rate limiting, and CSRF protection.
|
|
4
4
|
Zero dependencies — stdlib only.
|
|
5
5
|
|
|
6
|
-
from tina4_python.core.middleware import CorsMiddleware, RateLimiter
|
|
6
|
+
from tina4_python.core.middleware import CorsMiddleware, RateLimiter, CsrfMiddleware
|
|
7
7
|
|
|
8
8
|
CORS is configured via environment variables:
|
|
9
9
|
TINA4_CORS_ORIGINS=* # Allowed origins (* = all)
|
|
@@ -14,9 +14,13 @@ CORS is configured via environment variables:
|
|
|
14
14
|
Rate limiter uses a sliding window in memory:
|
|
15
15
|
TINA4_RATE_LIMIT=100 # Requests per window
|
|
16
16
|
TINA4_RATE_WINDOW=60 # Window in seconds
|
|
17
|
+
|
|
18
|
+
CSRF protection (off by default):
|
|
19
|
+
TINA4_CSRF=true # Enable CSRF token validation
|
|
17
20
|
"""
|
|
18
21
|
import os
|
|
19
22
|
import time
|
|
23
|
+
import logging
|
|
20
24
|
import threading
|
|
21
25
|
|
|
22
26
|
|
|
@@ -185,3 +189,122 @@ class SecurityHeadersMiddleware:
|
|
|
185
189
|
)
|
|
186
190
|
|
|
187
191
|
return request, response
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class CsrfMiddleware:
|
|
195
|
+
"""CSRF token validation middleware.
|
|
196
|
+
|
|
197
|
+
Off by default — only active when TINA4_CSRF=true in .env or when
|
|
198
|
+
registered explicitly via Router.use(CsrfMiddleware).
|
|
199
|
+
|
|
200
|
+
Behaviour:
|
|
201
|
+
- Skips GET, HEAD, OPTIONS requests.
|
|
202
|
+
- Skips routes marked @noauth().
|
|
203
|
+
- Skips requests with a valid Authorization: Bearer header (API clients).
|
|
204
|
+
- Checks request.body["formToken"] then request.headers["X-Form-Token"].
|
|
205
|
+
- Rejects if token found in request.query["formToken"] (log warning, 403).
|
|
206
|
+
- Validates token with Auth.valid_token using SECRET env var.
|
|
207
|
+
- If token payload has session_id, verifies it matches request.session.session_id.
|
|
208
|
+
- Returns 403 with response.error("CSRF_INVALID", ...) on failure.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
_logger = logging.getLogger("tina4.csrf")
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def before_csrf(request, response):
|
|
215
|
+
"""Validate CSRF token before the route handler runs."""
|
|
216
|
+
# Check if CSRF is enabled via env (middleware registration bypasses this)
|
|
217
|
+
csrf_env = os.environ.get("TINA4_CSRF", "true").lower() not in ("false", "0", "no")
|
|
218
|
+
# When registered via Router.use(), this method always runs.
|
|
219
|
+
# The env check is only for auto-activation scenarios.
|
|
220
|
+
|
|
221
|
+
# Skip safe HTTP methods
|
|
222
|
+
method = getattr(request, "method", "GET").upper()
|
|
223
|
+
if method in ("GET", "HEAD", "OPTIONS"):
|
|
224
|
+
return request, response
|
|
225
|
+
|
|
226
|
+
# Skip routes marked @noauth()
|
|
227
|
+
handler = getattr(request, "_handler", None)
|
|
228
|
+
if handler and getattr(handler, "_noauth", False):
|
|
229
|
+
return request, response
|
|
230
|
+
|
|
231
|
+
# Skip requests with valid Bearer token (API clients)
|
|
232
|
+
auth_header = ""
|
|
233
|
+
headers = getattr(request, "headers", {})
|
|
234
|
+
if isinstance(headers, dict):
|
|
235
|
+
auth_header = headers.get("authorization", headers.get("Authorization", ""))
|
|
236
|
+
elif hasattr(headers, "get"):
|
|
237
|
+
auth_header = headers.get("authorization", "")
|
|
238
|
+
|
|
239
|
+
if auth_header.startswith("Bearer "):
|
|
240
|
+
bearer_token = auth_header[7:].strip()
|
|
241
|
+
if bearer_token:
|
|
242
|
+
from tina4_python.auth import Auth as _CsrfAuth
|
|
243
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
244
|
+
auth = _CsrfAuth(secret=secret)
|
|
245
|
+
if auth.valid_token(bearer_token) is not None:
|
|
246
|
+
return request, response
|
|
247
|
+
|
|
248
|
+
# Reject if token is in query string (security risk — log warning)
|
|
249
|
+
query = getattr(request, "params", None) or getattr(request, "query", None) or {}
|
|
250
|
+
if isinstance(query, dict) and query.get("formToken"):
|
|
251
|
+
CsrfMiddleware._logger.warning(
|
|
252
|
+
"CSRF token found in query string — rejected for security. "
|
|
253
|
+
"Use POST body or X-Form-Token header instead."
|
|
254
|
+
)
|
|
255
|
+
return request, response.error(
|
|
256
|
+
"CSRF_INVALID",
|
|
257
|
+
"Form token must not be sent in the URL query string",
|
|
258
|
+
403,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Extract token: body first, then header
|
|
262
|
+
token = None
|
|
263
|
+
body = getattr(request, "body", None) or {}
|
|
264
|
+
if isinstance(body, dict):
|
|
265
|
+
token = body.get("formToken")
|
|
266
|
+
|
|
267
|
+
if not token:
|
|
268
|
+
if isinstance(headers, dict):
|
|
269
|
+
token = headers.get("x-form-token", headers.get("X-Form-Token", ""))
|
|
270
|
+
elif hasattr(headers, "get"):
|
|
271
|
+
token = headers.get("x-form-token", "")
|
|
272
|
+
|
|
273
|
+
if not token:
|
|
274
|
+
return request, response.error(
|
|
275
|
+
"CSRF_INVALID",
|
|
276
|
+
"Invalid or missing form token",
|
|
277
|
+
403,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Validate the token
|
|
281
|
+
from tina4_python.auth import Auth as _CsrfAuth
|
|
282
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
283
|
+
auth = _CsrfAuth(secret=secret)
|
|
284
|
+
payload = auth.valid_token(token)
|
|
285
|
+
|
|
286
|
+
if payload is None:
|
|
287
|
+
return request, response.error(
|
|
288
|
+
"CSRF_INVALID",
|
|
289
|
+
"Invalid or missing form token",
|
|
290
|
+
403,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Session binding — if token has session_id, verify it matches
|
|
294
|
+
token_session_id = payload.get("session_id")
|
|
295
|
+
if token_session_id:
|
|
296
|
+
session = getattr(request, "session", None)
|
|
297
|
+
current_session_id = None
|
|
298
|
+
if session is not None:
|
|
299
|
+
current_session_id = getattr(session, "session_id", None)
|
|
300
|
+
if current_session_id is None and hasattr(session, "get"):
|
|
301
|
+
current_session_id = session.get("session_id")
|
|
302
|
+
|
|
303
|
+
if current_session_id and token_session_id != current_session_id:
|
|
304
|
+
return request, response.error(
|
|
305
|
+
"CSRF_INVALID",
|
|
306
|
+
"Invalid or missing form token",
|
|
307
|
+
403,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return request, response
|
|
@@ -1102,7 +1102,7 @@ def _print_banner(host: str, port: int, server_name: str = "asyncio"):
|
|
|
1102
1102
|
from tina4_python.dotenv import is_truthy
|
|
1103
1103
|
|
|
1104
1104
|
is_debug = is_truthy(os.environ.get("TINA4_DEBUG", ""))
|
|
1105
|
-
log_level = os.environ.get("TINA4_LOG_LEVEL", "
|
|
1105
|
+
log_level = os.environ.get("TINA4_LOG_LEVEL", "error").upper()
|
|
1106
1106
|
display = "localhost" if host in ("0.0.0.0", "::") else host
|
|
1107
1107
|
|
|
1108
1108
|
# Blue color for Python, only when stdout is a TTY
|
|
@@ -1145,7 +1145,7 @@ def run(host: str | None = None, port: int | None = None):
|
|
|
1145
1145
|
|
|
1146
1146
|
# Init logger
|
|
1147
1147
|
is_production = os.environ.get("TINA4_ENV", "development") == "production"
|
|
1148
|
-
log_level = os.environ.get("TINA4_LOG_LEVEL", "
|
|
1148
|
+
log_level = os.environ.get("TINA4_LOG_LEVEL", "error" if not is_production else "error")
|
|
1149
1149
|
Log.init(level=log_level, production=is_production)
|
|
1150
1150
|
|
|
1151
1151
|
# Ensure folders
|
|
@@ -578,7 +578,7 @@ def _wordwrap(text: str, width: int) -> str:
|
|
|
578
578
|
# ── Form Token ─────────────────────────────────────────────────
|
|
579
579
|
|
|
580
580
|
|
|
581
|
-
def _form_token(descriptor: str = "") -> str:
|
|
581
|
+
def _form_token(descriptor: str = "", session_id: str = "") -> str:
|
|
582
582
|
"""Generate a JWT form token and return a hidden input element.
|
|
583
583
|
|
|
584
584
|
Args:
|
|
@@ -586,6 +586,9 @@ def _form_token(descriptor: str = "") -> str:
|
|
|
586
586
|
- Empty or omitted: payload is ``{"type": "form"}``
|
|
587
587
|
- ``"admin_panel"``: payload is ``{"type": "form", "context": "admin_panel"}``
|
|
588
588
|
- ``"checkout|order_123"``: payload is ``{"type": "form", "context": "checkout", "ref": "order_123"}``
|
|
589
|
+
session_id: Optional session ID to bind the token to a specific session.
|
|
590
|
+
When provided, the CSRF middleware will verify the token belongs to
|
|
591
|
+
the same session. If empty, checks ``_form_token_session_id`` global.
|
|
589
592
|
|
|
590
593
|
Returns:
|
|
591
594
|
``<input type="hidden" name="formToken" value="TOKEN">``
|
|
@@ -600,6 +603,11 @@ def _form_token(descriptor: str = "") -> str:
|
|
|
600
603
|
else:
|
|
601
604
|
payload["context"] = descriptor
|
|
602
605
|
|
|
606
|
+
# Include session_id in payload for CSRF session binding
|
|
607
|
+
sid = session_id or _form_token_session_id
|
|
608
|
+
if sid:
|
|
609
|
+
payload["session_id"] = sid
|
|
610
|
+
|
|
603
611
|
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
604
612
|
ttl = int(os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60"))
|
|
605
613
|
auth = _FrondAuth(secret=secret, expires_in=ttl)
|
|
@@ -607,6 +615,17 @@ def _form_token(descriptor: str = "") -> str:
|
|
|
607
615
|
return SafeString(f'<input type="hidden" name="formToken" value="{token}">')
|
|
608
616
|
|
|
609
617
|
|
|
618
|
+
# Module-level session ID holder — set by the server before rendering templates
|
|
619
|
+
# so that form_token() can bind tokens to the current session.
|
|
620
|
+
_form_token_session_id: str = ""
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def set_form_token_session_id(session_id: str) -> None:
|
|
624
|
+
"""Set the session ID used by form_token() for CSRF session binding."""
|
|
625
|
+
global _form_token_session_id
|
|
626
|
+
_form_token_session_id = session_id or ""
|
|
627
|
+
|
|
628
|
+
|
|
610
629
|
# ── Frond Engine ────────────────────────────────────────────────
|
|
611
630
|
|
|
612
631
|
|