core-framework 0.12.1__py3-none-any.whl → 0.12.2__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 +66 -2
- core/app.py +65 -3
- core/auth/__init__.py +27 -2
- core/auth/base.py +146 -0
- core/auth/middleware.py +316 -0
- core/auth/models.py +139 -23
- core/auth/schemas.py +5 -1
- core/auth/views.py +168 -50
- core/config.py +27 -0
- core/middleware.py +774 -0
- core/migrations/operations.py +88 -10
- core/views.py +453 -28
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/METADATA +1 -1
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/RECORD +16 -14
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/WHEEL +0 -0
- {core_framework-0.12.1.dist-info → core_framework-0.12.2.dist-info}/entry_points.txt +0 -0
core/auth/middleware.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication Middleware for Core Framework.
|
|
3
|
+
|
|
4
|
+
Bug #8 Fix: Provides built-in middleware for populating request.state.user.
|
|
5
|
+
|
|
6
|
+
This middleware automatically authenticates requests using the configured
|
|
7
|
+
authentication backend and populates request.state.user.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from core.auth.middleware import AuthenticationMiddleware
|
|
11
|
+
|
|
12
|
+
app = CoreApp(
|
|
13
|
+
middlewares=[(AuthenticationMiddleware, {})],
|
|
14
|
+
)
|
|
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)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Any, TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
35
|
+
from starlette.requests import Request
|
|
36
|
+
from starlette.responses import Response
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from collections.abc import Callable, Awaitable
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
43
|
+
"""
|
|
44
|
+
Middleware that authenticates requests and populates request.state.user.
|
|
45
|
+
|
|
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
|
|
52
|
+
|
|
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}
|
|
67
|
+
|
|
68
|
+
Configuration via kwargs:
|
|
69
|
+
- user_model: User model class (uses global config if None)
|
|
70
|
+
- header_name: Header to extract token from (default: "Authorization")
|
|
71
|
+
- scheme: Expected scheme (default: "Bearer")
|
|
72
|
+
- skip_paths: List of paths to skip authentication (e.g., ["/health"])
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
app: "Callable[[Request], Awaitable[Response]]",
|
|
78
|
+
user_model: type | None = None,
|
|
79
|
+
header_name: str = "Authorization",
|
|
80
|
+
scheme: str = "Bearer",
|
|
81
|
+
skip_paths: list[str] | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
super().__init__(app)
|
|
84
|
+
self._user_model = user_model
|
|
85
|
+
self.header_name = header_name
|
|
86
|
+
self.scheme = scheme
|
|
87
|
+
self.skip_paths = skip_paths or []
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def user_model(self) -> type | None:
|
|
91
|
+
"""Get user model from instance or global config."""
|
|
92
|
+
if self._user_model is not None:
|
|
93
|
+
return self._user_model
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
from core.auth.models import get_user_model
|
|
97
|
+
return get_user_model()
|
|
98
|
+
except Exception:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
async def dispatch(
|
|
102
|
+
self,
|
|
103
|
+
request: Request,
|
|
104
|
+
call_next: "Callable[[Request], Awaitable[Response]]",
|
|
105
|
+
) -> Response:
|
|
106
|
+
"""
|
|
107
|
+
Process request and authenticate user.
|
|
108
|
+
|
|
109
|
+
Always sets request.state.user (to User or None).
|
|
110
|
+
"""
|
|
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)
|
|
138
|
+
|
|
139
|
+
if not auth_header:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
parts = auth_header.split()
|
|
143
|
+
|
|
144
|
+
if len(parts) != 2:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
scheme, token = parts
|
|
148
|
+
|
|
149
|
+
if scheme.lower() != self.scheme.lower():
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
return token
|
|
153
|
+
|
|
154
|
+
async def _authenticate(self, request: Request) -> Any | None:
|
|
155
|
+
"""
|
|
156
|
+
Authenticate request and return user.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
User instance if authenticated, None otherwise
|
|
160
|
+
"""
|
|
161
|
+
token = self._extract_token(request)
|
|
162
|
+
|
|
163
|
+
if not token:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# Verify token
|
|
167
|
+
from core.auth.tokens import verify_token
|
|
168
|
+
|
|
169
|
+
payload = verify_token(token, token_type="access")
|
|
170
|
+
|
|
171
|
+
if payload is None:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Get user_id from token
|
|
175
|
+
user_id = payload.get("sub") or payload.get("user_id")
|
|
176
|
+
|
|
177
|
+
if not user_id:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
# Get user from database
|
|
181
|
+
User = self.user_model
|
|
182
|
+
if User is None:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# Get database session
|
|
186
|
+
from core.database import get_read_session
|
|
187
|
+
|
|
188
|
+
async for db in get_read_session():
|
|
189
|
+
try:
|
|
190
|
+
# Convert user_id to correct type
|
|
191
|
+
user_id_converted = self._convert_user_id(user_id, User)
|
|
192
|
+
|
|
193
|
+
user = await User.objects.using(db).filter(id=user_id_converted).first()
|
|
194
|
+
|
|
195
|
+
if user is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Check if user is active
|
|
199
|
+
if hasattr(user, "is_active") and not user.is_active:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
return user
|
|
203
|
+
except Exception:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def _convert_user_id(self, user_id: str, User: type) -> Any:
|
|
209
|
+
"""
|
|
210
|
+
Convert user_id string to the correct type based on model.
|
|
211
|
+
|
|
212
|
+
Handles INTEGER, UUID, and string IDs.
|
|
213
|
+
"""
|
|
214
|
+
from uuid import UUID
|
|
215
|
+
|
|
216
|
+
# Try to detect PK type
|
|
217
|
+
try:
|
|
218
|
+
from core.auth.models import _get_pk_column_type
|
|
219
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
220
|
+
from sqlalchemy import Integer, BigInteger
|
|
221
|
+
|
|
222
|
+
pk_type = _get_pk_column_type(User)
|
|
223
|
+
|
|
224
|
+
if pk_type == PG_UUID:
|
|
225
|
+
return UUID(user_id)
|
|
226
|
+
elif pk_type in (Integer, BigInteger):
|
|
227
|
+
return int(user_id)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Try UUID first, then int, then string
|
|
232
|
+
try:
|
|
233
|
+
return UUID(user_id)
|
|
234
|
+
except (ValueError, TypeError):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
return int(user_id)
|
|
239
|
+
except (ValueError, TypeError):
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
return user_id
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class OptionalAuthenticationMiddleware(AuthenticationMiddleware):
|
|
246
|
+
"""
|
|
247
|
+
Same as AuthenticationMiddleware but never raises errors.
|
|
248
|
+
|
|
249
|
+
Always proceeds with the request, setting user to None on any failure.
|
|
250
|
+
Useful for endpoints that work both with and without authentication.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
async def dispatch(
|
|
254
|
+
self,
|
|
255
|
+
request: Request,
|
|
256
|
+
call_next: "Callable[[Request], Awaitable[Response]]",
|
|
257
|
+
) -> Response:
|
|
258
|
+
"""Process request, never failing on auth errors."""
|
|
259
|
+
request.state.user = None
|
|
260
|
+
|
|
261
|
+
if not self._should_skip(request.url.path):
|
|
262
|
+
try:
|
|
263
|
+
user = await self._authenticate(request)
|
|
264
|
+
request.state.user = user
|
|
265
|
+
except Exception:
|
|
266
|
+
pass # Silently ignore all errors
|
|
267
|
+
|
|
268
|
+
return await call_next(request)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# =============================================================================
|
|
272
|
+
# Auto-configuration helper
|
|
273
|
+
# =============================================================================
|
|
274
|
+
|
|
275
|
+
_middleware_registered = False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def ensure_auth_middleware(app: Any) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Ensure AuthenticationMiddleware is registered on the app.
|
|
281
|
+
|
|
282
|
+
Call this from configure_auth() when auto_middleware=True.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
app: FastAPI or CoreApp instance
|
|
286
|
+
"""
|
|
287
|
+
global _middleware_registered
|
|
288
|
+
|
|
289
|
+
if _middleware_registered:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# Try to add middleware
|
|
293
|
+
try:
|
|
294
|
+
if hasattr(app, "add_middleware"):
|
|
295
|
+
app.add_middleware(AuthenticationMiddleware)
|
|
296
|
+
_middleware_registered = True
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def reset_middleware_state() -> None:
|
|
302
|
+
"""Reset middleware registration state (for testing)."""
|
|
303
|
+
global _middleware_registered
|
|
304
|
+
_middleware_registered = False
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# Exports
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
__all__ = [
|
|
312
|
+
"AuthenticationMiddleware",
|
|
313
|
+
"OptionalAuthenticationMiddleware",
|
|
314
|
+
"ensure_auth_middleware",
|
|
315
|
+
"reset_middleware_state",
|
|
316
|
+
]
|
core/auth/models.py
CHANGED
|
@@ -47,11 +47,16 @@ if TYPE_CHECKING:
|
|
|
47
47
|
# Cache de tabelas criadas para evitar duplicação
|
|
48
48
|
_association_tables: dict[str, Table] = {}
|
|
49
49
|
|
|
50
|
+
# Cache de tipos de PK detectados
|
|
51
|
+
_pk_type_cache: dict[str, type] = {}
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
def _get_pk_column_type(model_class: type) -> type:
|
|
52
55
|
"""
|
|
53
56
|
Detecta o tipo da coluna PK de um modelo.
|
|
54
57
|
|
|
58
|
+
Bug #3 Fix: Detecção robusta do tipo de PK para FKs.
|
|
59
|
+
|
|
55
60
|
Suporta:
|
|
56
61
|
- Integer (int)
|
|
57
62
|
- BigInteger (int com bigint=True)
|
|
@@ -63,42 +68,84 @@ def _get_pk_column_type(model_class: type) -> type:
|
|
|
63
68
|
"""
|
|
64
69
|
from sqlalchemy import Integer, BigInteger, String
|
|
65
70
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
71
|
+
from sqlalchemy import Uuid
|
|
66
72
|
import uuid
|
|
67
73
|
|
|
68
|
-
#
|
|
74
|
+
# Verifica cache primeiro
|
|
75
|
+
cache_key = f"{model_class.__module__}.{model_class.__name__}"
|
|
76
|
+
if cache_key in _pk_type_cache:
|
|
77
|
+
return _pk_type_cache[cache_key]
|
|
78
|
+
|
|
79
|
+
detected_type = Integer # Default
|
|
80
|
+
|
|
81
|
+
# Método 1: Tenta obter da tabela já mapeada
|
|
69
82
|
if hasattr(model_class, "__table__"):
|
|
70
|
-
# Modelo já mapeado - pega direto da tabela
|
|
71
83
|
pk_columns = [c for c in model_class.__table__.columns if c.primary_key]
|
|
72
84
|
if pk_columns:
|
|
73
|
-
|
|
85
|
+
pk_col_type = pk_columns[0].type
|
|
86
|
+
# Verifica se é UUID
|
|
87
|
+
if isinstance(pk_col_type, (PG_UUID, Uuid)):
|
|
88
|
+
detected_type = PG_UUID
|
|
89
|
+
elif isinstance(pk_col_type, BigInteger):
|
|
90
|
+
detected_type = BigInteger
|
|
91
|
+
elif isinstance(pk_col_type, String):
|
|
92
|
+
detected_type = String
|
|
93
|
+
elif isinstance(pk_col_type, Integer):
|
|
94
|
+
detected_type = Integer
|
|
95
|
+
else:
|
|
96
|
+
# Tenta pelo nome do tipo
|
|
97
|
+
type_name = type(pk_col_type).__name__.upper()
|
|
98
|
+
if "UUID" in type_name:
|
|
99
|
+
detected_type = PG_UUID
|
|
100
|
+
|
|
101
|
+
_pk_type_cache[cache_key] = detected_type
|
|
102
|
+
return detected_type
|
|
74
103
|
|
|
75
|
-
# Tenta via annotations
|
|
104
|
+
# Método 2: Tenta via annotations
|
|
76
105
|
annotations = getattr(model_class, "__annotations__", {})
|
|
77
106
|
|
|
78
107
|
if "id" in annotations:
|
|
79
108
|
ann = annotations["id"]
|
|
80
109
|
ann_str = str(ann)
|
|
81
110
|
|
|
82
|
-
# Detecta UUID
|
|
83
|
-
if "UUID" in ann_str or "uuid" in ann_str:
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
# Detecta UUID (vários formatos)
|
|
112
|
+
if "UUID" in ann_str or "uuid" in ann_str or "Uuid" in ann_str:
|
|
113
|
+
detected_type = PG_UUID
|
|
86
114
|
# Detecta int
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
elif "int" in ann_str.lower() and "uuid" not in ann_str.lower():
|
|
116
|
+
detected_type = Integer
|
|
90
117
|
# Detecta str
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
118
|
+
elif "str" in ann_str.lower() and "uuid" not in ann_str.lower():
|
|
119
|
+
detected_type = String
|
|
120
|
+
|
|
121
|
+
# Método 3: Verifica campos na classe (declared_attr ou column_property)
|
|
122
|
+
for attr_name in dir(model_class):
|
|
123
|
+
if attr_name == "id":
|
|
124
|
+
attr = getattr(model_class, attr_name, None)
|
|
125
|
+
if attr is not None:
|
|
126
|
+
# Pode ser um InstrumentedAttribute
|
|
127
|
+
if hasattr(attr, "type"):
|
|
128
|
+
attr_type = attr.type
|
|
129
|
+
if isinstance(attr_type, (PG_UUID, Uuid)):
|
|
130
|
+
detected_type = PG_UUID
|
|
131
|
+
break
|
|
132
|
+
# Pode ser um mapped_column
|
|
133
|
+
if hasattr(attr, "property") and hasattr(attr.property, "columns"):
|
|
134
|
+
for col in attr.property.columns:
|
|
135
|
+
if isinstance(col.type, (PG_UUID, Uuid)):
|
|
136
|
+
detected_type = PG_UUID
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
_pk_type_cache[cache_key] = detected_type
|
|
140
|
+
return detected_type
|
|
96
141
|
|
|
97
142
|
|
|
98
143
|
def _create_user_fk_column(user_tablename: str, user_model: type | None = None):
|
|
99
144
|
"""
|
|
100
145
|
Cria coluna FK para user_id detectando automaticamente o tipo.
|
|
101
146
|
|
|
147
|
+
Bug #3 Fix: Suporte correto a UUID em FKs.
|
|
148
|
+
|
|
102
149
|
Args:
|
|
103
150
|
user_tablename: Nome da tabela do usuário
|
|
104
151
|
user_model: Classe do modelo (opcional, para detecção de tipo)
|
|
@@ -108,9 +155,10 @@ def _create_user_fk_column(user_tablename: str, user_model: type | None = None):
|
|
|
108
155
|
"""
|
|
109
156
|
from sqlalchemy import Integer, BigInteger, String
|
|
110
157
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
158
|
+
from sqlalchemy import Uuid
|
|
111
159
|
|
|
112
160
|
# Tenta detectar o tipo do modelo
|
|
113
|
-
col_type = Integer # Default
|
|
161
|
+
col_type: Any = Integer # Default
|
|
114
162
|
|
|
115
163
|
if user_model is not None:
|
|
116
164
|
detected = _get_pk_column_type(user_model)
|
|
@@ -122,6 +170,16 @@ def _create_user_fk_column(user_tablename: str, user_model: type | None = None):
|
|
|
122
170
|
col_type = String(36) # UUID como string
|
|
123
171
|
else:
|
|
124
172
|
col_type = Integer
|
|
173
|
+
else:
|
|
174
|
+
# Sem modelo, tenta inferir da configuração global
|
|
175
|
+
try:
|
|
176
|
+
config = get_auth_config()
|
|
177
|
+
if config.user_model is not None:
|
|
178
|
+
detected = _get_pk_column_type(config.user_model)
|
|
179
|
+
if detected == PG_UUID:
|
|
180
|
+
col_type = PG_UUID(as_uuid=True)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
125
183
|
|
|
126
184
|
return Column(
|
|
127
185
|
"user_id",
|
|
@@ -131,6 +189,17 @@ def _create_user_fk_column(user_tablename: str, user_model: type | None = None):
|
|
|
131
189
|
)
|
|
132
190
|
|
|
133
191
|
|
|
192
|
+
def clear_association_table_cache() -> None:
|
|
193
|
+
"""
|
|
194
|
+
Limpa o cache de tabelas de associação.
|
|
195
|
+
|
|
196
|
+
Útil para testes ou quando o modelo de usuário muda.
|
|
197
|
+
"""
|
|
198
|
+
global _association_tables, _pk_type_cache
|
|
199
|
+
_association_tables.clear()
|
|
200
|
+
_pk_type_cache.clear()
|
|
201
|
+
|
|
202
|
+
|
|
134
203
|
def get_user_groups_table(user_tablename: str = "auth_users", user_model: type | None = None) -> Table:
|
|
135
204
|
"""
|
|
136
205
|
Obtém ou cria tabela de associação user <-> groups.
|
|
@@ -433,17 +502,29 @@ class AbstractUser(Model):
|
|
|
433
502
|
"""
|
|
434
503
|
Modelo abstrato base para usuários.
|
|
435
504
|
|
|
505
|
+
Bug #4 Fix: Suporta tanto INTEGER quanto UUID como PK.
|
|
506
|
+
|
|
436
507
|
Herde desta classe para criar seu modelo de usuário customizado:
|
|
437
508
|
|
|
509
|
+
# Com INTEGER (padrão)
|
|
438
510
|
class User(AbstractUser, PermissionsMixin):
|
|
439
511
|
__tablename__ = "users"
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
512
|
+
|
|
513
|
+
# Com UUID - sobrescreva o campo id
|
|
514
|
+
from core.fields import AdvancedField
|
|
515
|
+
from uuid import UUID
|
|
516
|
+
|
|
517
|
+
class User(AbstractUser, PermissionsMixin):
|
|
518
|
+
__tablename__ = "users"
|
|
519
|
+
id: Mapped[UUID] = AdvancedField.uuid_pk() # Override para UUID
|
|
520
|
+
|
|
521
|
+
Ou use AbstractUUIDUser para UUID por padrão:
|
|
522
|
+
|
|
523
|
+
class User(AbstractUUIDUser, PermissionsMixin):
|
|
524
|
+
__tablename__ = "users"
|
|
444
525
|
|
|
445
526
|
Campos incluídos:
|
|
446
|
-
- id: Chave primária
|
|
527
|
+
- id: Chave primária (INTEGER por padrão, pode ser UUID)
|
|
447
528
|
- email: Email único (usado para login)
|
|
448
529
|
- password_hash: Hash da senha
|
|
449
530
|
- is_active: Se o usuário está ativo
|
|
@@ -455,7 +536,7 @@ class AbstractUser(Model):
|
|
|
455
536
|
|
|
456
537
|
__abstract__ = True
|
|
457
538
|
|
|
458
|
-
# Campos de autenticação
|
|
539
|
+
# Campos de autenticação - id pode ser sobrescrito por subclasses
|
|
459
540
|
id: Mapped[int] = Field.pk()
|
|
460
541
|
email: Mapped[str] = Field.string(max_length=255, unique=True, index=True)
|
|
461
542
|
password_hash: Mapped[str] = Field.string(max_length=255)
|
|
@@ -682,6 +763,41 @@ class AbstractUser(Model):
|
|
|
682
763
|
return await cls.objects.using(db).filter(email=email.lower()).first()
|
|
683
764
|
|
|
684
765
|
|
|
766
|
+
# =============================================================================
|
|
767
|
+
# AbstractUUIDUser Model (Bug #4 Fix)
|
|
768
|
+
# =============================================================================
|
|
769
|
+
|
|
770
|
+
class AbstractUUIDUser(AbstractUser):
|
|
771
|
+
"""
|
|
772
|
+
Modelo abstrato base para usuários com UUID como PK.
|
|
773
|
+
|
|
774
|
+
Bug #4 Fix: Fornece uma versão do AbstractUser que usa UUID por padrão.
|
|
775
|
+
|
|
776
|
+
Ideal para:
|
|
777
|
+
- Sistemas distribuídos
|
|
778
|
+
- APIs públicas (UUIDs são mais seguros que IDs sequenciais)
|
|
779
|
+
- Microservices
|
|
780
|
+
|
|
781
|
+
Exemplo:
|
|
782
|
+
from core.auth import AbstractUUIDUser, PermissionsMixin
|
|
783
|
+
|
|
784
|
+
class User(AbstractUUIDUser, PermissionsMixin):
|
|
785
|
+
__tablename__ = "users"
|
|
786
|
+
|
|
787
|
+
# Campos adicionais
|
|
788
|
+
name: Mapped[str] = Field.string(max_length=100)
|
|
789
|
+
"""
|
|
790
|
+
|
|
791
|
+
__abstract__ = True
|
|
792
|
+
|
|
793
|
+
# Importa aqui para evitar circular import
|
|
794
|
+
from core.fields import AdvancedField
|
|
795
|
+
from uuid import UUID as UUIDType
|
|
796
|
+
|
|
797
|
+
# Override: usa UUID como PK
|
|
798
|
+
id: Mapped[UUIDType] = AdvancedField.uuid_pk()
|
|
799
|
+
|
|
800
|
+
|
|
685
801
|
# =============================================================================
|
|
686
802
|
# PermissionsMixin
|
|
687
803
|
# =============================================================================
|
core/auth/schemas.py
CHANGED
|
@@ -100,12 +100,16 @@ class BaseUserOutput(OutputSchema):
|
|
|
100
100
|
"""
|
|
101
101
|
Base user output schema.
|
|
102
102
|
|
|
103
|
+
Bug #4 Fix: Now supports both int and UUID ids.
|
|
104
|
+
|
|
103
105
|
Extend to add custom fields:
|
|
104
106
|
class UserOutput(BaseUserOutput):
|
|
105
107
|
phone: str | None = None
|
|
106
108
|
avatar_url: str | None = None
|
|
109
|
+
|
|
110
|
+
For UUID users, the id will be automatically serialized to string.
|
|
107
111
|
"""
|
|
108
|
-
id: int
|
|
112
|
+
id: int | str # Supports both INTEGER and UUID (serialized as string)
|
|
109
113
|
email: str
|
|
110
114
|
is_active: bool = True
|
|
111
115
|
is_staff: bool = False
|