core-framework 0.12.3__py3-none-any.whl → 0.12.5__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.
core/__init__.py CHANGED
@@ -278,7 +278,7 @@ from core.exceptions import (
278
278
  MissingDependency,
279
279
  )
280
280
 
281
- __version__ = "0.12.3"
281
+ __version__ = "0.12.5"
282
282
  __all__ = [
283
283
  # Models
284
284
  "Model",
core/auth/middleware.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """
2
2
  Authentication Middleware for Core Framework.
3
3
 
4
- Bug #8 Fix: Provides built-in middleware for populating request.state.user.
4
+ Uses Starlette's AuthenticationMiddleware pattern which correctly propagates
5
+ user to views via scope["user"] (accessed as request.user).
5
6
 
6
- This middleware automatically authenticates requests using the configured
7
- authentication backend and populates request.state.user.
7
+ IMPORTANT: Use request.user (not request.state.user) in your views!
8
8
 
9
9
  Usage:
10
10
  from core.auth.middleware import AuthenticationMiddleware
@@ -13,78 +13,117 @@ Usage:
13
13
  middlewares=[(AuthenticationMiddleware, {})],
14
14
  )
15
15
 
16
- # Or configure via configure_auth()
17
- from core.auth import configure_auth
18
- configure_auth(
19
- user_model=User,
20
- auto_middleware=True, # Automatically adds middleware
21
- )
22
-
23
- The middleware will:
24
- 1. Extract Bearer token from Authorization header
25
- 2. Validate the token
26
- 3. Fetch the user from database
27
- 4. Set request.state.user to the authenticated user (or None)
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}
28
22
  """
29
23
 
30
24
  from __future__ import annotations
31
25
 
26
+ import logging
32
27
  from typing import Any, TYPE_CHECKING
28
+ from uuid import UUID
33
29
 
34
- from starlette.middleware.base import BaseHTTPMiddleware
35
- from starlette.requests import Request
36
- from starlette.responses import Response
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
+ )
37
47
 
38
48
  if TYPE_CHECKING:
39
- from collections.abc import Callable, Awaitable
49
+ from sqlalchemy.ext.asyncio import AsyncSession
50
+
51
+ # Logger for authentication - NEVER silent!
52
+ logger = logging.getLogger("core.auth")
40
53
 
41
54
 
42
- class AuthenticationMiddleware(BaseHTTPMiddleware):
55
+ # =============================================================================
56
+ # Authenticated User Wrapper
57
+ # =============================================================================
58
+
59
+ class AuthenticatedUser(BaseUser):
43
60
  """
44
- Middleware that authenticates requests and populates request.state.user.
61
+ Wrapper for authenticated user that implements Starlette's BaseUser.
45
62
 
46
- This middleware:
47
- 1. Initializes request.state.user to None
48
- 2. Extracts Bearer token from Authorization header
49
- 3. Validates the token using configured backend
50
- 4. Fetches user from database
51
- 5. Sets request.state.user to authenticated user
63
+ Provides access to the underlying database user model while implementing
64
+ the required Starlette interface.
52
65
 
53
- Example:
54
- from core.auth.middleware import AuthenticationMiddleware
55
-
56
- app = CoreApp(
57
- middlewares=[(AuthenticationMiddleware, {})],
58
- )
59
-
60
- # In your view
61
- @router.get("/me")
62
- async def me(request: Request):
63
- user = request.state.user # User or None
64
- if user is None:
65
- raise HTTPException(401, "Not authenticated")
66
- return {"id": user.id, "email": user.email}
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!
67
111
 
68
- Configuration via kwargs:
112
+ Configuration:
69
113
  - user_model: User model class (uses global config if None)
70
114
  - header_name: Header to extract token from (default: "Authorization")
71
115
  - scheme: Expected scheme (default: "Bearer")
72
- - skip_paths: List of paths to skip authentication (e.g., ["/health"])
73
116
  """
74
117
 
75
118
  def __init__(
76
119
  self,
77
- app: "Callable[[Request], Awaitable[Response]]",
78
120
  user_model: type | None = None,
79
121
  header_name: str = "Authorization",
80
122
  scheme: str = "Bearer",
81
- skip_paths: list[str] | None = None,
82
123
  ) -> None:
83
- super().__init__(app)
84
124
  self._user_model = user_model
85
125
  self.header_name = header_name
86
126
  self.scheme = scheme
87
- self.skip_paths = skip_paths or []
88
127
 
89
128
  @property
90
129
  def user_model(self) -> type | None:
@@ -95,139 +134,181 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
95
134
  try:
96
135
  from core.auth.models import get_user_model
97
136
  return get_user_model()
98
- except Exception:
137
+ except Exception as e:
138
+ logger.error(f"Failed to get user model: {e}")
99
139
  return None
100
140
 
101
- async def dispatch(
102
- self,
103
- request: Request,
104
- call_next: "Callable[[Request], Awaitable[Response]]",
105
- ) -> Response:
141
+ async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None:
106
142
  """
107
- Process request and authenticate user.
143
+ Authenticate the request.
108
144
 
109
- Always sets request.state.user (to User or None).
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!
110
150
  """
111
- # Initialize user to None
112
- request.state.user = None
113
-
114
- # Skip authentication for configured paths
115
- if self._should_skip(request.url.path):
116
- return await call_next(request)
117
-
118
- # Try to authenticate
119
- try:
120
- user = await self._authenticate(request)
121
- request.state.user = user
122
- except Exception:
123
- # Authentication failed, keep user as None
124
- pass
125
-
126
- return await call_next(request)
127
-
128
- def _should_skip(self, path: str) -> bool:
129
- """Check if path should skip authentication."""
130
- for skip_path in self.skip_paths:
131
- if path.startswith(skip_path):
132
- return True
133
- return False
134
-
135
- def _extract_token(self, request: Request) -> str | None:
136
- """Extract token from Authorization header."""
137
- auth_header = request.headers.get(self.header_name)
151
+ # Extract token from header
152
+ auth_header = conn.headers.get(self.header_name)
138
153
 
139
154
  if not auth_header:
155
+ logger.debug("No Authorization header present")
140
156
  return None
141
157
 
158
+ # Parse header
142
159
  parts = auth_header.split()
143
-
144
160
  if len(parts) != 2:
161
+ logger.warning(f"Malformed Authorization header: expected 2 parts, got {len(parts)}")
145
162
  return None
146
163
 
147
164
  scheme, token = parts
148
-
149
165
  if scheme.lower() != self.scheme.lower():
166
+ logger.warning(f"Unexpected auth scheme: expected '{self.scheme}', got '{scheme}'")
150
167
  return None
151
168
 
152
- return token
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!
153
202
 
154
- async def _authenticate(self, request: Request) -> Any | None:
203
+ async def _verify_and_get_user(self, token: str) -> Any | None:
155
204
  """
156
- Authenticate request and return user.
157
-
158
- Returns:
159
- User instance if authenticated, None otherwise
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
160
214
  """
161
- token = self._extract_token(request)
162
-
163
- if not token:
164
- return None
215
+ from core.auth.tokens import verify_token, decode_token
216
+ from core.auth.base import TokenError
165
217
 
166
218
  # Verify token
167
- from core.auth.tokens import verify_token
168
-
169
- payload = verify_token(token, token_type="access")
219
+ try:
220
+ payload = verify_token(token, token_type="access")
221
+ except TokenError as e:
222
+ raise InvalidToken(f"Token verification failed: {e}")
170
223
 
171
224
  if payload is None:
172
- return 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}")
173
237
 
174
238
  # Get user_id from token
175
239
  user_id = payload.get("sub") or payload.get("user_id")
176
-
177
240
  if not user_id:
178
- return None
241
+ raise InvalidToken("Token missing 'sub' or 'user_id' claim")
179
242
 
180
- # Get user from database
243
+ logger.debug(f"User ID from token: {user_id}")
244
+
245
+ # Get user model
181
246
  User = self.user_model
182
247
  if User is None:
183
- return None
248
+ raise ConfigurationError(
249
+ "No user model configured. "
250
+ "Set user_model in AuthenticationMiddleware or call configure_auth(user_model=...)"
251
+ )
184
252
 
185
- # Bug #3 & #4 Fix: Get database session correctly
186
- # The middleware runs outside FastAPI DI context, so we need to
187
- # handle database session creation carefully
253
+ # Get database session
188
254
  db = await self._get_db_session()
189
255
  if db is None:
190
- return 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
+ )
191
260
 
192
261
  try:
193
262
  # Convert user_id to correct type
194
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__})")
195
265
 
266
+ # Fetch user
196
267
  user = await User.objects.using(db).filter(id=user_id_converted).first()
197
268
 
198
269
  if user is None:
199
- return None
270
+ raise UserNotFound(f"User with id={user_id} not found")
200
271
 
201
272
  # Check if user is active
202
273
  if hasattr(user, "is_active") and not user.is_active:
203
- return None
274
+ raise UserInactive(f"User {user_id} is inactive")
204
275
 
276
+ logger.debug(f"User found: {getattr(user, 'email', user)}")
205
277
  return user
206
- except Exception:
207
- return None
278
+
279
+ except (UserNotFound, UserInactive):
280
+ raise # Re-raise our exceptions
281
+ except Exception as e:
282
+ raise DatabaseException(f"Database query failed: {e}")
208
283
  finally:
209
284
  await db.close()
210
285
 
211
- async def _get_db_session(self) -> Any | None:
286
+ async def _get_db_session(self) -> "AsyncSession | None":
212
287
  """
213
288
  Get a database session for authentication.
214
289
 
215
- Bug #3 & #4 Fix: Handles both initialized and uninitialized database states.
216
- Creates session directly from engine if normal path fails.
290
+ Tries multiple strategies and logs each attempt.
217
291
 
218
292
  Returns:
219
- AsyncSession or None if database not available
293
+ AsyncSession or None if all strategies fail
220
294
  """
221
- # Try 1: Use the standard get_read_session (if database is initialized)
295
+ errors: list[str] = []
296
+
297
+ # Strategy 1: Use initialized session factory
222
298
  try:
223
- from core.database import get_read_session, _read_session_factory
299
+ from core.database import _read_session_factory
224
300
 
225
301
  if _read_session_factory is not None:
302
+ logger.debug("Using initialized session factory")
226
303
  return _read_session_factory()
227
- except (RuntimeError, ImportError):
228
- pass
229
-
230
- # Try 2: Create session from settings (lazy initialization)
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
231
312
  try:
232
313
  from core.config import get_settings
233
314
  from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
@@ -236,12 +317,22 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
236
317
  db_url = getattr(settings, 'database_read_url', None) or getattr(settings, 'database_url', None)
237
318
 
238
319
  if db_url:
320
+ logger.debug(f"Creating session from settings: {db_url[:30]}...")
239
321
  engine = create_async_engine(db_url, echo=False)
240
322
  session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
241
323
  return session_factory()
242
- except Exception:
243
- pass
244
-
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
+ )
245
336
  return None
246
337
 
247
338
  def _convert_user_id(self, user_id: str, User: type) -> Any:
@@ -250,9 +341,7 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
250
341
 
251
342
  Handles INTEGER, UUID, and string IDs.
252
343
  """
253
- from uuid import UUID
254
-
255
- # Try to detect PK type
344
+ # Try to detect PK type from model
256
345
  try:
257
346
  from core.auth.models import _get_pk_column_type
258
347
  from sqlalchemy.dialects.postgresql import UUID as PG_UUID
@@ -261,86 +350,113 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
261
350
  pk_type = _get_pk_column_type(User)
262
351
 
263
352
  if pk_type == PG_UUID:
353
+ logger.debug(f"Converting user_id to UUID: {user_id}")
264
354
  return UUID(user_id)
265
355
  elif pk_type in (Integer, BigInteger):
356
+ logger.debug(f"Converting user_id to int: {user_id}")
266
357
  return int(user_id)
267
- except Exception:
268
- pass
358
+ except Exception as e:
359
+ logger.debug(f"Could not detect PK type, trying heuristics: {e}")
269
360
 
270
- # Try UUID first, then int, then string
361
+ # Fallback: Try UUID first (common in modern systems)
271
362
  try:
272
363
  return UUID(user_id)
273
364
  except (ValueError, TypeError):
274
365
  pass
275
366
 
367
+ # Try int
276
368
  try:
277
369
  return int(user_id)
278
370
  except (ValueError, TypeError):
279
371
  pass
280
372
 
373
+ # Return as string
374
+ logger.debug(f"Using user_id as string: {user_id}")
281
375
  return user_id
282
376
 
283
377
 
284
- class OptionalAuthenticationMiddleware(AuthenticationMiddleware):
378
+ # =============================================================================
379
+ # Middleware Factory
380
+ # =============================================================================
381
+
382
+ class AuthenticationMiddleware(StarletteAuthMiddleware):
285
383
  """
286
- Same as AuthenticationMiddleware but never raises errors.
384
+ Authentication Middleware using Starlette's proper pattern.
287
385
 
288
- Always proceeds with the request, setting user to None on any failure.
289
- Useful for endpoints that work both with and without authentication.
290
- """
386
+ This middleware correctly propagates user to views via request.user
387
+ (not request.state.user which doesn't work with BaseHTTPMiddleware).
291
388
 
292
- async def dispatch(
293
- self,
294
- request: Request,
295
- call_next: "Callable[[Request], Awaitable[Response]]",
296
- ) -> Response:
297
- """Process request, never failing on auth errors."""
298
- request.state.user = None
389
+ Usage:
390
+ app = CoreApp(
391
+ middlewares=[(AuthenticationMiddleware, {})],
392
+ )
299
393
 
300
- if not self._should_skip(request.url.path):
301
- try:
302
- user = await self._authenticate(request)
303
- request.state.user = user
304
- except Exception:
305
- pass # Silently ignore all errors
394
+ # Or with shortcut:
395
+ app = CoreApp(middleware=["auth"])
306
396
 
307
- return await call_next(request)
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")
308
422
 
309
423
 
310
- # =============================================================================
311
- # Auto-configuration helper
312
- # =============================================================================
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
313
432
 
314
- _middleware_registered = False
315
433
 
434
+ # =============================================================================
435
+ # Legacy Compatibility
436
+ # =============================================================================
316
437
 
317
438
  def ensure_auth_middleware(app: Any) -> None:
318
439
  """
319
440
  Ensure AuthenticationMiddleware is registered on the app.
320
441
 
321
- Call this from configure_auth() when auto_middleware=True.
322
-
323
- Args:
324
- app: FastAPI or CoreApp instance
442
+ DEPRECATED: Use middleware=["auth"] instead.
325
443
  """
326
- global _middleware_registered
327
-
328
- if _middleware_registered:
329
- return
330
-
331
- # Try to add middleware
444
+ logger.warning(
445
+ "ensure_auth_middleware is deprecated. "
446
+ "Use middleware=['auth'] or add AuthenticationMiddleware directly."
447
+ )
332
448
  try:
333
449
  if hasattr(app, "add_middleware"):
334
450
  app.add_middleware(AuthenticationMiddleware)
335
- _middleware_registered = True
336
- except Exception:
337
- pass
451
+ logger.info("AuthenticationMiddleware added to app")
452
+ except Exception as e:
453
+ logger.error(f"Failed to add AuthenticationMiddleware: {e}")
454
+ raise
338
455
 
339
456
 
340
457
  def reset_middleware_state() -> None:
341
- """Reset middleware registration state (for testing)."""
342
- global _middleware_registered
343
- _middleware_registered = False
458
+ """Reset middleware state (for testing)."""
459
+ pass # No longer needed with new implementation
344
460
 
345
461
 
346
462
  # =============================================================================
@@ -350,6 +466,8 @@ def reset_middleware_state() -> None:
350
466
  __all__ = [
351
467
  "AuthenticationMiddleware",
352
468
  "OptionalAuthenticationMiddleware",
469
+ "JWTAuthBackend",
470
+ "AuthenticatedUser",
353
471
  "ensure_auth_middleware",
354
472
  "reset_middleware_state",
355
473
  ]
core/auth/models.py CHANGED
@@ -58,6 +58,7 @@ def _get_pk_column_type(model_class: type) -> type:
58
58
  Detecta o tipo da coluna PK de um modelo.
59
59
 
60
60
  Bug #3 Fix: Detecção robusta do tipo de PK para FKs.
61
+ Verifica toda a cadeia de herança (MRO) para detectar corretamente.
61
62
 
62
63
  Suporta:
63
64
  - Integer (int)
@@ -71,6 +72,7 @@ def _get_pk_column_type(model_class: type) -> type:
71
72
  from sqlalchemy import Integer, BigInteger, String
72
73
  from sqlalchemy.dialects.postgresql import UUID as PG_UUID
73
74
  from sqlalchemy import Uuid
75
+ import uuid as uuid_module
74
76
 
75
77
  # Verifica cache primeiro
76
78
  cache_key = f"{model_class.__module__}.{model_class.__name__}"
@@ -79,7 +81,30 @@ def _get_pk_column_type(model_class: type) -> type:
79
81
 
80
82
  detected_type = Integer # Default
81
83
 
82
- # Método 1: Tenta obter da tabela mapeada
84
+ # Método 0 (MAIS IMPORTANTE): Verifica herança de AbstractUUIDUser
85
+ # Isso é verificado PRIMEIRO porque funciona mesmo durante o mapeamento
86
+ for base in model_class.__mro__:
87
+ base_name = base.__name__
88
+ # Verifica se herda de AbstractUUIDUser ou qualquer classe com UUID no nome
89
+ if base_name == "AbstractUUIDUser":
90
+ detected_type = PG_UUID
91
+ _pk_type_cache[cache_key] = detected_type
92
+ return detected_type
93
+
94
+ # Método 1: Verifica annotations em TODA a cadeia de herança (MRO)
95
+ for base in model_class.__mro__:
96
+ annotations = getattr(base, "__annotations__", {})
97
+ if "id" in annotations:
98
+ ann = annotations["id"]
99
+ ann_str = str(ann)
100
+
101
+ # Detecta UUID (vários formatos)
102
+ if "UUID" in ann_str or "uuid" in ann_str or "Uuid" in ann_str:
103
+ detected_type = PG_UUID
104
+ _pk_type_cache[cache_key] = detected_type
105
+ return detected_type
106
+
107
+ # Método 2: Tenta obter da tabela já mapeada (se existir)
83
108
  if hasattr(model_class, "__table__"):
84
109
  pk_columns = [c for c in model_class.__table__.columns if c.primary_key]
85
110
  if pk_columns:
@@ -102,40 +127,41 @@ def _get_pk_column_type(model_class: type) -> type:
102
127
  _pk_type_cache[cache_key] = detected_type
103
128
  return detected_type
104
129
 
105
- # Método 2: Tenta via annotations
106
- annotations = getattr(model_class, "__annotations__", {})
107
-
108
- if "id" in annotations:
109
- ann = annotations["id"]
110
- ann_str = str(ann)
111
-
112
- # Detecta UUID (vários formatos)
113
- if "UUID" in ann_str or "uuid" in ann_str or "Uuid" in ann_str:
114
- detected_type = PG_UUID
115
- # Detecta int
116
- elif "int" in ann_str.lower() and "uuid" not in ann_str.lower():
117
- detected_type = Integer
118
- # Detecta str
119
- elif "str" in ann_str.lower() and "uuid" not in ann_str.lower():
120
- detected_type = String
121
-
122
- # Método 3: Verifica campos na classe (declared_attr ou column_property)
123
- for attr_name in dir(model_class):
124
- if attr_name == "id":
125
- attr = getattr(model_class, attr_name, None)
130
+ # Método 3: Verifica atributo 'id' diretamente na classe e bases
131
+ for base in model_class.__mro__:
132
+ if hasattr(base, "id"):
133
+ attr = getattr(base, "id", None)
126
134
  if attr is not None:
127
- # Pode ser um InstrumentedAttribute
135
+ # Pode ser um InstrumentedAttribute ou MappedColumn
128
136
  if hasattr(attr, "type"):
129
137
  attr_type = attr.type
130
138
  if isinstance(attr_type, (PG_UUID, Uuid)):
131
139
  detected_type = PG_UUID
132
140
  break
141
+ type_name = type(attr_type).__name__.upper()
142
+ if "UUID" in type_name:
143
+ detected_type = PG_UUID
144
+ break
133
145
  # Pode ser um mapped_column
134
146
  if hasattr(attr, "property") and hasattr(attr.property, "columns"):
135
147
  for col in attr.property.columns:
136
148
  if isinstance(col.type, (PG_UUID, Uuid)):
137
149
  detected_type = PG_UUID
138
150
  break
151
+ type_name = type(col.type).__name__.upper()
152
+ if "UUID" in type_name:
153
+ detected_type = PG_UUID
154
+ break
155
+ # Verifica se é um MappedColumn com tipo definido
156
+ if hasattr(attr, "column") and hasattr(attr.column, "type"):
157
+ col_type = attr.column.type
158
+ if isinstance(col_type, (PG_UUID, Uuid)):
159
+ detected_type = PG_UUID
160
+ break
161
+ type_name = type(col_type).__name__.upper()
162
+ if "UUID" in type_name:
163
+ detected_type = PG_UUID
164
+ break
139
165
 
140
166
  _pk_type_cache[cache_key] = detected_type
141
167
  return detected_type
core/auth/tokens.py CHANGED
@@ -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:
core/auth/views.py CHANGED
@@ -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
  Metadata-Version: 2.4
2
2
  Name: core-framework
3
- Version: 0.12.3
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
@@ -1,4 +1,4 @@
1
- core/__init__.py,sha256=sy9liwZ93LJntl_s54pgi3oAM8vHPC_dgaOeVAydmtY,12058
1
+ core/__init__.py,sha256=pc5DiPlXIs8HLdJ6-rz30XpNpVTCGRhDxLw8VG65iDg,12058
2
2
  core/app.py,sha256=sCA3mJI696i7MIjrPxfOr5zEYt0njarQfHHy3EAajk4,21071
3
3
  core/choices.py,sha256=rhcL3p2dB7RK99zIilpmoTFVcibQEIaRpz0CY0kImCE,10502
4
4
  core/config.py,sha256=2-MVF9nLoYmxpYYH_Gzn4-Sa3MU87YZskRPtlNyhg6Q,14049
@@ -22,12 +22,12 @@ core/auth/backends.py,sha256=R-siIE8TrNqDHkCx42zXN1WVvvuWOun1nj8D5elrC9g,10425
22
22
  core/auth/base.py,sha256=Q7vXgwTmgdmyW7G8eJmDket2bKB_8YFnraZ_kK9_gTs,21425
23
23
  core/auth/decorators.py,sha256=tmC7prKUvHuzQ3J872nM6r83DR9d82dCLXKLvUB1Os8,12288
24
24
  core/auth/hashers.py,sha256=0gIf67TU0k5H744FADpyh9_ugxA7m3mhYPZxLh_lEtc,12808
25
- core/auth/middleware.py,sha256=r4F3AIb4k9Z7gbTwcyG-MVWCyGikQqP54IHNKmyNtdc,10963
26
- core/auth/models.py,sha256=3ekHuaiSNhyQ6K1-w-TNmvtC406qhTT8AttA03Zl3pQ,32636
25
+ core/auth/middleware.py,sha256=3Wddxi2MKHrHguKfdW9LRKhHaHcmQ807edVcsFLqhVc,16645
26
+ core/auth/models.py,sha256=aEE7deQKPS1aH0Btzzh3Z1Bwuqy8zvLZwu4JFEmiUNk,34058
27
27
  core/auth/permissions.py,sha256=v3ykAgNpq5wJ0NkuC_FuveMctOkDfM9Xp11XEnUAuBg,12461
28
28
  core/auth/schemas.py,sha256=L0W96dOD348rJDGeu1K5Rz3aJj-GdwMr2vbwwsYfo2g,3469
29
- core/auth/tokens.py,sha256=jk-TnMRdVGPhy6pWqSF2Ef8RTqLrP6Mkuo5GvRQh9no,8489
30
- core/auth/views.py,sha256=n-WhSIVHJCsjyxBFrI2JCfy-kRpg4YybsOCPnbRpwWM,13277
29
+ core/auth/tokens.py,sha256=jOF40D5O8WRG8klRwMBuSG-jOhdsp1irXn2aZ2puNSg,9149
30
+ core/auth/views.py,sha256=3gMaq8pzWoWr29ExJk21JgGay2fE3Fq3Tz99Wb_ftvE,14203
31
31
  core/cli/__init__.py,sha256=obodnvfe8DUziqpk-IAaHTEOb1KSfYQeuBZEAofut4o,449
32
32
  core/cli/main.py,sha256=daWz8tuMkMYrkNBfueDH5OghncdqLs3k7BUMmvDsSvk,119635
33
33
  core/deployment/__init__.py,sha256=RNcBRO9oB3WRnhtTTwM6wzVEcUKpKF4XfRkGSbbykIc,794
@@ -78,7 +78,7 @@ example/auth.py,sha256=zBpLutb8lVKnGfQqQ2wnyygsSutHYZzeJBuhnFhxBaQ,4971
78
78
  example/models.py,sha256=xKdx0kJ9n0tZ7sCce3KhV3BTvKvsh6m7G69eFm3ukf0,4549
79
79
  example/schemas.py,sha256=wJ9QofnuHp4PjtM_IuMMBLVFVDJ4YlwcF6uQm1ooKiY,6139
80
80
  example/views.py,sha256=GQwgQcW6yoeUIDbF7-lsaZV7cs8G1S1vGVtiwVpZIQE,14338
81
- core_framework-0.12.3.dist-info/METADATA,sha256=XE6KCQJj3FgemixbQXBicIsp3CrsX5al-SzEPH-vtNk,12791
82
- core_framework-0.12.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
83
- core_framework-0.12.3.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
84
- core_framework-0.12.3.dist-info/RECORD,,
81
+ core_framework-0.12.5.dist-info/METADATA,sha256=INxj7mhfca6AsK23r_uZ4pMozXdOumZN9EM9BUV2IcI,12791
82
+ core_framework-0.12.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
83
+ core_framework-0.12.5.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
84
+ core_framework-0.12.5.dist-info/RECORD,,