tortoise-auth 0.2.0__py3-none-any.whl

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 (41) hide show
  1. tortoise_auth/__init__.py +65 -0
  2. tortoise_auth/config.py +101 -0
  3. tortoise_auth/events.py +69 -0
  4. tortoise_auth/exceptions.py +73 -0
  5. tortoise_auth/hashers/__init__.py +61 -0
  6. tortoise_auth/hashers/argon2.py +20 -0
  7. tortoise_auth/hashers/bcrypt.py +11 -0
  8. tortoise_auth/hashers/pbkdf2.py +61 -0
  9. tortoise_auth/integrations/__init__.py +1 -0
  10. tortoise_auth/integrations/starlette.py +142 -0
  11. tortoise_auth/models/__init__.py +10 -0
  12. tortoise_auth/models/api_keys.py +0 -0
  13. tortoise_auth/models/base.py +85 -0
  14. tortoise_auth/models/jwt_blacklist.py +37 -0
  15. tortoise_auth/models/rate_limit.py +0 -0
  16. tortoise_auth/models/totp.py +0 -0
  17. tortoise_auth/py.typed +0 -0
  18. tortoise_auth/rate_limit/__init__.py +0 -0
  19. tortoise_auth/rate_limit/database.py +0 -0
  20. tortoise_auth/rate_limit/memory.py +0 -0
  21. tortoise_auth/services/__init__.py +5 -0
  22. tortoise_auth/services/api_keys.py +0 -0
  23. tortoise_auth/services/auth.py +151 -0
  24. tortoise_auth/services/password.py +0 -0
  25. tortoise_auth/services/registration.py +0 -0
  26. tortoise_auth/services/totp.py +0 -0
  27. tortoise_auth/signing.py +97 -0
  28. tortoise_auth/tokens/__init__.py +49 -0
  29. tortoise_auth/tokens/api_keys.py +0 -0
  30. tortoise_auth/tokens/jwt.py +211 -0
  31. tortoise_auth/utils.py +35 -0
  32. tortoise_auth/validators/__init__.py +54 -0
  33. tortoise_auth/validators/common.py +30 -0
  34. tortoise_auth/validators/common_passwords.txt +19640 -0
  35. tortoise_auth/validators/length.py +22 -0
  36. tortoise_auth/validators/numeric.py +16 -0
  37. tortoise_auth/validators/similarity.py +45 -0
  38. tortoise_auth-0.2.0.dist-info/METADATA +30 -0
  39. tortoise_auth-0.2.0.dist-info/RECORD +41 -0
  40. tortoise_auth-0.2.0.dist-info/WHEEL +4 -0
  41. tortoise_auth-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,65 @@
1
+ """Async authentication and user management for Tortoise ORM."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ from tortoise_auth.config import AuthConfig, configure, get_config
6
+ from tortoise_auth.events import emit, emitter, on
7
+ from tortoise_auth.exceptions import (
8
+ AuthenticationError,
9
+ BadSignatureError,
10
+ ConfigurationError,
11
+ EventError,
12
+ InvalidHashError,
13
+ InvalidPasswordError,
14
+ SignatureExpiredError,
15
+ SigningError,
16
+ TokenError,
17
+ TokenExpiredError,
18
+ TokenInvalidError,
19
+ TokenRevokedError,
20
+ TortoiseAuthError,
21
+ )
22
+ from tortoise_auth.models import (
23
+ AbstractUser,
24
+ BlacklistedToken,
25
+ OutstandingToken,
26
+ )
27
+ from tortoise_auth.services import AuthService
28
+ from tortoise_auth.signing import Signer, TimestampSigner, make_token, verify_token
29
+ from tortoise_auth.tokens import AuthResult, TokenBackend, TokenPair, TokenPayload
30
+ from tortoise_auth.tokens.jwt import JWTBackend
31
+
32
+ __all__ = [
33
+ "AbstractUser",
34
+ "AuthConfig",
35
+ "AuthResult",
36
+ "AuthService",
37
+ "AuthenticationError",
38
+ "BadSignatureError",
39
+ "BlacklistedToken",
40
+ "ConfigurationError",
41
+ "EventError",
42
+ "InvalidHashError",
43
+ "InvalidPasswordError",
44
+ "JWTBackend",
45
+ "OutstandingToken",
46
+ "SignatureExpiredError",
47
+ "Signer",
48
+ "SigningError",
49
+ "TimestampSigner",
50
+ "TokenBackend",
51
+ "TokenError",
52
+ "TokenExpiredError",
53
+ "TokenInvalidError",
54
+ "TokenPair",
55
+ "TokenPayload",
56
+ "TokenRevokedError",
57
+ "TortoiseAuthError",
58
+ "configure",
59
+ "emit",
60
+ "emitter",
61
+ "get_config",
62
+ "make_token",
63
+ "on",
64
+ "verify_token",
65
+ ]
@@ -0,0 +1,101 @@
1
+ """Global configuration for tortoise-auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ from tortoise_auth.exceptions import ConfigurationError
9
+ from tortoise_auth.hashers import default_password_hash
10
+ from tortoise_auth.validators.common import CommonPasswordValidator
11
+ from tortoise_auth.validators.length import MinimumLengthValidator
12
+ from tortoise_auth.validators.numeric import NumericPasswordValidator
13
+ from tortoise_auth.validators.similarity import UserAttributeSimilarityValidator
14
+
15
+ if TYPE_CHECKING:
16
+ from pwdlib import PasswordHash
17
+
18
+ from tortoise_auth.validators import PasswordValidator
19
+
20
+
21
+ def _default_validators() -> list[PasswordValidator]:
22
+ return [
23
+ MinimumLengthValidator(),
24
+ CommonPasswordValidator(),
25
+ NumericPasswordValidator(),
26
+ UserAttributeSimilarityValidator(),
27
+ ]
28
+
29
+
30
+ @dataclass
31
+ class AuthConfig:
32
+ """Configuration for tortoise-auth."""
33
+
34
+ user_model: str = ""
35
+
36
+ # Hasher parameters
37
+ argon2_time_cost: int = 3
38
+ argon2_memory_cost: int = 65536
39
+ argon2_parallelism: int = 4
40
+ bcrypt_rounds: int = 12
41
+ pbkdf2_iterations: int = 600_000
42
+
43
+ # Validators
44
+ password_validators: list[PasswordValidator] = field(default_factory=_default_validators)
45
+
46
+ # Token settings
47
+ access_token_lifetime: int = 900 # 15 minutes
48
+ refresh_token_lifetime: int = 604_800 # 7 days
49
+
50
+ # Password limits
51
+ max_password_length: int = 4096
52
+
53
+ # Signing (HMAC)
54
+ signing_secret: str = ""
55
+ signing_token_lifetime: int = 86_400 # 24 hours
56
+
57
+ # JWT settings
58
+ jwt_secret: str = ""
59
+ jwt_algorithm: str = "HS256"
60
+ jwt_issuer: str = ""
61
+ jwt_audience: str = ""
62
+ jwt_blacklist_enabled: bool = False
63
+
64
+ def validate(self) -> None:
65
+ """Validate config. Raises ConfigurationError."""
66
+ if self.access_token_lifetime <= 0:
67
+ raise ConfigurationError("access_token_lifetime must be positive")
68
+ if self.refresh_token_lifetime <= 0:
69
+ raise ConfigurationError("refresh_token_lifetime must be positive")
70
+
71
+ @property
72
+ def effective_signing_secret(self) -> str:
73
+ """Return signing_secret."""
74
+ return self.signing_secret
75
+
76
+ def get_password_hash(self) -> PasswordHash:
77
+ """Build a PasswordHash instance from current config."""
78
+ return default_password_hash(
79
+ argon2_time_cost=self.argon2_time_cost,
80
+ argon2_memory_cost=self.argon2_memory_cost,
81
+ argon2_parallelism=self.argon2_parallelism,
82
+ bcrypt_rounds=self.bcrypt_rounds,
83
+ pbkdf2_iterations=self.pbkdf2_iterations,
84
+ )
85
+
86
+
87
+ _config: AuthConfig | None = None
88
+
89
+
90
+ def configure(config: AuthConfig) -> None:
91
+ """Set the global auth configuration."""
92
+ global _config
93
+ _config = config
94
+
95
+
96
+ def get_config() -> AuthConfig:
97
+ """Get the global auth configuration, creating a default if needed."""
98
+ global _config
99
+ if _config is None:
100
+ _config = AuthConfig()
101
+ return _config
@@ -0,0 +1,69 @@
1
+ """Async event emitter for tortoise-auth lifecycle hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections import defaultdict
7
+ from collections.abc import Callable, Coroutine
8
+ from typing import Any
9
+
10
+ from tortoise_auth.exceptions import EventError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ Handler = Callable[..., Coroutine[Any, Any, Any]]
15
+
16
+
17
+ class EventEmitter:
18
+ """Simple async event emitter with sequential handler execution."""
19
+
20
+ def __init__(self, *, propagate_errors: bool = False) -> None:
21
+ self.propagate_errors = propagate_errors
22
+ self._listeners: dict[str, list[Handler]] = defaultdict(list)
23
+
24
+ def on(self, event_name: str) -> Callable[[Handler], Handler]:
25
+ """Decorator to register a handler for an event."""
26
+
27
+ def decorator(handler: Handler) -> Handler:
28
+ self.add_listener(event_name, handler)
29
+ return handler
30
+
31
+ return decorator
32
+
33
+ def add_listener(self, event_name: str, handler: Handler) -> None:
34
+ """Register a handler for an event."""
35
+ self._listeners[event_name].append(handler)
36
+
37
+ def remove_listener(self, event_name: str, handler: Handler) -> None:
38
+ """Remove a handler for an event."""
39
+ self._listeners[event_name].remove(handler)
40
+
41
+ async def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None:
42
+ """Emit an event, calling all registered handlers sequentially."""
43
+ for handler in self._listeners[event_name][:]:
44
+ try:
45
+ await handler(*args, **kwargs)
46
+ except Exception as exc:
47
+ error = EventError(event_name, handler.__name__, exc)
48
+ if self.propagate_errors:
49
+ raise error from exc
50
+ logger.error(str(error))
51
+
52
+ def listeners(self, event_name: str) -> list[Handler]:
53
+ """Return a copy of the listener list for an event."""
54
+ return list(self._listeners[event_name])
55
+
56
+ def clear(self, event_name: str | None = None) -> None:
57
+ """Clear listeners. If event_name is None, clear all."""
58
+ if event_name is None:
59
+ self._listeners.clear()
60
+ else:
61
+ self._listeners.pop(event_name, None)
62
+
63
+
64
+ # Module-level emitter instance and convenience aliases
65
+ emitter = EventEmitter()
66
+ on = emitter.on
67
+ emit = emitter.emit
68
+ add_listener = emitter.add_listener
69
+ remove_listener = emitter.remove_listener
@@ -0,0 +1,73 @@
1
+ """Exception classes for tortoise-auth."""
2
+
3
+
4
+ class TortoiseAuthError(Exception):
5
+ """Base exception for all tortoise-auth errors."""
6
+
7
+
8
+ class AuthenticationError(TortoiseAuthError):
9
+ """Raised when authentication fails."""
10
+
11
+
12
+ class InvalidPasswordError(TortoiseAuthError):
13
+ """Raised when password validation fails.
14
+
15
+ Collects all validation errors before raising.
16
+ """
17
+
18
+ def __init__(self, errors: list[str]) -> None:
19
+ self.errors = errors
20
+ super().__init__("; ".join(errors))
21
+
22
+
23
+ class InvalidHashError(TortoiseAuthError):
24
+ """Raised when a password hash is not recognized by any hasher."""
25
+
26
+ def __init__(self, hash: str) -> None:
27
+ self.hash = hash
28
+ super().__init__(f"Unknown password hashing algorithm for hash: {hash!r}")
29
+
30
+
31
+ class ConfigurationError(TortoiseAuthError):
32
+ """Raised when configuration is invalid."""
33
+
34
+
35
+ class EventError(TortoiseAuthError):
36
+ """Raised when an event handler fails."""
37
+
38
+ def __init__(self, event_name: str, handler_name: str, original: Exception) -> None:
39
+ self.event_name = event_name
40
+ self.handler_name = handler_name
41
+ self.original = original
42
+ super().__init__(
43
+ f"Handler {handler_name!r} for event {event_name!r} "
44
+ f"raised {type(original).__name__}: {original}"
45
+ )
46
+
47
+
48
+ class TokenError(TortoiseAuthError):
49
+ """Raised when a token operation fails."""
50
+
51
+
52
+ class TokenExpiredError(TokenError):
53
+ """Raised when a token has expired."""
54
+
55
+
56
+ class TokenInvalidError(TokenError):
57
+ """Raised when a token is structurally invalid or cannot be decoded."""
58
+
59
+
60
+ class TokenRevokedError(TokenError):
61
+ """Raised when a revoked token is presented."""
62
+
63
+
64
+ class SigningError(TortoiseAuthError):
65
+ """Raised when token signing or verification fails."""
66
+
67
+
68
+ class SignatureExpiredError(SigningError):
69
+ """Raised when a signed token has expired."""
70
+
71
+
72
+ class BadSignatureError(SigningError):
73
+ """Raised when a signed token has an invalid signature."""
@@ -0,0 +1,61 @@
1
+ """Password hashing utilities for tortoise-auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pwdlib import PasswordHash
6
+ from pwdlib.hashers import HasherProtocol
7
+ from pwdlib.hashers.argon2 import Argon2Hasher
8
+ from pwdlib.hashers.bcrypt import BcryptHasher
9
+
10
+ from tortoise_auth.hashers.argon2 import default_hasher as _argon2_default
11
+ from tortoise_auth.hashers.bcrypt import default_hasher as _bcrypt_default
12
+ from tortoise_auth.hashers.pbkdf2 import PBKDF2Hasher
13
+
14
+
15
+ def default_password_hash(
16
+ *,
17
+ argon2_time_cost: int = 3,
18
+ argon2_memory_cost: int = 65536,
19
+ argon2_parallelism: int = 4,
20
+ bcrypt_rounds: int = 12,
21
+ pbkdf2_iterations: int = 600_000,
22
+ ) -> PasswordHash:
23
+ """Create a PasswordHash with all supported hashers.
24
+
25
+ Argon2 is the primary hasher; Bcrypt and PBKDF2 are kept for migration.
26
+ """
27
+ return PasswordHash([
28
+ _argon2_default(
29
+ time_cost=argon2_time_cost,
30
+ memory_cost=argon2_memory_cost,
31
+ parallelism=argon2_parallelism,
32
+ ),
33
+ _bcrypt_default(rounds=bcrypt_rounds),
34
+ PBKDF2Hasher(iterations=pbkdf2_iterations),
35
+ ])
36
+
37
+
38
+ def make_password(password: str) -> str:
39
+ """Hash a password using the primary hasher (Argon2)."""
40
+ return default_password_hash().hash(password)
41
+
42
+
43
+ def check_password(password: str, hashed: str) -> tuple[bool, str | None]:
44
+ """Verify a password and return (valid, updated_hash).
45
+
46
+ If the hash was made with a non-primary hasher or outdated params,
47
+ ``updated_hash`` contains the re-hashed value for transparent migration.
48
+ """
49
+ return default_password_hash().verify_and_update(password, hashed)
50
+
51
+
52
+ __all__ = [
53
+ "Argon2Hasher",
54
+ "BcryptHasher",
55
+ "HasherProtocol",
56
+ "PBKDF2Hasher",
57
+ "PasswordHash",
58
+ "check_password",
59
+ "default_password_hash",
60
+ "make_password",
61
+ ]
@@ -0,0 +1,20 @@
1
+ """Argon2 password hasher (wraps pwdlib)."""
2
+
3
+ from pwdlib.hashers.argon2 import Argon2Hasher
4
+
5
+
6
+ def default_hasher(
7
+ *,
8
+ time_cost: int = 3,
9
+ memory_cost: int = 65536,
10
+ parallelism: int = 4,
11
+ ) -> Argon2Hasher:
12
+ """Create an Argon2Hasher with OWASP-recommended defaults."""
13
+ return Argon2Hasher(
14
+ time_cost=time_cost,
15
+ memory_cost=memory_cost,
16
+ parallelism=parallelism,
17
+ )
18
+
19
+
20
+ __all__ = ["Argon2Hasher", "default_hasher"]
@@ -0,0 +1,11 @@
1
+ """Bcrypt password hasher (wraps pwdlib)."""
2
+
3
+ from pwdlib.hashers.bcrypt import BcryptHasher
4
+
5
+
6
+ def default_hasher(*, rounds: int = 12) -> BcryptHasher:
7
+ """Create a BcryptHasher with OWASP-recommended defaults."""
8
+ return BcryptHasher(rounds=rounds)
9
+
10
+
11
+ __all__ = ["BcryptHasher", "default_hasher"]
@@ -0,0 +1,61 @@
1
+ """PBKDF2-SHA256 password hasher (Django-compatible format)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import os
9
+
10
+ _DEFAULT_ITERATIONS = 600_000
11
+
12
+
13
+ class PBKDF2Hasher:
14
+ """PBKDF2-SHA256 hasher implementing pwdlib's HasherProtocol.
15
+
16
+ Hash format: ``pbkdf2_sha256$iterations$salt_b64$hash_b64``
17
+ """
18
+
19
+ def __init__(self, iterations: int = _DEFAULT_ITERATIONS) -> None:
20
+ self.iterations = iterations
21
+
22
+ @classmethod
23
+ def identify(cls, hash: str | bytes) -> bool:
24
+ if isinstance(hash, bytes):
25
+ hash = hash.decode("utf-8")
26
+ return hash.startswith("pbkdf2_sha256$")
27
+
28
+ def hash(self, password: str | bytes, *, salt: bytes | None = None) -> str:
29
+ if isinstance(password, str):
30
+ password = password.encode("utf-8")
31
+ if salt is None:
32
+ salt = os.urandom(16)
33
+ dk = hashlib.pbkdf2_hmac("sha256", password, salt, self.iterations)
34
+ salt_b64 = base64.b64encode(salt).decode("ascii")
35
+ hash_b64 = base64.b64encode(dk).decode("ascii")
36
+ return f"pbkdf2_sha256${self.iterations}${salt_b64}${hash_b64}"
37
+
38
+ def verify(self, password: str | bytes, hash: str | bytes) -> bool:
39
+ if isinstance(password, str):
40
+ password = password.encode("utf-8")
41
+ if isinstance(hash, bytes):
42
+ hash = hash.decode("utf-8")
43
+ parts = hash.split("$")
44
+ if len(parts) != 4 or parts[0] != "pbkdf2_sha256":
45
+ return False
46
+ iterations = int(parts[1])
47
+ salt = base64.b64decode(parts[2])
48
+ stored_hash = base64.b64decode(parts[3])
49
+ dk = hashlib.pbkdf2_hmac("sha256", password, salt, iterations)
50
+ return hmac.compare_digest(dk, stored_hash)
51
+
52
+ def check_needs_rehash(self, hash: str | bytes) -> bool:
53
+ if isinstance(hash, bytes):
54
+ hash = hash.decode("utf-8")
55
+ parts = hash.split("$")
56
+ if len(parts) != 4:
57
+ return True
58
+ return int(parts[1]) != self.iterations
59
+
60
+
61
+ __all__ = ["PBKDF2Hasher"]
@@ -0,0 +1 @@
1
+ """Framework integrations for tortoise-auth."""
@@ -0,0 +1,142 @@
1
+ """Starlette integration for tortoise-auth.
2
+
3
+ Provides ``AuthenticationMiddleware``-compatible backend and route helpers
4
+ so that ``request.user`` is automatically populated from a Bearer token.
5
+
6
+ Usage::
7
+
8
+ from starlette.applications import Starlette
9
+ from starlette.middleware.authentication import AuthenticationMiddleware
10
+ from tortoise_auth.integrations.starlette import TokenAuthBackend
11
+
12
+ app = Starlette(...)
13
+ app.add_middleware(AuthenticationMiddleware, backend=TokenAuthBackend())
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import functools
19
+ from collections.abc import Callable
20
+ from typing import Any
21
+
22
+ from starlette.authentication import AuthCredentials, AuthenticationBackend
23
+ from starlette.requests import HTTPConnection, Request
24
+ from starlette.responses import JSONResponse, RedirectResponse, Response
25
+
26
+ from tortoise_auth.exceptions import AuthenticationError, TokenError
27
+ from tortoise_auth.services import AuthService
28
+
29
+ __all__ = ["AnonymousUser", "TokenAuthBackend", "login_required", "require_auth"]
30
+
31
+
32
+ class AnonymousUser:
33
+ """Unauthenticated user placeholder.
34
+
35
+ Compatible with Starlette's ``BaseUser`` protocol and
36
+ ``AbstractUser``'s boolean properties.
37
+ """
38
+
39
+ @property
40
+ def is_authenticated(self) -> bool:
41
+ return False
42
+
43
+ @property
44
+ def is_anonymous(self) -> bool:
45
+ return True
46
+
47
+ @property
48
+ def display_name(self) -> str:
49
+ return ""
50
+
51
+
52
+ class TokenAuthBackend(AuthenticationBackend):
53
+ """Bearer-token authentication backend for Starlette's ``AuthenticationMiddleware``."""
54
+
55
+ def __init__(
56
+ self,
57
+ auth_service: AuthService | None = None,
58
+ *,
59
+ scopes: tuple[str, ...] = ("authenticated",),
60
+ ) -> None:
61
+ self._auth_service = auth_service
62
+ self._scopes = scopes
63
+
64
+ @property
65
+ def auth_service(self) -> AuthService:
66
+ if self._auth_service is None:
67
+ self._auth_service = AuthService()
68
+ return self._auth_service
69
+
70
+ async def authenticate(
71
+ self, conn: HTTPConnection
72
+ ) -> tuple[AuthCredentials, Any]:
73
+ anonymous = (AuthCredentials([]), AnonymousUser())
74
+
75
+ authorization = conn.headers.get("Authorization")
76
+ if not authorization:
77
+ return anonymous
78
+
79
+ parts = authorization.split(" ", 1)
80
+ if len(parts) != 2 or parts[0].lower() != "bearer":
81
+ return anonymous
82
+
83
+ token = parts[1]
84
+ try:
85
+ user = await self.auth_service.authenticate(token)
86
+ except (TokenError, AuthenticationError):
87
+ return anonymous
88
+
89
+ return AuthCredentials(list(self._scopes)), user
90
+
91
+
92
+ def require_auth(request: Request) -> Any:
93
+ """Extract the authenticated user from a request or raise ``AuthenticationError``.
94
+
95
+ This is a synchronous helper intended for use inside route handlers::
96
+
97
+ async def my_route(request: Request) -> Response:
98
+ user = require_auth(request)
99
+ return JSONResponse({"email": user.email})
100
+ """
101
+ if not request.user.is_authenticated:
102
+ raise AuthenticationError("Authentication required")
103
+ return request.user
104
+
105
+
106
+ def login_required(
107
+ fn: Callable[..., Any] | None = None,
108
+ *,
109
+ status_code: int = 401,
110
+ redirect_url: str | None = None,
111
+ ) -> Any:
112
+ """Decorator that rejects unauthenticated requests.
113
+
114
+ Supports usage with and without parentheses::
115
+
116
+ @login_required
117
+ async def view(request): ...
118
+
119
+ @login_required(status_code=403)
120
+ async def admin_view(request): ...
121
+
122
+ @login_required(redirect_url="/login")
123
+ async def html_view(request): ...
124
+ """
125
+
126
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
127
+ @functools.wraps(func)
128
+ async def wrapper(request: Request, *args: Any, **kwargs: Any) -> Response:
129
+ if not request.user.is_authenticated:
130
+ if redirect_url is not None:
131
+ return RedirectResponse(url=redirect_url, status_code=302)
132
+ return JSONResponse(
133
+ {"detail": "Authentication required"},
134
+ status_code=status_code,
135
+ )
136
+ return await func(request, *args, **kwargs) # type: ignore[no-any-return]
137
+
138
+ return wrapper
139
+
140
+ if fn is not None:
141
+ return decorator(fn)
142
+ return decorator
@@ -0,0 +1,10 @@
1
+ """User and token models for tortoise-auth."""
2
+
3
+ from tortoise_auth.models.base import AbstractUser
4
+ from tortoise_auth.models.jwt_blacklist import BlacklistedToken, OutstandingToken
5
+
6
+ __all__ = [
7
+ "AbstractUser",
8
+ "BlacklistedToken",
9
+ "OutstandingToken",
10
+ ]
File without changes