Przejdź do treści

🏢 Multi-tenancy - Architektura Wielodostępowa

Panel Księgowy używa virtual tenant isolation - wszystkie dane są w jednej bazie PostgreSQL, ale logicznie izolowane przez filtrowanie po team_id.


🎯 Koncepcja

Virtual Tenant Isolation

Zamiast osobnych baz danych dla każdego zespołu, używamy jednej bazy danych z filtrowaniem po team_id.

Zalety: - ✅ Prostsze zarządzanie (jedna baza) - ✅ Łatwiejsze backup i restore - ✅ Lepsze wykorzystanie zasobów - ✅ Prostsze skalowanie - ✅ Łatwiejsze migracje

Wady: - ⚠️ Wymaga ścisłego filtrowania (ale BaseTeamModel to zapewnia) - ⚠️ Potencjalnie większa baza (ale PostgreSQL to obsługuje)


🔐 Bezpieczeństwo

BaseTeamModel

Wszystkie modele biznesowe dziedziczą po BaseTeamModel:

from apps.teams.models import BaseTeamModel

class Client(BaseTeamModel):
    name = models.CharField(max_length=200)
    nip = models.CharField(max_length=10)
    # team field jest automatycznie dodany

BaseTeamModel zapewnia: - Automatyczne pole team (ForeignKey) - Wymuszenie filtrowania po team - Izolację danych

Filtrowanie zapytań

✅ ZAWSZE:

# Poprawne - filtrowanie po team
clients = Client.objects.filter(team=request.team)

❌ NIGDY:

# BŁĄD - brak filtrowania po team!
clients = Client.objects.all()  # DANGEROUS!

Decorators

@login_and_team_required
def client_list(request, team_slug):
    """Automatycznie sprawdza dostęp do team."""
    team = request.team  # Automatycznie dodane
    clients = Client.objects.filter(team=team)
    # ...

🏗️ Implementacja

Team Model

class Team(SubscriptionModelBase, BaseModel):
    """Team (virtual tenant) model."""

    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    nip = models.CharField(max_length=10)
    regon = models.CharField(max_length=14, blank=True)
    # ...

Membership

class Membership(BaseModel):
    """Członkostwo użytkownika w zespole."""

    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
    role = models.CharField(max_length=20, choices=ROLE_CHOICES)
    # ...

BaseTeamModel

class BaseTeamModel(BaseModel):
    """Abstract model for objects that are part of a team."""

    team = models.ForeignKey("teams.Team", on_delete=models.CASCADE)

    class Meta:
        abstract = True

🔄 URL Structure

Team-based URLs

Wszystkie URL-e biznesowe są w kontekście zespołu:

/a/<team_slug>/<module>/<action>/

Przykłady: - /a/my-team/clients/ - Lista klientów - /a/my-team/clients/123/ - Szczegóły klienta - /a/my-team/invoicing/ - Faktury

URL Patterns

# urls.py
app_name = 'crm'

team_urlpatterns = [
    path('', views.client_list, name='client_list'),
    path('<int:pk>/', views.client_detail, name='client_detail'),
]

🛡️ Best Practices

1. Zawsze filtruj po team

# ✅ DOBRZE
clients = Client.objects.filter(team=request.team)

# ❌ ŹLE
clients = Client.objects.all()

2. Używaj BaseTeamModel

# ✅ DOBRZE
class Client(BaseTeamModel):
    name = models.CharField(max_length=200)

# ❌ ŹLE
class Client(models.Model):
    team = models.ForeignKey(Team, on_delete=models.CASCADE)
    name = models.CharField(max_length=200)

3. Używaj decorators

# ✅ DOBRZE
@login_and_team_required
def client_list(request, team_slug):
    # ...

# ❌ ŹLE
def client_list(request, team_slug):
    # Brak sprawdzania dostępu!

4. Optymalizuj zapytania

# ✅ DOBRZE - select_related dla FK
clients = Client.objects.filter(team=team).select_related('created_by')

# ✅ DOBRZE - prefetch_related dla M2M
clients = Client.objects.filter(team=team).prefetch_related('contacts')

📊 Database Indexes

Wymagane indeksy

Wszystkie tabele z team_id powinny mieć indeks:

class Meta:
    indexes = [
        models.Index(fields=['team', '-created_at']),
        models.Index(fields=['team', 'name']),
    ]

🧪 Testowanie

Testy izolacji

def test_team_isolation(client):
    """Test czy dane są izolowane między teamami."""
    team1 = Team.objects.create(name="Team 1", slug="team1")
    team2 = Team.objects.create(name="Team 2", slug="team2")

    client1 = Client.objects.create(team=team1, name="Client 1")
    client2 = Client.objects.create(team=team2, name="Client 2")

    # Team 1 nie powinien widzieć klientów Team 2
    clients_team1 = Client.objects.filter(team=team1)
    assert client1 in clients_team1
    assert client2 not in clients_team1

📚 Więcej informacji


Ostatnia aktualizacja: 2025-11-29
Wersja dokumentacji: 1.0