tina4-python 0.2.201__tar.gz → 0.2.202__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-0.2.201 → tina4_python-0.2.202}/PKG-INFO +19 -3
- {tina4_python-0.2.201 → tina4_python-0.2.202}/pyproject.toml +18 -4
- tina4_python-0.2.202/tina4_python/Auth.py +236 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Messages.py +1 -0
- tina4_python-0.2.201/tina4_python/Auth.py +0 -354
- {tina4_python-0.2.201 → tina4_python-0.2.202}/.gitignore +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/README.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Api.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/CRUD.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Constant.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Database.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/DatabaseResult.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/DatabaseTypes.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Debug.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/DevReload.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Env.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/FieldTypes.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/GraphQL.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Localization.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/MiddleWare.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Migration.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/ORM.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Queue.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Request.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Response.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Router.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/SQLToMongo.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Seeder.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Session.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/ShellColors.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Swagger.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Template.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Testing.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/WSDL.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Webserver.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/Websocket.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/__init__.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/cli.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/messages.pot +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/js/reconnecting-websocket.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/js/tina4.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/js/tina4helper.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.202
|
|
4
4
|
Summary: Tina4Python - This is not another framework for Python
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
Requires-Python: <4.0,>=3.12
|
|
7
7
|
Requires-Dist: asyncer>=0.0.8
|
|
8
8
|
Requires-Dist: bcrypt<5.0.0,>=4.2.1
|
|
9
|
-
Requires-Dist: cryptography<45.0.0,>=44.0.0
|
|
10
9
|
Requires-Dist: hypercorn>=0.18.0
|
|
11
10
|
Requires-Dist: jinja2<4.0.0,>=3.1.5
|
|
12
|
-
Requires-Dist: jurigged>=0.6.0
|
|
13
11
|
Requires-Dist: libsass<0.24.0,>=0.23.0
|
|
14
12
|
Requires-Dist: litequeue<0.10,>=0.9
|
|
15
13
|
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
@@ -17,6 +15,24 @@ Requires-Dist: python-dotenv<2.0.0,>=1.0.1
|
|
|
17
15
|
Requires-Dist: requests>=2.32.5
|
|
18
16
|
Requires-Dist: simple-websocket<2.0.0,>=1.1.0
|
|
19
17
|
Requires-Dist: watchdog<7.0.0,>=6.0.0
|
|
18
|
+
Provides-Extra: all-db
|
|
19
|
+
Requires-Dist: firebird-driver>=1.10.0; extra == 'all-db'
|
|
20
|
+
Requires-Dist: mysql-connector-python>=9.3.0; extra == 'all-db'
|
|
21
|
+
Requires-Dist: psycopg2-binary>=2.9.10; extra == 'all-db'
|
|
22
|
+
Requires-Dist: pymongo>=4.0.0; extra == 'all-db'
|
|
23
|
+
Requires-Dist: pymssql>=2.3.0; extra == 'all-db'
|
|
24
|
+
Provides-Extra: dev-reload
|
|
25
|
+
Requires-Dist: jurigged>=0.6.0; extra == 'dev-reload'
|
|
26
|
+
Provides-Extra: firebird
|
|
27
|
+
Requires-Dist: firebird-driver>=1.10.0; extra == 'firebird'
|
|
28
|
+
Provides-Extra: mongo
|
|
29
|
+
Requires-Dist: pymongo>=4.0.0; extra == 'mongo'
|
|
30
|
+
Provides-Extra: mssql
|
|
31
|
+
Requires-Dist: pymssql>=2.3.0; extra == 'mssql'
|
|
32
|
+
Provides-Extra: mysql
|
|
33
|
+
Requires-Dist: mysql-connector-python>=9.3.0; extra == 'mysql'
|
|
34
|
+
Provides-Extra: postgres
|
|
35
|
+
Requires-Dist: psycopg2-binary>=2.9.10; extra == 'postgres'
|
|
20
36
|
Description-Content-Type: text/markdown
|
|
21
37
|
|
|
22
38
|
# Tina4 Python — This is not a framework
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tina4-python"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.202"
|
|
4
4
|
description = "Tina4Python - This is not another framework for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
|
|
@@ -11,8 +11,7 @@ dependencies = [
|
|
|
11
11
|
"jinja2>=3.1.5,<4.0.0",
|
|
12
12
|
"libsass (>=0.23.0,<0.24.0)",
|
|
13
13
|
"python-dotenv (>=1.0.1,<2.0.0)",
|
|
14
|
-
"pyjwt
|
|
15
|
-
"cryptography (>=44.0.0,<45.0.0)",
|
|
14
|
+
"pyjwt>=2.10.1,<3.0.0",
|
|
16
15
|
"watchdog (>=6.0.0,<7.0.0)",
|
|
17
16
|
"bcrypt (>=4.2.1,<5.0.0)",
|
|
18
17
|
"litequeue (>=0.9,<0.10)",
|
|
@@ -20,11 +19,26 @@ dependencies = [
|
|
|
20
19
|
"asyncer>=0.0.8",
|
|
21
20
|
"hypercorn>=0.18.0",
|
|
22
21
|
"requests>=2.32.5",
|
|
23
|
-
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev-reload = ["jurigged>=0.6.0"]
|
|
26
|
+
postgres = ["psycopg2-binary>=2.9.10"]
|
|
27
|
+
mysql = ["mysql-connector-python>=9.3.0"]
|
|
28
|
+
mssql = ["pymssql>=2.3.0"]
|
|
29
|
+
firebird = ["firebird-driver>=1.10.0"]
|
|
30
|
+
mongo = ["pymongo>=4.0.0"]
|
|
31
|
+
all-db = [
|
|
32
|
+
"psycopg2-binary>=2.9.10",
|
|
33
|
+
"mysql-connector-python>=9.3.0",
|
|
34
|
+
"pymssql>=2.3.0",
|
|
35
|
+
"firebird-driver>=1.10.0",
|
|
36
|
+
"pymongo>=4.0.0",
|
|
24
37
|
]
|
|
25
38
|
|
|
26
39
|
[dependency-groups]
|
|
27
40
|
dev = [
|
|
41
|
+
"jurigged>=0.6.0",
|
|
28
42
|
"flake8>=7.2.0",
|
|
29
43
|
"mkdocs>=1.6.1",
|
|
30
44
|
"mysql-connector-python>=9.3.0",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
"""JWT authentication and password hashing for Tina4.
|
|
8
|
+
|
|
9
|
+
Provides the ``Auth`` class which handles:
|
|
10
|
+
- HS256 JWT token creation and validation using the ``SECRET`` env var
|
|
11
|
+
- Configurable token expiry (default 2 minutes, override via
|
|
12
|
+
``TINA4_TOKEN_LIMIT`` environment variable)
|
|
13
|
+
- Password hashing and verification using bcrypt
|
|
14
|
+
- Payload extraction from valid or expired tokens
|
|
15
|
+
|
|
16
|
+
The framework creates a global ``tina4_python.tina4_auth`` instance at
|
|
17
|
+
startup. Sessions, secured routes, and middleware all delegate to this
|
|
18
|
+
instance for token operations.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
from tina4_python import tina4_auth
|
|
23
|
+
|
|
24
|
+
token = tina4_auth.get_token({"user_id": 42})
|
|
25
|
+
is_valid = tina4_auth.valid(token)
|
|
26
|
+
payload = tina4_auth.get_payload(token)
|
|
27
|
+
hashed = tina4_auth.get_password("secret")
|
|
28
|
+
ok = tina4_auth.check_password("secret", hashed)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__all__ = ["Auth", "AuthJSONSerializer"]
|
|
32
|
+
|
|
33
|
+
import datetime
|
|
34
|
+
import os
|
|
35
|
+
import jwt
|
|
36
|
+
import bcrypt
|
|
37
|
+
from json import JSONEncoder
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuthJSONSerializer(JSONEncoder):
|
|
41
|
+
"""
|
|
42
|
+
Custom JSON encoder used by PyJWT when serializing payload objects.
|
|
43
|
+
|
|
44
|
+
Ensures that ``datetime.datetime`` instances are correctly converted to ISO-8601
|
|
45
|
+
strings so they can be embedded in JWT claims.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def default(self, o):
|
|
49
|
+
if isinstance(o, datetime.datetime):
|
|
50
|
+
return o.isoformat()
|
|
51
|
+
return super().default(o)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Auth:
|
|
55
|
+
"""
|
|
56
|
+
Authentication & authorization helper for Tina4 projects.
|
|
57
|
+
|
|
58
|
+
Handles:
|
|
59
|
+
- Password hashing/verification (bcrypt)
|
|
60
|
+
- Signing JWT tokens with HS256 (SECRET env var)
|
|
61
|
+
- Verifying JWT tokens with HS256
|
|
62
|
+
- Simple API-KEY fallback validation
|
|
63
|
+
|
|
64
|
+
Environment variables used:
|
|
65
|
+
- ``SECRET`` → HMAC-SHA256 signing secret for JWT
|
|
66
|
+
- ``API_KEY`` → optional static API key (checked before JWT)
|
|
67
|
+
- ``TINA4_TOKEN_LIMIT`` → default token lifetime in minutes (default: 2)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
secret: str | None = None
|
|
71
|
+
root_path: str = None
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Password handling (bcrypt)
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
def hash_password(self, text: str) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Generate a bcrypt hash for the given plain-text password.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
text (str): Plain-text password.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
str: bcrypt hash (as UTF-8 string). Safe to store in a database.
|
|
85
|
+
"""
|
|
86
|
+
password_bytes = text.encode("utf-8")
|
|
87
|
+
salt = bcrypt.gensalt()
|
|
88
|
+
return bcrypt.hashpw(password_bytes, salt).decode("utf-8")
|
|
89
|
+
|
|
90
|
+
def check_password(self, password_hash: str, text: str) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Verify a plain-text password against a previously created bcrypt hash.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
password_hash (str): Hash returned by :meth:`hash_password`.
|
|
96
|
+
text (str): Plain-text password to verify.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
bool: ``True`` if the password matches the hash.
|
|
100
|
+
"""
|
|
101
|
+
password_bytes = text.encode("utf-8")
|
|
102
|
+
return bcrypt.checkpw(password_bytes, password_hash.encode("utf-8"))
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Constructor
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
def __init__(self, root_path: str):
|
|
108
|
+
"""
|
|
109
|
+
Initialise the Auth helper.
|
|
110
|
+
|
|
111
|
+
Detects and removes legacy RS256 key files from older Tina4 installs,
|
|
112
|
+
printing a one-time migration notice when found.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
root_path (str): Absolute path to the project root.
|
|
116
|
+
"""
|
|
117
|
+
from tina4_python.Debug import Debug
|
|
118
|
+
from tina4_python import Messages
|
|
119
|
+
|
|
120
|
+
self.root_path = root_path
|
|
121
|
+
self.secret = os.environ.get("SECRET", "{self.secret}")
|
|
122
|
+
if self.secret == "{self.secret}":
|
|
123
|
+
Debug.warning(Messages.MSG_AUTH_NO_SECRET)
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# One-time RS256 → HS256 migration: remove old key files if present
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
secrets_dir = os.path.join(root_path, "secrets")
|
|
129
|
+
legacy_files = [
|
|
130
|
+
os.path.join(secrets_dir, "private.key"),
|
|
131
|
+
os.path.join(secrets_dir, "public.key"),
|
|
132
|
+
os.path.join(secrets_dir, "domain.cert"),
|
|
133
|
+
]
|
|
134
|
+
if any(os.path.isfile(f) for f in legacy_files):
|
|
135
|
+
for f in legacy_files:
|
|
136
|
+
if os.path.isfile(f):
|
|
137
|
+
os.remove(f)
|
|
138
|
+
Debug.warning(Messages.MSG_AUTH_RS256_MIGRATION)
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# JWT creation & verification (HS256)
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
def get_token(self, payload_data: dict, expiry_minutes: int = 0) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Create a signed JWT (HS256) containing the supplied payload.
|
|
146
|
+
|
|
147
|
+
If ``expires`` is not present in ``payload_data`` an expiration claim
|
|
148
|
+
will be added automatically using ``TINA4_TOKEN_LIMIT`` (default 2 minutes)
|
|
149
|
+
or the value supplied via ``expiry_minutes``.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
payload_data (dict): Claims to embed in the token.
|
|
153
|
+
expiry_minutes (int): Override default token lifetime (0 = use env default).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
str: Signed JWT (compact serialization).
|
|
157
|
+
"""
|
|
158
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
159
|
+
|
|
160
|
+
if "expires" not in payload_data:
|
|
161
|
+
token_limit_minutes = int(os.environ.get("TINA4_TOKEN_LIMIT", 2))
|
|
162
|
+
if expiry_minutes != 0:
|
|
163
|
+
token_limit_minutes = expiry_minutes
|
|
164
|
+
expiry_time = now + datetime.timedelta(minutes=token_limit_minutes)
|
|
165
|
+
payload_data["expires"] = expiry_time.isoformat()
|
|
166
|
+
|
|
167
|
+
token = jwt.encode(
|
|
168
|
+
payload=payload_data,
|
|
169
|
+
key=self.secret,
|
|
170
|
+
algorithm="HS256",
|
|
171
|
+
json_encoder=AuthJSONSerializer,
|
|
172
|
+
)
|
|
173
|
+
return token
|
|
174
|
+
|
|
175
|
+
def get_payload(self, token: str) -> dict | None:
|
|
176
|
+
"""
|
|
177
|
+
Decode a JWT and return its payload (without verification of expiry).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
token (str): JWT to decode.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
dict | None: Payload dictionary or ``None`` on invalid signature.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
payload = jwt.decode(
|
|
187
|
+
token,
|
|
188
|
+
key=self.secret,
|
|
189
|
+
algorithms=["HS256"],
|
|
190
|
+
options={"verify_exp": False},
|
|
191
|
+
)
|
|
192
|
+
return payload
|
|
193
|
+
except Exception:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def validate(self, token: str) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Full token validation.
|
|
199
|
+
|
|
200
|
+
Checks:
|
|
201
|
+
1. Optional static ``API_KEY`` environment variable (quick bypass)
|
|
202
|
+
2. HS256 signature using SECRET
|
|
203
|
+
3. Presence and validity of the ``expires`` claim
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
token (str): Bearer token.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
bool: ``True`` if the token is valid and not expired.
|
|
210
|
+
"""
|
|
211
|
+
if os.environ.get("API_KEY") and token == os.environ.get("API_KEY"):
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
payload = jwt.decode(
|
|
216
|
+
token,
|
|
217
|
+
key=self.secret,
|
|
218
|
+
algorithms=["HS256"],
|
|
219
|
+
options={"verify_exp": False},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if "expires" not in payload:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
expiry_time = datetime.datetime.fromisoformat(payload["expires"])
|
|
226
|
+
if expiry_time.tzinfo is None:
|
|
227
|
+
expiry_time = expiry_time.replace(tzinfo=datetime.timezone.utc)
|
|
228
|
+
|
|
229
|
+
return datetime.datetime.now(datetime.timezone.utc) <= expiry_time
|
|
230
|
+
|
|
231
|
+
except Exception: # noqa: BLE001
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def valid(self, token: str) -> bool:
|
|
235
|
+
"""Alias of :meth:`validate`. Kept for backward compatibility."""
|
|
236
|
+
return self.validate(token)
|
|
@@ -60,6 +60,7 @@ MSG_DB_UNIMPLEMENTED = _('Please implement {driver} in Database.py and make a pu
|
|
|
60
60
|
|
|
61
61
|
# --- Auth messages ---
|
|
62
62
|
MSG_AUTH_NO_SECRET = _('No SECRET env var set - using default secret. Set SECRET in your .env for production.')
|
|
63
|
+
MSG_AUTH_RS256_MIGRATION = _('JWT upgraded RS256 → HS256: old key files removed, all existing tokens invalidated. This is a one-time migration.')
|
|
63
64
|
|
|
64
65
|
# --- WebSocket messages ---
|
|
65
66
|
MSG_WS_CREATE_ERROR = _('Error creating Websocket, perhaps you need to install simple_websocket ?')
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Tina4 - This is not a 4ramework.
|
|
3
|
-
# Copy-right 2007 - current Tina4
|
|
4
|
-
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
-
#
|
|
6
|
-
# flake8: noqa: E501
|
|
7
|
-
"""JWT authentication and password hashing for Tina4.
|
|
8
|
-
|
|
9
|
-
Provides the ``Auth`` class which handles:
|
|
10
|
-
- RS256 JWT token creation and validation using auto-generated
|
|
11
|
-
self-signed certificates (stored in the ``cert/`` directory)
|
|
12
|
-
- Configurable token expiry (default 24 hours, override via
|
|
13
|
-
``TINA4_TOKEN_EXPIRES_IN`` environment variable)
|
|
14
|
-
- Password hashing and verification using bcrypt
|
|
15
|
-
- Payload extraction from valid or expired tokens
|
|
16
|
-
|
|
17
|
-
The framework creates a global ``tina4_python.tina4_auth`` instance at
|
|
18
|
-
startup. Sessions, secured routes, and middleware all delegate to this
|
|
19
|
-
instance for token operations.
|
|
20
|
-
|
|
21
|
-
Example::
|
|
22
|
-
|
|
23
|
-
from tina4_python import tina4_auth
|
|
24
|
-
|
|
25
|
-
token = tina4_auth.get_token({"user_id": 42})
|
|
26
|
-
is_valid = tina4_auth.valid(token)
|
|
27
|
-
payload = tina4_auth.get_payload(token)
|
|
28
|
-
hashed = tina4_auth.get_password("secret")
|
|
29
|
-
ok = tina4_auth.check_password("secret", hashed)
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
__all__ = ["Auth", "AuthJSONSerializer"]
|
|
33
|
-
|
|
34
|
-
import datetime
|
|
35
|
-
import os
|
|
36
|
-
import jwt
|
|
37
|
-
import bcrypt
|
|
38
|
-
from json import JSONEncoder
|
|
39
|
-
from cryptography import x509
|
|
40
|
-
from cryptography.x509 import NameOID
|
|
41
|
-
from cryptography.hazmat.primitives import serialization, hashes
|
|
42
|
-
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
43
|
-
from cryptography.hazmat.backends import default_backend
|
|
44
|
-
|
|
45
|
-
class AuthJSONSerializer(JSONEncoder):
|
|
46
|
-
"""
|
|
47
|
-
Custom JSON encoder used by PyJWT when serializing payload objects.
|
|
48
|
-
|
|
49
|
-
Ensures that ``datetime.datetime`` instances are correctly converted to ISO-8601
|
|
50
|
-
strings so they can be embedded in JWT claims.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def default(self, o):
|
|
54
|
-
if isinstance(o, datetime.datetime):
|
|
55
|
-
return o.isoformat()
|
|
56
|
-
return super().default(o)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class Auth:
|
|
60
|
-
"""
|
|
61
|
-
Authentication & authorization helper for Tina4 projects.
|
|
62
|
-
|
|
63
|
-
Handles:
|
|
64
|
-
- Password hashing/verification (bcrypt)
|
|
65
|
-
- Automatic creation and loading of an RSA private/public key pair
|
|
66
|
-
- Self-signed certificate generation for local HTTPS development
|
|
67
|
-
- Signing JWT tokens with RS256 (private key)
|
|
68
|
-
- Verifying JWT tokens with RS256 (public key)
|
|
69
|
-
- Simple API-KEY fallback validation
|
|
70
|
-
|
|
71
|
-
Keys and certificates are stored in ``<root_path>/secrets/``:
|
|
72
|
-
- private.key → encrypted PEM private key
|
|
73
|
-
- public.key → PEM public key (unencrypted)
|
|
74
|
-
- domain.cert → self-signed certificate (for local dev servers)
|
|
75
|
-
|
|
76
|
-
Environment variables used:
|
|
77
|
-
- ``SECRET`` → passphrase used to encrypt the private key
|
|
78
|
-
- ``API_KEY`` → optional static API key (checked before JWT)
|
|
79
|
-
- ``TINA4_TOKEN_LIMIT`` → default token lifetime in minutes (default: 2)
|
|
80
|
-
- Country/State/City/Organization/Domain variables for the cert
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
# ------------------------------------------------------------------
|
|
84
|
-
# Class-level attributes (set in __init__)
|
|
85
|
-
# ------------------------------------------------------------------
|
|
86
|
-
secret: str | None = None # Passphrase for private key encryption
|
|
87
|
-
private_key: str = None # Path to encrypted private key file
|
|
88
|
-
public_key: str = None # Path to public key file
|
|
89
|
-
self_signed: str = None # Path to self-signed cert file
|
|
90
|
-
root_path: str = None # Project root directory
|
|
91
|
-
|
|
92
|
-
# Cached key objects (avoid reloading from disk on every request)
|
|
93
|
-
loaded_private_key = None
|
|
94
|
-
loaded_public_key = None
|
|
95
|
-
|
|
96
|
-
# ------------------------------------------------------------------
|
|
97
|
-
# Password handling (bcrypt)
|
|
98
|
-
# ------------------------------------------------------------------
|
|
99
|
-
def hash_password(self, text: str) -> str:
|
|
100
|
-
"""
|
|
101
|
-
Generate a bcrypt hash for the given plain-text password.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
text (str): Plain-text password.
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
str: bcrypt hash (as UTF-8 string). Safe to store in a database.
|
|
108
|
-
"""
|
|
109
|
-
password_bytes = text.encode("utf-8")
|
|
110
|
-
salt = bcrypt.gensalt()
|
|
111
|
-
return bcrypt.hashpw(password_bytes, salt).decode("utf-8")
|
|
112
|
-
|
|
113
|
-
def check_password(self, password_hash: str, text: str) -> bool:
|
|
114
|
-
"""
|
|
115
|
-
Verify a plain-text password against a previously created bcrypt hash.
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
password_hash (str): Hash returned by :meth:`hash_password`.
|
|
119
|
-
text (str): Plain-text password to verify.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
bool: ``True`` if the password matches the hash.
|
|
123
|
-
"""
|
|
124
|
-
password_bytes = text.encode("utf-8")
|
|
125
|
-
return bcrypt.checkpw(password_bytes, password_hash.encode("utf-8"))
|
|
126
|
-
|
|
127
|
-
# ------------------------------------------------------------------
|
|
128
|
-
# Private / public key loading (with caching)
|
|
129
|
-
# ------------------------------------------------------------------
|
|
130
|
-
def load_private_key(self):
|
|
131
|
-
"""
|
|
132
|
-
Load (and cache) the RSA private key from ``secrets/private.key``.
|
|
133
|
-
|
|
134
|
-
The key is encrypted with the passphrase stored in ``self.secret``
|
|
135
|
-
(which comes from the environment variable ``SECRET``).
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey
|
|
139
|
-
"""
|
|
140
|
-
if self.loaded_private_key:
|
|
141
|
-
return self.loaded_private_key
|
|
142
|
-
|
|
143
|
-
with open(self.private_key, "rb") as f:
|
|
144
|
-
private_key = serialization.load_pem_private_key(
|
|
145
|
-
f.read(),
|
|
146
|
-
password=self.secret.encode(),
|
|
147
|
-
backend=default_backend(),
|
|
148
|
-
)
|
|
149
|
-
self.loaded_private_key = private_key
|
|
150
|
-
return private_key
|
|
151
|
-
|
|
152
|
-
def load_public_key(self):
|
|
153
|
-
"""
|
|
154
|
-
Load (and cache) the RSA public key from ``secrets/public.key``.
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey
|
|
158
|
-
"""
|
|
159
|
-
if self.loaded_public_key:
|
|
160
|
-
return self.loaded_public_key
|
|
161
|
-
|
|
162
|
-
with open(self.public_key, "rb") as f:
|
|
163
|
-
public_key = serialization.load_pem_public_key(
|
|
164
|
-
f.read(), backend=default_backend()
|
|
165
|
-
)
|
|
166
|
-
self.loaded_public_key = public_key
|
|
167
|
-
return public_key
|
|
168
|
-
|
|
169
|
-
# ------------------------------------------------------------------
|
|
170
|
-
# Constructor – creates secrets folder & keys if missing
|
|
171
|
-
# ------------------------------------------------------------------
|
|
172
|
-
def __init__(self, root_path: str):
|
|
173
|
-
"""
|
|
174
|
-
Initialise the Auth helper and ensure cryptographic material exists.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
root_path (str): Absolute path to the project root (where the
|
|
178
|
-
``secrets`` folder will be created).
|
|
179
|
-
"""
|
|
180
|
-
self.root_path = root_path
|
|
181
|
-
self.secret = os.environ.get("SECRET", "{self.secret}")
|
|
182
|
-
if self.secret == "{self.secret}":
|
|
183
|
-
from tina4_python.Debug import Debug
|
|
184
|
-
from tina4_python import Messages
|
|
185
|
-
Debug.warning(Messages.MSG_AUTH_NO_SECRET)
|
|
186
|
-
self.private_key = os.path.join(root_path, "secrets", "private.key")
|
|
187
|
-
self.public_key = os.path.join(root_path, "secrets", "public.key")
|
|
188
|
-
self.self_signed = os.path.join(root_path, "secrets", "domain.cert")
|
|
189
|
-
|
|
190
|
-
# Ensure secrets directory exists
|
|
191
|
-
os.makedirs(os.path.join(root_path, "secrets"), exist_ok=True)
|
|
192
|
-
|
|
193
|
-
# ------------------------------------------------------------------
|
|
194
|
-
# 1. Private key – generate if missing
|
|
195
|
-
# ------------------------------------------------------------------
|
|
196
|
-
if not os.path.isfile(self.private_key):
|
|
197
|
-
private_key = rsa.generate_private_key(
|
|
198
|
-
public_exponent=65537,
|
|
199
|
-
key_size=2048,
|
|
200
|
-
)
|
|
201
|
-
with open(self.private_key, "wb") as f:
|
|
202
|
-
f.write(private_key.private_bytes(
|
|
203
|
-
encoding=serialization.Encoding.PEM,
|
|
204
|
-
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
205
|
-
encryption_algorithm=serialization.BestAvailableEncryption(self.secret.encode()),
|
|
206
|
-
))
|
|
207
|
-
else:
|
|
208
|
-
private_key = self.load_private_key()
|
|
209
|
-
|
|
210
|
-
# ------------------------------------------------------------------
|
|
211
|
-
# 2. Public key – derive from private key if missing
|
|
212
|
-
# ------------------------------------------------------------------
|
|
213
|
-
if not os.path.isfile(self.public_key):
|
|
214
|
-
public_key = private_key.public_key()
|
|
215
|
-
public_pem = public_key.public_bytes(
|
|
216
|
-
encoding=serialization.Encoding.PEM,
|
|
217
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
218
|
-
)
|
|
219
|
-
with open(self.public_key, "wb") as f:
|
|
220
|
-
f.write(public_pem)
|
|
221
|
-
|
|
222
|
-
# ------------------------------------------------------------------
|
|
223
|
-
# 3. Self-signed certificate (useful for local HTTPS servers)
|
|
224
|
-
# ------------------------------------------------------------------
|
|
225
|
-
if not os.path.isfile(self.self_signed):
|
|
226
|
-
subject = issuer = x509.Name(
|
|
227
|
-
[
|
|
228
|
-
x509.NameAttribute(NameOID.COUNTRY_NAME, os.environ.get("COUNTRY", "ZA")),
|
|
229
|
-
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, os.environ.get("STATE", "WESTERN CAPE")),
|
|
230
|
-
x509.NameAttribute(NameOID.LOCALITY_NAME, os.environ.get("CITY", "CAPE TOWN")),
|
|
231
|
-
x509.NameAttribute(NameOID.ORGANIZATION_NAME, os.environ.get("ORGANIZATION", "Tina4")),
|
|
232
|
-
x509.NameAttribute(NameOID.COMMON_NAME, os.environ.get("DOMAIN_NAME", "localhost")),
|
|
233
|
-
]
|
|
234
|
-
)
|
|
235
|
-
cert = (
|
|
236
|
-
x509.CertificateBuilder()
|
|
237
|
-
.subject_name(subject)
|
|
238
|
-
.issuer_name(issuer)
|
|
239
|
-
.public_key(private_key.public_key())
|
|
240
|
-
.serial_number(x509.random_serial_number())
|
|
241
|
-
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
|
|
242
|
-
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=99999))
|
|
243
|
-
.add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False)
|
|
244
|
-
.sign(private_key, hashes.SHA256())
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
with open(self.self_signed, "wb") as f:
|
|
248
|
-
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
249
|
-
|
|
250
|
-
# ------------------------------------------------------------------
|
|
251
|
-
# JWT creation & verification
|
|
252
|
-
# ------------------------------------------------------------------
|
|
253
|
-
def get_token(self, payload_data: dict, expiry_minutes: int = 0) -> str:
|
|
254
|
-
"""
|
|
255
|
-
Create a signed JWT (RS256) containing the supplied payload.
|
|
256
|
-
|
|
257
|
-
If ``expires`` is not present in ``payload_data`` an expiration claim
|
|
258
|
-
will be added automatically using ``TINA4_TOKEN_LIMIT`` (default 2 minutes)
|
|
259
|
-
or the value supplied via ``expiry_minutes``.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
payload_data (dict): Claims to embed in the token.
|
|
263
|
-
expiry_minutes (int): Override default token lifetime (0 = use env default).
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
str: Signed JWT (compact serialization).
|
|
267
|
-
"""
|
|
268
|
-
private_key = self.load_private_key()
|
|
269
|
-
now = datetime.datetime.now(datetime.timezone.utc)
|
|
270
|
-
|
|
271
|
-
if "expires" not in payload_data:
|
|
272
|
-
token_limit_minutes = int(os.environ.get("TINA4_TOKEN_LIMIT", 2))
|
|
273
|
-
if expiry_minutes != 0:
|
|
274
|
-
token_limit_minutes = expiry_minutes
|
|
275
|
-
expiry_time = now + datetime.timedelta(minutes=token_limit_minutes)
|
|
276
|
-
payload_data["expires"] = expiry_time.isoformat()
|
|
277
|
-
|
|
278
|
-
token = jwt.encode(
|
|
279
|
-
payload=payload_data,
|
|
280
|
-
key=private_key,
|
|
281
|
-
algorithm="RS256",
|
|
282
|
-
json_encoder=AuthJSONSerializer,
|
|
283
|
-
)
|
|
284
|
-
return token
|
|
285
|
-
|
|
286
|
-
def get_payload(self, token: str) -> dict | None:
|
|
287
|
-
"""
|
|
288
|
-
Decode a JWT and return its payload (without verification of expiry).
|
|
289
|
-
|
|
290
|
-
Used when you only need the claims and will perform your own validation.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
token (str): JWT to decode.
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
dict | None: Payload dictionary or ``None`` on invalid signature.
|
|
297
|
-
"""
|
|
298
|
-
public_key = self.load_public_key()
|
|
299
|
-
try:
|
|
300
|
-
payload = jwt.decode(token, key=public_key, algorithms=["RS256"])
|
|
301
|
-
return payload
|
|
302
|
-
except Exception:
|
|
303
|
-
return None
|
|
304
|
-
|
|
305
|
-
def validate(self, token: str) -> bool:
|
|
306
|
-
"""
|
|
307
|
-
Full token validation.
|
|
308
|
-
|
|
309
|
-
Checks:
|
|
310
|
-
1. Optional static ``API_KEY`` environment variable (quick bypass)
|
|
311
|
-
2. RS256 signature using the public key
|
|
312
|
-
3. Presence and validity of the ``expires`` claim
|
|
313
|
-
|
|
314
|
-
Args:
|
|
315
|
-
token (str): Bearer token.
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
bool: ``True`` if the token is valid and not expired.
|
|
319
|
-
"""
|
|
320
|
-
# Simple API-KEY fallback (useful for quick internal scripts)
|
|
321
|
-
if os.environ.get("API_KEY") and token == os.environ.get("API_KEY"):
|
|
322
|
-
return True
|
|
323
|
-
|
|
324
|
-
public_key = self.load_public_key()
|
|
325
|
-
try:
|
|
326
|
-
payload = jwt.decode(token, key=public_key, algorithms=["RS256"])
|
|
327
|
-
|
|
328
|
-
if "expires" not in payload:
|
|
329
|
-
return False
|
|
330
|
-
|
|
331
|
-
expiry_time = datetime.datetime.fromisoformat(payload["expires"])
|
|
332
|
-
if expiry_time.tzinfo is None:
|
|
333
|
-
# Treat naive datetime as UTC for backward compatibility
|
|
334
|
-
expiry_time = expiry_time.replace(tzinfo=datetime.timezone.utc)
|
|
335
|
-
|
|
336
|
-
return datetime.datetime.now(datetime.timezone.utc) <= expiry_time
|
|
337
|
-
|
|
338
|
-
except Exception: # noqa: BLE001 – we intentionally catch everything here
|
|
339
|
-
return False
|
|
340
|
-
|
|
341
|
-
# ------------------------------------------------------------------
|
|
342
|
-
# Alias for backward compatibility
|
|
343
|
-
# ------------------------------------------------------------------
|
|
344
|
-
def valid(self, token: str) -> bool:
|
|
345
|
-
"""
|
|
346
|
-
Alias of :meth:`validate`. Kept for older codebases.
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
token (str): Bearer token.
|
|
350
|
-
|
|
351
|
-
Returns:
|
|
352
|
-
bool: ``True`` if token is valid.
|
|
353
|
-
"""
|
|
354
|
-
return self.validate(token)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/js/reconnecting-websocket.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-0.2.201 → tina4_python-0.2.202}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|