maquinaweb-shared-auth 0.2.60__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.
- maquinaweb_shared_auth-0.2.60.dist-info/METADATA +1003 -0
- maquinaweb_shared_auth-0.2.60.dist-info/RECORD +28 -0
- maquinaweb_shared_auth-0.2.60.dist-info/WHEEL +5 -0
- maquinaweb_shared_auth-0.2.60.dist-info/top_level.txt +1 -0
- shared_auth/__init__.py +7 -0
- shared_auth/abstract_models.py +897 -0
- shared_auth/app.py +9 -0
- shared_auth/authentication.py +55 -0
- shared_auth/conf.py +33 -0
- shared_auth/decorators.py +122 -0
- shared_auth/exceptions.py +23 -0
- shared_auth/fields.py +51 -0
- shared_auth/management/__init__.py +0 -0
- shared_auth/management/commands/__init__.py +0 -0
- shared_auth/management/commands/generate_permissions.py +147 -0
- shared_auth/managers.py +344 -0
- shared_auth/middleware.py +281 -0
- shared_auth/mixins.py +475 -0
- shared_auth/models.py +191 -0
- shared_auth/permissions.py +266 -0
- shared_auth/permissions_cache.py +249 -0
- shared_auth/permissions_helpers.py +251 -0
- shared_auth/router.py +22 -0
- shared_auth/serializers.py +439 -0
- shared_auth/storage_backend.py +6 -0
- shared_auth/urls.py +8 -0
- shared_auth/utils.py +356 -0
- shared_auth/views.py +40 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maquinaweb-shared-auth
|
|
3
|
+
Version: 0.2.60
|
|
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: pyyaml>=6.0.3
|
|
21
|
+
Requires-Dist: setuptools>=70
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# 🔐 Maquinaweb Shared Auth
|
|
25
|
+
|
|
26
|
+
> Biblioteca Django para autenticação compartilhada entre múltiplos sistemas usando um único banco de dados centralizado.
|
|
27
|
+
|
|
28
|
+
[](https://www.python.org/downloads/)
|
|
29
|
+
[](https://www.djangoproject.com/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
## 📋 Índice
|
|
33
|
+
|
|
34
|
+
- [Visão Geral](#-visão-geral)
|
|
35
|
+
- [Características](#-características)
|
|
36
|
+
- [Arquitetura](#️-arquitetura)
|
|
37
|
+
- [Instalação](#-instalação)
|
|
38
|
+
- [Configuração](#️-configuração)
|
|
39
|
+
- [Uso Básico](#-uso-básico)
|
|
40
|
+
- [Guias Avançados](#-guias-avançados)
|
|
41
|
+
- [API Reference](#-api-reference)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🎯 Visão Geral
|
|
46
|
+
|
|
47
|
+
A **Maquinaweb Shared Auth** permite que múltiplos sistemas Django compartilhem dados de autenticação, usuários e organizações através de um banco de dados centralizado, sem necessidade de requisições HTTP.
|
|
48
|
+
|
|
49
|
+
### Problema Resolvido
|
|
50
|
+
|
|
51
|
+
Ao invés de:
|
|
52
|
+
- ❌ Duplicar dados de usuários em cada sistema
|
|
53
|
+
- ❌ Fazer requisições HTTP entre sistemas
|
|
54
|
+
- ❌ Manter múltiplos bancos de autenticação sincronizados
|
|
55
|
+
|
|
56
|
+
Você pode:
|
|
57
|
+
- ✅ Acessar dados de autenticação diretamente do banco central
|
|
58
|
+
- ✅ Usar a interface familiar do Django ORM
|
|
59
|
+
- ✅ Garantir consistência de dados entre sistemas
|
|
60
|
+
- ✅ Trabalhar com models read-only seguros
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ✨ Características
|
|
65
|
+
|
|
66
|
+
### Core Features
|
|
67
|
+
|
|
68
|
+
- **🔐 Autenticação Centralizada**: Token-based authentication compartilhado
|
|
69
|
+
- **🏢 Multi-Tenancy**: Suporte completo a organizações e filiais
|
|
70
|
+
- **👥 Gestão de Membros**: Relacionamento usuários ↔ organizações
|
|
71
|
+
- **🔒 Read-Only Safety**: Proteção contra modificações acidentais
|
|
72
|
+
- **⚡ Performance**: Managers otimizados com prefetch automático
|
|
73
|
+
- **🎨 DRF Ready**: Mixins para serializers com dados aninhados
|
|
74
|
+
|
|
75
|
+
### Componentes Principais
|
|
76
|
+
|
|
77
|
+
| Componente | Descrição |
|
|
78
|
+
|------------|-----------|
|
|
79
|
+
| **Models** | SharedOrganization, User, SharedMember, SharedToken |
|
|
80
|
+
| **Mixins** | OrganizationMixin, UserMixin, OrganizationUserMixin |
|
|
81
|
+
| **Serializers** | OrganizationSerializerMixin, UserSerializerMixin |
|
|
82
|
+
| **Authentication** | SharedTokenAuthentication |
|
|
83
|
+
| **Middleware** | SharedAuthMiddleware, OrganizationMiddleware |
|
|
84
|
+
| **Permissions** | IsAuthenticated, HasActiveOrganization, IsSameOrganization |
|
|
85
|
+
| **Managers** | Métodos otimizados com prefetch e validações |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🏗️ Arquitetura
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
┌─────────────────────────────────────┐
|
|
93
|
+
│ Sistema de Autenticação Central │
|
|
94
|
+
│ │
|
|
95
|
+
│ ┌──────────────┐ ┌────────────┐ │
|
|
96
|
+
│ │Organization │ │ User │ │
|
|
97
|
+
│ └──────┬───────┘ └─────┬──────┘ │
|
|
98
|
+
│ │ │ │
|
|
99
|
+
│ └────────┬───────┘ │
|
|
100
|
+
│ │ │
|
|
101
|
+
│ ┌──────▼──────┐ │
|
|
102
|
+
│ │ Member │ │
|
|
103
|
+
│ │ Token │ │
|
|
104
|
+
│ └─────────────┘ │
|
|
105
|
+
└──────────────────┬──────────────────┘
|
|
106
|
+
│
|
|
107
|
+
┌────────────┴────────────┐
|
|
108
|
+
│ PostgreSQL/MySQL │
|
|
109
|
+
│ (auth_db) │
|
|
110
|
+
└────────────┬────────────┘
|
|
111
|
+
│
|
|
112
|
+
┌────────────┴────────────┐
|
|
113
|
+
│ │
|
|
114
|
+
┌─────▼─────┐ ┌─────▼─────┐
|
|
115
|
+
│ Sistema A │ │ Sistema B │
|
|
116
|
+
│ │ │ │
|
|
117
|
+
│ Pedidos │ │ Estoque │
|
|
118
|
+
│ ├─ org │ │ ├─ org │
|
|
119
|
+
│ └─ user │ │ └─ user │
|
|
120
|
+
└───────────┘ └───────────┘
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Fluxo de Autenticação:**
|
|
124
|
+
|
|
125
|
+
1. Cliente envia request com token no header
|
|
126
|
+
2. Middleware valida token no banco `auth_db`
|
|
127
|
+
3. Dados do usuário e organização são anexados ao `request`
|
|
128
|
+
4. Sistema cliente acessa dados via ORM (read-only)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 📦 Instalação
|
|
133
|
+
|
|
134
|
+
### 1. Instalar a Biblioteca
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Via pip (quando publicado)
|
|
138
|
+
pip install maquinaweb-shared-auth
|
|
139
|
+
|
|
140
|
+
# Ou modo desenvolvimento
|
|
141
|
+
pip install -e /path/to/maquinaweb-shared-auth
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Adicionar ao requirements.txt
|
|
145
|
+
|
|
146
|
+
```txt
|
|
147
|
+
Django>=4.2
|
|
148
|
+
djangorestframework>=3.14
|
|
149
|
+
maquinaweb-shared-auth>=0.2.25
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## ⚙️ Configuração
|
|
155
|
+
|
|
156
|
+
### 1. Settings do Django
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# settings.py
|
|
160
|
+
|
|
161
|
+
INSTALLED_APPS = [
|
|
162
|
+
'django.contrib.admin',
|
|
163
|
+
'django.contrib.auth',
|
|
164
|
+
'django.contrib.contenttypes',
|
|
165
|
+
'django.contrib.sessions',
|
|
166
|
+
'rest_framework',
|
|
167
|
+
|
|
168
|
+
# Adicionar shared_auth
|
|
169
|
+
'shared_auth',
|
|
170
|
+
|
|
171
|
+
# Suas apps
|
|
172
|
+
'myapp',
|
|
173
|
+
]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 2. Configurar Banco de Dados
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# settings.py
|
|
180
|
+
|
|
181
|
+
DATABASES = {
|
|
182
|
+
'default': {
|
|
183
|
+
# Banco do sistema atual
|
|
184
|
+
'ENGINE': 'django.db.backends.postgresql',
|
|
185
|
+
'NAME': 'meu_sistema_db',
|
|
186
|
+
'USER': 'meu_user',
|
|
187
|
+
'PASSWORD': 'senha',
|
|
188
|
+
'HOST': 'localhost',
|
|
189
|
+
'PORT': '5432',
|
|
190
|
+
},
|
|
191
|
+
'auth_db': {
|
|
192
|
+
# Banco centralizado de autenticação (READ-ONLY)
|
|
193
|
+
'ENGINE': 'django.db.backends.postgresql',
|
|
194
|
+
'NAME': 'sistema_auth_db',
|
|
195
|
+
'USER': 'readonly_user',
|
|
196
|
+
'PASSWORD': 'senha_readonly',
|
|
197
|
+
'HOST': 'auth-server.example.com',
|
|
198
|
+
'PORT': '5432',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Router para direcionar queries
|
|
203
|
+
DATABASE_ROUTERS = ['shared_auth.router.SharedAuthRouter']
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 3. Configurar Autenticação (DRF)
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# settings.py
|
|
210
|
+
|
|
211
|
+
REST_FRAMEWORK = {
|
|
212
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
213
|
+
'shared_auth.authentication.SharedTokenAuthentication',
|
|
214
|
+
],
|
|
215
|
+
'DEFAULT_PERMISSION_CLASSES': [
|
|
216
|
+
'shared_auth.permissions.IsAuthenticated',
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 4. Configurar Middleware (Opcional)
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# settings.py
|
|
225
|
+
|
|
226
|
+
MIDDLEWARE = [
|
|
227
|
+
'django.middleware.security.SecurityMiddleware',
|
|
228
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
229
|
+
'django.middleware.common.CommonMiddleware',
|
|
230
|
+
|
|
231
|
+
# Middlewares da shared_auth
|
|
232
|
+
'shared_auth.middleware.SharedAuthMiddleware',
|
|
233
|
+
'shared_auth.middleware.OrganizationMiddleware',
|
|
234
|
+
|
|
235
|
+
'django.middleware.csrf.CsrfViewMiddleware',
|
|
236
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
237
|
+
'django.contrib.messages.middleware.MessageMiddleware',
|
|
238
|
+
]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 5. Configurar Tabelas (Opcional)
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
# settings.py
|
|
245
|
+
|
|
246
|
+
# Customizar nomes das tabelas (se necessário)
|
|
247
|
+
SHARED_AUTH_ORGANIZATION_TABLE = 'organization_organization'
|
|
248
|
+
SHARED_AUTH_USER_TABLE = 'auth_user'
|
|
249
|
+
SHARED_AUTH_MEMBER_TABLE = 'organization_member'
|
|
250
|
+
SHARED_AUTH_TOKEN_TABLE = 'authtoken_token'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 6. Criar Usuário Read-Only no PostgreSQL
|
|
254
|
+
|
|
255
|
+
```sql
|
|
256
|
+
-- No servidor de autenticação
|
|
257
|
+
CREATE USER readonly_user WITH PASSWORD 'senha_segura_aqui';
|
|
258
|
+
|
|
259
|
+
-- Conceder permissões
|
|
260
|
+
GRANT CONNECT ON DATABASE sistema_auth_db TO readonly_user;
|
|
261
|
+
GRANT USAGE ON SCHEMA public TO readonly_user;
|
|
262
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
|
|
263
|
+
|
|
264
|
+
-- Para tabelas futuras
|
|
265
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
266
|
+
GRANT SELECT ON TABLES TO readonly_user;
|
|
267
|
+
|
|
268
|
+
-- Garantir read-only
|
|
269
|
+
ALTER USER readonly_user SET default_transaction_read_only = on;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 🚀 Uso Básico
|
|
275
|
+
|
|
276
|
+
### 1. Models com Mixins
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
# myapp/models.py
|
|
280
|
+
from django.db import models
|
|
281
|
+
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
282
|
+
from shared_auth.managers import BaseAuthManager
|
|
283
|
+
|
|
284
|
+
class Pedido(OrganizationUserMixin, TimestampedMixin):
|
|
285
|
+
"""Model que pertence a organização e usuário"""
|
|
286
|
+
|
|
287
|
+
numero = models.CharField(max_length=20, unique=True)
|
|
288
|
+
valor_total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
289
|
+
status = models.CharField(max_length=20, default='pending')
|
|
290
|
+
|
|
291
|
+
objects = BaseAuthManager()
|
|
292
|
+
|
|
293
|
+
def __str__(self):
|
|
294
|
+
return f"Pedido {self.numero}"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**O que você ganha automaticamente:**
|
|
298
|
+
- ✅ Campos: `organization_id`, `user_id`, `created_at`, `updated_at`
|
|
299
|
+
- ✅ Properties: `organization`, `user`, `organization_name`, `user_email`
|
|
300
|
+
- ✅ Métodos: `validate_user_belongs_to_organization()`, `user_can_access()`
|
|
301
|
+
|
|
302
|
+
### 2. Serializers com Dados Aninhados
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
# myapp/serializers.py
|
|
306
|
+
from rest_framework import serializers
|
|
307
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
308
|
+
from .models import Pedido
|
|
309
|
+
|
|
310
|
+
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
311
|
+
class Meta:
|
|
312
|
+
model = Pedido
|
|
313
|
+
fields = [
|
|
314
|
+
'id', 'numero', 'valor_total', 'status',
|
|
315
|
+
'organization', # Objeto completo
|
|
316
|
+
'user', # Objeto completo
|
|
317
|
+
'created_at',
|
|
318
|
+
]
|
|
319
|
+
read_only_fields = ['organization', 'user', 'created_at']
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Response JSON:**
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"id": 1,
|
|
326
|
+
"numero": "PED-001",
|
|
327
|
+
"valor_total": "1500.00",
|
|
328
|
+
"status": "pending",
|
|
329
|
+
"organization": {
|
|
330
|
+
"id": 123,
|
|
331
|
+
"name": "Empresa XYZ Ltda",
|
|
332
|
+
"fantasy_name": "XYZ",
|
|
333
|
+
"cnpj": "12.345.678/0001-90",
|
|
334
|
+
"email": "contato@xyz.com",
|
|
335
|
+
"is_active": true
|
|
336
|
+
},
|
|
337
|
+
"user": {
|
|
338
|
+
"id": 456,
|
|
339
|
+
"username": "joao.silva",
|
|
340
|
+
"email": "joao@xyz.com",
|
|
341
|
+
"full_name": "João Silva",
|
|
342
|
+
"is_active": true
|
|
343
|
+
},
|
|
344
|
+
"created_at": "2025-10-01T10:00:00Z"
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 3. ViewSets com Organização
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
# myapp/views.py
|
|
352
|
+
from rest_framework import viewsets
|
|
353
|
+
from shared_auth.mixins import LoggedOrganizationMixin
|
|
354
|
+
from shared_auth.permissions import HasActiveOrganization, IsSameOrganization
|
|
355
|
+
from .models import Pedido
|
|
356
|
+
from .serializers import PedidoSerializer
|
|
357
|
+
|
|
358
|
+
class PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):
|
|
359
|
+
"""
|
|
360
|
+
ViewSet que filtra automaticamente por organização logada
|
|
361
|
+
"""
|
|
362
|
+
serializer_class = PedidoSerializer
|
|
363
|
+
permission_classes = [HasActiveOrganization, IsSameOrganization]
|
|
364
|
+
|
|
365
|
+
# get_queryset() já filtra por organization_id automaticamente
|
|
366
|
+
# perform_create() já adiciona organization_id automaticamente
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### 4. Acessar Dados Compartilhados
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# Em qualquer lugar do código
|
|
373
|
+
from shared_auth.models import SharedOrganization, User, SharedMember
|
|
374
|
+
|
|
375
|
+
# Buscar organização
|
|
376
|
+
org = SharedOrganization.objects.get_or_fail(123)
|
|
377
|
+
print(org.name) # "Empresa XYZ"
|
|
378
|
+
print(org.members) # QuerySet de membros
|
|
379
|
+
|
|
380
|
+
# Buscar usuário
|
|
381
|
+
user = User.objects.get_or_fail(456)
|
|
382
|
+
print(user.email) # "joao@xyz.com"
|
|
383
|
+
print(user.organizations) # Organizações do usuário
|
|
384
|
+
|
|
385
|
+
# Verificar membership
|
|
386
|
+
member = SharedMember.objects.filter(
|
|
387
|
+
user_id=456,
|
|
388
|
+
organization_id=123
|
|
389
|
+
).first()
|
|
390
|
+
|
|
391
|
+
if member:
|
|
392
|
+
print(f"{member.user.email} é membro de {member.organization.name}")
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## 📚 Guias Avançados
|
|
398
|
+
|
|
399
|
+
### Mixins para Models
|
|
400
|
+
|
|
401
|
+
#### 1. OrganizationMixin
|
|
402
|
+
Para models que pertencem apenas a uma organização.
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from shared_auth.mixins import OrganizationMixin
|
|
406
|
+
|
|
407
|
+
class EmpresaConfig(OrganizationMixin):
|
|
408
|
+
tema_cor = models.CharField(max_length=7, default='#3490dc')
|
|
409
|
+
logo = models.ImageField(upload_to='logos/')
|
|
410
|
+
|
|
411
|
+
# Uso
|
|
412
|
+
config = EmpresaConfig.objects.create(organization_id=123, tema_cor='#ff0000')
|
|
413
|
+
print(config.organization.name) # Acesso automático
|
|
414
|
+
print(config.organization_members) # Membros da organização
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### 2. UserMixin
|
|
418
|
+
Para models que pertencem apenas a um usuário.
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
from shared_auth.mixins import UserMixin
|
|
422
|
+
|
|
423
|
+
class UserPreferences(UserMixin):
|
|
424
|
+
theme = models.CharField(max_length=20, default='light')
|
|
425
|
+
notifications_enabled = models.BooleanField(default=True)
|
|
426
|
+
|
|
427
|
+
# Uso
|
|
428
|
+
prefs = UserPreferences.objects.create(user_id=456, theme='dark')
|
|
429
|
+
print(prefs.user.email)
|
|
430
|
+
print(prefs.user_full_name)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### 3. OrganizationUserMixin
|
|
434
|
+
Para models que pertencem a organização E usuário (mais comum).
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
438
|
+
|
|
439
|
+
class Tarefa(OrganizationUserMixin, TimestampedMixin):
|
|
440
|
+
titulo = models.CharField(max_length=200)
|
|
441
|
+
descricao = models.TextField()
|
|
442
|
+
status = models.CharField(max_length=20, default='pending')
|
|
443
|
+
|
|
444
|
+
# Uso
|
|
445
|
+
tarefa = Tarefa.objects.create(
|
|
446
|
+
organization_id=123,
|
|
447
|
+
user_id=456,
|
|
448
|
+
titulo='Implementar feature X'
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Validações
|
|
452
|
+
if tarefa.validate_user_belongs_to_organization():
|
|
453
|
+
print("✓ Usuário pertence à organização")
|
|
454
|
+
|
|
455
|
+
if tarefa.user_can_access(outro_user_id):
|
|
456
|
+
print("✓ Outro usuário pode acessar")
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Managers Otimizados
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
from shared_auth.managers import BaseAuthManager
|
|
463
|
+
|
|
464
|
+
class Pedido(OrganizationUserMixin):
|
|
465
|
+
# ...
|
|
466
|
+
objects = BaseAuthManager()
|
|
467
|
+
|
|
468
|
+
# Filtrar por organização
|
|
469
|
+
pedidos = Pedido.objects.for_organization(123)
|
|
470
|
+
|
|
471
|
+
# Filtrar por usuário
|
|
472
|
+
meus_pedidos = Pedido.objects.for_user(456)
|
|
473
|
+
|
|
474
|
+
# Prefetch automático (evita N+1)
|
|
475
|
+
pedidos = Pedido.objects.with_auth_data()
|
|
476
|
+
for pedido in pedidos:
|
|
477
|
+
print(pedido.organization.name) # Sem query adicional
|
|
478
|
+
print(pedido.user.email) # Sem query adicional
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Serializers - Variações
|
|
482
|
+
|
|
483
|
+
#### Versão Completa (Detail)
|
|
484
|
+
```python
|
|
485
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
486
|
+
|
|
487
|
+
class PedidoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
488
|
+
class Meta:
|
|
489
|
+
model = Pedido
|
|
490
|
+
fields = ['id', 'numero', 'organization', 'user', 'created_at']
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### Versão Simplificada (List)
|
|
494
|
+
```python
|
|
495
|
+
from shared_auth.serializers import (
|
|
496
|
+
OrganizationSimpleSerializerMixin,
|
|
497
|
+
UserSimpleSerializerMixin
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
class PedidoListSerializer(
|
|
501
|
+
OrganizationSimpleSerializerMixin,
|
|
502
|
+
UserSimpleSerializerMixin,
|
|
503
|
+
serializers.ModelSerializer
|
|
504
|
+
):
|
|
505
|
+
class Meta:
|
|
506
|
+
model = Pedido
|
|
507
|
+
fields = ['id', 'numero', 'organization', 'user']
|
|
508
|
+
|
|
509
|
+
# Response com dados reduzidos
|
|
510
|
+
{
|
|
511
|
+
"id": 1,
|
|
512
|
+
"numero": "PED-001",
|
|
513
|
+
"organization": {
|
|
514
|
+
"id": 123,
|
|
515
|
+
"name": "Empresa XYZ",
|
|
516
|
+
"cnpj": "12.345.678/0001-90"
|
|
517
|
+
},
|
|
518
|
+
"user": {
|
|
519
|
+
"id": 456,
|
|
520
|
+
"email": "joao@xyz.com",
|
|
521
|
+
"full_name": "João Silva"
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
#### Customização Avançada
|
|
527
|
+
```python
|
|
528
|
+
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
529
|
+
|
|
530
|
+
def get_organization(self, obj):
|
|
531
|
+
"""Override para adicionar campos customizados"""
|
|
532
|
+
org_data = super().get_organization(obj)
|
|
533
|
+
|
|
534
|
+
if org_data:
|
|
535
|
+
# Adicionar dados extras
|
|
536
|
+
org_data['logo_url'] = f"/logos/{obj.organization_id}.png"
|
|
537
|
+
org_data['member_count'] = obj.organization.members.count()
|
|
538
|
+
|
|
539
|
+
return org_data
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Middleware
|
|
543
|
+
|
|
544
|
+
#### SharedAuthMiddleware
|
|
545
|
+
Autentica usuário baseado no token.
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
# settings.py
|
|
549
|
+
MIDDLEWARE = [
|
|
550
|
+
# ...
|
|
551
|
+
'shared_auth.middleware.SharedAuthMiddleware',
|
|
552
|
+
]
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Busca token em:**
|
|
556
|
+
- Header: `Authorization: Token <token>`
|
|
557
|
+
- Header: `X-Auth-Token: <token>`
|
|
558
|
+
- Cookie: `auth_token`
|
|
559
|
+
|
|
560
|
+
**Adiciona ao request:**
|
|
561
|
+
- `request.user` - Objeto User autenticado
|
|
562
|
+
- `request.auth` - Token object
|
|
563
|
+
|
|
564
|
+
#### OrganizationMiddleware
|
|
565
|
+
Adiciona organização logada ao request.
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
# settings.py
|
|
569
|
+
MIDDLEWARE = [
|
|
570
|
+
'shared_auth.middleware.SharedAuthMiddleware',
|
|
571
|
+
'shared_auth.middleware.OrganizationMiddleware', # Depois do Auth
|
|
572
|
+
]
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Busca organização:**
|
|
576
|
+
1. Header `X-Organization: <org_id>`
|
|
577
|
+
2. Primeira organização do usuário autenticado
|
|
578
|
+
|
|
579
|
+
**Adiciona ao request:**
|
|
580
|
+
- `request.organization_id` - ID da organização
|
|
581
|
+
- `request.organization` - Objeto SharedOrganization
|
|
582
|
+
|
|
583
|
+
**Uso em views:**
|
|
584
|
+
```python
|
|
585
|
+
def my_view(request):
|
|
586
|
+
org_id = request.organization_id
|
|
587
|
+
org = request.organization
|
|
588
|
+
|
|
589
|
+
if org:
|
|
590
|
+
print(f"Organização logada: {org.name}")
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Permissions
|
|
594
|
+
|
|
595
|
+
```python
|
|
596
|
+
from shared_auth.permissions import (
|
|
597
|
+
IsAuthenticated,
|
|
598
|
+
HasActiveOrganization,
|
|
599
|
+
IsSameOrganization,
|
|
600
|
+
IsOwnerOrSameOrganization,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
class PedidoViewSet(viewsets.ModelViewSet):
|
|
604
|
+
permission_classes = [
|
|
605
|
+
IsAuthenticated, # Requer autenticação
|
|
606
|
+
HasActiveOrganization, # Requer organização ativa
|
|
607
|
+
IsSameOrganization, # Objeto da mesma org
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
# Ou combinações
|
|
611
|
+
class TarefaViewSet(viewsets.ModelViewSet):
|
|
612
|
+
permission_classes = [IsOwnerOrSameOrganization]
|
|
613
|
+
# Permite se for dono OU da mesma organização
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Authentication
|
|
617
|
+
|
|
618
|
+
```python
|
|
619
|
+
# Em qualquer view/viewset DRF
|
|
620
|
+
from shared_auth.authentication import SharedTokenAuthentication
|
|
621
|
+
|
|
622
|
+
class MyAPIView(APIView):
|
|
623
|
+
authentication_classes = [SharedTokenAuthentication]
|
|
624
|
+
|
|
625
|
+
def get(self, request):
|
|
626
|
+
user = request.user # User autenticado
|
|
627
|
+
token = request.auth # SharedToken object
|
|
628
|
+
|
|
629
|
+
return Response({
|
|
630
|
+
'user': user.email,
|
|
631
|
+
'token_created': token.created
|
|
632
|
+
})
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## 🔍 API Reference
|
|
638
|
+
|
|
639
|
+
### Models
|
|
640
|
+
|
|
641
|
+
#### SharedOrganization
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
from shared_auth.models import SharedOrganization
|
|
645
|
+
|
|
646
|
+
# Campos
|
|
647
|
+
org.id
|
|
648
|
+
org.name
|
|
649
|
+
org.fantasy_name
|
|
650
|
+
org.cnpj
|
|
651
|
+
org.email
|
|
652
|
+
org.telephone
|
|
653
|
+
org.cellphone
|
|
654
|
+
org.image_organization
|
|
655
|
+
org.is_branch
|
|
656
|
+
org.main_organization_id
|
|
657
|
+
org.created_at
|
|
658
|
+
org.updated_at
|
|
659
|
+
org.deleted_at
|
|
660
|
+
|
|
661
|
+
# Properties
|
|
662
|
+
org.main_organization # SharedOrganization | None
|
|
663
|
+
org.branches # QuerySet[SharedOrganization]
|
|
664
|
+
org.members # QuerySet[SharedMember]
|
|
665
|
+
org.users # QuerySet[User]
|
|
666
|
+
|
|
667
|
+
# Métodos
|
|
668
|
+
org.is_active() # bool
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
#### User
|
|
672
|
+
|
|
673
|
+
```python
|
|
674
|
+
from shared_auth.models import User
|
|
675
|
+
|
|
676
|
+
# Campos (AbstractUser + custom)
|
|
677
|
+
user.id
|
|
678
|
+
user.username
|
|
679
|
+
user.email
|
|
680
|
+
user.first_name
|
|
681
|
+
user.last_name
|
|
682
|
+
user.is_active
|
|
683
|
+
user.is_staff
|
|
684
|
+
user.is_superuser
|
|
685
|
+
user.date_joined
|
|
686
|
+
user.last_login
|
|
687
|
+
user.avatar
|
|
688
|
+
user.createdat
|
|
689
|
+
user.updatedat
|
|
690
|
+
user.deleted_at
|
|
691
|
+
|
|
692
|
+
# Properties
|
|
693
|
+
user.organizations # QuerySet[SharedOrganization]
|
|
694
|
+
|
|
695
|
+
# Métodos
|
|
696
|
+
user.get_full_name() # str
|
|
697
|
+
user.get_org(organization_id) # SharedOrganization | raise
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### SharedMember
|
|
701
|
+
|
|
702
|
+
```python
|
|
703
|
+
from shared_auth.models import SharedMember
|
|
704
|
+
|
|
705
|
+
# Campos
|
|
706
|
+
member.id
|
|
707
|
+
member.user_id
|
|
708
|
+
member.organization_id
|
|
709
|
+
member.metadata # JSONField
|
|
710
|
+
|
|
711
|
+
# Properties
|
|
712
|
+
member.user # User
|
|
713
|
+
member.organization # SharedOrganization
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
#### SharedToken
|
|
717
|
+
|
|
718
|
+
```python
|
|
719
|
+
from shared_auth.models import SharedToken
|
|
720
|
+
|
|
721
|
+
# Campos
|
|
722
|
+
token.key # Primary Key
|
|
723
|
+
token.user_id
|
|
724
|
+
token.created
|
|
725
|
+
|
|
726
|
+
# Properties
|
|
727
|
+
token.user # User
|
|
728
|
+
|
|
729
|
+
# Métodos
|
|
730
|
+
token.is_valid() # bool
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Managers
|
|
734
|
+
|
|
735
|
+
#### SharedOrganizationManager
|
|
736
|
+
|
|
737
|
+
```python
|
|
738
|
+
from shared_auth.models import SharedOrganization
|
|
739
|
+
|
|
740
|
+
SharedOrganization.objects.get_or_fail(123) # Org | raise OrganizationNotFoundError
|
|
741
|
+
SharedOrganization.objects.active() # QuerySet (deleted_at is null)
|
|
742
|
+
SharedOrganization.objects.branches() # QuerySet (is_branch=True)
|
|
743
|
+
SharedOrganization.objects.main_organizations() # QuerySet (is_branch=False)
|
|
744
|
+
SharedOrganization.objects.by_cnpj('12.345.678/0001-90') # Org | None
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
#### UserManager
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
from shared_auth.models import User
|
|
751
|
+
|
|
752
|
+
User.objects.get_or_fail(456) # User | raise UserNotFoundError
|
|
753
|
+
User.objects.active() # QuerySet (is_active=True, deleted_at is null)
|
|
754
|
+
User.objects.by_email('user@example.com') # User | None
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
#### SharedMemberManager
|
|
758
|
+
|
|
759
|
+
```python
|
|
760
|
+
from shared_auth.models import SharedMember
|
|
761
|
+
|
|
762
|
+
SharedMember.objects.for_user(456) # QuerySet
|
|
763
|
+
SharedMember.objects.for_organization(123) # QuerySet
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### BaseAuthManager (para seus models)
|
|
767
|
+
|
|
768
|
+
```python
|
|
769
|
+
# Quando usa OrganizationMixin
|
|
770
|
+
Model.objects.for_organization(123) # QuerySet
|
|
771
|
+
Model.objects.for_organizations([123, 456]) # QuerySet
|
|
772
|
+
Model.objects.with_organization_data() # List com prefetch
|
|
773
|
+
|
|
774
|
+
# Quando usa UserMixin
|
|
775
|
+
Model.objects.for_user(456) # QuerySet
|
|
776
|
+
Model.objects.for_users([456, 789]) # QuerySet
|
|
777
|
+
Model.objects.with_user_data() # List com prefetch
|
|
778
|
+
|
|
779
|
+
# Quando usa OrganizationUserMixin
|
|
780
|
+
Model.objects.with_auth_data() # List com prefetch de org e user
|
|
781
|
+
Model.objects.create_with_validation(
|
|
782
|
+
organization_id=123,
|
|
783
|
+
user_id=456,
|
|
784
|
+
**kwargs
|
|
785
|
+
) # Valida membership antes de criar
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Exceptions
|
|
789
|
+
|
|
790
|
+
```python
|
|
791
|
+
from shared_auth.exceptions import (
|
|
792
|
+
SharedAuthError,
|
|
793
|
+
OrganizationNotFoundError,
|
|
794
|
+
UserNotFoundError,
|
|
795
|
+
DatabaseConnectionError,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
org = SharedOrganization.objects.get_or_fail(999)
|
|
800
|
+
except OrganizationNotFoundError as e:
|
|
801
|
+
print(e) # "Organização com ID 999 não encontrada"
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## 🎯 Casos de Uso Reais
|
|
807
|
+
|
|
808
|
+
### Sistema de Pedidos Multi-Tenant
|
|
809
|
+
|
|
810
|
+
```python
|
|
811
|
+
# models.py
|
|
812
|
+
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
813
|
+
|
|
814
|
+
class Pedido(OrganizationUserMixin, TimestampedMixin):
|
|
815
|
+
numero = models.CharField(max_length=20, unique=True)
|
|
816
|
+
valor_total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
817
|
+
status = models.CharField(max_length=20)
|
|
818
|
+
|
|
819
|
+
objects = BaseAuthManager()
|
|
820
|
+
|
|
821
|
+
class ItemPedido(models.Model):
|
|
822
|
+
pedido = models.ForeignKey(Pedido, related_name='itens')
|
|
823
|
+
produto = models.CharField(max_length=200)
|
|
824
|
+
quantidade = models.IntegerField()
|
|
825
|
+
valor_unitario = models.DecimalField(max_digits=10, decimal_places=2)
|
|
826
|
+
|
|
827
|
+
# serializers.py
|
|
828
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
829
|
+
|
|
830
|
+
class ItemPedidoSerializer(serializers.ModelSerializer):
|
|
831
|
+
class Meta:
|
|
832
|
+
model = ItemPedido
|
|
833
|
+
fields = ['id', 'produto', 'quantidade', 'valor_unitario']
|
|
834
|
+
|
|
835
|
+
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
836
|
+
itens = ItemPedidoSerializer(many=True, read_only=True)
|
|
837
|
+
|
|
838
|
+
class Meta:
|
|
839
|
+
model = Pedido
|
|
840
|
+
fields = [
|
|
841
|
+
'id', 'numero', 'valor_total', 'status',
|
|
842
|
+
'organization', 'user', 'itens', 'created_at'
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
# views.py
|
|
846
|
+
from shared_auth.mixins import LoggedOrganizationMixin
|
|
847
|
+
from shared_auth.permissions import HasActiveOrganization
|
|
848
|
+
|
|
849
|
+
class PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):
|
|
850
|
+
serializer_class = PedidoSerializer
|
|
851
|
+
permission_classes = [HasActiveOrganization]
|
|
852
|
+
|
|
853
|
+
def get_queryset(self):
|
|
854
|
+
# Já filtra por organization_id automaticamente
|
|
855
|
+
return super().get_queryset().with_auth_data()
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Sistema de Tarefas com Responsáveis
|
|
859
|
+
|
|
860
|
+
```python
|
|
861
|
+
# models.py
|
|
862
|
+
class Tarefa(OrganizationUserMixin, TimestampedMixin):
|
|
863
|
+
"""
|
|
864
|
+
user_id = criador
|
|
865
|
+
responsavel_id = quem vai executar
|
|
866
|
+
"""
|
|
867
|
+
titulo = models.CharField(max_length=200)
|
|
868
|
+
descricao = models.TextField()
|
|
869
|
+
responsavel_id = models.IntegerField()
|
|
870
|
+
status = models.CharField(max_length=20, default='pending')
|
|
871
|
+
|
|
872
|
+
objects = BaseAuthManager()
|
|
873
|
+
|
|
874
|
+
@property
|
|
875
|
+
def responsavel(self):
|
|
876
|
+
"""Acessa usuário responsável"""
|
|
877
|
+
if not hasattr(self, '_cached_responsavel'):
|
|
878
|
+
from shared_auth.models import User
|
|
879
|
+
self._cached_responsavel = User.objects.get_or_fail(self.responsavel_id)
|
|
880
|
+
return self._cached_responsavel
|
|
881
|
+
|
|
882
|
+
# serializers.py
|
|
883
|
+
class TarefaSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
884
|
+
responsavel = serializers.SerializerMethodField()
|
|
885
|
+
|
|
886
|
+
def get_responsavel(self, obj):
|
|
887
|
+
try:
|
|
888
|
+
resp = obj.responsavel
|
|
889
|
+
return {
|
|
890
|
+
'id': resp.pk,
|
|
891
|
+
'email': resp.email,
|
|
892
|
+
'full_name': resp.get_full_name(),
|
|
893
|
+
}
|
|
894
|
+
except:
|
|
895
|
+
return None
|
|
896
|
+
|
|
897
|
+
class Meta:
|
|
898
|
+
model = Tarefa
|
|
899
|
+
fields = [
|
|
900
|
+
'id', 'titulo', 'descricao', 'status',
|
|
901
|
+
'organization', # Organização dona
|
|
902
|
+
'user', # Criador
|
|
903
|
+
'responsavel', # Executor
|
|
904
|
+
'created_at'
|
|
905
|
+
]
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## 🔧 Troubleshooting
|
|
911
|
+
|
|
912
|
+
### Problema: Queries lentas (N+1)
|
|
913
|
+
|
|
914
|
+
**Solução:** Use os managers com prefetch
|
|
915
|
+
|
|
916
|
+
```python
|
|
917
|
+
# ❌ Ruim - Causa N+1
|
|
918
|
+
pedidos = Pedido.objects.all()
|
|
919
|
+
for pedido in pedidos:
|
|
920
|
+
print(pedido.organization.name) # Query por item!
|
|
921
|
+
|
|
922
|
+
# ✅ Bom - 3 queries total
|
|
923
|
+
pedidos = Pedido.objects.with_auth_data()
|
|
924
|
+
for pedido in pedidos:
|
|
925
|
+
print(pedido.organization.name) # Sem query adicional
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Problema: OrganizationNotFoundError
|
|
929
|
+
|
|
930
|
+
**Causa:** ID de organização inválido ou deletada
|
|
931
|
+
|
|
932
|
+
**Solução:**
|
|
933
|
+
```python
|
|
934
|
+
# Usar try/except
|
|
935
|
+
try:
|
|
936
|
+
org = SharedOrganization.objects.get_or_fail(org_id)
|
|
937
|
+
except OrganizationNotFoundError:
|
|
938
|
+
# Tratar erro
|
|
939
|
+
return Response({'error': 'Organização não encontrada'}, status=404)
|
|
940
|
+
|
|
941
|
+
# Ou usar filter
|
|
942
|
+
org = SharedOrganization.objects.filter(pk=org_id).first()
|
|
943
|
+
if not org:
|
|
944
|
+
# Tratar
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Problema: Erro de conexão com auth_db
|
|
948
|
+
|
|
949
|
+
**Solução:** Verificar configuração do database router e permissões
|
|
950
|
+
|
|
951
|
+
```python
|
|
952
|
+
# Testar conexão
|
|
953
|
+
from django.db import connections
|
|
954
|
+
|
|
955
|
+
connection = connections['auth_db']
|
|
956
|
+
with connection.cursor() as cursor:
|
|
957
|
+
cursor.execute("SELECT 1")
|
|
958
|
+
print("✓ Conexão OK")
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## 📝 Changelog
|
|
964
|
+
|
|
965
|
+
### v0.2.25
|
|
966
|
+
- ✨ Adicionado suporte a imagens (avatar, logo)
|
|
967
|
+
- ✨ StorageBackend para arquivos compartilhados
|
|
968
|
+
- 🐛 Correções nos serializers
|
|
969
|
+
- 📚 Documentação melhorada
|
|
970
|
+
|
|
971
|
+
### v0.2.0
|
|
972
|
+
- ✨ Middlewares: SharedAuthMiddleware, OrganizationMiddleware
|
|
973
|
+
- ✨ Permissions customizadas
|
|
974
|
+
- ✨ Managers otimizados com prefetch
|
|
975
|
+
- ✨ Serializer mixins com dados aninhados
|
|
976
|
+
|
|
977
|
+
### v0.1.0
|
|
978
|
+
- 🎉 Versão inicial
|
|
979
|
+
- ✨ Models compartilhados
|
|
980
|
+
- ✨ Mixins básicos
|
|
981
|
+
- ✨ Autenticação via token
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## 📄 Licença
|
|
986
|
+
|
|
987
|
+
MIT License - veja [LICENSE](LICENSE) para detalhes.
|
|
988
|
+
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
## 🤝 Contribuindo
|
|
992
|
+
|
|
993
|
+
Contribuições são bem-vindas! Por favor, abra uma issue ou pull request.
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
## 📧 Suporte
|
|
998
|
+
|
|
999
|
+
Para suporte, abra uma issue no GitHub ou entre em contato com a equipe Maquinaweb.
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
**Desenvolvido com ❤️ por Maquinaweb**
|