maquinaweb-shared-auth


Namemaquinaweb-shared-auth JSON
Version 0.2.38 PyPI version JSON
download
home_pageNone
SummaryModels read-only para autenticação compartilhada entre projetos Django.
upload_time2025-10-23 13:01:06
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords django auth models shared
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # 🔐 Maquinaweb Shared Auth

> Biblioteca Django para autenticação compartilhada entre múltiplos sistemas usando um único banco de dados centralizado.

[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Django](https://img.shields.io/badge/django-4.2+-green.svg)](https://www.djangoproject.com/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## 📋 Índice

- [Visão Geral](#-visão-geral)
- [Características](#-características)
- [Arquitetura](#️-arquitetura)
- [Instalação](#-instalação)
- [Configuração](#️-configuração)
- [Uso Básico](#-uso-básico)
- [Guias Avançados](#-guias-avançados)
- [API Reference](#-api-reference)

---

## 🎯 Visão Geral

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.

### Problema Resolvido

Ao invés de:
- ❌ Duplicar dados de usuários em cada sistema
- ❌ Fazer requisições HTTP entre sistemas
- ❌ Manter múltiplos bancos de autenticação sincronizados

Você pode:
- ✅ Acessar dados de autenticação diretamente do banco central
- ✅ Usar a interface familiar do Django ORM
- ✅ Garantir consistência de dados entre sistemas
- ✅ Trabalhar com models read-only seguros

---

## ✨ Características

### Core Features

- **🔐 Autenticação Centralizada**: Token-based authentication compartilhado
- **🏢 Multi-Tenancy**: Suporte completo a organizações e filiais
- **👥 Gestão de Membros**: Relacionamento usuários ↔ organizações
- **🔒 Read-Only Safety**: Proteção contra modificações acidentais
- **⚡ Performance**: Managers otimizados com prefetch automático
- **🎨 DRF Ready**: Mixins para serializers com dados aninhados

### Componentes Principais

| Componente | Descrição |
|------------|-----------|
| **Models** | SharedOrganization, User, SharedMember, SharedToken |
| **Mixins** | OrganizationMixin, UserMixin, OrganizationUserMixin |
| **Serializers** | OrganizationSerializerMixin, UserSerializerMixin |
| **Authentication** | SharedTokenAuthentication |
| **Middleware** | SharedAuthMiddleware, OrganizationMiddleware |
| **Permissions** | IsAuthenticated, HasActiveOrganization, IsSameOrganization |
| **Managers** | Métodos otimizados com prefetch e validações |

---

## 🏗️ Arquitetura

```
┌─────────────────────────────────────┐
│   Sistema de Autenticação Central  │
│                                     │
│  ┌──────────────┐  ┌────────────┐ │
│  │Organization  │  │    User    │ │
│  └──────┬───────┘  └─────┬──────┘ │
│         │                │         │
│         └────────┬───────┘         │
│                  │                 │
│           ┌──────▼──────┐         │
│           │   Member    │         │
│           │   Token     │         │
│           └─────────────┘         │
└──────────────────┬──────────────────┘
                   │
      ┌────────────┴────────────┐
      │  PostgreSQL/MySQL       │
      │  (auth_db)              │
      └────────────┬────────────┘
                   │
      ┌────────────┴────────────┐
      │                         │
┌─────▼─────┐            ┌─────▼─────┐
│ Sistema A │            │ Sistema B │
│           │            │           │
│ Pedidos   │            │ Estoque   │
│ ├─ org    │            │ ├─ org    │
│ └─ user   │            │ └─ user   │
└───────────┘            └───────────┘
```

**Fluxo de Autenticação:**

1. Cliente envia request com token no header
2. Middleware valida token no banco `auth_db`
3. Dados do usuário e organização são anexados ao `request`
4. Sistema cliente acessa dados via ORM (read-only)

---

## 📦 Instalação

### 1. Instalar a Biblioteca

```bash
# Via pip (quando publicado)
pip install maquinaweb-shared-auth

# Ou modo desenvolvimento
pip install -e /path/to/maquinaweb-shared-auth
```

### 2. Adicionar ao requirements.txt

```txt
Django>=4.2
djangorestframework>=3.14
maquinaweb-shared-auth>=0.2.25
```

---

## ⚙️ Configuração

### 1. Settings do Django

```python
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'rest_framework',
    
    # Adicionar shared_auth
    'shared_auth',
    
    # Suas apps
    'myapp',
]
```

### 2. Configurar Banco de Dados

```python
# settings.py

DATABASES = {
    'default': {
        # Banco do sistema atual
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'meu_sistema_db',
        'USER': 'meu_user',
        'PASSWORD': 'senha',
        'HOST': 'localhost',
        'PORT': '5432',
    },
    'auth_db': {
        # Banco centralizado de autenticação (READ-ONLY)
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'sistema_auth_db',
        'USER': 'readonly_user',
        'PASSWORD': 'senha_readonly',
        'HOST': 'auth-server.example.com',
        'PORT': '5432',
    }
}

# Router para direcionar queries
DATABASE_ROUTERS = ['shared_auth.router.SharedAuthRouter']
```

### 3. Configurar Autenticação (DRF)

```python
# settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'shared_auth.authentication.SharedTokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'shared_auth.permissions.IsAuthenticated',
    ],
}
```

### 4. Configurar Middleware (Opcional)

```python
# settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    
    # Middlewares da shared_auth
    'shared_auth.middleware.SharedAuthMiddleware',
    'shared_auth.middleware.OrganizationMiddleware',
    
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]
```

### 5. Configurar Tabelas (Opcional)

```python
# settings.py

# Customizar nomes das tabelas (se necessário)
SHARED_AUTH_ORGANIZATION_TABLE = 'organization_organization'
SHARED_AUTH_USER_TABLE = 'auth_user'
SHARED_AUTH_MEMBER_TABLE = 'organization_member'
SHARED_AUTH_TOKEN_TABLE = 'authtoken_token'
```

### 6. Criar Usuário Read-Only no PostgreSQL

```sql
-- No servidor de autenticação
CREATE USER readonly_user WITH PASSWORD 'senha_segura_aqui';

-- Conceder permissões
GRANT CONNECT ON DATABASE sistema_auth_db TO readonly_user;
GRANT USAGE ON SCHEMA public TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;

-- Para tabelas futuras
ALTER DEFAULT PRIVILEGES IN SCHEMA public 
GRANT SELECT ON TABLES TO readonly_user;

-- Garantir read-only
ALTER USER readonly_user SET default_transaction_read_only = on;
```

---

## 🚀 Uso Básico

### 1. Models com Mixins

```python
# myapp/models.py
from django.db import models
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
from shared_auth.managers import BaseAuthManager

class Pedido(OrganizationUserMixin, TimestampedMixin):
    """Model que pertence a organização e usuário"""
    
    numero = models.CharField(max_length=20, unique=True)
    valor_total = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20, default='pending')
    
    objects = BaseAuthManager()
    
    def __str__(self):
        return f"Pedido {self.numero}"
```

**O que você ganha automaticamente:**
- ✅ Campos: `organization_id`, `user_id`, `created_at`, `updated_at`
- ✅ Properties: `organization`, `user`, `organization_name`, `user_email`
- ✅ Métodos: `validate_user_belongs_to_organization()`, `user_can_access()`

### 2. Serializers com Dados Aninhados

```python
# myapp/serializers.py
from rest_framework import serializers
from shared_auth.serializers import OrganizationUserSerializerMixin
from .models import Pedido

class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = Pedido
        fields = [
            'id', 'numero', 'valor_total', 'status',
            'organization',  # Objeto completo
            'user',          # Objeto completo
            'created_at',
        ]
        read_only_fields = ['organization', 'user', 'created_at']
```

**Response JSON:**
```json
{
  "id": 1,
  "numero": "PED-001",
  "valor_total": "1500.00",
  "status": "pending",
  "organization": {
    "id": 123,
    "name": "Empresa XYZ Ltda",
    "fantasy_name": "XYZ",
    "cnpj": "12.345.678/0001-90",
    "email": "contato@xyz.com",
    "is_active": true
  },
  "user": {
    "id": 456,
    "username": "joao.silva",
    "email": "joao@xyz.com",
    "full_name": "João Silva",
    "is_active": true
  },
  "created_at": "2025-10-01T10:00:00Z"
}
```

### 3. ViewSets com Organização

```python
# myapp/views.py
from rest_framework import viewsets
from shared_auth.mixins import LoggedOrganizationMixin
from shared_auth.permissions import HasActiveOrganization, IsSameOrganization
from .models import Pedido
from .serializers import PedidoSerializer

class PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):
    """
    ViewSet que filtra automaticamente por organização logada
    """
    serializer_class = PedidoSerializer
    permission_classes = [HasActiveOrganization, IsSameOrganization]
    
    # get_queryset() já filtra por organization_id automaticamente
    # perform_create() já adiciona organization_id automaticamente
```

### 4. Acessar Dados Compartilhados

```python
# Em qualquer lugar do código
from shared_auth.models import SharedOrganization, User, SharedMember

# Buscar organização
org = SharedOrganization.objects.get_or_fail(123)
print(org.name)  # "Empresa XYZ"
print(org.members)  # QuerySet de membros

# Buscar usuário
user = User.objects.get_or_fail(456)
print(user.email)  # "joao@xyz.com"
print(user.organizations)  # Organizações do usuário

# Verificar membership
member = SharedMember.objects.filter(
    user_id=456,
    organization_id=123
).first()

if member:
    print(f"{member.user.email} é membro de {member.organization.name}")
```

---

## 📚 Guias Avançados

### Mixins para Models

#### 1. OrganizationMixin
Para models que pertencem apenas a uma organização.

```python
from shared_auth.mixins import OrganizationMixin

class EmpresaConfig(OrganizationMixin):
    tema_cor = models.CharField(max_length=7, default='#3490dc')
    logo = models.ImageField(upload_to='logos/')
    
# Uso
config = EmpresaConfig.objects.create(organization_id=123, tema_cor='#ff0000')
print(config.organization.name)  # Acesso automático
print(config.organization_members)  # Membros da organização
```

#### 2. UserMixin
Para models que pertencem apenas a um usuário.

```python
from shared_auth.mixins import UserMixin

class UserPreferences(UserMixin):
    theme = models.CharField(max_length=20, default='light')
    notifications_enabled = models.BooleanField(default=True)

# Uso
prefs = UserPreferences.objects.create(user_id=456, theme='dark')
print(prefs.user.email)
print(prefs.user_full_name)
```

#### 3. OrganizationUserMixin
Para models que pertencem a organização E usuário (mais comum).

```python
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin

class Tarefa(OrganizationUserMixin, TimestampedMixin):
    titulo = models.CharField(max_length=200)
    descricao = models.TextField()
    status = models.CharField(max_length=20, default='pending')

# Uso
tarefa = Tarefa.objects.create(
    organization_id=123,
    user_id=456,
    titulo='Implementar feature X'
)

# Validações
if tarefa.validate_user_belongs_to_organization():
    print("✓ Usuário pertence à organização")

if tarefa.user_can_access(outro_user_id):
    print("✓ Outro usuário pode acessar")
```

### Managers Otimizados

```python
from shared_auth.managers import BaseAuthManager

class Pedido(OrganizationUserMixin):
    # ...
    objects = BaseAuthManager()

# Filtrar por organização
pedidos = Pedido.objects.for_organization(123)

# Filtrar por usuário
meus_pedidos = Pedido.objects.for_user(456)

# Prefetch automático (evita N+1)
pedidos = Pedido.objects.with_auth_data()
for pedido in pedidos:
    print(pedido.organization.name)  # Sem query adicional
    print(pedido.user.email)  # Sem query adicional
```

### Serializers - Variações

#### Versão Completa (Detail)
```python
from shared_auth.serializers import OrganizationUserSerializerMixin

class PedidoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = Pedido
        fields = ['id', 'numero', 'organization', 'user', 'created_at']
```

#### Versão Simplificada (List)
```python
from shared_auth.serializers import (
    OrganizationSimpleSerializerMixin,
    UserSimpleSerializerMixin
)

class PedidoListSerializer(
    OrganizationSimpleSerializerMixin,
    UserSimpleSerializerMixin,
    serializers.ModelSerializer
):
    class Meta:
        model = Pedido
        fields = ['id', 'numero', 'organization', 'user']
    
# Response com dados reduzidos
{
  "id": 1,
  "numero": "PED-001",
  "organization": {
    "id": 123,
    "name": "Empresa XYZ",
    "cnpj": "12.345.678/0001-90"
  },
  "user": {
    "id": 456,
    "email": "joao@xyz.com",
    "full_name": "João Silva"
  }
}
```

#### Customização Avançada
```python
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
    
    def get_organization(self, obj):
        """Override para adicionar campos customizados"""
        org_data = super().get_organization(obj)
        
        if org_data:
            # Adicionar dados extras
            org_data['logo_url'] = f"/logos/{obj.organization_id}.png"
            org_data['member_count'] = obj.organization.members.count()
        
        return org_data
```

### Middleware

#### SharedAuthMiddleware
Autentica usuário baseado no token.

```python
# settings.py
MIDDLEWARE = [
    # ...
    'shared_auth.middleware.SharedAuthMiddleware',
]
```

**Busca token em:**
- Header: `Authorization: Token <token>`
- Header: `X-Auth-Token: <token>`
- Cookie: `auth_token`

**Adiciona ao request:**
- `request.user` - Objeto User autenticado
- `request.auth` - Token object

#### OrganizationMiddleware
Adiciona organização logada ao request.

```python
# settings.py
MIDDLEWARE = [
    'shared_auth.middleware.SharedAuthMiddleware',
    'shared_auth.middleware.OrganizationMiddleware',  # Depois do Auth
]
```

**Busca organização:**
1. Header `X-Organization: <org_id>`
2. Primeira organização do usuário autenticado

**Adiciona ao request:**
- `request.organization_id` - ID da organização
- `request.organization` - Objeto SharedOrganization

**Uso em views:**
```python
def my_view(request):
    org_id = request.organization_id
    org = request.organization
    
    if org:
        print(f"Organização logada: {org.name}")
```

### Permissions

```python
from shared_auth.permissions import (
    IsAuthenticated,
    HasActiveOrganization,
    IsSameOrganization,
    IsOwnerOrSameOrganization,
)

class PedidoViewSet(viewsets.ModelViewSet):
    permission_classes = [
        IsAuthenticated,           # Requer autenticação
        HasActiveOrganization,     # Requer organização ativa
        IsSameOrganization,        # Objeto da mesma org
    ]
    
# Ou combinações
class TarefaViewSet(viewsets.ModelViewSet):
    permission_classes = [IsOwnerOrSameOrganization]
    # Permite se for dono OU da mesma organização
```

### Authentication

```python
# Em qualquer view/viewset DRF
from shared_auth.authentication import SharedTokenAuthentication

class MyAPIView(APIView):
    authentication_classes = [SharedTokenAuthentication]
    
    def get(self, request):
        user = request.user  # User autenticado
        token = request.auth  # SharedToken object
        
        return Response({
            'user': user.email,
            'token_created': token.created
        })
```

---

## 🔍 API Reference

### Models

#### SharedOrganization

```python
from shared_auth.models import SharedOrganization

# Campos
org.id
org.name
org.fantasy_name
org.cnpj
org.email
org.telephone
org.cellphone
org.image_organization
org.is_branch
org.main_organization_id
org.created_at
org.updated_at
org.deleted_at

# Properties
org.main_organization  # SharedOrganization | None
org.branches  # QuerySet[SharedOrganization]
org.members  # QuerySet[SharedMember]
org.users  # QuerySet[User]

# Métodos
org.is_active()  # bool
```

#### User

```python
from shared_auth.models import User

# Campos (AbstractUser + custom)
user.id
user.username
user.email
user.first_name
user.last_name
user.is_active
user.is_staff
user.is_superuser
user.date_joined
user.last_login
user.avatar
user.createdat
user.updatedat
user.deleted_at

# Properties
user.organizations  # QuerySet[SharedOrganization]

# Métodos
user.get_full_name()  # str
user.get_org(organization_id)  # SharedOrganization | raise
```

#### SharedMember

```python
from shared_auth.models import SharedMember

# Campos
member.id
member.user_id
member.organization_id
member.metadata  # JSONField

# Properties
member.user  # User
member.organization  # SharedOrganization
```

#### SharedToken

```python
from shared_auth.models import SharedToken

# Campos
token.key  # Primary Key
token.user_id
token.created

# Properties
token.user  # User

# Métodos
token.is_valid()  # bool
```

### Managers

#### SharedOrganizationManager

```python
from shared_auth.models import SharedOrganization

SharedOrganization.objects.get_or_fail(123)  # Org | raise OrganizationNotFoundError
SharedOrganization.objects.active()  # QuerySet (deleted_at is null)
SharedOrganization.objects.branches()  # QuerySet (is_branch=True)
SharedOrganization.objects.main_organizations()  # QuerySet (is_branch=False)
SharedOrganization.objects.by_cnpj('12.345.678/0001-90')  # Org | None
```

#### UserManager

```python
from shared_auth.models import User

User.objects.get_or_fail(456)  # User | raise UserNotFoundError
User.objects.active()  # QuerySet (is_active=True, deleted_at is null)
User.objects.by_email('user@example.com')  # User | None
```

#### SharedMemberManager

```python
from shared_auth.models import SharedMember

SharedMember.objects.for_user(456)  # QuerySet
SharedMember.objects.for_organization(123)  # QuerySet
```

#### BaseAuthManager (para seus models)

```python
# Quando usa OrganizationMixin
Model.objects.for_organization(123)  # QuerySet
Model.objects.for_organizations([123, 456])  # QuerySet
Model.objects.with_organization_data()  # List com prefetch

# Quando usa UserMixin
Model.objects.for_user(456)  # QuerySet
Model.objects.for_users([456, 789])  # QuerySet
Model.objects.with_user_data()  # List com prefetch

# Quando usa OrganizationUserMixin
Model.objects.with_auth_data()  # List com prefetch de org e user
Model.objects.create_with_validation(
    organization_id=123,
    user_id=456,
    **kwargs
)  # Valida membership antes de criar
```

### Exceptions

```python
from shared_auth.exceptions import (
    SharedAuthError,
    OrganizationNotFoundError,
    UserNotFoundError,
    DatabaseConnectionError,
)

try:
    org = SharedOrganization.objects.get_or_fail(999)
except OrganizationNotFoundError as e:
    print(e)  # "Organização com ID 999 não encontrada"
```

---

## 🎯 Casos de Uso Reais

### Sistema de Pedidos Multi-Tenant

```python
# models.py
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin

class Pedido(OrganizationUserMixin, TimestampedMixin):
    numero = models.CharField(max_length=20, unique=True)
    valor_total = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20)
    
    objects = BaseAuthManager()

class ItemPedido(models.Model):
    pedido = models.ForeignKey(Pedido, related_name='itens')
    produto = models.CharField(max_length=200)
    quantidade = models.IntegerField()
    valor_unitario = models.DecimalField(max_digits=10, decimal_places=2)

# serializers.py
from shared_auth.serializers import OrganizationUserSerializerMixin

class ItemPedidoSerializer(serializers.ModelSerializer):
    class Meta:
        model = ItemPedido
        fields = ['id', 'produto', 'quantidade', 'valor_unitario']

class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
    itens = ItemPedidoSerializer(many=True, read_only=True)
    
    class Meta:
        model = Pedido
        fields = [
            'id', 'numero', 'valor_total', 'status',
            'organization', 'user', 'itens', 'created_at'
        ]

# views.py
from shared_auth.mixins import LoggedOrganizationMixin
from shared_auth.permissions import HasActiveOrganization

class PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):
    serializer_class = PedidoSerializer
    permission_classes = [HasActiveOrganization]
    
    def get_queryset(self):
        # Já filtra por organization_id automaticamente
        return super().get_queryset().with_auth_data()
```

### Sistema de Tarefas com Responsáveis

```python
# models.py
class Tarefa(OrganizationUserMixin, TimestampedMixin):
    """
    user_id = criador
    responsavel_id = quem vai executar
    """
    titulo = models.CharField(max_length=200)
    descricao = models.TextField()
    responsavel_id = models.IntegerField()
    status = models.CharField(max_length=20, default='pending')
    
    objects = BaseAuthManager()
    
    @property
    def responsavel(self):
        """Acessa usuário responsável"""
        if not hasattr(self, '_cached_responsavel'):
            from shared_auth.models import User
            self._cached_responsavel = User.objects.get_or_fail(self.responsavel_id)
        return self._cached_responsavel

# serializers.py
class TarefaSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
    responsavel = serializers.SerializerMethodField()
    
    def get_responsavel(self, obj):
        try:
            resp = obj.responsavel
            return {
                'id': resp.pk,
                'email': resp.email,
                'full_name': resp.get_full_name(),
            }
        except:
            return None
    
    class Meta:
        model = Tarefa
        fields = [
            'id', 'titulo', 'descricao', 'status',
            'organization',  # Organização dona
            'user',  # Criador
            'responsavel',  # Executor
            'created_at'
        ]
```

---

## 🔧 Troubleshooting

### Problema: Queries lentas (N+1)

**Solução:** Use os managers com prefetch

```python
# ❌ Ruim - Causa N+1
pedidos = Pedido.objects.all()
for pedido in pedidos:
    print(pedido.organization.name)  # Query por item!

# ✅ Bom - 3 queries total
pedidos = Pedido.objects.with_auth_data()
for pedido in pedidos:
    print(pedido.organization.name)  # Sem query adicional
```

### Problema: OrganizationNotFoundError

**Causa:** ID de organização inválido ou deletada

**Solução:**
```python
# Usar try/except
try:
    org = SharedOrganization.objects.get_or_fail(org_id)
except OrganizationNotFoundError:
    # Tratar erro
    return Response({'error': 'Organização não encontrada'}, status=404)

# Ou usar filter
org = SharedOrganization.objects.filter(pk=org_id).first()
if not org:
    # Tratar
```

### Problema: Erro de conexão com auth_db

**Solução:** Verificar configuração do database router e permissões

```python
# Testar conexão
from django.db import connections

connection = connections['auth_db']
with connection.cursor() as cursor:
    cursor.execute("SELECT 1")
    print("✓ Conexão OK")
```

---

## 📝 Changelog

### v0.2.25
- ✨ Adicionado suporte a imagens (avatar, logo)
- ✨ StorageBackend para arquivos compartilhados
- 🐛 Correções nos serializers
- 📚 Documentação melhorada

### v0.2.0
- ✨ Middlewares: SharedAuthMiddleware, OrganizationMiddleware
- ✨ Permissions customizadas
- ✨ Managers otimizados com prefetch
- ✨ Serializer mixins com dados aninhados

### v0.1.0
- 🎉 Versão inicial
- ✨ Models compartilhados
- ✨ Mixins básicos
- ✨ Autenticação via token

---

## 📄 Licença

MIT License - veja [LICENSE](LICENSE) para detalhes.

---

## 🤝 Contribuindo

Contribuições são bem-vindas! Por favor, abra uma issue ou pull request.

---

## 📧 Suporte

Para suporte, abra uma issue no GitHub ou entre em contato com a equipe Maquinaweb.

---

**Desenvolvido com ❤️ por Maquinaweb**

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "maquinaweb-shared-auth",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "django, auth, models, shared",
    "author": null,
    "author_email": "Seu Nome <seuemail@dominio.com>",
    "download_url": "https://files.pythonhosted.org/packages/d1/f2/d14e3265a190e87ecb8fad183100c5de8f477a88054cec1c27ef2c573db1/maquinaweb_shared_auth-0.2.38.tar.gz",
    "platform": null,
    "description": "# \ud83d\udd10 Maquinaweb Shared Auth\n\n> Biblioteca Django para autentica\u00e7\u00e3o compartilhada entre m\u00faltiplos sistemas usando um \u00fanico banco de dados centralizado.\n\n[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)\n[![Django](https://img.shields.io/badge/django-4.2+-green.svg)](https://www.djangoproject.com/)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n## \ud83d\udccb \u00cdndice\n\n- [Vis\u00e3o Geral](#-vis\u00e3o-geral)\n- [Caracter\u00edsticas](#-caracter\u00edsticas)\n- [Arquitetura](#\ufe0f-arquitetura)\n- [Instala\u00e7\u00e3o](#-instala\u00e7\u00e3o)\n- [Configura\u00e7\u00e3o](#\ufe0f-configura\u00e7\u00e3o)\n- [Uso B\u00e1sico](#-uso-b\u00e1sico)\n- [Guias Avan\u00e7ados](#-guias-avan\u00e7ados)\n- [API Reference](#-api-reference)\n\n---\n\n## \ud83c\udfaf Vis\u00e3o Geral\n\nA **Maquinaweb Shared Auth** permite que m\u00faltiplos sistemas Django compartilhem dados de autentica\u00e7\u00e3o, usu\u00e1rios e organiza\u00e7\u00f5es atrav\u00e9s de um banco de dados centralizado, sem necessidade de requisi\u00e7\u00f5es HTTP.\n\n### Problema Resolvido\n\nAo inv\u00e9s de:\n- \u274c Duplicar dados de usu\u00e1rios em cada sistema\n- \u274c Fazer requisi\u00e7\u00f5es HTTP entre sistemas\n- \u274c Manter m\u00faltiplos bancos de autentica\u00e7\u00e3o sincronizados\n\nVoc\u00ea pode:\n- \u2705 Acessar dados de autentica\u00e7\u00e3o diretamente do banco central\n- \u2705 Usar a interface familiar do Django ORM\n- \u2705 Garantir consist\u00eancia de dados entre sistemas\n- \u2705 Trabalhar com models read-only seguros\n\n---\n\n## \u2728 Caracter\u00edsticas\n\n### Core Features\n\n- **\ud83d\udd10 Autentica\u00e7\u00e3o Centralizada**: Token-based authentication compartilhado\n- **\ud83c\udfe2 Multi-Tenancy**: Suporte completo a organiza\u00e7\u00f5es e filiais\n- **\ud83d\udc65 Gest\u00e3o de Membros**: Relacionamento usu\u00e1rios \u2194 organiza\u00e7\u00f5es\n- **\ud83d\udd12 Read-Only Safety**: Prote\u00e7\u00e3o contra modifica\u00e7\u00f5es acidentais\n- **\u26a1 Performance**: Managers otimizados com prefetch autom\u00e1tico\n- **\ud83c\udfa8 DRF Ready**: Mixins para serializers com dados aninhados\n\n### Componentes Principais\n\n| Componente | Descri\u00e7\u00e3o |\n|------------|-----------|\n| **Models** | SharedOrganization, User, SharedMember, SharedToken |\n| **Mixins** | OrganizationMixin, UserMixin, OrganizationUserMixin |\n| **Serializers** | OrganizationSerializerMixin, UserSerializerMixin |\n| **Authentication** | SharedTokenAuthentication |\n| **Middleware** | SharedAuthMiddleware, OrganizationMiddleware |\n| **Permissions** | IsAuthenticated, HasActiveOrganization, IsSameOrganization |\n| **Managers** | M\u00e9todos otimizados com prefetch e valida\u00e7\u00f5es |\n\n---\n\n## \ud83c\udfd7\ufe0f Arquitetura\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   Sistema de Autentica\u00e7\u00e3o Central  \u2502\n\u2502                                     \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502  \u2502Organization  \u2502  \u2502    User    \u2502 \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502         \u2502                \u2502         \u2502\n\u2502         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2502\n\u2502                  \u2502                 \u2502\n\u2502           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u2502\n\u2502           \u2502   Member    \u2502         \u2502\n\u2502           \u2502   Token     \u2502         \u2502\n\u2502           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                   \u2502\n      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      \u2502  PostgreSQL/MySQL       \u2502\n      \u2502  (auth_db)              \u2502\n      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                   \u2502\n      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      \u2502                         \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2510            \u250c\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Sistema A \u2502            \u2502 Sistema B \u2502\n\u2502           \u2502            \u2502           \u2502\n\u2502 Pedidos   \u2502            \u2502 Estoque   \u2502\n\u2502 \u251c\u2500 org    \u2502            \u2502 \u251c\u2500 org    \u2502\n\u2502 \u2514\u2500 user   \u2502            \u2502 \u2514\u2500 user   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n**Fluxo de Autentica\u00e7\u00e3o:**\n\n1. Cliente envia request com token no header\n2. Middleware valida token no banco `auth_db`\n3. Dados do usu\u00e1rio e organiza\u00e7\u00e3o s\u00e3o anexados ao `request`\n4. Sistema cliente acessa dados via ORM (read-only)\n\n---\n\n## \ud83d\udce6 Instala\u00e7\u00e3o\n\n### 1. Instalar a Biblioteca\n\n```bash\n# Via pip (quando publicado)\npip install maquinaweb-shared-auth\n\n# Ou modo desenvolvimento\npip install -e /path/to/maquinaweb-shared-auth\n```\n\n### 2. Adicionar ao requirements.txt\n\n```txt\nDjango>=4.2\ndjangorestframework>=3.14\nmaquinaweb-shared-auth>=0.2.25\n```\n\n---\n\n## \u2699\ufe0f Configura\u00e7\u00e3o\n\n### 1. Settings do Django\n\n```python\n# settings.py\n\nINSTALLED_APPS = [\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'rest_framework',\n    \n    # Adicionar shared_auth\n    'shared_auth',\n    \n    # Suas apps\n    'myapp',\n]\n```\n\n### 2. Configurar Banco de Dados\n\n```python\n# settings.py\n\nDATABASES = {\n    'default': {\n        # Banco do sistema atual\n        'ENGINE': 'django.db.backends.postgresql',\n        'NAME': 'meu_sistema_db',\n        'USER': 'meu_user',\n        'PASSWORD': 'senha',\n        'HOST': 'localhost',\n        'PORT': '5432',\n    },\n    'auth_db': {\n        # Banco centralizado de autentica\u00e7\u00e3o (READ-ONLY)\n        'ENGINE': 'django.db.backends.postgresql',\n        'NAME': 'sistema_auth_db',\n        'USER': 'readonly_user',\n        'PASSWORD': 'senha_readonly',\n        'HOST': 'auth-server.example.com',\n        'PORT': '5432',\n    }\n}\n\n# Router para direcionar queries\nDATABASE_ROUTERS = ['shared_auth.router.SharedAuthRouter']\n```\n\n### 3. Configurar Autentica\u00e7\u00e3o (DRF)\n\n```python\n# settings.py\n\nREST_FRAMEWORK = {\n    'DEFAULT_AUTHENTICATION_CLASSES': [\n        'shared_auth.authentication.SharedTokenAuthentication',\n    ],\n    'DEFAULT_PERMISSION_CLASSES': [\n        'shared_auth.permissions.IsAuthenticated',\n    ],\n}\n```\n\n### 4. Configurar Middleware (Opcional)\n\n```python\n# settings.py\n\nMIDDLEWARE = [\n    'django.middleware.security.SecurityMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    \n    # Middlewares da shared_auth\n    'shared_auth.middleware.SharedAuthMiddleware',\n    'shared_auth.middleware.OrganizationMiddleware',\n    \n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n]\n```\n\n### 5. Configurar Tabelas (Opcional)\n\n```python\n# settings.py\n\n# Customizar nomes das tabelas (se necess\u00e1rio)\nSHARED_AUTH_ORGANIZATION_TABLE = 'organization_organization'\nSHARED_AUTH_USER_TABLE = 'auth_user'\nSHARED_AUTH_MEMBER_TABLE = 'organization_member'\nSHARED_AUTH_TOKEN_TABLE = 'authtoken_token'\n```\n\n### 6. Criar Usu\u00e1rio Read-Only no PostgreSQL\n\n```sql\n-- No servidor de autentica\u00e7\u00e3o\nCREATE USER readonly_user WITH PASSWORD 'senha_segura_aqui';\n\n-- Conceder permiss\u00f5es\nGRANT CONNECT ON DATABASE sistema_auth_db TO readonly_user;\nGRANT USAGE ON SCHEMA public TO readonly_user;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;\n\n-- Para tabelas futuras\nALTER DEFAULT PRIVILEGES IN SCHEMA public \nGRANT SELECT ON TABLES TO readonly_user;\n\n-- Garantir read-only\nALTER USER readonly_user SET default_transaction_read_only = on;\n```\n\n---\n\n## \ud83d\ude80 Uso B\u00e1sico\n\n### 1. Models com Mixins\n\n```python\n# myapp/models.py\nfrom django.db import models\nfrom shared_auth.mixins import OrganizationUserMixin, TimestampedMixin\nfrom shared_auth.managers import BaseAuthManager\n\nclass Pedido(OrganizationUserMixin, TimestampedMixin):\n    \"\"\"Model que pertence a organiza\u00e7\u00e3o e usu\u00e1rio\"\"\"\n    \n    numero = models.CharField(max_length=20, unique=True)\n    valor_total = models.DecimalField(max_digits=10, decimal_places=2)\n    status = models.CharField(max_length=20, default='pending')\n    \n    objects = BaseAuthManager()\n    \n    def __str__(self):\n        return f\"Pedido {self.numero}\"\n```\n\n**O que voc\u00ea ganha automaticamente:**\n- \u2705 Campos: `organization_id`, `user_id`, `created_at`, `updated_at`\n- \u2705 Properties: `organization`, `user`, `organization_name`, `user_email`\n- \u2705 M\u00e9todos: `validate_user_belongs_to_organization()`, `user_can_access()`\n\n### 2. Serializers com Dados Aninhados\n\n```python\n# myapp/serializers.py\nfrom rest_framework import serializers\nfrom shared_auth.serializers import OrganizationUserSerializerMixin\nfrom .models import Pedido\n\nclass PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):\n    class Meta:\n        model = Pedido\n        fields = [\n            'id', 'numero', 'valor_total', 'status',\n            'organization',  # Objeto completo\n            'user',          # Objeto completo\n            'created_at',\n        ]\n        read_only_fields = ['organization', 'user', 'created_at']\n```\n\n**Response JSON:**\n```json\n{\n  \"id\": 1,\n  \"numero\": \"PED-001\",\n  \"valor_total\": \"1500.00\",\n  \"status\": \"pending\",\n  \"organization\": {\n    \"id\": 123,\n    \"name\": \"Empresa XYZ Ltda\",\n    \"fantasy_name\": \"XYZ\",\n    \"cnpj\": \"12.345.678/0001-90\",\n    \"email\": \"contato@xyz.com\",\n    \"is_active\": true\n  },\n  \"user\": {\n    \"id\": 456,\n    \"username\": \"joao.silva\",\n    \"email\": \"joao@xyz.com\",\n    \"full_name\": \"Jo\u00e3o Silva\",\n    \"is_active\": true\n  },\n  \"created_at\": \"2025-10-01T10:00:00Z\"\n}\n```\n\n### 3. ViewSets com Organiza\u00e7\u00e3o\n\n```python\n# myapp/views.py\nfrom rest_framework import viewsets\nfrom shared_auth.mixins import LoggedOrganizationMixin\nfrom shared_auth.permissions import HasActiveOrganization, IsSameOrganization\nfrom .models import Pedido\nfrom .serializers import PedidoSerializer\n\nclass PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):\n    \"\"\"\n    ViewSet que filtra automaticamente por organiza\u00e7\u00e3o logada\n    \"\"\"\n    serializer_class = PedidoSerializer\n    permission_classes = [HasActiveOrganization, IsSameOrganization]\n    \n    # get_queryset() j\u00e1 filtra por organization_id automaticamente\n    # perform_create() j\u00e1 adiciona organization_id automaticamente\n```\n\n### 4. Acessar Dados Compartilhados\n\n```python\n# Em qualquer lugar do c\u00f3digo\nfrom shared_auth.models import SharedOrganization, User, SharedMember\n\n# Buscar organiza\u00e7\u00e3o\norg = SharedOrganization.objects.get_or_fail(123)\nprint(org.name)  # \"Empresa XYZ\"\nprint(org.members)  # QuerySet de membros\n\n# Buscar usu\u00e1rio\nuser = User.objects.get_or_fail(456)\nprint(user.email)  # \"joao@xyz.com\"\nprint(user.organizations)  # Organiza\u00e7\u00f5es do usu\u00e1rio\n\n# Verificar membership\nmember = SharedMember.objects.filter(\n    user_id=456,\n    organization_id=123\n).first()\n\nif member:\n    print(f\"{member.user.email} \u00e9 membro de {member.organization.name}\")\n```\n\n---\n\n## \ud83d\udcda Guias Avan\u00e7ados\n\n### Mixins para Models\n\n#### 1. OrganizationMixin\nPara models que pertencem apenas a uma organiza\u00e7\u00e3o.\n\n```python\nfrom shared_auth.mixins import OrganizationMixin\n\nclass EmpresaConfig(OrganizationMixin):\n    tema_cor = models.CharField(max_length=7, default='#3490dc')\n    logo = models.ImageField(upload_to='logos/')\n    \n# Uso\nconfig = EmpresaConfig.objects.create(organization_id=123, tema_cor='#ff0000')\nprint(config.organization.name)  # Acesso autom\u00e1tico\nprint(config.organization_members)  # Membros da organiza\u00e7\u00e3o\n```\n\n#### 2. UserMixin\nPara models que pertencem apenas a um usu\u00e1rio.\n\n```python\nfrom shared_auth.mixins import UserMixin\n\nclass UserPreferences(UserMixin):\n    theme = models.CharField(max_length=20, default='light')\n    notifications_enabled = models.BooleanField(default=True)\n\n# Uso\nprefs = UserPreferences.objects.create(user_id=456, theme='dark')\nprint(prefs.user.email)\nprint(prefs.user_full_name)\n```\n\n#### 3. OrganizationUserMixin\nPara models que pertencem a organiza\u00e7\u00e3o E usu\u00e1rio (mais comum).\n\n```python\nfrom shared_auth.mixins import OrganizationUserMixin, TimestampedMixin\n\nclass Tarefa(OrganizationUserMixin, TimestampedMixin):\n    titulo = models.CharField(max_length=200)\n    descricao = models.TextField()\n    status = models.CharField(max_length=20, default='pending')\n\n# Uso\ntarefa = Tarefa.objects.create(\n    organization_id=123,\n    user_id=456,\n    titulo='Implementar feature X'\n)\n\n# Valida\u00e7\u00f5es\nif tarefa.validate_user_belongs_to_organization():\n    print(\"\u2713 Usu\u00e1rio pertence \u00e0 organiza\u00e7\u00e3o\")\n\nif tarefa.user_can_access(outro_user_id):\n    print(\"\u2713 Outro usu\u00e1rio pode acessar\")\n```\n\n### Managers Otimizados\n\n```python\nfrom shared_auth.managers import BaseAuthManager\n\nclass Pedido(OrganizationUserMixin):\n    # ...\n    objects = BaseAuthManager()\n\n# Filtrar por organiza\u00e7\u00e3o\npedidos = Pedido.objects.for_organization(123)\n\n# Filtrar por usu\u00e1rio\nmeus_pedidos = Pedido.objects.for_user(456)\n\n# Prefetch autom\u00e1tico (evita N+1)\npedidos = Pedido.objects.with_auth_data()\nfor pedido in pedidos:\n    print(pedido.organization.name)  # Sem query adicional\n    print(pedido.user.email)  # Sem query adicional\n```\n\n### Serializers - Varia\u00e7\u00f5es\n\n#### Vers\u00e3o Completa (Detail)\n```python\nfrom shared_auth.serializers import OrganizationUserSerializerMixin\n\nclass PedidoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):\n    class Meta:\n        model = Pedido\n        fields = ['id', 'numero', 'organization', 'user', 'created_at']\n```\n\n#### Vers\u00e3o Simplificada (List)\n```python\nfrom shared_auth.serializers import (\n    OrganizationSimpleSerializerMixin,\n    UserSimpleSerializerMixin\n)\n\nclass PedidoListSerializer(\n    OrganizationSimpleSerializerMixin,\n    UserSimpleSerializerMixin,\n    serializers.ModelSerializer\n):\n    class Meta:\n        model = Pedido\n        fields = ['id', 'numero', 'organization', 'user']\n    \n# Response com dados reduzidos\n{\n  \"id\": 1,\n  \"numero\": \"PED-001\",\n  \"organization\": {\n    \"id\": 123,\n    \"name\": \"Empresa XYZ\",\n    \"cnpj\": \"12.345.678/0001-90\"\n  },\n  \"user\": {\n    \"id\": 456,\n    \"email\": \"joao@xyz.com\",\n    \"full_name\": \"Jo\u00e3o Silva\"\n  }\n}\n```\n\n#### Customiza\u00e7\u00e3o Avan\u00e7ada\n```python\nclass PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):\n    \n    def get_organization(self, obj):\n        \"\"\"Override para adicionar campos customizados\"\"\"\n        org_data = super().get_organization(obj)\n        \n        if org_data:\n            # Adicionar dados extras\n            org_data['logo_url'] = f\"/logos/{obj.organization_id}.png\"\n            org_data['member_count'] = obj.organization.members.count()\n        \n        return org_data\n```\n\n### Middleware\n\n#### SharedAuthMiddleware\nAutentica usu\u00e1rio baseado no token.\n\n```python\n# settings.py\nMIDDLEWARE = [\n    # ...\n    'shared_auth.middleware.SharedAuthMiddleware',\n]\n```\n\n**Busca token em:**\n- Header: `Authorization: Token <token>`\n- Header: `X-Auth-Token: <token>`\n- Cookie: `auth_token`\n\n**Adiciona ao request:**\n- `request.user` - Objeto User autenticado\n- `request.auth` - Token object\n\n#### OrganizationMiddleware\nAdiciona organiza\u00e7\u00e3o logada ao request.\n\n```python\n# settings.py\nMIDDLEWARE = [\n    'shared_auth.middleware.SharedAuthMiddleware',\n    'shared_auth.middleware.OrganizationMiddleware',  # Depois do Auth\n]\n```\n\n**Busca organiza\u00e7\u00e3o:**\n1. Header `X-Organization: <org_id>`\n2. Primeira organiza\u00e7\u00e3o do usu\u00e1rio autenticado\n\n**Adiciona ao request:**\n- `request.organization_id` - ID da organiza\u00e7\u00e3o\n- `request.organization` - Objeto SharedOrganization\n\n**Uso em views:**\n```python\ndef my_view(request):\n    org_id = request.organization_id\n    org = request.organization\n    \n    if org:\n        print(f\"Organiza\u00e7\u00e3o logada: {org.name}\")\n```\n\n### Permissions\n\n```python\nfrom shared_auth.permissions import (\n    IsAuthenticated,\n    HasActiveOrganization,\n    IsSameOrganization,\n    IsOwnerOrSameOrganization,\n)\n\nclass PedidoViewSet(viewsets.ModelViewSet):\n    permission_classes = [\n        IsAuthenticated,           # Requer autentica\u00e7\u00e3o\n        HasActiveOrganization,     # Requer organiza\u00e7\u00e3o ativa\n        IsSameOrganization,        # Objeto da mesma org\n    ]\n    \n# Ou combina\u00e7\u00f5es\nclass TarefaViewSet(viewsets.ModelViewSet):\n    permission_classes = [IsOwnerOrSameOrganization]\n    # Permite se for dono OU da mesma organiza\u00e7\u00e3o\n```\n\n### Authentication\n\n```python\n# Em qualquer view/viewset DRF\nfrom shared_auth.authentication import SharedTokenAuthentication\n\nclass MyAPIView(APIView):\n    authentication_classes = [SharedTokenAuthentication]\n    \n    def get(self, request):\n        user = request.user  # User autenticado\n        token = request.auth  # SharedToken object\n        \n        return Response({\n            'user': user.email,\n            'token_created': token.created\n        })\n```\n\n---\n\n## \ud83d\udd0d API Reference\n\n### Models\n\n#### SharedOrganization\n\n```python\nfrom shared_auth.models import SharedOrganization\n\n# Campos\norg.id\norg.name\norg.fantasy_name\norg.cnpj\norg.email\norg.telephone\norg.cellphone\norg.image_organization\norg.is_branch\norg.main_organization_id\norg.created_at\norg.updated_at\norg.deleted_at\n\n# Properties\norg.main_organization  # SharedOrganization | None\norg.branches  # QuerySet[SharedOrganization]\norg.members  # QuerySet[SharedMember]\norg.users  # QuerySet[User]\n\n# M\u00e9todos\norg.is_active()  # bool\n```\n\n#### User\n\n```python\nfrom shared_auth.models import User\n\n# Campos (AbstractUser + custom)\nuser.id\nuser.username\nuser.email\nuser.first_name\nuser.last_name\nuser.is_active\nuser.is_staff\nuser.is_superuser\nuser.date_joined\nuser.last_login\nuser.avatar\nuser.createdat\nuser.updatedat\nuser.deleted_at\n\n# Properties\nuser.organizations  # QuerySet[SharedOrganization]\n\n# M\u00e9todos\nuser.get_full_name()  # str\nuser.get_org(organization_id)  # SharedOrganization | raise\n```\n\n#### SharedMember\n\n```python\nfrom shared_auth.models import SharedMember\n\n# Campos\nmember.id\nmember.user_id\nmember.organization_id\nmember.metadata  # JSONField\n\n# Properties\nmember.user  # User\nmember.organization  # SharedOrganization\n```\n\n#### SharedToken\n\n```python\nfrom shared_auth.models import SharedToken\n\n# Campos\ntoken.key  # Primary Key\ntoken.user_id\ntoken.created\n\n# Properties\ntoken.user  # User\n\n# M\u00e9todos\ntoken.is_valid()  # bool\n```\n\n### Managers\n\n#### SharedOrganizationManager\n\n```python\nfrom shared_auth.models import SharedOrganization\n\nSharedOrganization.objects.get_or_fail(123)  # Org | raise OrganizationNotFoundError\nSharedOrganization.objects.active()  # QuerySet (deleted_at is null)\nSharedOrganization.objects.branches()  # QuerySet (is_branch=True)\nSharedOrganization.objects.main_organizations()  # QuerySet (is_branch=False)\nSharedOrganization.objects.by_cnpj('12.345.678/0001-90')  # Org | None\n```\n\n#### UserManager\n\n```python\nfrom shared_auth.models import User\n\nUser.objects.get_or_fail(456)  # User | raise UserNotFoundError\nUser.objects.active()  # QuerySet (is_active=True, deleted_at is null)\nUser.objects.by_email('user@example.com')  # User | None\n```\n\n#### SharedMemberManager\n\n```python\nfrom shared_auth.models import SharedMember\n\nSharedMember.objects.for_user(456)  # QuerySet\nSharedMember.objects.for_organization(123)  # QuerySet\n```\n\n#### BaseAuthManager (para seus models)\n\n```python\n# Quando usa OrganizationMixin\nModel.objects.for_organization(123)  # QuerySet\nModel.objects.for_organizations([123, 456])  # QuerySet\nModel.objects.with_organization_data()  # List com prefetch\n\n# Quando usa UserMixin\nModel.objects.for_user(456)  # QuerySet\nModel.objects.for_users([456, 789])  # QuerySet\nModel.objects.with_user_data()  # List com prefetch\n\n# Quando usa OrganizationUserMixin\nModel.objects.with_auth_data()  # List com prefetch de org e user\nModel.objects.create_with_validation(\n    organization_id=123,\n    user_id=456,\n    **kwargs\n)  # Valida membership antes de criar\n```\n\n### Exceptions\n\n```python\nfrom shared_auth.exceptions import (\n    SharedAuthError,\n    OrganizationNotFoundError,\n    UserNotFoundError,\n    DatabaseConnectionError,\n)\n\ntry:\n    org = SharedOrganization.objects.get_or_fail(999)\nexcept OrganizationNotFoundError as e:\n    print(e)  # \"Organiza\u00e7\u00e3o com ID 999 n\u00e3o encontrada\"\n```\n\n---\n\n## \ud83c\udfaf Casos de Uso Reais\n\n### Sistema de Pedidos Multi-Tenant\n\n```python\n# models.py\nfrom shared_auth.mixins import OrganizationUserMixin, TimestampedMixin\n\nclass Pedido(OrganizationUserMixin, TimestampedMixin):\n    numero = models.CharField(max_length=20, unique=True)\n    valor_total = models.DecimalField(max_digits=10, decimal_places=2)\n    status = models.CharField(max_length=20)\n    \n    objects = BaseAuthManager()\n\nclass ItemPedido(models.Model):\n    pedido = models.ForeignKey(Pedido, related_name='itens')\n    produto = models.CharField(max_length=200)\n    quantidade = models.IntegerField()\n    valor_unitario = models.DecimalField(max_digits=10, decimal_places=2)\n\n# serializers.py\nfrom shared_auth.serializers import OrganizationUserSerializerMixin\n\nclass ItemPedidoSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = ItemPedido\n        fields = ['id', 'produto', 'quantidade', 'valor_unitario']\n\nclass PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):\n    itens = ItemPedidoSerializer(many=True, read_only=True)\n    \n    class Meta:\n        model = Pedido\n        fields = [\n            'id', 'numero', 'valor_total', 'status',\n            'organization', 'user', 'itens', 'created_at'\n        ]\n\n# views.py\nfrom shared_auth.mixins import LoggedOrganizationMixin\nfrom shared_auth.permissions import HasActiveOrganization\n\nclass PedidoViewSet(LoggedOrganizationMixin, viewsets.ModelViewSet):\n    serializer_class = PedidoSerializer\n    permission_classes = [HasActiveOrganization]\n    \n    def get_queryset(self):\n        # J\u00e1 filtra por organization_id automaticamente\n        return super().get_queryset().with_auth_data()\n```\n\n### Sistema de Tarefas com Respons\u00e1veis\n\n```python\n# models.py\nclass Tarefa(OrganizationUserMixin, TimestampedMixin):\n    \"\"\"\n    user_id = criador\n    responsavel_id = quem vai executar\n    \"\"\"\n    titulo = models.CharField(max_length=200)\n    descricao = models.TextField()\n    responsavel_id = models.IntegerField()\n    status = models.CharField(max_length=20, default='pending')\n    \n    objects = BaseAuthManager()\n    \n    @property\n    def responsavel(self):\n        \"\"\"Acessa usu\u00e1rio respons\u00e1vel\"\"\"\n        if not hasattr(self, '_cached_responsavel'):\n            from shared_auth.models import User\n            self._cached_responsavel = User.objects.get_or_fail(self.responsavel_id)\n        return self._cached_responsavel\n\n# serializers.py\nclass TarefaSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):\n    responsavel = serializers.SerializerMethodField()\n    \n    def get_responsavel(self, obj):\n        try:\n            resp = obj.responsavel\n            return {\n                'id': resp.pk,\n                'email': resp.email,\n                'full_name': resp.get_full_name(),\n            }\n        except:\n            return None\n    \n    class Meta:\n        model = Tarefa\n        fields = [\n            'id', 'titulo', 'descricao', 'status',\n            'organization',  # Organiza\u00e7\u00e3o dona\n            'user',  # Criador\n            'responsavel',  # Executor\n            'created_at'\n        ]\n```\n\n---\n\n## \ud83d\udd27 Troubleshooting\n\n### Problema: Queries lentas (N+1)\n\n**Solu\u00e7\u00e3o:** Use os managers com prefetch\n\n```python\n# \u274c Ruim - Causa N+1\npedidos = Pedido.objects.all()\nfor pedido in pedidos:\n    print(pedido.organization.name)  # Query por item!\n\n# \u2705 Bom - 3 queries total\npedidos = Pedido.objects.with_auth_data()\nfor pedido in pedidos:\n    print(pedido.organization.name)  # Sem query adicional\n```\n\n### Problema: OrganizationNotFoundError\n\n**Causa:** ID de organiza\u00e7\u00e3o inv\u00e1lido ou deletada\n\n**Solu\u00e7\u00e3o:**\n```python\n# Usar try/except\ntry:\n    org = SharedOrganization.objects.get_or_fail(org_id)\nexcept OrganizationNotFoundError:\n    # Tratar erro\n    return Response({'error': 'Organiza\u00e7\u00e3o n\u00e3o encontrada'}, status=404)\n\n# Ou usar filter\norg = SharedOrganization.objects.filter(pk=org_id).first()\nif not org:\n    # Tratar\n```\n\n### Problema: Erro de conex\u00e3o com auth_db\n\n**Solu\u00e7\u00e3o:** Verificar configura\u00e7\u00e3o do database router e permiss\u00f5es\n\n```python\n# Testar conex\u00e3o\nfrom django.db import connections\n\nconnection = connections['auth_db']\nwith connection.cursor() as cursor:\n    cursor.execute(\"SELECT 1\")\n    print(\"\u2713 Conex\u00e3o OK\")\n```\n\n---\n\n## \ud83d\udcdd Changelog\n\n### v0.2.25\n- \u2728 Adicionado suporte a imagens (avatar, logo)\n- \u2728 StorageBackend para arquivos compartilhados\n- \ud83d\udc1b Corre\u00e7\u00f5es nos serializers\n- \ud83d\udcda Documenta\u00e7\u00e3o melhorada\n\n### v0.2.0\n- \u2728 Middlewares: SharedAuthMiddleware, OrganizationMiddleware\n- \u2728 Permissions customizadas\n- \u2728 Managers otimizados com prefetch\n- \u2728 Serializer mixins com dados aninhados\n\n### v0.1.0\n- \ud83c\udf89 Vers\u00e3o inicial\n- \u2728 Models compartilhados\n- \u2728 Mixins b\u00e1sicos\n- \u2728 Autentica\u00e7\u00e3o via token\n\n---\n\n## \ud83d\udcc4 Licen\u00e7a\n\nMIT License - veja [LICENSE](LICENSE) para detalhes.\n\n---\n\n## \ud83e\udd1d Contribuindo\n\nContribui\u00e7\u00f5es s\u00e3o bem-vindas! Por favor, abra uma issue ou pull request.\n\n---\n\n## \ud83d\udce7 Suporte\n\nPara suporte, abra uma issue no GitHub ou entre em contato com a equipe Maquinaweb.\n\n---\n\n**Desenvolvido com \u2764\ufe0f por Maquinaweb**\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Models read-only para autentica\u00e7\u00e3o compartilhada entre projetos Django.",
    "version": "0.2.38",
    "project_urls": {
        "Homepage": "https://github.com/maquinaweb/maquinaweb-shared-auth",
        "Issues": "https://github.com/maquinaweb/maquinaweb-shared-auth/issues",
        "Repository": "https://github.com/maquinaweb/maquinaweb-shared-auth"
    },
    "split_keywords": [
        "django",
        " auth",
        " models",
        " shared"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "f41f16aa87542e20c5d42ead718e99e4d42a94820dffcb2fa3492804566eabe7",
                "md5": "b9a67293080ca2e6b7f394676535a283",
                "sha256": "c094803c46525af457f7965c0179bc6f59ef07d93d416e50aae23cb41036391f"
            },
            "downloads": -1,
            "filename": "maquinaweb_shared_auth-0.2.38-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b9a67293080ca2e6b7f394676535a283",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 26677,
            "upload_time": "2025-10-23T13:01:05",
            "upload_time_iso_8601": "2025-10-23T13:01:05.138495Z",
            "url": "https://files.pythonhosted.org/packages/f4/1f/16aa87542e20c5d42ead718e99e4d42a94820dffcb2fa3492804566eabe7/maquinaweb_shared_auth-0.2.38-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d1f2d14e3265a190e87ecb8fad183100c5de8f477a88054cec1c27ef2c573db1",
                "md5": "4cbd2817327115e274027d71bd0cea57",
                "sha256": "af3117994f25b28dec0a6d923717dd570ed0f456d6b4fd72e91f259d3342c8f7"
            },
            "downloads": -1,
            "filename": "maquinaweb_shared_auth-0.2.38.tar.gz",
            "has_sig": false,
            "md5_digest": "4cbd2817327115e274027d71bd0cea57",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 21859,
            "upload_time": "2025-10-23T13:01:06",
            "upload_time_iso_8601": "2025-10-23T13:01:06.339311Z",
            "url": "https://files.pythonhosted.org/packages/d1/f2/d14e3265a190e87ecb8fad183100c5de8f477a88054cec1c27ef2c573db1/maquinaweb_shared_auth-0.2.38.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-23 13:01:06",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "maquinaweb",
    "github_project": "maquinaweb-shared-auth",
    "github_not_found": true,
    "lcname": "maquinaweb-shared-auth"
}
        
Elapsed time: 2.39718s