core-framework 0.12.4__tar.gz → 0.12.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {core_framework-0.12.4 → core_framework-0.12.5}/PKG-INFO +1 -1
  2. {core_framework-0.12.4 → core_framework-0.12.5}/core/__init__.py +1 -1
  3. core_framework-0.12.5/core/auth/middleware.py +473 -0
  4. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/tokens.py +17 -3
  5. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/views.py +27 -7
  6. {core_framework-0.12.4 → core_framework-0.12.5}/pyproject.toml +1 -1
  7. core_framework-0.12.4/core/auth/middleware.py +0 -355
  8. {core_framework-0.12.4 → core_framework-0.12.5}/.gitignore +0 -0
  9. {core_framework-0.12.4 → core_framework-0.12.5}/README.md +0 -0
  10. {core_framework-0.12.4 → core_framework-0.12.5}/core/app.py +0 -0
  11. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/__init__.py +0 -0
  12. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/backends.py +0 -0
  13. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/base.py +0 -0
  14. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/decorators.py +0 -0
  15. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/hashers.py +0 -0
  16. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/models.py +0 -0
  17. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/permissions.py +0 -0
  18. {core_framework-0.12.4 → core_framework-0.12.5}/core/auth/schemas.py +0 -0
  19. {core_framework-0.12.4 → core_framework-0.12.5}/core/choices.py +0 -0
  20. {core_framework-0.12.4 → core_framework-0.12.5}/core/cli/__init__.py +0 -0
  21. {core_framework-0.12.4 → core_framework-0.12.5}/core/cli/main.py +0 -0
  22. {core_framework-0.12.4 → core_framework-0.12.5}/core/config.py +0 -0
  23. {core_framework-0.12.4 → core_framework-0.12.5}/core/database.py +0 -0
  24. {core_framework-0.12.4 → core_framework-0.12.5}/core/datetime.py +0 -0
  25. {core_framework-0.12.4 → core_framework-0.12.5}/core/dependencies.py +0 -0
  26. {core_framework-0.12.4 → core_framework-0.12.5}/core/deployment/__init__.py +0 -0
  27. {core_framework-0.12.4 → core_framework-0.12.5}/core/deployment/docker.py +0 -0
  28. {core_framework-0.12.4 → core_framework-0.12.5}/core/deployment/kubernetes.py +0 -0
  29. {core_framework-0.12.4 → core_framework-0.12.5}/core/deployment/pm2.py +0 -0
  30. {core_framework-0.12.4 → core_framework-0.12.5}/core/exceptions.py +0 -0
  31. {core_framework-0.12.4 → core_framework-0.12.5}/core/fields.py +0 -0
  32. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/__init__.py +0 -0
  33. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/avro.py +0 -0
  34. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/base.py +0 -0
  35. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/config.py +0 -0
  36. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/confluent/__init__.py +0 -0
  37. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/confluent/consumer.py +0 -0
  38. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/confluent/producer.py +0 -0
  39. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/decorators.py +0 -0
  40. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/kafka/__init__.py +0 -0
  41. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/kafka/admin.py +0 -0
  42. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/kafka/broker.py +0 -0
  43. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/kafka/consumer.py +0 -0
  44. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/kafka/producer.py +0 -0
  45. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/rabbitmq/__init__.py +0 -0
  46. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/rabbitmq/broker.py +0 -0
  47. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/rabbitmq/consumer.py +0 -0
  48. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/rabbitmq/producer.py +0 -0
  49. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/redis/__init__.py +0 -0
  50. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/redis/broker.py +0 -0
  51. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/redis/consumer.py +0 -0
  52. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/redis/producer.py +0 -0
  53. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/registry.py +0 -0
  54. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/topics.py +0 -0
  55. {core_framework-0.12.4 → core_framework-0.12.5}/core/messaging/workers.py +0 -0
  56. {core_framework-0.12.4 → core_framework-0.12.5}/core/middleware.py +0 -0
  57. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/__init__.py +0 -0
  58. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/analyzer.py +0 -0
  59. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/cli.py +0 -0
  60. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/engine.py +0 -0
  61. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/migration.py +0 -0
  62. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/operations.py +0 -0
  63. {core_framework-0.12.4 → core_framework-0.12.5}/core/migrations/state.py +0 -0
  64. {core_framework-0.12.4 → core_framework-0.12.5}/core/models.py +0 -0
  65. {core_framework-0.12.4 → core_framework-0.12.5}/core/permissions.py +0 -0
  66. {core_framework-0.12.4 → core_framework-0.12.5}/core/querysets.py +0 -0
  67. {core_framework-0.12.4 → core_framework-0.12.5}/core/relations.py +0 -0
  68. {core_framework-0.12.4 → core_framework-0.12.5}/core/routing.py +0 -0
  69. {core_framework-0.12.4 → core_framework-0.12.5}/core/serializers.py +0 -0
  70. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/__init__.py +0 -0
  71. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/base.py +0 -0
  72. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/config.py +0 -0
  73. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/decorators.py +0 -0
  74. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/registry.py +0 -0
  75. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/scheduler.py +0 -0
  76. {core_framework-0.12.4 → core_framework-0.12.5}/core/tasks/worker.py +0 -0
  77. {core_framework-0.12.4 → core_framework-0.12.5}/core/tenancy.py +0 -0
  78. {core_framework-0.12.4 → core_framework-0.12.5}/core/validators.py +0 -0
  79. {core_framework-0.12.4 → core_framework-0.12.5}/core/views.py +0 -0
  80. {core_framework-0.12.4 → core_framework-0.12.5}/docs/01-quickstart.md +0 -0
  81. {core_framework-0.12.4 → core_framework-0.12.5}/docs/02-viewsets.md +0 -0
  82. {core_framework-0.12.4 → core_framework-0.12.5}/docs/03-authentication.md +0 -0
  83. {core_framework-0.12.4 → core_framework-0.12.5}/docs/04-messaging.md +0 -0
  84. {core_framework-0.12.4 → core_framework-0.12.5}/docs/05-multi-service.md +0 -0
  85. {core_framework-0.12.4 → core_framework-0.12.5}/docs/06-tasks.md +0 -0
  86. {core_framework-0.12.4 → core_framework-0.12.5}/docs/07-deployment.md +0 -0
  87. {core_framework-0.12.4 → core_framework-0.12.5}/docs/08-complete-example.md +0 -0
  88. {core_framework-0.12.4 → core_framework-0.12.5}/docs/09-settings.md +0 -0
  89. {core_framework-0.12.4 → core_framework-0.12.5}/docs/10-migrations.md +0 -0
  90. {core_framework-0.12.4 → core_framework-0.12.5}/docs/11-permissions.md +0 -0
  91. {core_framework-0.12.4 → core_framework-0.12.5}/docs/12-auth-backends.md +0 -0
  92. {core_framework-0.12.4 → core_framework-0.12.5}/docs/13-validators.md +0 -0
  93. {core_framework-0.12.4 → core_framework-0.12.5}/docs/14-querysets.md +0 -0
  94. {core_framework-0.12.4 → core_framework-0.12.5}/docs/15-routing.md +0 -0
  95. {core_framework-0.12.4 → core_framework-0.12.5}/docs/16-serializers.md +0 -0
  96. {core_framework-0.12.4 → core_framework-0.12.5}/docs/17-datetime.md +0 -0
  97. {core_framework-0.12.4 → core_framework-0.12.5}/docs/18-dependencies.md +0 -0
  98. {core_framework-0.12.4 → core_framework-0.12.5}/docs/19-views.md +0 -0
  99. {core_framework-0.12.4 → core_framework-0.12.5}/docs/20-fields.md +0 -0
  100. {core_framework-0.12.4 → core_framework-0.12.5}/docs/21-tenancy.md +0 -0
  101. {core_framework-0.12.4 → core_framework-0.12.5}/docs/22-replicas.md +0 -0
  102. {core_framework-0.12.4 → core_framework-0.12.5}/docs/23-soft-delete.md +0 -0
  103. {core_framework-0.12.4 → core_framework-0.12.5}/docs/24-relations.md +0 -0
  104. {core_framework-0.12.4 → core_framework-0.12.5}/docs/25-exceptions.md +0 -0
  105. {core_framework-0.12.4 → core_framework-0.12.5}/docs/26-choices.md +0 -0
  106. {core_framework-0.12.4 → core_framework-0.12.5}/docs/27-workers.md +0 -0
  107. {core_framework-0.12.4 → core_framework-0.12.5}/docs/28-avro.md +0 -0
  108. {core_framework-0.12.4 → core_framework-0.12.5}/docs/29-topics.md +0 -0
  109. {core_framework-0.12.4 → core_framework-0.12.5}/docs/30-changelog-0.12.2.md +0 -0
  110. {core_framework-0.12.4 → core_framework-0.12.5}/docs/31-middleware.md +0 -0
  111. {core_framework-0.12.4 → core_framework-0.12.5}/docs/32-migration-guide-0.12.2.md +0 -0
  112. {core_framework-0.12.4 → core_framework-0.12.5}/docs/33-changelog-0.12.3.md +0 -0
  113. {core_framework-0.12.4 → core_framework-0.12.5}/docs/99-faq-troubleshooting.md +0 -0
  114. {core_framework-0.12.4 → core_framework-0.12.5}/docs/GUIDE.md +0 -0
  115. {core_framework-0.12.4 → core_framework-0.12.5}/docs/README.md +0 -0
  116. {core_framework-0.12.4 → core_framework-0.12.5}/example/__init__.py +0 -0
  117. {core_framework-0.12.4 → core_framework-0.12.5}/example/app.py +0 -0
  118. {core_framework-0.12.4 → core_framework-0.12.5}/example/auth.py +0 -0
  119. {core_framework-0.12.4 → core_framework-0.12.5}/example/models.py +0 -0
  120. {core_framework-0.12.4 → core_framework-0.12.5}/example/schemas.py +0 -0
  121. {core_framework-0.12.4 → core_framework-0.12.5}/example/views.py +0 -0
  122. {core_framework-0.12.4 → core_framework-0.12.5}/libs/__init__.py +0 -0
  123. {core_framework-0.12.4 → core_framework-0.12.5}/main.py +0 -0
  124. {core_framework-0.12.4 → core_framework-0.12.5}/tests/__init__.py +0 -0
  125. {core_framework-0.12.4 → core_framework-0.12.5}/tests/conftest.py +0 -0
  126. {core_framework-0.12.4 → core_framework-0.12.5}/tests/test_models.py +0 -0
  127. {core_framework-0.12.4 → core_framework-0.12.5}/tests/test_querysets.py +0 -0
  128. {core_framework-0.12.4 → core_framework-0.12.5}/tests/test_serializers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.4
3
+ Version: 0.12.5
4
4
  Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
5
5
  Project-URL: Homepage, https://github.com/SorPuti/core-framework
6
6
  Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.4"
281
+ __version__ = "0.12.5"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
@@ -0,0 +1,473 @@
1
+ """
2
+ Authentication Middleware for Core Framework.
3
+
4
+ Uses Starlette's AuthenticationMiddleware pattern which correctly propagates
5
+ user to views via scope["user"] (accessed as request.user).
6
+
7
+ IMPORTANT: Use request.user (not request.state.user) in your views!
8
+
9
+ Usage:
10
+ from core.auth.middleware import AuthenticationMiddleware
11
+
12
+ app = CoreApp(
13
+ middlewares=[(AuthenticationMiddleware, {})],
14
+ )
15
+
16
+ # In your view - use request.user
17
+ @router.get("/me")
18
+ async def me(request: Request):
19
+ if not request.user.is_authenticated:
20
+ raise HTTPException(401, "Not authenticated")
21
+ return {"id": request.user.id, "email": request.user.email}
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from typing import Any, TYPE_CHECKING
28
+ from uuid import UUID
29
+
30
+ from starlette.authentication import (
31
+ AuthenticationBackend,
32
+ AuthCredentials,
33
+ BaseUser,
34
+ UnauthenticatedUser,
35
+ )
36
+ from starlette.middleware.authentication import AuthenticationMiddleware as StarletteAuthMiddleware
37
+ from starlette.requests import HTTPConnection
38
+
39
+ from core.exceptions import (
40
+ InvalidToken,
41
+ TokenExpired,
42
+ UserNotFound,
43
+ UserInactive,
44
+ DatabaseException,
45
+ ConfigurationError,
46
+ )
47
+
48
+ if TYPE_CHECKING:
49
+ from sqlalchemy.ext.asyncio import AsyncSession
50
+
51
+ # Logger for authentication - NEVER silent!
52
+ logger = logging.getLogger("core.auth")
53
+
54
+
55
+ # =============================================================================
56
+ # Authenticated User Wrapper
57
+ # =============================================================================
58
+
59
+ class AuthenticatedUser(BaseUser):
60
+ """
61
+ Wrapper for authenticated user that implements Starlette's BaseUser.
62
+
63
+ Provides access to the underlying database user model while implementing
64
+ the required Starlette interface.
65
+
66
+ Usage in views:
67
+ user = request.user
68
+ if user.is_authenticated:
69
+ print(user.email) # Access model attributes
70
+ print(user.id) # Access model attributes
71
+ """
72
+
73
+ def __init__(self, user: Any) -> None:
74
+ self._user = user
75
+
76
+ @property
77
+ def is_authenticated(self) -> bool:
78
+ return True
79
+
80
+ @property
81
+ def display_name(self) -> str:
82
+ return getattr(self._user, "email", str(self._user))
83
+
84
+ @property
85
+ def identity(self) -> str:
86
+ return str(getattr(self._user, "id", ""))
87
+
88
+ def __getattr__(self, name: str) -> Any:
89
+ """Proxy attribute access to underlying user model."""
90
+ return getattr(self._user, name)
91
+
92
+ def __repr__(self) -> str:
93
+ return f"<AuthenticatedUser {self.display_name}>"
94
+
95
+
96
+ # =============================================================================
97
+ # Authentication Backend
98
+ # =============================================================================
99
+
100
+ class JWTAuthBackend(AuthenticationBackend):
101
+ """
102
+ JWT Authentication Backend for Starlette.
103
+
104
+ This backend:
105
+ 1. Extracts Bearer token from Authorization header
106
+ 2. Verifies the token using core.auth.tokens
107
+ 3. Fetches user from database
108
+ 4. Returns AuthCredentials and AuthenticatedUser
109
+
110
+ IMPORTANT: All errors are logged, NEVER silenced!
111
+
112
+ Configuration:
113
+ - user_model: User model class (uses global config if None)
114
+ - header_name: Header to extract token from (default: "Authorization")
115
+ - scheme: Expected scheme (default: "Bearer")
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ user_model: type | None = None,
121
+ header_name: str = "Authorization",
122
+ scheme: str = "Bearer",
123
+ ) -> None:
124
+ self._user_model = user_model
125
+ self.header_name = header_name
126
+ self.scheme = scheme
127
+
128
+ @property
129
+ def user_model(self) -> type | None:
130
+ """Get user model from instance or global config."""
131
+ if self._user_model is not None:
132
+ return self._user_model
133
+
134
+ try:
135
+ from core.auth.models import get_user_model
136
+ return get_user_model()
137
+ except Exception as e:
138
+ logger.error(f"Failed to get user model: {e}")
139
+ return None
140
+
141
+ async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None:
142
+ """
143
+ Authenticate the request.
144
+
145
+ Returns:
146
+ Tuple of (AuthCredentials, AuthenticatedUser) if authenticated
147
+ None if no credentials provided
148
+
149
+ Note: Returns None for missing credentials, but LOGS all errors!
150
+ """
151
+ # Extract token from header
152
+ auth_header = conn.headers.get(self.header_name)
153
+
154
+ if not auth_header:
155
+ logger.debug("No Authorization header present")
156
+ return None
157
+
158
+ # Parse header
159
+ parts = auth_header.split()
160
+ if len(parts) != 2:
161
+ logger.warning(f"Malformed Authorization header: expected 2 parts, got {len(parts)}")
162
+ return None
163
+
164
+ scheme, token = parts
165
+ if scheme.lower() != self.scheme.lower():
166
+ logger.warning(f"Unexpected auth scheme: expected '{self.scheme}', got '{scheme}'")
167
+ return None
168
+
169
+ logger.debug(f"Token extracted: {token[:20]}...")
170
+
171
+ # Verify token
172
+ try:
173
+ user = await self._verify_and_get_user(token)
174
+ if user is None:
175
+ return None
176
+
177
+ logger.info(f"User authenticated: {getattr(user, 'email', user)}")
178
+ return AuthCredentials(["authenticated"]), AuthenticatedUser(user)
179
+
180
+ except InvalidToken as e:
181
+ logger.warning(f"Invalid token: {e.message}")
182
+ return None
183
+ except TokenExpired as e:
184
+ logger.warning(f"Token expired: {e.message}")
185
+ return None
186
+ except UserNotFound as e:
187
+ logger.warning(f"User not found: {e.message}")
188
+ return None
189
+ except UserInactive as e:
190
+ logger.warning(f"User inactive: {e.message}")
191
+ return None
192
+ except DatabaseException as e:
193
+ logger.error(f"Database error during authentication: {e.message}", exc_info=True)
194
+ raise # Re-raise database errors - these are critical!
195
+ except ConfigurationError as e:
196
+ logger.error(f"Configuration error: {e.message}", exc_info=True)
197
+ raise # Re-raise configuration errors - these need to be fixed!
198
+ except Exception as e:
199
+ # Log unexpected errors with full stack trace
200
+ logger.exception(f"Unexpected error during authentication: {e}")
201
+ raise # NEVER silence unexpected errors!
202
+
203
+ async def _verify_and_get_user(self, token: str) -> Any | None:
204
+ """
205
+ Verify token and fetch user from database.
206
+
207
+ Raises:
208
+ InvalidToken: If token is invalid or malformed
209
+ TokenExpired: If token has expired
210
+ UserNotFound: If user doesn't exist
211
+ UserInactive: If user is inactive
212
+ DatabaseException: If database query fails
213
+ ConfigurationError: If auth is not properly configured
214
+ """
215
+ from core.auth.tokens import verify_token, decode_token
216
+ from core.auth.base import TokenError
217
+
218
+ # Verify token
219
+ try:
220
+ payload = verify_token(token, token_type="access")
221
+ except TokenError as e:
222
+ raise InvalidToken(f"Token verification failed: {e}")
223
+
224
+ if payload is None:
225
+ # verify_token returns None for various reasons - let's be more specific
226
+ try:
227
+ # Try to decode to get more info
228
+ raw_payload = decode_token(token)
229
+ token_type = raw_payload.get("type")
230
+ if token_type != "access":
231
+ raise InvalidToken(f"Token type mismatch: expected 'access', got '{token_type}'")
232
+ except Exception as e:
233
+ raise InvalidToken(f"Token decode failed: {e}")
234
+ raise InvalidToken("Token verification returned None")
235
+
236
+ logger.debug(f"Token payload: {payload}")
237
+
238
+ # Get user_id from token
239
+ user_id = payload.get("sub") or payload.get("user_id")
240
+ if not user_id:
241
+ raise InvalidToken("Token missing 'sub' or 'user_id' claim")
242
+
243
+ logger.debug(f"User ID from token: {user_id}")
244
+
245
+ # Get user model
246
+ User = self.user_model
247
+ if User is None:
248
+ raise ConfigurationError(
249
+ "No user model configured. "
250
+ "Set user_model in AuthenticationMiddleware or call configure_auth(user_model=...)"
251
+ )
252
+
253
+ # Get database session
254
+ db = await self._get_db_session()
255
+ if db is None:
256
+ raise DatabaseException(
257
+ "Could not obtain database session. "
258
+ "Ensure database is initialized with init_replicas() or database_url is set in settings."
259
+ )
260
+
261
+ try:
262
+ # Convert user_id to correct type
263
+ user_id_converted = self._convert_user_id(user_id, User)
264
+ logger.debug(f"User ID converted: {user_id_converted} (type: {type(user_id_converted).__name__})")
265
+
266
+ # Fetch user
267
+ user = await User.objects.using(db).filter(id=user_id_converted).first()
268
+
269
+ if user is None:
270
+ raise UserNotFound(f"User with id={user_id} not found")
271
+
272
+ # Check if user is active
273
+ if hasattr(user, "is_active") and not user.is_active:
274
+ raise UserInactive(f"User {user_id} is inactive")
275
+
276
+ logger.debug(f"User found: {getattr(user, 'email', user)}")
277
+ return user
278
+
279
+ except (UserNotFound, UserInactive):
280
+ raise # Re-raise our exceptions
281
+ except Exception as e:
282
+ raise DatabaseException(f"Database query failed: {e}")
283
+ finally:
284
+ await db.close()
285
+
286
+ async def _get_db_session(self) -> "AsyncSession | None":
287
+ """
288
+ Get a database session for authentication.
289
+
290
+ Tries multiple strategies and logs each attempt.
291
+
292
+ Returns:
293
+ AsyncSession or None if all strategies fail
294
+ """
295
+ errors: list[str] = []
296
+
297
+ # Strategy 1: Use initialized session factory
298
+ try:
299
+ from core.database import _read_session_factory
300
+
301
+ if _read_session_factory is not None:
302
+ logger.debug("Using initialized session factory")
303
+ return _read_session_factory()
304
+ else:
305
+ errors.append("Session factory not initialized (init_replicas not called)")
306
+ except ImportError as e:
307
+ errors.append(f"Could not import database module: {e}")
308
+ except Exception as e:
309
+ errors.append(f"Session factory error: {e}")
310
+
311
+ # Strategy 2: Create session from settings
312
+ try:
313
+ from core.config import get_settings
314
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
315
+
316
+ settings = get_settings()
317
+ db_url = getattr(settings, 'database_read_url', None) or getattr(settings, 'database_url', None)
318
+
319
+ if db_url:
320
+ logger.debug(f"Creating session from settings: {db_url[:30]}...")
321
+ engine = create_async_engine(db_url, echo=False)
322
+ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
323
+ return session_factory()
324
+ else:
325
+ errors.append("No database_url in settings")
326
+ except ImportError as e:
327
+ errors.append(f"Could not import config module: {e}")
328
+ except Exception as e:
329
+ errors.append(f"Settings engine error: {e}")
330
+
331
+ # All strategies failed - log detailed error
332
+ logger.error(
333
+ f"Could not obtain database session. Attempted strategies:\n" +
334
+ "\n".join(f" - {err}" for err in errors)
335
+ )
336
+ return None
337
+
338
+ def _convert_user_id(self, user_id: str, User: type) -> Any:
339
+ """
340
+ Convert user_id string to the correct type based on model.
341
+
342
+ Handles INTEGER, UUID, and string IDs.
343
+ """
344
+ # Try to detect PK type from model
345
+ try:
346
+ from core.auth.models import _get_pk_column_type
347
+ from sqlalchemy.dialects.postgresql import UUID as PG_UUID
348
+ from sqlalchemy import Integer, BigInteger
349
+
350
+ pk_type = _get_pk_column_type(User)
351
+
352
+ if pk_type == PG_UUID:
353
+ logger.debug(f"Converting user_id to UUID: {user_id}")
354
+ return UUID(user_id)
355
+ elif pk_type in (Integer, BigInteger):
356
+ logger.debug(f"Converting user_id to int: {user_id}")
357
+ return int(user_id)
358
+ except Exception as e:
359
+ logger.debug(f"Could not detect PK type, trying heuristics: {e}")
360
+
361
+ # Fallback: Try UUID first (common in modern systems)
362
+ try:
363
+ return UUID(user_id)
364
+ except (ValueError, TypeError):
365
+ pass
366
+
367
+ # Try int
368
+ try:
369
+ return int(user_id)
370
+ except (ValueError, TypeError):
371
+ pass
372
+
373
+ # Return as string
374
+ logger.debug(f"Using user_id as string: {user_id}")
375
+ return user_id
376
+
377
+
378
+ # =============================================================================
379
+ # Middleware Factory
380
+ # =============================================================================
381
+
382
+ class AuthenticationMiddleware(StarletteAuthMiddleware):
383
+ """
384
+ Authentication Middleware using Starlette's proper pattern.
385
+
386
+ This middleware correctly propagates user to views via request.user
387
+ (not request.state.user which doesn't work with BaseHTTPMiddleware).
388
+
389
+ Usage:
390
+ app = CoreApp(
391
+ middlewares=[(AuthenticationMiddleware, {})],
392
+ )
393
+
394
+ # Or with shortcut:
395
+ app = CoreApp(middleware=["auth"])
396
+
397
+ # In views, use request.user:
398
+ @router.get("/me")
399
+ async def me(request: Request):
400
+ if not request.user.is_authenticated:
401
+ raise HTTPException(401, "Not authenticated")
402
+ return {"email": request.user.email}
403
+
404
+ Note: Also sets request.state.user for backward compatibility.
405
+ """
406
+
407
+ def __init__(
408
+ self,
409
+ app: Any,
410
+ user_model: type | None = None,
411
+ header_name: str = "Authorization",
412
+ scheme: str = "Bearer",
413
+ on_error: Any = None,
414
+ ) -> None:
415
+ backend = JWTAuthBackend(
416
+ user_model=user_model,
417
+ header_name=header_name,
418
+ scheme=scheme,
419
+ )
420
+ super().__init__(app, backend=backend, on_error=on_error)
421
+ logger.info("AuthenticationMiddleware initialized")
422
+
423
+
424
+ class OptionalAuthenticationMiddleware(AuthenticationMiddleware):
425
+ """
426
+ Same as AuthenticationMiddleware but doesn't require authentication.
427
+
428
+ Useful for endpoints that work both with and without authentication.
429
+ User will be UnauthenticatedUser if no valid token provided.
430
+ """
431
+ pass
432
+
433
+
434
+ # =============================================================================
435
+ # Legacy Compatibility
436
+ # =============================================================================
437
+
438
+ def ensure_auth_middleware(app: Any) -> None:
439
+ """
440
+ Ensure AuthenticationMiddleware is registered on the app.
441
+
442
+ DEPRECATED: Use middleware=["auth"] instead.
443
+ """
444
+ logger.warning(
445
+ "ensure_auth_middleware is deprecated. "
446
+ "Use middleware=['auth'] or add AuthenticationMiddleware directly."
447
+ )
448
+ try:
449
+ if hasattr(app, "add_middleware"):
450
+ app.add_middleware(AuthenticationMiddleware)
451
+ logger.info("AuthenticationMiddleware added to app")
452
+ except Exception as e:
453
+ logger.error(f"Failed to add AuthenticationMiddleware: {e}")
454
+ raise
455
+
456
+
457
+ def reset_middleware_state() -> None:
458
+ """Reset middleware state (for testing)."""
459
+ pass # No longer needed with new implementation
460
+
461
+
462
+ # =============================================================================
463
+ # Exports
464
+ # =============================================================================
465
+
466
+ __all__ = [
467
+ "AuthenticationMiddleware",
468
+ "OptionalAuthenticationMiddleware",
469
+ "JWTAuthBackend",
470
+ "AuthenticatedUser",
471
+ "ensure_auth_middleware",
472
+ "reset_middleware_state",
473
+ ]
@@ -18,6 +18,7 @@ Uso:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import logging
21
22
  from datetime import timedelta
22
23
  from typing import Any
23
24
 
@@ -29,6 +30,9 @@ from core.auth.base import (
29
30
  )
30
31
  from core.datetime import timezone
31
32
 
33
+ # Logger for token operations
34
+ logger = logging.getLogger("core.auth.tokens")
35
+
32
36
 
33
37
  class JWTBackend(TokenBackend):
34
38
  """
@@ -123,10 +127,14 @@ class JWTBackend(TokenBackend):
123
127
  import jwt
124
128
 
125
129
  try:
126
- return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
130
+ payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
131
+ logger.debug(f"Token decoded: sub={payload.get('sub')}, type={payload.get('type')}")
132
+ return payload
127
133
  except jwt.ExpiredSignatureError:
134
+ logger.debug("Token decode failed: token expired")
128
135
  raise TokenError("Token expired")
129
136
  except jwt.InvalidTokenError as e:
137
+ logger.debug(f"Token decode failed: {e}")
130
138
  raise TokenError(f"Invalid token: {e}")
131
139
 
132
140
  def verify_token(
@@ -147,11 +155,17 @@ class JWTBackend(TokenBackend):
147
155
  try:
148
156
  payload = self.decode_token(token)
149
157
 
150
- if payload.get("type") != token_type:
158
+ actual_type = payload.get("type")
159
+ if actual_type != token_type:
160
+ logger.debug(
161
+ f"Token type mismatch: expected '{token_type}', got '{actual_type}'"
162
+ )
151
163
  return None
152
164
 
165
+ logger.debug(f"Token verified successfully: sub={payload.get('sub')}")
153
166
  return payload
154
- except TokenError:
167
+ except TokenError as e:
168
+ logger.debug(f"Token verification failed: {e}")
155
169
  return None
156
170
 
157
171
  def refresh_token(self, refresh_token: str) -> tuple[str, str] | None:
@@ -360,15 +360,27 @@ class AuthViewSet(ViewSet):
360
360
  ) -> dict:
361
361
  """
362
362
  Get current authenticated user.
363
+
364
+ Uses request.user (Starlette pattern) with fallback to request.state.user
365
+ for backward compatibility.
363
366
  """
367
+ # Try request.user first (Starlette AuthenticationMiddleware pattern)
368
+ user = getattr(request, "user", None)
369
+ if user is not None and getattr(user, "is_authenticated", False):
370
+ # user is an AuthenticatedUser wrapper - get underlying model
371
+ if hasattr(user, "_user"):
372
+ user = user._user
373
+ return self.user_output_schema.model_validate(user).model_dump()
374
+
375
+ # Fallback to request.state.user (legacy pattern)
364
376
  user = getattr(request.state, "user", None)
365
- if user is None:
366
- raise HTTPException(
367
- status_code=401,
368
- detail="Not authenticated"
369
- )
377
+ if user is not None:
378
+ return self.user_output_schema.model_validate(user).model_dump()
370
379
 
371
- return self.user_output_schema.model_validate(user).model_dump()
380
+ raise HTTPException(
381
+ status_code=401,
382
+ detail="Not authenticated"
383
+ )
372
384
 
373
385
  @action(methods=["POST"], detail=False, permission_classes=[IsAuthenticated])
374
386
  async def change_password(
@@ -381,7 +393,15 @@ class AuthViewSet(ViewSet):
381
393
  """
382
394
  Change password for current user.
383
395
  """
384
- user = getattr(request.state, "user", None)
396
+ # Try request.user first (Starlette pattern)
397
+ user = getattr(request, "user", None)
398
+ if user is not None and getattr(user, "is_authenticated", False):
399
+ if hasattr(user, "_user"):
400
+ user = user._user
401
+ else:
402
+ # Fallback to request.state.user
403
+ user = getattr(request.state, "user", None)
404
+
385
405
  if user is None:
386
406
  raise HTTPException(
387
407
  status_code=401,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "core-framework"
3
- version = "0.12.4"
3
+ version = "0.12.5"
4
4
  description = "Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema."
5
5
  requires-python = ">=3.10,<4.0"
6
6
  readme = "README.md"