maquinaweb-shared-auth 0.2.25__py3-none-any.whl → 0.2.27__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.
Potentially problematic release.
This version of maquinaweb-shared-auth might be problematic. Click here for more details.
- maquinaweb_shared_auth-0.2.27.dist-info/METADATA +1002 -0
- {maquinaweb_shared_auth-0.2.25.dist-info → maquinaweb_shared_auth-0.2.27.dist-info}/RECORD +6 -6
- shared_auth/models.py +11 -9
- shared_auth/serializers.py +3 -1
- maquinaweb_shared_auth-0.2.25.dist-info/METADATA +0 -1017
- {maquinaweb_shared_auth-0.2.25.dist-info → maquinaweb_shared_auth-0.2.27.dist-info}/WHEEL +0 -0
- {maquinaweb_shared_auth-0.2.25.dist-info → maquinaweb_shared_auth-0.2.27.dist-info}/top_level.txt +0 -0
|
@@ -1,1017 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: maquinaweb-shared-auth
|
|
3
|
-
Version: 0.2.25
|
|
4
|
-
Summary: Models read-only para autenticação compartilhada entre projetos Django.
|
|
5
|
-
Author-email: Seu Nome <seuemail@dominio.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/maquinaweb/maquinaweb-shared-auth
|
|
8
|
-
Project-URL: Repository, https://github.com/maquinaweb/maquinaweb-shared-auth
|
|
9
|
-
Project-URL: Issues, https://github.com/maquinaweb/maquinaweb-shared-auth/issues
|
|
10
|
-
Keywords: django,auth,models,shared
|
|
11
|
-
Classifier: Framework :: Django
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Requires-Python: >=3.8
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
Requires-Dist: django>=5
|
|
18
|
-
Requires-Dist: django-storages>=1.14.6
|
|
19
|
-
Requires-Dist: djangorestframework>=3
|
|
20
|
-
Requires-Dist: setuptools>=70
|
|
21
|
-
Dynamic: requires-python
|
|
22
|
-
|
|
23
|
-
# 📚 Guia Completo - Biblioteca Compartilhada de Autenticação
|
|
24
|
-
|
|
25
|
-
## 🎯 O que Esta Solução Faz?
|
|
26
|
-
|
|
27
|
-
Permite que **múltiplos sistemas Django** acessem os dados de **autenticação e organizações** diretamente do banco de dados, sem fazer requisições HTTP, mantendo a mesma interface do Django ORM que você conhece.
|
|
28
|
-
|
|
29
|
-
### Vantagens
|
|
30
|
-
|
|
31
|
-
✅ **Acesso direto ao banco** - Sem latência de API
|
|
32
|
-
✅ **Interface Django nativa** - `rascunho.user` funciona igual ao Django normal
|
|
33
|
-
✅ **Read-only** - Impossível modificar dados por engano
|
|
34
|
-
✅ **Sem duplicação** - Um único banco de autenticação para todos os sistemas
|
|
35
|
-
✅ **Type-safe** - Validações e exceções customizadas
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## 🏗️ Arquitetura
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
┌─────────────────────────────────────────┐
|
|
43
|
-
│ Sistema de Autenticação (Principal) │
|
|
44
|
-
│ │
|
|
45
|
-
│ ┌──────────────┐ ┌──────────────┐ │
|
|
46
|
-
│ │ Organization │ │ User │ │
|
|
47
|
-
│ └──────────────┘ └──────────────┘ │
|
|
48
|
-
│ │ │ │
|
|
49
|
-
│ └─────────┬─────────┘ │
|
|
50
|
-
│ │ │
|
|
51
|
-
│ ┌──────▼──────┐ │
|
|
52
|
-
│ │ Member │ │
|
|
53
|
-
│ └─────────────┘ │
|
|
54
|
-
└──────────────────┬──────────────────────┘
|
|
55
|
-
│
|
|
56
|
-
┌────────────┴────────────┐
|
|
57
|
-
│ Banco de Dados Auth │
|
|
58
|
-
│ (PostgreSQL/MySQL) │
|
|
59
|
-
└────────────┬────────────┘
|
|
60
|
-
│
|
|
61
|
-
┌────────────┴────────────┐
|
|
62
|
-
│ │
|
|
63
|
-
┌─────▼─────┐ ┌─────▼─────┐
|
|
64
|
-
│ Sistema A │ │ Sistema B │
|
|
65
|
-
│ │ │ │
|
|
66
|
-
│ Rascunho │ │ Pedido │
|
|
67
|
-
│ ├─ org │ │ ├─ org │
|
|
68
|
-
│ └─ user │ │ └─ user │
|
|
69
|
-
└───────────┘ └───────────┘
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 📦 Passo 1: Criar a Biblioteca
|
|
75
|
-
|
|
76
|
-
### 1.1. Estrutura de Diretórios
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
mkdir shared-auth-lib
|
|
80
|
-
cd shared-auth-lib
|
|
81
|
-
|
|
82
|
-
# Criar estrutura
|
|
83
|
-
mkdir shared_auth
|
|
84
|
-
touch setup.py README.md
|
|
85
|
-
touch shared_auth/__init__.py
|
|
86
|
-
touch shared_auth/models.py
|
|
87
|
-
touch shared_auth/managers.py
|
|
88
|
-
touch shared_auth/exceptions.py
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### 1.2. Copiar o Código
|
|
92
|
-
|
|
93
|
-
Copie os arquivos do artifact anterior para a estrutura criada.
|
|
94
|
-
|
|
95
|
-
### 1.3. Instalar Localmente
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
# Modo desenvolvimento (changes refletem automaticamente)
|
|
99
|
-
pip install -e /path/to/shared-auth-lib
|
|
100
|
-
|
|
101
|
-
# Ou adicionar ao requirements.txt
|
|
102
|
-
echo "-e /path/to/shared-auth-lib" >> requirements.txt
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 🔧 Passo 2: Configurar Banco de Dados
|
|
108
|
-
|
|
109
|
-
### 2.1. Criar Usuário Read-Only no PostgreSQL
|
|
110
|
-
|
|
111
|
-
```sql
|
|
112
|
-
-- No banco do sistema de autenticação
|
|
113
|
-
CREATE USER readonly_user WITH PASSWORD 'senha_segura';
|
|
114
|
-
|
|
115
|
-
-- Conceder permissões de leitura
|
|
116
|
-
GRANT CONNECT ON DATABASE sistema_auth_db TO readonly_user;
|
|
117
|
-
GRANT USAGE ON SCHEMA public TO readonly_user;
|
|
118
|
-
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
|
|
119
|
-
|
|
120
|
-
-- Para tabelas futuras
|
|
121
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
122
|
-
GRANT SELECT ON TABLES TO readonly_user;
|
|
123
|
-
|
|
124
|
-
-- Garantir read-only
|
|
125
|
-
ALTER USER readonly_user SET default_transaction_read_only = on;
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 2.2. Para MySQL
|
|
129
|
-
|
|
130
|
-
```sql
|
|
131
|
-
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'senha_segura';
|
|
132
|
-
GRANT SELECT ON sistema_auth_db.* TO 'readonly_user'@'%';
|
|
133
|
-
FLUSH PRIVILEGES;
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## ⚙️ Passo 3: Configurar Sistema Cliente
|
|
139
|
-
|
|
140
|
-
### 3.1. Settings.py
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
# settings.py do seu outro sistema
|
|
144
|
-
|
|
145
|
-
DATABASES = {
|
|
146
|
-
'default': {
|
|
147
|
-
# Banco do sistema atual
|
|
148
|
-
'ENGINE': 'django.db.backends.postgresql',
|
|
149
|
-
'NAME': 'meu_sistema_db',
|
|
150
|
-
'USER': 'meu_user',
|
|
151
|
-
'PASSWORD': 'senha',
|
|
152
|
-
'HOST': 'localhost',
|
|
153
|
-
'PORT': '5432',
|
|
154
|
-
},
|
|
155
|
-
'auth_db': {
|
|
156
|
-
# Banco do sistema de autenticação (READ-ONLY)
|
|
157
|
-
'ENGINE': 'django.db.backends.postgresql',
|
|
158
|
-
'NAME': 'sistema_auth_db',
|
|
159
|
-
'USER': 'readonly_user',
|
|
160
|
-
'PASSWORD': 'senha_readonly',
|
|
161
|
-
'HOST': 'localhost', # ou IP do servidor de auth
|
|
162
|
-
'PORT': '5432',
|
|
163
|
-
'OPTIONS': {
|
|
164
|
-
'options': '-c default_transaction_read_only=on'
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Router para direcionar queries
|
|
170
|
-
DATABASE_ROUTERS = ['myapp.routers.SharedAuthRouter']
|
|
171
|
-
|
|
172
|
-
INSTALLED_APPS = [
|
|
173
|
-
# ... suas apps
|
|
174
|
-
'shared_auth', # Adicionar biblioteca
|
|
175
|
-
]
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 3.2. Database Router
|
|
179
|
-
|
|
180
|
-
```python
|
|
181
|
-
# myapp/routers.py
|
|
182
|
-
|
|
183
|
-
class SharedAuthRouter:
|
|
184
|
-
"""
|
|
185
|
-
Direciona queries dos models compartilhados para o banco correto
|
|
186
|
-
"""
|
|
187
|
-
|
|
188
|
-
route_app_labels = {'shared_auth'}
|
|
189
|
-
|
|
190
|
-
def db_for_read(self, model, **hints):
|
|
191
|
-
"""Direciona leituras para auth_db"""
|
|
192
|
-
if model._meta.app_label in self.route_app_labels:
|
|
193
|
-
return 'auth_db'
|
|
194
|
-
return None
|
|
195
|
-
|
|
196
|
-
def db_for_write(self, model, **hints):
|
|
197
|
-
"""Bloqueia escritas"""
|
|
198
|
-
if model._meta.app_label in self.route_app_labels:
|
|
199
|
-
return None # Impede qualquer escrita
|
|
200
|
-
return None
|
|
201
|
-
|
|
202
|
-
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
|
203
|
-
"""Bloqueia migrations"""
|
|
204
|
-
if app_label in self.route_app_labels:
|
|
205
|
-
return False
|
|
206
|
-
return None
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## 💻 Passo 4: Usar nos Seus Models
|
|
212
|
-
|
|
213
|
-
### 4.1. Model Básico
|
|
214
|
-
|
|
215
|
-
```python
|
|
216
|
-
# myapp/models.py
|
|
217
|
-
from django.db import models
|
|
218
|
-
|
|
219
|
-
class Rascunho(models.Model):
|
|
220
|
-
titulo = models.CharField(max_length=200)
|
|
221
|
-
conteudo = models.TextField()
|
|
222
|
-
|
|
223
|
-
# IDs de referência
|
|
224
|
-
organization_id = models.IntegerField()
|
|
225
|
-
user_id = models.IntegerField()
|
|
226
|
-
|
|
227
|
-
created_at = models.DateTimeField(auto_now_add=True)
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def organization(self):
|
|
231
|
-
"""Acessa organização do banco de auth"""
|
|
232
|
-
from shared_auth.models import SharedOrganization
|
|
233
|
-
return SharedOrganization.objects.using('auth_db').get_or_fail(
|
|
234
|
-
self.organization_id
|
|
235
|
-
```
|
|
236
|
-
# 🚀 Guia Prático - Mixins e Serializers Compartilhados
|
|
237
|
-
|
|
238
|
-
```python
|
|
239
|
-
# models.py - UMA LINHA!
|
|
240
|
-
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
241
|
-
from shared_auth.managers import BaseAuthManager
|
|
242
|
-
|
|
243
|
-
class Rascunho(OrganizationUserMixin, TimestampedMixin):
|
|
244
|
-
titulo = models.CharField(max_length=200)
|
|
245
|
-
conteudo = models.TextField()
|
|
246
|
-
|
|
247
|
-
objects = BaseAuthManager()
|
|
248
|
-
|
|
249
|
-
# Pronto! Já tem tudo:
|
|
250
|
-
# - organization_id, user_id
|
|
251
|
-
# - properties: organization, user
|
|
252
|
-
# - métodos: validate_user_belongs_to_organization()
|
|
253
|
-
# - created_at, updated_at
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# serializers.py - UMA LINHA!
|
|
257
|
-
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
258
|
-
|
|
259
|
-
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
260
|
-
class Meta:
|
|
261
|
-
model = Rascunho
|
|
262
|
-
fields = [
|
|
263
|
-
'id', 'titulo', 'conteudo',
|
|
264
|
-
'organization_name', 'organization_email', # Já disponíveis!
|
|
265
|
-
'user_email', 'user_full_name', # Já disponíveis!
|
|
266
|
-
]
|
|
267
|
-
|
|
268
|
-
# Pronto! Todos os campos já funcionam!
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
## 📝 Exemplos de Uso por Cenário
|
|
274
|
-
|
|
275
|
-
### 1. Model que Pertence APENAS a Organização
|
|
276
|
-
|
|
277
|
-
```python
|
|
278
|
-
# Exemplo: Configuração da empresa
|
|
279
|
-
from shared_auth.mixins import OrganizationMixin
|
|
280
|
-
|
|
281
|
-
class EmpresaConfig(OrganizationMixin):
|
|
282
|
-
"""Configurações específicas da organização"""
|
|
283
|
-
|
|
284
|
-
tema_cor = models.CharField(max_length=7, default='#3490dc')
|
|
285
|
-
logo = models.ImageField(upload_to='logos/')
|
|
286
|
-
timezone = models.CharField(max_length=50, default='America/Sao_Paulo')
|
|
287
|
-
|
|
288
|
-
objects = BaseAuthManager()
|
|
289
|
-
|
|
290
|
-
def __str__(self):
|
|
291
|
-
return f"Config de {self.organization.name}"
|
|
292
|
-
|
|
293
|
-
# Uso
|
|
294
|
-
config = EmpresaConfig.objects.create(
|
|
295
|
-
organization_id=123,
|
|
296
|
-
tema_cor='#ff0000'
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
print(config.organization.name) # Funciona!
|
|
300
|
-
print(config.organization_is_active()) # Método do mixin
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### 2. Model que Pertence APENAS a Usuário
|
|
304
|
-
|
|
305
|
-
```python
|
|
306
|
-
# Exemplo: Preferências do usuário
|
|
307
|
-
from shared_auth.mixins import UserMixin
|
|
308
|
-
|
|
309
|
-
class UserPreferences(UserMixin):
|
|
310
|
-
"""Preferências pessoais do usuário"""
|
|
311
|
-
|
|
312
|
-
theme = models.CharField(max_length=20, default='light')
|
|
313
|
-
notifications_enabled = models.BooleanField(default=True)
|
|
314
|
-
language = models.CharField(max_length=5, default='pt-BR')
|
|
315
|
-
|
|
316
|
-
objects = BaseAuthManager()
|
|
317
|
-
|
|
318
|
-
# Uso
|
|
319
|
-
prefs = UserPreferences.objects.create(
|
|
320
|
-
user_id=456,
|
|
321
|
-
theme='dark'
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
print(prefs.user.email) # Funciona!
|
|
325
|
-
print(prefs.user_full_name) # Property do mixin
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### 3. Model com Organização E Usuário (mais comum)
|
|
329
|
-
|
|
330
|
-
```python
|
|
331
|
-
# Exemplo: Pedido, Rascunho, Tarefa, etc
|
|
332
|
-
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
333
|
-
|
|
334
|
-
class Pedido(OrganizationUserMixin, TimestampedMixin):
|
|
335
|
-
"""Pedido pertence a organização e foi criado por usuário"""
|
|
336
|
-
|
|
337
|
-
numero = models.CharField(max_length=20, unique=True)
|
|
338
|
-
valor_total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
339
|
-
status = models.CharField(max_length=20, default='pending')
|
|
340
|
-
|
|
341
|
-
objects = BaseAuthManager()
|
|
342
|
-
|
|
343
|
-
def __str__(self):
|
|
344
|
-
return f"Pedido {self.numero}"
|
|
345
|
-
|
|
346
|
-
# Uso
|
|
347
|
-
pedido = Pedido.objects.create(
|
|
348
|
-
organization_id=123,
|
|
349
|
-
user_id=456,
|
|
350
|
-
numero='PED-001',
|
|
351
|
-
valor_total=100.00
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Acessos automáticos
|
|
355
|
-
print(pedido.organization.name) # Organização
|
|
356
|
-
print(pedido.user.email) # Usuário que criou
|
|
357
|
-
print(pedido.user_full_name) # Nome completo
|
|
358
|
-
|
|
359
|
-
# Validação automática
|
|
360
|
-
if pedido.validate_user_belongs_to_organization():
|
|
361
|
-
print("OK - Usuário pertence à organização")
|
|
362
|
-
|
|
363
|
-
# Verificar se outro usuário pode acessar
|
|
364
|
-
if pedido.user_can_access(outro_user_id):
|
|
365
|
-
print("Pode acessar")
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
## 🎨 Serializers - Casos de Uso
|
|
371
|
-
|
|
372
|
-
### Caso 1: Serializer Básico
|
|
373
|
-
|
|
374
|
-
```python
|
|
375
|
-
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
376
|
-
|
|
377
|
-
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
378
|
-
class Meta:
|
|
379
|
-
model = Pedido
|
|
380
|
-
fields = [
|
|
381
|
-
'id', 'numero', 'valor_total', 'status',
|
|
382
|
-
# Campos automáticos do mixin:
|
|
383
|
-
'organization_name',
|
|
384
|
-
'organization_cnpj',
|
|
385
|
-
'organization_email',
|
|
386
|
-
'user_email',
|
|
387
|
-
'user_full_name',
|
|
388
|
-
'created_at', 'updated_at',
|
|
389
|
-
]
|
|
390
|
-
|
|
391
|
-
# Response JSON automática:
|
|
392
|
-
{
|
|
393
|
-
"id": 1,
|
|
394
|
-
"numero": "PED-001",
|
|
395
|
-
"valor_total": "100.00",
|
|
396
|
-
"status": "pending",
|
|
397
|
-
"organization_name": "Empresa XYZ",
|
|
398
|
-
"organization_cnpj": "12.345.678/0001-90",
|
|
399
|
-
"organization_email": "contato@xyz.com",
|
|
400
|
-
"user_email": "joao@xyz.com",
|
|
401
|
-
"user_full_name": "João Silva",
|
|
402
|
-
"created_at": "2025-10-01T10:00:00Z",
|
|
403
|
-
"updated_at": "2025-10-01T10:00:00Z"
|
|
404
|
-
}
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### Caso 2: Serializer com Fields Customizados
|
|
408
|
-
|
|
409
|
-
```python
|
|
410
|
-
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
411
|
-
from shared_auth.fields import OrganizationField, UserField
|
|
412
|
-
|
|
413
|
-
class PedidoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
414
|
-
# Fields customizados que retornam objetos completos
|
|
415
|
-
organization_full = OrganizationField(source='*')
|
|
416
|
-
user_full = UserField(source='*')
|
|
417
|
-
|
|
418
|
-
class Meta:
|
|
419
|
-
model = Pedido
|
|
420
|
-
fields = [
|
|
421
|
-
'id', 'numero', 'valor_total',
|
|
422
|
-
'organization_full', # Objeto completo
|
|
423
|
-
'user_full', # Objeto completo
|
|
424
|
-
]
|
|
425
|
-
|
|
426
|
-
# Response JSON:
|
|
427
|
-
{
|
|
428
|
-
"id": 1,
|
|
429
|
-
"numero": "PED-001",
|
|
430
|
-
"valor_total": "100.00",
|
|
431
|
-
"organization_full": {
|
|
432
|
-
"id": 123,
|
|
433
|
-
"name": "Empresa XYZ",
|
|
434
|
-
"fantasy_name": "XYZ Ltda",
|
|
435
|
-
"cnpj": "12.345.678/0001-90",
|
|
436
|
-
"email": "contato@xyz.com",
|
|
437
|
-
"is_active": true
|
|
438
|
-
},
|
|
439
|
-
"user_full": {
|
|
440
|
-
"id": 456,
|
|
441
|
-
"username": "joao",
|
|
442
|
-
"email": "joao@xyz.com",
|
|
443
|
-
"full_name": "João Silva",
|
|
444
|
-
"is_active": true
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Caso 3: Serializer Apenas com Organization
|
|
450
|
-
|
|
451
|
-
```python
|
|
452
|
-
from shared_auth.serializers import OrganizationSerializerMixin
|
|
453
|
-
|
|
454
|
-
class EmpresaConfigSerializer(OrganizationSerializerMixin, serializers.ModelSerializer):
|
|
455
|
-
class Meta:
|
|
456
|
-
model = EmpresaConfig
|
|
457
|
-
fields = [
|
|
458
|
-
'id', 'tema_cor', 'logo', 'timezone',
|
|
459
|
-
'organization_name', # Do
|
|
460
|
-
# 📦 Guia Completo - Serializers com Objetos Aninhados
|
|
461
|
-
|
|
462
|
-
## Estrutura da Resposta JSON
|
|
463
|
-
|
|
464
|
-
### ✅ Antes (campos separados)
|
|
465
|
-
```json
|
|
466
|
-
{
|
|
467
|
-
"id": 1,
|
|
468
|
-
"titulo": "Meu Rascunho",
|
|
469
|
-
"organization_id": 123,
|
|
470
|
-
"organization_name": "Empresa XYZ",
|
|
471
|
-
"organization_cnpj": "12.345.678/0001-90",
|
|
472
|
-
"user_id": 456,
|
|
473
|
-
"user_email": "joao@xyz.com",
|
|
474
|
-
"user_full_name": "João Silva"
|
|
475
|
-
}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### ✨ Depois (objetos aninhados)
|
|
479
|
-
```json
|
|
480
|
-
{
|
|
481
|
-
"id": 1,
|
|
482
|
-
"titulo": "Meu Rascunho",
|
|
483
|
-
"organization": {
|
|
484
|
-
"id": 123,
|
|
485
|
-
"name": "Empresa XYZ",
|
|
486
|
-
"cnpj": "12.345.678/0001-90",
|
|
487
|
-
"email": "contato@xyz.com"
|
|
488
|
-
},
|
|
489
|
-
"user": {
|
|
490
|
-
"id": 456,
|
|
491
|
-
"email": "joao@xyz.com",
|
|
492
|
-
"full_name": "João Silva"
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## 🎯 Cenários de Uso
|
|
500
|
-
|
|
501
|
-
### 1. Serializer Completo (Detail)
|
|
502
|
-
|
|
503
|
-
```python
|
|
504
|
-
# serializers.py
|
|
505
|
-
from rest_framework import serializers
|
|
506
|
-
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
507
|
-
from .models import Rascunho
|
|
508
|
-
|
|
509
|
-
class RascunhoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
510
|
-
"""Serializer completo para detalhes do rascunho"""
|
|
511
|
-
|
|
512
|
-
class Meta:
|
|
513
|
-
model = Rascunho
|
|
514
|
-
fields = [
|
|
515
|
-
'id',
|
|
516
|
-
'titulo',
|
|
517
|
-
'conteudo',
|
|
518
|
-
'organization', # Objeto completo
|
|
519
|
-
'user', # Objeto completo
|
|
520
|
-
'created_at',
|
|
521
|
-
'updated_at',
|
|
522
|
-
]
|
|
523
|
-
read_only_fields = ['organization', 'user', 'created_at', 'updated_at']
|
|
524
|
-
|
|
525
|
-
# views
|
|
526
|
-
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
527
|
-
queryset = Rascunho.objects.all()
|
|
528
|
-
|
|
529
|
-
def get_serializer_class(self):
|
|
530
|
-
if self.action == 'retrieve':
|
|
531
|
-
return RascunhoDetailSerializer
|
|
532
|
-
return RascunhoListSerializer
|
|
533
|
-
|
|
534
|
-
# Response GET /api/rascunhos/1/
|
|
535
|
-
{
|
|
536
|
-
"id": 1,
|
|
537
|
-
"titulo": "Meu Rascunho",
|
|
538
|
-
"conteudo": "Conteúdo completo aqui...",
|
|
539
|
-
"organization": {
|
|
540
|
-
"id": 123,
|
|
541
|
-
"name": "Empresa XYZ Ltda",
|
|
542
|
-
"fantasy_name": "XYZ",
|
|
543
|
-
"cnpj": "12.345.678/0001-90",
|
|
544
|
-
"email": "contato@xyz.com",
|
|
545
|
-
"telephone": "11-3333-4444",
|
|
546
|
-
"cellphone": "11-99999-8888",
|
|
547
|
-
"is_branch": false,
|
|
548
|
-
"is_active": true
|
|
549
|
-
},
|
|
550
|
-
"user": {
|
|
551
|
-
"id": 456,
|
|
552
|
-
"username": "joao.silva",
|
|
553
|
-
"email": "joao@xyz.com",
|
|
554
|
-
"first_name": "João",
|
|
555
|
-
"last_name": "Silva",
|
|
556
|
-
"full_name": "João Silva",
|
|
557
|
-
"is_active": true
|
|
558
|
-
},
|
|
559
|
-
"created_at": "2025-10-01T10:00:00Z",
|
|
560
|
-
"updated_at": "2025-10-01T15:30:00Z"
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### 2. Serializer Simplificado (List)
|
|
565
|
-
|
|
566
|
-
```python
|
|
567
|
-
from shared_auth.serializers import OrganizationSimpleSerializerMixin, UserSimpleSerializerMixin
|
|
568
|
-
|
|
569
|
-
class RascunhoListSerializer(OrganizationSimpleSerializerMixin, UserSimpleSerializerMixin, serializers.ModelSerializer):
|
|
570
|
-
"""Serializer simplificado para listagens"""
|
|
571
|
-
|
|
572
|
-
class Meta:
|
|
573
|
-
model = Rascunho
|
|
574
|
-
fields = [
|
|
575
|
-
'id',
|
|
576
|
-
'titulo',
|
|
577
|
-
'organization', # Simplificado
|
|
578
|
-
'user', # Simplificado
|
|
579
|
-
'created_at',
|
|
580
|
-
]
|
|
581
|
-
|
|
582
|
-
# Response GET /api/rascunhos/
|
|
583
|
-
{
|
|
584
|
-
"count": 10,
|
|
585
|
-
"results": [
|
|
586
|
-
{
|
|
587
|
-
"id": 1,
|
|
588
|
-
"titulo": "Rascunho 1",
|
|
589
|
-
"organization": {
|
|
590
|
-
"id": 123,
|
|
591
|
-
"name": "Empresa XYZ",
|
|
592
|
-
"cnpj": "12.345.678/0001-90"
|
|
593
|
-
},
|
|
594
|
-
"user": {
|
|
595
|
-
"id": 456,
|
|
596
|
-
"email": "joao@xyz.com",
|
|
597
|
-
"full_name": "João Silva"
|
|
598
|
-
},
|
|
599
|
-
"created_at": "2025-10-01T10:00:00Z"
|
|
600
|
-
},
|
|
601
|
-
// ... mais resultados
|
|
602
|
-
]
|
|
603
|
-
}
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### 3. Apenas Organization
|
|
607
|
-
|
|
608
|
-
```python
|
|
609
|
-
from shared_auth.serializers import OrganizationSerializerMixin
|
|
610
|
-
|
|
611
|
-
class EmpresaConfigSerializer(OrganizationSerializerMixin, serializers.ModelSerializer):
|
|
612
|
-
"""Configurações da empresa"""
|
|
613
|
-
|
|
614
|
-
class Meta:
|
|
615
|
-
model = EmpresaConfig
|
|
616
|
-
fields = [
|
|
617
|
-
'id',
|
|
618
|
-
'tema_cor',
|
|
619
|
-
'logo',
|
|
620
|
-
'timezone',
|
|
621
|
-
'organization', # Objeto completo
|
|
622
|
-
]
|
|
623
|
-
|
|
624
|
-
# Response
|
|
625
|
-
{
|
|
626
|
-
"id": 1,
|
|
627
|
-
"tema_cor": "#3490dc",
|
|
628
|
-
"logo": "https://example.com/logos/xyz.png",
|
|
629
|
-
"timezone": "America/Sao_Paulo",
|
|
630
|
-
"organization": {
|
|
631
|
-
"id": 123,
|
|
632
|
-
"name": "Empresa XYZ Ltda",
|
|
633
|
-
"fantasy_name": "XYZ",
|
|
634
|
-
"cnpj": "12.345.678/0001-90",
|
|
635
|
-
"email": "contato@xyz.com",
|
|
636
|
-
"telephone": "11-3333-4444",
|
|
637
|
-
"cellphone": "11-99999-8888",
|
|
638
|
-
"is_branch": false,
|
|
639
|
-
"is_active": true
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### 4. Apenas User
|
|
645
|
-
|
|
646
|
-
```python
|
|
647
|
-
from shared_auth.serializers import UserSerializerMixin
|
|
648
|
-
|
|
649
|
-
class UserPreferencesSerializer(UserSerializerMixin, serializers.ModelSerializer):
|
|
650
|
-
"""Preferências do usuário"""
|
|
651
|
-
|
|
652
|
-
class Meta:
|
|
653
|
-
model = UserPreferences
|
|
654
|
-
fields = [
|
|
655
|
-
'id',
|
|
656
|
-
'theme',
|
|
657
|
-
'notifications_enabled',
|
|
658
|
-
'language',
|
|
659
|
-
'user', # Objeto completo
|
|
660
|
-
]
|
|
661
|
-
|
|
662
|
-
# Response
|
|
663
|
-
{
|
|
664
|
-
"id": 1,
|
|
665
|
-
"theme": "dark",
|
|
666
|
-
"notifications_enabled": true,
|
|
667
|
-
"language": "pt-BR",
|
|
668
|
-
"user": {
|
|
669
|
-
"id": 456,
|
|
670
|
-
"username": "joao.silva",
|
|
671
|
-
"email": "joao@xyz.com",
|
|
672
|
-
"first_name": "João",
|
|
673
|
-
"last_name": "Silva",
|
|
674
|
-
"full_name": "João Silva",
|
|
675
|
-
"is_active": true
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
---
|
|
681
|
-
|
|
682
|
-
## 🔧 Customização Avançada
|
|
683
|
-
|
|
684
|
-
### Adicionar Campos Extras ao Organization
|
|
685
|
-
|
|
686
|
-
```python
|
|
687
|
-
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
688
|
-
|
|
689
|
-
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
690
|
-
# Sobrescrever o método para adicionar campos extras
|
|
691
|
-
def get_organization(self, obj):
|
|
692
|
-
org_data = super().get_organization(obj)
|
|
693
|
-
if org_data:
|
|
694
|
-
# Adicionar campos customizados
|
|
695
|
-
org_data['logo_url'] = f"/logos/{obj.organization_id}.png"
|
|
696
|
-
org_data['member_count'] = obj.organization.members.count()
|
|
697
|
-
return org_data
|
|
698
|
-
|
|
699
|
-
class Meta:
|
|
700
|
-
model = Rascunho
|
|
701
|
-
fields = ['id', 'titulo', 'organization', 'user']
|
|
702
|
-
|
|
703
|
-
# Response
|
|
704
|
-
{
|
|
705
|
-
"id": 1,
|
|
706
|
-
"titulo": "Teste",
|
|
707
|
-
"organization": {
|
|
708
|
-
"id": 123,
|
|
709
|
-
"name": "Empresa XYZ",
|
|
710
|
-
// ... campos padrão
|
|
711
|
-
"logo_url": "/logos/123.png",
|
|
712
|
-
"member_count": 15
|
|
713
|
-
},
|
|
714
|
-
"user": { ... }
|
|
715
|
-
}
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
### Condicional - Mostrar Mais ou Menos Dados
|
|
719
|
-
|
|
720
|
-
```python
|
|
721
|
-
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
722
|
-
|
|
723
|
-
def get_organization(self, obj):
|
|
724
|
-
# Usuário logado é da mesma organização?
|
|
725
|
-
request = self.context.get('request')
|
|
726
|
-
same_org = (request and
|
|
727
|
-
hasattr(request.user, 'logged_organization_id') and
|
|
728
|
-
request.user.logged_organization_id == obj.organization_id)
|
|
729
|
-
|
|
730
|
-
try:
|
|
731
|
-
org = obj.organization
|
|
732
|
-
data = {
|
|
733
|
-
'id': org.pk,
|
|
734
|
-
'name': org.name,
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
# Mostrar mais dados se for da mesma organização
|
|
738
|
-
if same_org:
|
|
739
|
-
data.update({
|
|
740
|
-
'cnpj': org.cnpj,
|
|
741
|
-
'email': org.email,
|
|
742
|
-
'telephone': org.telephone,
|
|
743
|
-
})
|
|
744
|
-
|
|
745
|
-
return data
|
|
746
|
-
except:
|
|
747
|
-
return None
|
|
748
|
-
|
|
749
|
-
class Meta:
|
|
750
|
-
model = Rascunho
|
|
751
|
-
fields = ['id', 'titulo', 'organization']
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
---
|
|
755
|
-
|
|
756
|
-
## 🚀 Performance - Otimização
|
|
757
|
-
|
|
758
|
-
### Problema N+1
|
|
759
|
-
|
|
760
|
-
```python
|
|
761
|
-
# RUIM: Causa N+1 queries
|
|
762
|
-
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
763
|
-
queryset = Rascunho.objects.all() # 1 query
|
|
764
|
-
serializer_class = RascunhoSerializer
|
|
765
|
-
|
|
766
|
-
# Cada serialização faz 2 queries (org + user)
|
|
767
|
-
# Total: 1 + (N * 2) queries
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
### Solução: Prefetch
|
|
771
|
-
|
|
772
|
-
```python
|
|
773
|
-
# BOM: Usa prefetch do manager
|
|
774
|
-
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
775
|
-
serializer_class = RascunhoSerializer
|
|
776
|
-
|
|
777
|
-
def get_queryset(self):
|
|
778
|
-
# Usa o manager customizado
|
|
779
|
-
return Rascunho.objects.with_auth_data()
|
|
780
|
-
# Total: 3 queries (rascunhos + orgs + users)
|
|
781
|
-
|
|
782
|
-
# Ainda MELHOR: Cache no serializer
|
|
783
|
-
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
784
|
-
|
|
785
|
-
def get_organization(self, obj):
|
|
786
|
-
# Verifica se já está cacheado
|
|
787
|
-
if hasattr(obj, '_cached_organization'):
|
|
788
|
-
org = obj._cached_organization
|
|
789
|
-
else:
|
|
790
|
-
org = obj.organization
|
|
791
|
-
|
|
792
|
-
return {
|
|
793
|
-
'id': org.pk,
|
|
794
|
-
'name': org.name,
|
|
795
|
-
// ...
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
---
|
|
800
|
-
|
|
801
|
-
## 💡 Casos de Uso Reais
|
|
802
|
-
|
|
803
|
-
### Sistema de Pedidos
|
|
804
|
-
|
|
805
|
-
```python
|
|
806
|
-
class ItemPedidoSerializer(serializers.ModelSerializer):
|
|
807
|
-
class Meta:
|
|
808
|
-
model = ItemPedido
|
|
809
|
-
fields = ['id', 'produto', 'quantidade', 'valor_unitario']
|
|
810
|
-
|
|
811
|
-
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
812
|
-
itens = ItemPedidoSerializer(many=True, read_only=True)
|
|
813
|
-
|
|
814
|
-
class Meta:
|
|
815
|
-
model = Pedido
|
|
816
|
-
fields = [
|
|
817
|
-
'id', 'numero', 'valor_total', 'status',
|
|
818
|
-
'organization', # Cliente
|
|
819
|
-
'user', # Vendedor
|
|
820
|
-
'itens',
|
|
821
|
-
'created_at'
|
|
822
|
-
]
|
|
823
|
-
|
|
824
|
-
# Response
|
|
825
|
-
{
|
|
826
|
-
"id": 1,
|
|
827
|
-
"numero": "PED-001",
|
|
828
|
-
"valor_total": "1500.00",
|
|
829
|
-
"status": "pending",
|
|
830
|
-
"organization": {
|
|
831
|
-
"id": 123,
|
|
832
|
-
"name": "Cliente ABC",
|
|
833
|
-
"cnpj": "12.345.678/0001-90",
|
|
834
|
-
"email": "contato@abc.com"
|
|
835
|
-
},
|
|
836
|
-
"user": {
|
|
837
|
-
"id": 456,
|
|
838
|
-
"email": "vendedor@empresa.com",
|
|
839
|
-
"full_name": "João Vendedor"
|
|
840
|
-
},
|
|
841
|
-
"itens": [
|
|
842
|
-
{
|
|
843
|
-
"id": 1,
|
|
844
|
-
"produto": "Produto A",
|
|
845
|
-
"quantidade": 2,
|
|
846
|
-
"valor_unitario": "500.00"
|
|
847
|
-
},
|
|
848
|
-
{
|
|
849
|
-
"id": 2,
|
|
850
|
-
"produto": "Produto B",
|
|
851
|
-
"quantidade": 1,
|
|
852
|
-
"valor_unitario": "500.00"
|
|
853
|
-
}
|
|
854
|
-
],
|
|
855
|
-
"created_at": "2025-10-01T10:00:00Z"
|
|
856
|
-
}
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
### Sistema de Tarefas com Múltiplos Usuários
|
|
860
|
-
|
|
861
|
-
```python
|
|
862
|
-
class TarefaSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
863
|
-
# user do mixin é o criador
|
|
864
|
-
responsavel = serializers.SerializerMethodField()
|
|
865
|
-
|
|
866
|
-
def get_responsavel(self, obj):
|
|
867
|
-
"""Retorna usuário responsável"""
|
|
868
|
-
try:
|
|
869
|
-
resp = obj.responsavel
|
|
870
|
-
return {
|
|
871
|
-
'id': resp.pk,
|
|
872
|
-
'email': resp.email,
|
|
873
|
-
'full_name': resp.get_full_name(),
|
|
874
|
-
}
|
|
875
|
-
except:
|
|
876
|
-
return None
|
|
877
|
-
|
|
878
|
-
class Meta:
|
|
879
|
-
model = Tarefa
|
|
880
|
-
fields = [
|
|
881
|
-
'id', 'titulo', 'descricao', 'status', 'prioridade',
|
|
882
|
-
'organization', # Organização dona da tarefa
|
|
883
|
-
'user', # Quem criou
|
|
884
|
-
'responsavel', # Quem vai fazer
|
|
885
|
-
'created_at'
|
|
886
|
-
]
|
|
887
|
-
|
|
888
|
-
# Response
|
|
889
|
-
{
|
|
890
|
-
"id": 1,
|
|
891
|
-
"titulo": "Implementar feature X",
|
|
892
|
-
"descricao": "Descrição detalhada...",
|
|
893
|
-
"status": "in_progress",
|
|
894
|
-
"prioridade": "high",
|
|
895
|
-
"organization": {
|
|
896
|
-
"id": 123,
|
|
897
|
-
"name": "Empresa XYZ"
|
|
898
|
-
},
|
|
899
|
-
"user": {
|
|
900
|
-
"id": 456,
|
|
901
|
-
"email": "gerente@xyz.com",
|
|
902
|
-
"full_name": "Gerente Silva"
|
|
903
|
-
},
|
|
904
|
-
"responsavel": {
|
|
905
|
-
"id": 789,
|
|
906
|
-
"email": "dev@xyz.com",
|
|
907
|
-
"full_name": "Dev Junior"
|
|
908
|
-
},
|
|
909
|
-
"created_at": "2025-10-01T10:00:00Z"
|
|
910
|
-
}
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
---
|
|
914
|
-
|
|
915
|
-
## 🎨 Frontend - Consumindo a API
|
|
916
|
-
|
|
917
|
-
### React Example
|
|
918
|
-
|
|
919
|
-
```javascript
|
|
920
|
-
// components/RascunhoCard.jsx
|
|
921
|
-
function RascunhoCard({ rascunho }) {
|
|
922
|
-
return (
|
|
923
|
-
<div className="card">
|
|
924
|
-
<h3>{rascunho.titulo}</h3>
|
|
925
|
-
<p>{rascunho.conteudo}</p>
|
|
926
|
-
|
|
927
|
-
{/* Acessar dados aninhados */}
|
|
928
|
-
<div className="meta">
|
|
929
|
-
<span className="organization">
|
|
930
|
-
{rascunho.organization.name}
|
|
931
|
-
</span>
|
|
932
|
-
<span className="user">
|
|
933
|
-
Por: {rascunho.user.full_name}
|
|
934
|
-
</span>
|
|
935
|
-
</div>
|
|
936
|
-
|
|
937
|
-
{/* Verificar status */}
|
|
938
|
-
{!rascunho.organization.is_active && (
|
|
939
|
-
<div className="warning">
|
|
940
|
-
Organização inativa
|
|
941
|
-
</div>
|
|
942
|
-
)}
|
|
943
|
-
</div>
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Uso
|
|
948
|
-
fetch('/api/rascunhos/1/')
|
|
949
|
-
.then(res => res.json())
|
|
950
|
-
.then(data => {
|
|
951
|
-
console.log(data.organization.name); // Fácil!
|
|
952
|
-
console.log(data.user.email); // Direto!
|
|
953
|
-
});
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### Vue Example
|
|
957
|
-
|
|
958
|
-
```vue
|
|
959
|
-
<template>
|
|
960
|
-
<div class="rascunho">
|
|
961
|
-
<h3>{{ rascunho.titulo }}</h3>
|
|
962
|
-
|
|
963
|
-
<!-- Dados da organização -->
|
|
964
|
-
<div class="organization-info">
|
|
965
|
-
<h4>{{ rascunho.organization.name }}</h4>
|
|
966
|
-
<p>{{ rascunho.organization.cnpj }}</p>
|
|
967
|
-
<p>{{ rascunho.organization.email }}</p>
|
|
968
|
-
</div>
|
|
969
|
-
|
|
970
|
-
<!-- Dados do usuário -->
|
|
971
|
-
<div class="user-info">
|
|
972
|
-
<span>Criado por: {{ rascunho.user.full_name }}</span>
|
|
973
|
-
<span>Email: {{ rascunho.user.email }}</span>
|
|
974
|
-
</div>
|
|
975
|
-
</div>
|
|
976
|
-
</template>
|
|
977
|
-
|
|
978
|
-
<script>
|
|
979
|
-
export default {
|
|
980
|
-
data() {
|
|
981
|
-
return {
|
|
982
|
-
rascunho: null
|
|
983
|
-
}
|
|
984
|
-
},
|
|
985
|
-
async mounted() {
|
|
986
|
-
const response = await fetch('/api/rascunhos/1/');
|
|
987
|
-
this.rascunho = await response.json();
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
</script>
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
---
|
|
994
|
-
|
|
995
|
-
## ✅ Vantagens desta Abordagem
|
|
996
|
-
|
|
997
|
-
1. **Mais Semântico**: Dados relacionados agrupados
|
|
998
|
-
2. **Fácil de Consumir**: Frontend acessa `obj.organization.name` diretamente
|
|
999
|
-
3. **Flexível**: Pode ter versão completa e simplificada
|
|
1000
|
-
4. **Type-Safe**: TypeScript/Flow adoram objetos aninhados
|
|
1001
|
-
5. **Cacheable**: Pode cachear o objeto `organization` inteiro
|
|
1002
|
-
6. **Reutilizável**: Mesma estrutura em todos os endpoints
|
|
1003
|
-
|
|
1004
|
-
---
|
|
1005
|
-
|
|
1006
|
-
## 📋 Checklist de Implementação
|
|
1007
|
-
|
|
1008
|
-
- [ ] Atualizar `shared_auth/serializers.py` com novos mixins
|
|
1009
|
-
- [ ] Reinstalar biblioteca: `pip install -e /path/to/shared-auth-lib`
|
|
1010
|
-
- [ ] Atualizar serializers existentes
|
|
1011
|
-
- [ ] Testar responses dos endpoints
|
|
1012
|
-
- [ ] Atualizar documentação da API
|
|
1013
|
-
- [ ] Atualizar código do frontend
|
|
1014
|
-
- [ ] Verificar performance (N+1 queries)
|
|
1015
|
-
- [ ] Adicionar testes
|
|
1016
|
-
|
|
1017
|
-
Pronto! Agora seus serializers retornam dados organizados em objetos `organization` e `user`! 🎉
|