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/views.py
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
ViewSets de autenticação prontos para uso.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
- POST /auth/register -
|
|
6
|
-
- POST /auth/login -
|
|
7
|
-
- POST /auth/refresh -
|
|
8
|
-
- GET /auth/me -
|
|
9
|
-
- POST /auth/
|
|
10
|
-
- POST /auth/change-password - Change password
|
|
4
|
+
Endpoints fornecidos:
|
|
5
|
+
- POST /auth/register - Registro de usuário
|
|
6
|
+
- POST /auth/login - Login
|
|
7
|
+
- POST /auth/refresh - Renovar token
|
|
8
|
+
- GET /auth/me - Usuário atual
|
|
9
|
+
- POST /auth/change-password - Alterar senha
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
from core.auth.views import
|
|
11
|
+
Exemplo:
|
|
12
|
+
from core.auth.views import AuthViewSet
|
|
14
13
|
from myapp.models import User
|
|
15
14
|
|
|
16
|
-
class AuthViewSet
|
|
15
|
+
class MyAuthViewSet(AuthViewSet):
|
|
17
16
|
user_model = User
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
router.register_viewset("/auth", AuthViewSet, basename="auth")
|
|
18
|
+
router.register_viewset("/auth", MyAuthViewSet, basename="auth")
|
|
21
19
|
"""
|
|
22
20
|
|
|
23
21
|
from __future__ import annotations
|
|
24
22
|
|
|
23
|
+
from datetime import timedelta
|
|
25
24
|
from typing import Any, TYPE_CHECKING
|
|
25
|
+
from uuid import UUID
|
|
26
26
|
|
|
27
27
|
from fastapi import Request, HTTPException
|
|
28
|
+
from pydantic import create_model
|
|
28
29
|
|
|
29
30
|
from core.views import ViewSet, action
|
|
30
31
|
from core.permissions import AllowAny, IsAuthenticated
|
|
@@ -43,38 +44,34 @@ if TYPE_CHECKING:
|
|
|
43
44
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
class
|
|
47
|
+
class AuthViewSet(ViewSet):
|
|
47
48
|
"""
|
|
48
|
-
|
|
49
|
+
ViewSet de autenticação pronto para uso.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
Atributos configuráveis:
|
|
52
|
+
user_model: Classe do modelo User (obrigatório)
|
|
53
|
+
register_schema: Schema de registro customizado
|
|
54
|
+
login_schema: Schema de login customizado
|
|
55
|
+
user_output_schema: Schema de output do usuário
|
|
56
|
+
extra_register_fields: Campos extras aceitos no registro
|
|
57
|
+
access_token_expire_minutes: Expiração do access token (default: 30)
|
|
58
|
+
refresh_token_expire_days: Expiração do refresh token (default: 7)
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
register_schema: Custom registration schema (optional)
|
|
55
|
-
login_schema: Custom login schema (optional)
|
|
56
|
-
user_output_schema: Custom user output schema (optional)
|
|
57
|
-
access_token_expire_minutes: Token expiration (default: 30)
|
|
58
|
-
refresh_token_expire_days: Refresh token expiration (default: 7)
|
|
59
|
-
|
|
60
|
-
Example:
|
|
61
|
-
from core.auth.views import CoreAuthViewSet
|
|
62
|
-
from myapp.models import User
|
|
63
|
-
from myapp.schemas import RegisterInput, UserOutput
|
|
60
|
+
Exemplo:
|
|
61
|
+
from core.auth.views import AuthViewSet
|
|
64
62
|
|
|
65
|
-
class AuthViewSet
|
|
63
|
+
class MyAuthViewSet(AuthViewSet):
|
|
66
64
|
user_model = User
|
|
67
|
-
|
|
68
|
-
user_output_schema = UserOutput # Optional custom output
|
|
65
|
+
extra_register_fields = ["name", "phone"]
|
|
69
66
|
|
|
70
|
-
router.register_viewset("/auth",
|
|
67
|
+
router.register_viewset("/auth", MyAuthViewSet)
|
|
71
68
|
|
|
72
|
-
Endpoints
|
|
73
|
-
POST /auth/register
|
|
74
|
-
POST /auth/login
|
|
75
|
-
POST /auth/refresh
|
|
76
|
-
GET /auth/me
|
|
77
|
-
POST /auth/change-password
|
|
69
|
+
Endpoints:
|
|
70
|
+
POST /auth/register
|
|
71
|
+
POST /auth/login
|
|
72
|
+
POST /auth/refresh
|
|
73
|
+
GET /auth/me
|
|
74
|
+
POST /auth/change-password
|
|
78
75
|
"""
|
|
79
76
|
|
|
80
77
|
# Configuration - override in subclass or use get_user_model()
|
|
@@ -85,9 +82,15 @@ class CoreAuthViewSet(ViewSet):
|
|
|
85
82
|
access_token_expire_minutes: int = 30
|
|
86
83
|
refresh_token_expire_days: int = 7
|
|
87
84
|
|
|
85
|
+
# Bug #5 Fix: Extra fields to accept on registration
|
|
86
|
+
extra_register_fields: list[str] = []
|
|
87
|
+
|
|
88
88
|
# ViewSet config
|
|
89
89
|
tags: list[str] = ["auth"]
|
|
90
90
|
|
|
91
|
+
# Cache for dynamic schema
|
|
92
|
+
_dynamic_register_schema: type | None = None
|
|
93
|
+
|
|
91
94
|
def _get_user_model(self):
|
|
92
95
|
"""
|
|
93
96
|
Get user model from class attribute or global config.
|
|
@@ -103,15 +106,82 @@ class CoreAuthViewSet(ViewSet):
|
|
|
103
106
|
from core.auth.models import get_user_model
|
|
104
107
|
return get_user_model()
|
|
105
108
|
|
|
109
|
+
def _get_register_schema(self) -> type:
|
|
110
|
+
"""
|
|
111
|
+
Bug #5 Fix: Get registration schema with extra fields support.
|
|
112
|
+
|
|
113
|
+
If extra_register_fields is set, creates a dynamic schema that
|
|
114
|
+
accepts those additional fields.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Pydantic schema class for registration
|
|
118
|
+
"""
|
|
119
|
+
# If register_schema was explicitly overridden, use it
|
|
120
|
+
if self.register_schema != BaseRegisterInput:
|
|
121
|
+
return self.register_schema
|
|
122
|
+
|
|
123
|
+
# If no extra fields, use base schema
|
|
124
|
+
if not self.extra_register_fields:
|
|
125
|
+
return BaseRegisterInput
|
|
126
|
+
|
|
127
|
+
# Create dynamic schema with extra fields
|
|
128
|
+
if self._dynamic_register_schema is not None:
|
|
129
|
+
return self._dynamic_register_schema
|
|
130
|
+
|
|
131
|
+
# Build extra fields - all as optional strings by default
|
|
132
|
+
# Users can provide type hints via annotations in User model
|
|
133
|
+
extra_fields = {}
|
|
134
|
+
User = self._get_user_model()
|
|
135
|
+
user_annotations = getattr(User, "__annotations__", {})
|
|
136
|
+
|
|
137
|
+
for field_name in self.extra_register_fields:
|
|
138
|
+
# Try to get type from User model
|
|
139
|
+
field_type = user_annotations.get(field_name, str)
|
|
140
|
+
# Extract actual type from Mapped[...] if needed
|
|
141
|
+
field_type_str = str(field_type)
|
|
142
|
+
if "Mapped[" in field_type_str:
|
|
143
|
+
# It's a Mapped type, try to extract inner type
|
|
144
|
+
if "str" in field_type_str:
|
|
145
|
+
extra_fields[field_name] = (str | None, None)
|
|
146
|
+
elif "int" in field_type_str:
|
|
147
|
+
extra_fields[field_name] = (int | None, None)
|
|
148
|
+
elif "bool" in field_type_str:
|
|
149
|
+
extra_fields[field_name] = (bool | None, None)
|
|
150
|
+
else:
|
|
151
|
+
extra_fields[field_name] = (str | None, None)
|
|
152
|
+
else:
|
|
153
|
+
extra_fields[field_name] = (str | None, None)
|
|
154
|
+
|
|
155
|
+
# Create dynamic model
|
|
156
|
+
self._dynamic_register_schema = create_model(
|
|
157
|
+
"DynamicRegisterInput",
|
|
158
|
+
__base__=BaseRegisterInput,
|
|
159
|
+
__module__=__name__,
|
|
160
|
+
**extra_fields,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Allow extra fields
|
|
164
|
+
self._dynamic_register_schema.model_config = {
|
|
165
|
+
**BaseRegisterInput.model_config,
|
|
166
|
+
"extra": "ignore", # Ignore unknown fields instead of forbidding
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return self._dynamic_register_schema
|
|
170
|
+
|
|
106
171
|
def _create_tokens(self, user) -> dict:
|
|
107
|
-
"""
|
|
172
|
+
"""
|
|
173
|
+
Bug #6 Fix: Create access and refresh tokens using current API.
|
|
174
|
+
|
|
175
|
+
Uses the correct function signature with user_id and extra_claims.
|
|
176
|
+
"""
|
|
108
177
|
access_token = create_access_token(
|
|
109
|
-
|
|
110
|
-
|
|
178
|
+
user_id=str(user.id),
|
|
179
|
+
extra_claims={"email": getattr(user, "email", None)},
|
|
180
|
+
expires_delta=timedelta(minutes=self.access_token_expire_minutes),
|
|
111
181
|
)
|
|
112
182
|
refresh_token = create_refresh_token(
|
|
113
|
-
|
|
114
|
-
|
|
183
|
+
user_id=str(user.id),
|
|
184
|
+
expires_delta=timedelta(days=self.refresh_token_expire_days),
|
|
115
185
|
)
|
|
116
186
|
return {
|
|
117
187
|
"access_token": access_token,
|
|
@@ -120,6 +190,39 @@ class CoreAuthViewSet(ViewSet):
|
|
|
120
190
|
"expires_in": self.access_token_expire_minutes * 60,
|
|
121
191
|
}
|
|
122
192
|
|
|
193
|
+
def _convert_user_id(self, user_id: str, User: type) -> Any:
|
|
194
|
+
"""
|
|
195
|
+
Bug #7 Fix: Convert user_id string to the correct type.
|
|
196
|
+
|
|
197
|
+
Intelligently converts based on the User model's PK type.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
user_id: String representation of user ID
|
|
201
|
+
User: User model class
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Converted user ID in the correct type
|
|
205
|
+
"""
|
|
206
|
+
# Try to detect PK type from model
|
|
207
|
+
from core.auth.models import _get_pk_column_type
|
|
208
|
+
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
209
|
+
from sqlalchemy import Integer, BigInteger, String
|
|
210
|
+
|
|
211
|
+
pk_type = _get_pk_column_type(User)
|
|
212
|
+
|
|
213
|
+
if pk_type == PG_UUID:
|
|
214
|
+
try:
|
|
215
|
+
return UUID(user_id)
|
|
216
|
+
except (ValueError, TypeError):
|
|
217
|
+
return user_id
|
|
218
|
+
elif pk_type in (Integer, BigInteger):
|
|
219
|
+
try:
|
|
220
|
+
return int(user_id)
|
|
221
|
+
except (ValueError, TypeError):
|
|
222
|
+
return user_id
|
|
223
|
+
else:
|
|
224
|
+
return user_id
|
|
225
|
+
|
|
123
226
|
@action(methods=["POST"], detail=False, permission_classes=[AllowAny])
|
|
124
227
|
async def register(
|
|
125
228
|
self,
|
|
@@ -131,12 +234,15 @@ class CoreAuthViewSet(ViewSet):
|
|
|
131
234
|
"""
|
|
132
235
|
Register a new user.
|
|
133
236
|
|
|
237
|
+
Bug #5 Fix: Now supports extra_register_fields.
|
|
238
|
+
|
|
134
239
|
Returns tokens on successful registration.
|
|
135
240
|
"""
|
|
136
241
|
User = self._get_user_model()
|
|
137
242
|
|
|
138
|
-
#
|
|
139
|
-
|
|
243
|
+
# Bug #5 Fix: Use dynamic schema that includes extra fields
|
|
244
|
+
schema = self._get_register_schema()
|
|
245
|
+
validated = schema.model_validate(data)
|
|
140
246
|
|
|
141
247
|
# Check if user exists
|
|
142
248
|
existing = await User.get_by_email(validated.email, db)
|
|
@@ -146,11 +252,19 @@ class CoreAuthViewSet(ViewSet):
|
|
|
146
252
|
detail="User with this email already exists"
|
|
147
253
|
)
|
|
148
254
|
|
|
149
|
-
#
|
|
255
|
+
# Bug #5 Fix: Extract extra fields for user creation
|
|
256
|
+
extra_fields = {}
|
|
257
|
+
for field_name in self.extra_register_fields:
|
|
258
|
+
value = getattr(validated, field_name, None)
|
|
259
|
+
if value is not None:
|
|
260
|
+
extra_fields[field_name] = value
|
|
261
|
+
|
|
262
|
+
# Create user with extra fields
|
|
150
263
|
user = await User.create_user(
|
|
151
264
|
email=validated.email,
|
|
152
265
|
password=validated.password,
|
|
153
266
|
db=db,
|
|
267
|
+
**extra_fields,
|
|
154
268
|
)
|
|
155
269
|
|
|
156
270
|
# Commit the transaction
|
|
@@ -206,6 +320,8 @@ class CoreAuthViewSet(ViewSet):
|
|
|
206
320
|
) -> dict:
|
|
207
321
|
"""
|
|
208
322
|
Refresh access token using refresh token.
|
|
323
|
+
|
|
324
|
+
Bug #7 Fix: Now correctly handles UUID user IDs.
|
|
209
325
|
"""
|
|
210
326
|
User = self._get_user_model()
|
|
211
327
|
|
|
@@ -220,9 +336,11 @@ class CoreAuthViewSet(ViewSet):
|
|
|
220
336
|
detail="Invalid or expired refresh token"
|
|
221
337
|
)
|
|
222
338
|
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
339
|
+
# Bug #7 Fix: Convert user_id to correct type
|
|
340
|
+
user_id_str = payload.get("sub")
|
|
341
|
+
user_id = self._convert_user_id(user_id_str, User)
|
|
342
|
+
|
|
343
|
+
user = await User.objects.using(db).filter(id=user_id).first()
|
|
226
344
|
|
|
227
345
|
if user is None or not user.is_active:
|
|
228
346
|
raise HTTPException(
|
|
@@ -293,5 +411,5 @@ class CoreAuthViewSet(ViewSet):
|
|
|
293
411
|
# =============================================================================
|
|
294
412
|
|
|
295
413
|
__all__ = [
|
|
296
|
-
"
|
|
414
|
+
"AuthViewSet",
|
|
297
415
|
]
|
core/config.py
CHANGED
|
@@ -172,6 +172,33 @@ class Settings(BaseSettings):
|
|
|
172
172
|
description="Algoritmo de hash de senha (pbkdf2_sha256, argon2, bcrypt, scrypt)",
|
|
173
173
|
)
|
|
174
174
|
|
|
175
|
+
# =========================================================================
|
|
176
|
+
# Middleware (Django-style)
|
|
177
|
+
# =========================================================================
|
|
178
|
+
|
|
179
|
+
middleware: list[str] = PydanticField(
|
|
180
|
+
default=[],
|
|
181
|
+
description="""
|
|
182
|
+
Lista de middlewares a aplicar, estilo Django.
|
|
183
|
+
|
|
184
|
+
Formatos aceitos:
|
|
185
|
+
- String path: "core.auth.AuthenticationMiddleware"
|
|
186
|
+
- Shortcut: "auth", "timing", "logging", etc
|
|
187
|
+
|
|
188
|
+
Exemplo:
|
|
189
|
+
MIDDLEWARE='["timing", "auth", "core.middleware.LoggingMiddleware"]'
|
|
190
|
+
|
|
191
|
+
Shortcuts disponíveis:
|
|
192
|
+
- auth: AuthenticationMiddleware
|
|
193
|
+
- optional_auth: OptionalAuthenticationMiddleware
|
|
194
|
+
- timing: TimingMiddleware
|
|
195
|
+
- request_id: RequestIDMiddleware
|
|
196
|
+
- logging: LoggingMiddleware
|
|
197
|
+
- security_headers: SecurityHeadersMiddleware
|
|
198
|
+
- maintenance: MaintenanceModeMiddleware
|
|
199
|
+
""",
|
|
200
|
+
)
|
|
201
|
+
|
|
175
202
|
# =========================================================================
|
|
176
203
|
# DateTime / Timezone
|
|
177
204
|
# =========================================================================
|