tina4-python 3.3.0__tar.gz → 3.6.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.
- {tina4_python-3.3.0 → tina4_python-3.6.0}/PKG-INFO +5 -5
- {tina4_python-3.3.0 → tina4_python-3.6.0}/README.md +4 -4
- {tina4_python-3.3.0 → tina4_python-3.6.0}/pyproject.toml +1 -1
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/CLAUDE.md +6 -7
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/__init__.py +1 -1
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/auth/__init__.py +81 -15
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/request.py +1 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/response.py +4 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/crud/__init__.py +40 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/adapter.py +1 -1
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/connection.py +5 -4
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/firebird.py +3 -3
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/mssql.py +2 -2
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/mysql.py +2 -2
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/odbc.py +3 -3
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/postgres.py +2 -2
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/sqlite.py +2 -2
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/engine.py +3 -3
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +4 -4
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/i18n/__init__.py +3 -1
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/fields.py +1 -1
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/model.py +17 -17
- tina4_python-3.6.0/tina4_python/public/js/tina4js.min.js +47 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/.gitignore +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/Testing.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/events.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/router.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/core/server.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.3.0 → tina4_python-3.6.0}/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.
|
|
3
|
+
Version: 3.6.0
|
|
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
|
|
@@ -412,7 +412,7 @@ db = Database("firebird://SYSDBA:masterkey@localhost:3050//path/to/db")
|
|
|
412
412
|
db = Database("mongodb://localhost:27017/mydb")
|
|
413
413
|
db = Database("odbc://DSN=mydsn")
|
|
414
414
|
|
|
415
|
-
result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20,
|
|
415
|
+
result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, offset=0)
|
|
416
416
|
row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
|
|
417
417
|
db.insert("users", {"name": "Alice", "email": "alice@test.com"})
|
|
418
418
|
db.commit()
|
|
@@ -440,8 +440,8 @@ async def protected(request, response):
|
|
|
440
440
|
from tina4_python.auth import Auth
|
|
441
441
|
|
|
442
442
|
auth = Auth(secret="your-secret")
|
|
443
|
-
token = auth.
|
|
444
|
-
payload = auth.
|
|
443
|
+
token = auth.get_token({"user_id": 42})
|
|
444
|
+
payload = auth.valid_token(token)
|
|
445
445
|
```
|
|
446
446
|
|
|
447
447
|
POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `@noauth()` to make public, `@secured()` to protect GET routes.
|
|
@@ -699,7 +699,7 @@ SECRET=your-jwt-secret
|
|
|
699
699
|
DATABASE_URL=sqlite:///data/app.db
|
|
700
700
|
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
701
701
|
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
702
|
-
|
|
702
|
+
TINA4_LOCALE=en # en, fr, af, zh, ja, es
|
|
703
703
|
TINA4_SESSION_HANDLER=SessionFileHandler
|
|
704
704
|
SWAGGER_TITLE=My API
|
|
705
705
|
```
|
|
@@ -380,7 +380,7 @@ db = Database("firebird://SYSDBA:masterkey@localhost:3050//path/to/db")
|
|
|
380
380
|
db = Database("mongodb://localhost:27017/mydb")
|
|
381
381
|
db = Database("odbc://DSN=mydsn")
|
|
382
382
|
|
|
383
|
-
result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20,
|
|
383
|
+
result = db.fetch("SELECT * FROM users WHERE age > ?", [18], limit=20, offset=0)
|
|
384
384
|
row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
|
|
385
385
|
db.insert("users", {"name": "Alice", "email": "alice@test.com"})
|
|
386
386
|
db.commit()
|
|
@@ -408,8 +408,8 @@ async def protected(request, response):
|
|
|
408
408
|
from tina4_python.auth import Auth
|
|
409
409
|
|
|
410
410
|
auth = Auth(secret="your-secret")
|
|
411
|
-
token = auth.
|
|
412
|
-
payload = auth.
|
|
411
|
+
token = auth.get_token({"user_id": 42})
|
|
412
|
+
payload = auth.valid_token(token)
|
|
413
413
|
```
|
|
414
414
|
|
|
415
415
|
POST/PUT/PATCH/DELETE routes require `Authorization: Bearer <token>` by default. Use `@noauth()` to make public, `@secured()` to protect GET routes.
|
|
@@ -667,7 +667,7 @@ SECRET=your-jwt-secret
|
|
|
667
667
|
DATABASE_URL=sqlite:///data/app.db
|
|
668
668
|
TINA4_DEBUG=true # Enable dev toolbar, error overlay
|
|
669
669
|
TINA4_LOG_LEVEL=ALL # ALL, DEBUG, INFO, WARNING, ERROR
|
|
670
|
-
|
|
670
|
+
TINA4_LOCALE=en # en, fr, af, zh, ja, es
|
|
671
671
|
TINA4_SESSION_HANDLER=SessionFileHandler
|
|
672
672
|
SWAGGER_TITLE=My API
|
|
673
673
|
```
|
|
@@ -221,7 +221,7 @@ Tina4 provides a full toolkit. Before writing custom code, check if the framewor
|
|
|
221
221
|
|------|--------------------------------|
|
|
222
222
|
| Background jobs / async work | `Queue` from `tina4_python.queue` (use `queue.push()`, `queue.consume()`) |
|
|
223
223
|
| HTTP calls to external APIs | `Api` from `tina4_python.api` |
|
|
224
|
-
| JWT tokens & auth | `Auth` from `tina4_python.auth` (
|
|
224
|
+
| JWT tokens & auth | `Auth` from `tina4_python.auth` (get_token, valid_token, get_payload) |
|
|
225
225
|
| Password hashing | `Auth.hash_password()` / `Auth.check_password()` from `tina4_python.auth` |
|
|
226
226
|
| Session management | `Session` from `tina4_python.session` |
|
|
227
227
|
| Database queries & CRUD | `Database` from `tina4_python.database` |
|
|
@@ -480,7 +480,7 @@ Response.add_header("X-Custom", "value")
|
|
|
480
480
|
|
|
481
481
|
## Sessions
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
TINA4_TOKEN_EXPIRES_IN is used to set the session time, recommend 15-60 minutes
|
|
484
484
|
|
|
485
485
|
### Session Backends
|
|
486
486
|
|
|
@@ -775,7 +775,7 @@ row = db.fetch_one("SELECT * FROM users WHERE id = ?", [1])
|
|
|
775
775
|
# Result methods
|
|
776
776
|
result.to_json() # JSON string
|
|
777
777
|
result.to_array() # List of dicts
|
|
778
|
-
result.to_paginate() # Dict with records, count, limit,
|
|
778
|
+
result.to_paginate() # Dict with records, count, limit, offset
|
|
779
779
|
result.to_csv() # CSV string
|
|
780
780
|
|
|
781
781
|
# Transactions
|
|
@@ -1185,7 +1185,7 @@ Tina4 v3 supports translations via JSON files in `src/locales/`.
|
|
|
1185
1185
|
|
|
1186
1186
|
Set the language in `.env`:
|
|
1187
1187
|
```bash
|
|
1188
|
-
|
|
1188
|
+
TINA4_LOCALE=en # Default locale
|
|
1189
1189
|
TINA4_LOCALE_DIR=src/locales # Directory for translation files
|
|
1190
1190
|
```
|
|
1191
1191
|
|
|
@@ -1516,8 +1516,7 @@ Key `.env` settings:
|
|
|
1516
1516
|
# Authentication
|
|
1517
1517
|
SECRET=your-jwt-secret # JWT signing (default uses insecure placeholder)
|
|
1518
1518
|
API_KEY=your-api-key # Static bearer token for API auth
|
|
1519
|
-
|
|
1520
|
-
TINA4_TOKEN_EXPIRES_IN=2 # Alternative token expiry setting
|
|
1519
|
+
TINA4_TOKEN_EXPIRES_IN=60 # Token lifetime in minutes (default: 60)
|
|
1521
1520
|
|
|
1522
1521
|
# Database
|
|
1523
1522
|
DATABASE_URL=sqlite:///app.db # Connection URL (driver://host:port/database)
|
|
@@ -1527,7 +1526,7 @@ DATABASE_PASSWORD= # DB password
|
|
|
1527
1526
|
# Framework
|
|
1528
1527
|
TINA4_DEBUG=true # Enable dev mode (toolbar, live reload, error overlay)
|
|
1529
1528
|
TINA4_LOG_LEVEL=ALL # Log verbosity: ALL, DEBUG, INFO, WARNING, ERROR
|
|
1530
|
-
|
|
1529
|
+
TINA4_LOCALE=en # Language for framework messages (en, fr, af, zh, ja, es)
|
|
1531
1530
|
TINA4_DEFAULT_WEBSERVER=FALSE # Set to TRUE to use Tina4's built-in webserver instead of ASGI
|
|
1532
1531
|
HOST_NAME=localhost:7145
|
|
1533
1532
|
|
|
@@ -10,8 +10,8 @@ No PyJWT, no cryptography package.
|
|
|
10
10
|
payload = auth.valid_token(token)
|
|
11
11
|
|
|
12
12
|
# Legacy aliases also work:
|
|
13
|
-
token = auth.
|
|
14
|
-
payload = auth.
|
|
13
|
+
token = auth.get_token({"user_id": 1, "role": "admin"})
|
|
14
|
+
payload = auth.valid_token(token)
|
|
15
15
|
|
|
16
16
|
hashed = Auth.hash_password("secret123")
|
|
17
17
|
Auth.check_password(hashed, "secret123") # True
|
|
@@ -29,27 +29,38 @@ class Auth:
|
|
|
29
29
|
"""JWT authentication and password hashing — zero dependencies."""
|
|
30
30
|
|
|
31
31
|
def __init__(self, secret: str = None, algorithm: str = "HS256",
|
|
32
|
-
|
|
32
|
+
expires_in: int = None):
|
|
33
|
+
"""
|
|
34
|
+
Args:
|
|
35
|
+
secret: Signing secret (falls back to SECRET env var).
|
|
36
|
+
algorithm: JWT algorithm (default HS256).
|
|
37
|
+
expires_in: Token lifetime in seconds (default 3600).
|
|
38
|
+
"""
|
|
33
39
|
self.secret = secret or os.environ.get("SECRET", "tina4-default-secret")
|
|
34
40
|
self.algorithm = algorithm
|
|
35
|
-
self.
|
|
36
|
-
os.environ.get("
|
|
41
|
+
self.expires_in = expires_in or int(
|
|
42
|
+
os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60")
|
|
37
43
|
)
|
|
38
44
|
|
|
39
45
|
# ── JWT ────────────────────────────────────────────────────────
|
|
40
46
|
|
|
41
|
-
def get_token(self, payload: dict,
|
|
47
|
+
def get_token(self, payload: dict, expires_in: int = None) -> str:
|
|
42
48
|
"""Create a signed JWT token.
|
|
43
49
|
|
|
50
|
+
Args:
|
|
51
|
+
expires_in: Lifetime in minutes (default: self.expires_in).
|
|
52
|
+
|
|
44
53
|
Returns: header.payload.signature
|
|
45
54
|
"""
|
|
46
|
-
|
|
55
|
+
exp_minutes = expires_in if expires_in is not None else self.expires_in
|
|
56
|
+
exp_seconds = exp_minutes * 60
|
|
57
|
+
|
|
47
58
|
header = {"alg": self.algorithm, "typ": "JWT"}
|
|
48
59
|
|
|
49
60
|
claims = dict(payload)
|
|
50
61
|
claims["iat"] = int(time.time())
|
|
51
|
-
if
|
|
52
|
-
claims["exp"] = claims["iat"] +
|
|
62
|
+
if exp_seconds > 0:
|
|
63
|
+
claims["exp"] = claims["iat"] + exp_seconds
|
|
53
64
|
|
|
54
65
|
h = _b64url_encode(json.dumps(header).encode())
|
|
55
66
|
p = _b64url_encode(json.dumps(claims, default=str).encode())
|
|
@@ -88,14 +99,18 @@ class Auth:
|
|
|
88
99
|
except Exception:
|
|
89
100
|
return None
|
|
90
101
|
|
|
91
|
-
def refresh_token(self, token: str,
|
|
92
|
-
"""Validate and issue a fresh token with the same claims.
|
|
102
|
+
def refresh_token(self, token: str, expires_in: int = None) -> str | None:
|
|
103
|
+
"""Validate and issue a fresh token with the same claims.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
expires_in: Lifetime in minutes (default: self.expires_in).
|
|
107
|
+
"""
|
|
93
108
|
payload = self.valid_token(token)
|
|
94
109
|
if payload is None:
|
|
95
110
|
return None
|
|
96
111
|
payload.pop("iat", None)
|
|
97
112
|
payload.pop("exp", None)
|
|
98
|
-
return self.get_token(payload,
|
|
113
|
+
return self.get_token(payload, expires_in=expires_in)
|
|
99
114
|
|
|
100
115
|
def _sign(self, message: str) -> str:
|
|
101
116
|
sig = hmac.new(
|
|
@@ -105,8 +120,37 @@ class Auth:
|
|
|
105
120
|
|
|
106
121
|
# ── Legacy aliases ─────────────────────────────────────────────
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
# get_token and valid_token are the primary names
|
|
124
|
+
|
|
125
|
+
# ── Class-level convenience methods (read SECRET from env) ────
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_token_static(cls, payload: dict, expires_in: int = 60) -> str:
|
|
129
|
+
"""Create a JWT without instantiating Auth — reads SECRET from env."""
|
|
130
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
131
|
+
auth = cls(secret=secret, expires_in=expires_in)
|
|
132
|
+
return auth.get_token(payload)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def valid_token_static(cls, token: str) -> dict | None:
|
|
136
|
+
"""Validate a JWT without instantiating Auth — reads SECRET from env."""
|
|
137
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
138
|
+
auth = cls(secret=secret)
|
|
139
|
+
return auth.valid_token(token)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_payload_static(cls, token: str) -> dict | None:
|
|
143
|
+
"""Decode payload (no validation) without instantiating Auth."""
|
|
144
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
145
|
+
auth = cls(secret=secret)
|
|
146
|
+
return auth.get_payload(token)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def refresh_token_static(cls, token: str, expires_in: int = 60) -> str | None:
|
|
150
|
+
"""Refresh a JWT without instantiating Auth — reads SECRET from env."""
|
|
151
|
+
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
152
|
+
auth = cls(secret=secret, expires_in=expires_in)
|
|
153
|
+
return auth.refresh_token(token)
|
|
110
154
|
|
|
111
155
|
# ── Password Hashing ──────────────────────────────────────────
|
|
112
156
|
|
|
@@ -189,4 +233,26 @@ def _b64url_decode(s: str) -> bytes:
|
|
|
189
233
|
return base64.urlsafe_b64decode(s)
|
|
190
234
|
|
|
191
235
|
|
|
192
|
-
|
|
236
|
+
# ── Module-level convenience functions (use static methods) ────
|
|
237
|
+
|
|
238
|
+
def get_token(payload: dict, expires_in: int = 60) -> str:
|
|
239
|
+
"""Create a JWT — reads SECRET from env. Shortcut for Auth.get_token_static()."""
|
|
240
|
+
return Auth.get_token_static(payload, expires_in=expires_in)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def valid_token(token: str) -> dict | None:
|
|
244
|
+
"""Validate a JWT — reads SECRET from env. Shortcut for Auth.valid_token_static()."""
|
|
245
|
+
return Auth.valid_token_static(token)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def get_payload(token: str) -> dict | None:
|
|
249
|
+
"""Decode JWT payload (no validation). Shortcut for Auth.get_payload_static()."""
|
|
250
|
+
return Auth.get_payload_static(token)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def refresh_token(token: str, expires_in: int = 60) -> str | None:
|
|
254
|
+
"""Refresh a JWT — reads SECRET from env. Shortcut for Auth.refresh_token_static()."""
|
|
255
|
+
return Auth.refresh_token_static(token, expires_in=expires_in)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = ["Auth", "get_token", "valid_token", "get_payload", "refresh_token"]
|
|
@@ -191,6 +191,10 @@ class Response:
|
|
|
191
191
|
|
|
192
192
|
return self.html(f"<pre>Template not found: {template}</pre>", 404)
|
|
193
193
|
|
|
194
|
+
def template(self, template: str, data: dict = None) -> "Response":
|
|
195
|
+
"""Alias for render() — parity with PHP/Node.js naming."""
|
|
196
|
+
return self.render(template, data)
|
|
197
|
+
|
|
194
198
|
def build_headers(self, accept_encoding: str = "") -> list[tuple[bytes, bytes]]:
|
|
195
199
|
"""Build final ASGI headers with compression and ETag."""
|
|
196
200
|
# Compress if applicable
|
|
@@ -39,6 +39,32 @@ class AutoCrud:
|
|
|
39
39
|
# Track registered models for introspection
|
|
40
40
|
_registered: dict[str, type] = {}
|
|
41
41
|
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _build_example(model_class) -> dict:
|
|
44
|
+
"""Build a sample request body from ORM field definitions.
|
|
45
|
+
|
|
46
|
+
Generates a dict with field names as keys and example values
|
|
47
|
+
based on field types, suitable for Swagger request body examples.
|
|
48
|
+
"""
|
|
49
|
+
from datetime import datetime
|
|
50
|
+
|
|
51
|
+
example = {}
|
|
52
|
+
for name, field in model_class._fields.items():
|
|
53
|
+
if field.primary_key and field.auto_increment:
|
|
54
|
+
continue # Skip auto-generated PKs
|
|
55
|
+
ft = field.field_type
|
|
56
|
+
if ft == int:
|
|
57
|
+
example[name] = 0
|
|
58
|
+
elif ft == float:
|
|
59
|
+
example[name] = 0.0
|
|
60
|
+
elif ft == bool:
|
|
61
|
+
example[name] = True
|
|
62
|
+
elif ft == datetime:
|
|
63
|
+
example[name] = "2024-01-01T00:00:00"
|
|
64
|
+
else:
|
|
65
|
+
example[name] = "string"
|
|
66
|
+
return example
|
|
67
|
+
|
|
42
68
|
@staticmethod
|
|
43
69
|
def register(model_class, prefix: str = "/api"):
|
|
44
70
|
"""Register REST endpoints for a single ORM model class.
|
|
@@ -62,6 +88,8 @@ class AutoCrud:
|
|
|
62
88
|
base_path = f"{prefix}/{table}"
|
|
63
89
|
pk_field = model_class._get_pk()
|
|
64
90
|
generated = []
|
|
91
|
+
pretty_name = table.replace("_", " ").title()
|
|
92
|
+
example_body = AutoCrud._build_example(model_class)
|
|
65
93
|
|
|
66
94
|
# ── GET /api/{table} — list with pagination ──────────────
|
|
67
95
|
async def list_handler(request, response, _cls=model_class):
|
|
@@ -82,6 +110,8 @@ class AutoCrud:
|
|
|
82
110
|
|
|
83
111
|
list_handler.__name__ = f"autocrud_list_{table}"
|
|
84
112
|
list_handler.__qualname__ = f"autocrud_list_{table}"
|
|
113
|
+
list_handler._swagger_summary = f"List all {pretty_name}"
|
|
114
|
+
list_handler._swagger_tags = [table]
|
|
85
115
|
Router.add("GET", base_path, list_handler)
|
|
86
116
|
generated.append({"method": "GET", "path": base_path, "table": table})
|
|
87
117
|
|
|
@@ -95,6 +125,8 @@ class AutoCrud:
|
|
|
95
125
|
|
|
96
126
|
get_handler.__name__ = f"autocrud_get_{table}"
|
|
97
127
|
get_handler.__qualname__ = f"autocrud_get_{table}"
|
|
128
|
+
get_handler._swagger_summary = f"Get {pretty_name} by ID"
|
|
129
|
+
get_handler._swagger_tags = [table]
|
|
98
130
|
Router.add("GET", f"{base_path}/{{id}}", get_handler)
|
|
99
131
|
generated.append({"method": "GET", "path": f"{base_path}/{{id}}", "table": table})
|
|
100
132
|
|
|
@@ -114,6 +146,9 @@ class AutoCrud:
|
|
|
114
146
|
create_handler.__name__ = f"autocrud_create_{table}"
|
|
115
147
|
create_handler.__qualname__ = f"autocrud_create_{table}"
|
|
116
148
|
create_handler._noauth = True
|
|
149
|
+
create_handler._swagger_summary = f"Create {pretty_name}"
|
|
150
|
+
create_handler._swagger_tags = [table]
|
|
151
|
+
create_handler._swagger_example = example_body
|
|
117
152
|
Router.add("POST", base_path, create_handler)
|
|
118
153
|
generated.append({"method": "POST", "path": base_path, "table": table})
|
|
119
154
|
|
|
@@ -143,6 +178,9 @@ class AutoCrud:
|
|
|
143
178
|
update_handler.__name__ = f"autocrud_update_{table}"
|
|
144
179
|
update_handler.__qualname__ = f"autocrud_update_{table}"
|
|
145
180
|
update_handler._noauth = True
|
|
181
|
+
update_handler._swagger_summary = f"Update {pretty_name}"
|
|
182
|
+
update_handler._swagger_tags = [table]
|
|
183
|
+
update_handler._swagger_example = example_body
|
|
146
184
|
Router.add("PUT", f"{base_path}/{{id}}", update_handler)
|
|
147
185
|
generated.append({"method": "PUT", "path": f"{base_path}/{{id}}", "table": table})
|
|
148
186
|
|
|
@@ -162,6 +200,8 @@ class AutoCrud:
|
|
|
162
200
|
delete_handler.__name__ = f"autocrud_delete_{table}"
|
|
163
201
|
delete_handler.__qualname__ = f"autocrud_delete_{table}"
|
|
164
202
|
delete_handler._noauth = True
|
|
203
|
+
delete_handler._swagger_summary = f"Delete {pretty_name}"
|
|
204
|
+
delete_handler._swagger_tags = [table]
|
|
165
205
|
Router.add("DELETE", f"{base_path}/{{id}}", delete_handler)
|
|
166
206
|
generated.append({"method": "DELETE", "path": f"{base_path}/{{id}}", "table": table})
|
|
167
207
|
|
|
@@ -285,7 +285,7 @@ class DatabaseAdapter:
|
|
|
285
285
|
)
|
|
286
286
|
|
|
287
287
|
def fetch(self, sql: str, params: list = None,
|
|
288
|
-
limit: int = 20,
|
|
288
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
289
289
|
"""Execute a read query and return multiple rows."""
|
|
290
290
|
raise NotImplementedError
|
|
291
291
|
|
|
@@ -182,20 +182,21 @@ class Database:
|
|
|
182
182
|
return self._adapter.execute_many(sql, params_list)
|
|
183
183
|
|
|
184
184
|
def fetch(self, sql: str, params: list = None,
|
|
185
|
-
limit: int = 20,
|
|
185
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
186
|
+
"""Fetch rows with pagination."""
|
|
186
187
|
if self._cache_enabled:
|
|
187
|
-
key = self._cache_key(sql + f":L{limit}:S{
|
|
188
|
+
key = self._cache_key(sql + f":L{limit}:S{offset}", params)
|
|
188
189
|
cached = self._cache_get(key)
|
|
189
190
|
if cached is not None:
|
|
190
191
|
with self._cache_lock:
|
|
191
192
|
self._cache_hits += 1
|
|
192
193
|
return cached
|
|
193
|
-
result = self._adapter.fetch(sql, params, limit,
|
|
194
|
+
result = self._adapter.fetch(sql, params, limit, offset)
|
|
194
195
|
self._cache_set(key, result)
|
|
195
196
|
with self._cache_lock:
|
|
196
197
|
self._cache_misses += 1
|
|
197
198
|
return result
|
|
198
|
-
return self._adapter.fetch(sql, params, limit,
|
|
199
|
+
return self._adapter.fetch(sql, params, limit, offset)
|
|
199
200
|
|
|
200
201
|
def fetch_one(self, sql: str, params: list = None) -> dict | None:
|
|
201
202
|
if self._cache_enabled:
|
|
@@ -114,7 +114,7 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
114
114
|
)
|
|
115
115
|
|
|
116
116
|
def fetch(self, sql: str, params: list = None,
|
|
117
|
-
limit: int = 20,
|
|
117
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
118
118
|
sql = self._translate_sql(sql)
|
|
119
119
|
cursor = self._conn.cursor()
|
|
120
120
|
|
|
@@ -127,8 +127,8 @@ class FirebirdAdapter(DatabaseAdapter):
|
|
|
127
127
|
total = 0
|
|
128
128
|
|
|
129
129
|
# Apply Firebird pagination — ROWS start TO end
|
|
130
|
-
start =
|
|
131
|
-
end =
|
|
130
|
+
start = offset + 1
|
|
131
|
+
end = offset + limit
|
|
132
132
|
paginated_sql = f"{sql} ROWS {start} TO {end}"
|
|
133
133
|
cursor.execute(paginated_sql, params or [])
|
|
134
134
|
|
|
@@ -102,7 +102,7 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
def fetch(self, sql: str, params: list = None,
|
|
105
|
-
limit: int = 20,
|
|
105
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
106
106
|
sql = self._translate_sql(sql)
|
|
107
107
|
cursor = self._conn.cursor(as_dict=True)
|
|
108
108
|
|
|
@@ -121,7 +121,7 @@ class MSSQLAdapter(DatabaseAdapter):
|
|
|
121
121
|
else:
|
|
122
122
|
paginated_sql = f"{sql} OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
|
|
123
123
|
|
|
124
|
-
paginated_params = tuple(params or []) + (
|
|
124
|
+
paginated_params = tuple(params or []) + (offset, limit)
|
|
125
125
|
cursor.execute(paginated_sql, paginated_params)
|
|
126
126
|
rows = [dict(row) for row in cursor.fetchall()]
|
|
127
127
|
|
|
@@ -91,7 +91,7 @@ class MySQLAdapter(DatabaseAdapter):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
def fetch(self, sql: str, params: list = None,
|
|
94
|
-
limit: int = 20,
|
|
94
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
95
95
|
sql = self._translate_sql(sql)
|
|
96
96
|
cursor = self._conn.cursor(dictionary=True)
|
|
97
97
|
|
|
@@ -105,7 +105,7 @@ class MySQLAdapter(DatabaseAdapter):
|
|
|
105
105
|
|
|
106
106
|
# Apply pagination
|
|
107
107
|
paginated_sql = f"{sql} LIMIT %s OFFSET %s"
|
|
108
|
-
paginated_params = (params or []) + [limit,
|
|
108
|
+
paginated_params = (params or []) + [limit, offset]
|
|
109
109
|
cursor.execute(paginated_sql, paginated_params)
|
|
110
110
|
rows = [dict(row) for row in cursor.fetchall()]
|
|
111
111
|
|
|
@@ -74,7 +74,7 @@ class ODBCAdapter(DatabaseAdapter):
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
def fetch(self, sql: str, params: list = None,
|
|
77
|
-
limit: int = 20,
|
|
77
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
78
78
|
# Count total
|
|
79
79
|
count_sql = f"SELECT COUNT(*) FROM ({sql}) AS _t"
|
|
80
80
|
cursor = self._conn.cursor()
|
|
@@ -86,14 +86,14 @@ class ODBCAdapter(DatabaseAdapter):
|
|
|
86
86
|
|
|
87
87
|
# Apply pagination — use OFFSET/FETCH for ODBC (SQL Server style)
|
|
88
88
|
paginated_sql = f"{sql} OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"
|
|
89
|
-
paginated_params = (params or []) + [
|
|
89
|
+
paginated_params = (params or []) + [offset, limit]
|
|
90
90
|
|
|
91
91
|
try:
|
|
92
92
|
cursor.execute(paginated_sql, paginated_params)
|
|
93
93
|
except Exception:
|
|
94
94
|
# Fallback: try LIMIT/OFFSET for non-SQL Server ODBC sources
|
|
95
95
|
paginated_sql = f"{sql} LIMIT ? OFFSET ?"
|
|
96
|
-
paginated_params = (params or []) + [limit,
|
|
96
|
+
paginated_params = (params or []) + [limit, offset]
|
|
97
97
|
cursor.execute(paginated_sql, paginated_params)
|
|
98
98
|
|
|
99
99
|
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
|
@@ -100,7 +100,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
def fetch(self, sql: str, params: list = None,
|
|
103
|
-
limit: int = 20,
|
|
103
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
104
104
|
import psycopg2.extras
|
|
105
105
|
|
|
106
106
|
sql = self._translate_sql(sql)
|
|
@@ -117,7 +117,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
117
117
|
|
|
118
118
|
# Apply pagination
|
|
119
119
|
paginated_sql = f"{sql} LIMIT %s OFFSET %s"
|
|
120
|
-
paginated_params = (params or []) + [limit,
|
|
120
|
+
paginated_params = (params or []) + [limit, offset]
|
|
121
121
|
cursor.execute(paginated_sql, paginated_params)
|
|
122
122
|
rows = [dict(row) for row in cursor.fetchall()]
|
|
123
123
|
|
|
@@ -93,7 +93,7 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
def fetch(self, sql: str, params: list = None,
|
|
96
|
-
limit: int = 20,
|
|
96
|
+
limit: int = 20, offset: int = 0) -> DatabaseResult:
|
|
97
97
|
# Count total rows (without LIMIT/OFFSET)
|
|
98
98
|
count_sql = f"SELECT COUNT(*) as cnt FROM ({sql})"
|
|
99
99
|
try:
|
|
@@ -103,7 +103,7 @@ class SQLiteAdapter(DatabaseAdapter):
|
|
|
103
103
|
|
|
104
104
|
# Apply pagination
|
|
105
105
|
paginated_sql = f"{sql} LIMIT ? OFFSET ?"
|
|
106
|
-
paginated_params = (params or []) + [limit,
|
|
106
|
+
paginated_params = (params or []) + [limit, offset]
|
|
107
107
|
cursor = self._conn.execute(paginated_sql, paginated_params)
|
|
108
108
|
rows = [dict(row) for row in cursor.fetchall()]
|
|
109
109
|
|
|
@@ -599,9 +599,9 @@ def _form_token(descriptor: str = "") -> str:
|
|
|
599
599
|
payload["context"] = descriptor
|
|
600
600
|
|
|
601
601
|
secret = os.environ.get("SECRET", "tina4-default-secret")
|
|
602
|
-
ttl = int(os.environ.get("
|
|
603
|
-
auth = _FrondAuth(secret=secret,
|
|
604
|
-
token = auth.
|
|
602
|
+
ttl = int(os.environ.get("TINA4_TOKEN_EXPIRES_IN", "60"))
|
|
603
|
+
auth = _FrondAuth(secret=secret, expires_in=ttl)
|
|
604
|
+
token = auth.get_token(payload)
|
|
605
605
|
return SafeString(f'<input type="hidden" name="formToken" value="{token}">')
|
|
606
606
|
|
|
607
607
|
|
{tina4_python-3.3.0 → tina4_python-3.6.0}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py
RENAMED
|
@@ -49,9 +49,9 @@ async def gallery_auth_page(request, response):
|
|
|
49
49
|
<div class="card-body">
|
|
50
50
|
<h6 style="color:#e2e8f0;">How it works</h6>
|
|
51
51
|
<pre style="background:#0f172a;color:#4ade80;padding:1rem;border-radius:0.5rem;font-size:0.8rem;"><code>auth = Auth()
|
|
52
|
-
token = auth.
|
|
52
|
+
token = auth.get_token({"username": "admin"})
|
|
53
53
|
payload = auth.get_payload(token)
|
|
54
|
-
is_valid = auth.
|
|
54
|
+
is_valid = auth.valid_token(token)</code></pre>
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
@@ -107,7 +107,7 @@ async def gallery_login(request, response):
|
|
|
107
107
|
password = body.get("password", "")
|
|
108
108
|
if username and password:
|
|
109
109
|
auth = Auth()
|
|
110
|
-
token = auth.
|
|
110
|
+
token = auth.get_token({"username": username, "role": "user"})
|
|
111
111
|
return response({"token": token, "message": f"Welcome {username}!"})
|
|
112
112
|
return response({"error": "Username and password required"}, 401)
|
|
113
113
|
|
|
@@ -117,5 +117,5 @@ async def gallery_login(request, response):
|
|
|
117
117
|
async def gallery_verify(request, response):
|
|
118
118
|
token = request.params.get("token", "")
|
|
119
119
|
auth = Auth()
|
|
120
|
-
is_valid = auth.
|
|
120
|
+
is_valid = auth.valid_token(token)
|
|
121
121
|
return response({"valid": is_valid})
|
|
@@ -24,7 +24,9 @@ class I18n:
|
|
|
24
24
|
self._locale_dir = Path(
|
|
25
25
|
locale_dir or os.environ.get("TINA4_LOCALE_DIR", "src/locales")
|
|
26
26
|
)
|
|
27
|
-
self._default_locale = default_locale or os.environ.get(
|
|
27
|
+
self._default_locale = default_locale or os.environ.get(
|
|
28
|
+
"TINA4_LOCALE", "en"
|
|
29
|
+
)
|
|
28
30
|
self._current_locale = self._default_locale
|
|
29
31
|
self._translations: dict[str, dict] = {}
|
|
30
32
|
self._load_locale(self._default_locale)
|
|
@@ -232,7 +232,7 @@ class HasManyDescriptor(RelationshipDescriptor):
|
|
|
232
232
|
table = related_cls._get_table()
|
|
233
233
|
db = obj._get_db()
|
|
234
234
|
sql = f"SELECT * FROM {table} WHERE {fk} = ?"
|
|
235
|
-
result = db.fetch(sql, [pk_value], limit=1000,
|
|
235
|
+
result = db.fetch(sql, [pk_value], limit=1000, offset=0)
|
|
236
236
|
return [related_cls(row) for row in result.records]
|
|
237
237
|
|
|
238
238
|
|