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/auth/views.py CHANGED
@@ -1,30 +1,31 @@
1
1
  """
2
- Ready-to-use authentication ViewSets.
2
+ ViewSets de autenticação prontos para uso.
3
3
 
4
- Provides common auth endpoints out of the box:
5
- - POST /auth/register - User registration
6
- - POST /auth/login - User login
7
- - POST /auth/refresh - Token refresh
8
- - GET /auth/me - Current user info
9
- - POST /auth/logout - Logout (optional)
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
- Example:
13
- from core.auth.views import CoreAuthViewSet
11
+ Exemplo:
12
+ from core.auth.views import AuthViewSet
14
13
  from myapp.models import User
15
14
 
16
- class AuthViewSet(CoreAuthViewSet):
15
+ class MyAuthViewSet(AuthViewSet):
17
16
  user_model = User
18
17
 
19
- # Register routes
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 CoreAuthViewSet(ViewSet):
47
+ class AuthViewSet(ViewSet):
47
48
  """
48
- Ready-to-use authentication ViewSet.
49
+ ViewSet de autenticação pronto para uso.
49
50
 
50
- Provides standard auth endpoints. Configure by setting class attributes:
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
- Attributes:
53
- user_model: Your User model class (required)
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(CoreAuthViewSet):
63
+ class MyAuthViewSet(AuthViewSet):
66
64
  user_model = User
67
- register_schema = RegisterInput # Optional custom schema
68
- user_output_schema = UserOutput # Optional custom output
65
+ extra_register_fields = ["name", "phone"]
69
66
 
70
- router.register_viewset("/auth", AuthViewSet, basename="auth")
67
+ router.register_viewset("/auth", MyAuthViewSet)
71
68
 
72
- Endpoints created:
73
- POST /auth/register - Register new user
74
- POST /auth/login - Login and get tokens
75
- POST /auth/refresh - Refresh access token
76
- GET /auth/me - Get current user
77
- POST /auth/change-password - 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
- """Create access and refresh tokens for user."""
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
- data={"sub": str(user.id), "email": user.email},
110
- expires_minutes=self.access_token_expire_minutes,
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
- data={"sub": str(user.id)},
114
- expires_days=self.refresh_token_expire_days,
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
- # Validate input
139
- validated = self.register_schema.model_validate(data)
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
- # Create user
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
- # Get user
224
- user_id = payload.get("sub")
225
- user = await User.objects.using(db).filter(id=int(user_id)).first()
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
- "CoreAuthViewSet",
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
  # =========================================================================