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.
Files changed (138) hide show
  1. {tina4_python-3.8.7 → tina4_python-3.9.1}/PKG-INFO +1 -1
  2. {tina4_python-3.8.7 → tina4_python-3.9.1}/pyproject.toml +1 -1
  3. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/CLAUDE.md +1 -1
  4. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/auth/__init__.py +3 -3
  6. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/middleware.py +126 -3
  7. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/server.py +2 -2
  8. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/engine.py +20 -1
  9. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue/__init__.py +232 -132
  10. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session/__init__.py +1 -1
  11. {tina4_python-3.8.7 → tina4_python-3.9.1}/.gitignore +0 -0
  12. {tina4_python-3.8.7 → tina4_python-3.9.1}/README.md +0 -0
  13. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/HtmlElement.py +0 -0
  14. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/Testing.py +0 -0
  15. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/ai/__init__.py +0 -0
  16. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/api/__init__.py +0 -0
  17. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dev_reload.py +0 -0
  41. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/dotenv/__init__.py +0 -0
  42. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/FROND.md +0 -0
  43. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/frond/__init__.py +0 -0
  44. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/messenger/__init__.py +0 -0
  63. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/migration/__init__.py +0 -0
  64. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/migration/runner.py +0 -0
  65. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/__init__.py +0 -0
  66. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/fields.py +0 -0
  67. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/orm/model.py +0 -0
  68. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/css/tina4.css +0 -0
  69. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/css/tina4.min.css +0 -0
  70. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/favicon.ico +0 -0
  71. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/images/logo.svg +0 -0
  72. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  73. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/frond.min.js +0 -0
  74. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  75. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4.min.js +0 -0
  76. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/js/tina4js.min.js +0 -0
  77. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/swagger/index.html +0 -0
  78. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  79. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/query_builder/__init__.py +0 -0
  80. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/__init__.py +0 -0
  81. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/kafka_backend.py +0 -0
  82. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/mongo_backend.py +0 -0
  83. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  84. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/__init__.py +0 -0
  85. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  86. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_badges.scss +0 -0
  87. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  88. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_cards.scss +0 -0
  89. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_forms.scss +0 -0
  90. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_grid.scss +0 -0
  91. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_modals.scss +0 -0
  92. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_nav.scss +0 -0
  93. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_reset.scss +0 -0
  94. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_tables.scss +0 -0
  95. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_typography.scss +0 -0
  96. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  97. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/_variables.scss +0 -0
  98. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/base.scss +0 -0
  99. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/colors.scss +0 -0
  100. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/scss/tina4css/tina4.scss +0 -0
  101. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/seeder/__init__.py +0 -0
  102. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/service/__init__.py +0 -0
  103. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/__init__.py +0 -0
  104. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  105. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/redis_handler.py +0 -0
  106. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/session_handlers/valkey_handler.py +0 -0
  107. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/swagger/__init__.py +0 -0
  108. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/components/crud.twig +0 -0
  109. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  110. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  111. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/python/Dockerfile +0 -0
  112. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  113. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/302.twig +0 -0
  114. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/401.twig +0 -0
  115. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/403.twig +0 -0
  116. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/404.twig +0 -0
  117. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/500.twig +0 -0
  118. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/502.twig +0 -0
  119. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/503.twig +0 -0
  120. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/errors/base.twig +0 -0
  121. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/frontend/README.md +0 -0
  122. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/templates/readme.md +0 -0
  123. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/test_client/__init__.py +0 -0
  124. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  125. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  126. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  127. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  128. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  129. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  130. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  131. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  132. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  133. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  134. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  135. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  136. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/validator/__init__.py +0 -0
  137. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/websocket/__init__.py +0 -0
  138. {tina4_python-3.8.7 → tina4_python-3.9.1}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.8.7
3
+ Version: 3.9.1
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.8.7"
3
+ version = "3.9.1"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -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: litequeue (default/SQLite, zero-config), RabbitMQ, Kafka, MongoDB.
1018
+ Supports: file (default, zero-config), RabbitMQ, Kafka, MongoDB.
1019
1019
 
1020
1020
  ### Producing — enqueue work from a route
1021
1021
 
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.8.3"
11
+ __version__ = "3.9.1"
12
12
 
13
13
  # ── HTTP Constants ──
14
14
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -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("TINA4_TOKEN_EXPIRES_IN", "60")
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 API_KEY env var."""
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 and Rate Limiter.
1
+ # Tina4 Middleware — CORS, Rate Limiter, CSRF.
2
2
  """
3
- Built-in middleware for cross-origin requests and rate limiting.
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", "debug").upper()
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", "debug" if not is_production else "info")
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