pythonreactjsdjangodjango-rest-frameworkdjango-tenants

How can I get Django Rest Framework to work with Django Tenants and React?


Here is my setup:

settings.py

SHARED_APPS = (
    'django_tenants',
    'main',
    other apps...
)

TENANT_APPS = (
    'rest_framework',
    'company',
)

MIDDLEWARE = [
    'django_tenants.middleware.main.TenantMainMiddleware',
    other middleware...
]

DATABASE_ROUTERS = (
    'django_tenants.routers.TenantSyncRouter',
)

urls.py

from django.urls import include, path
from rest_framework import routers

# other imports

from main.api.v1 import projects

router = routers.DefaultRouter()
router.register(r'api/v1/project', projects.ProjectViewSet)

urlpatterns = [
    -- other paths --
    path('', include(router.urls)),
]

api/v1/project.py

# other imports
from company.models import Project

from rest_framework import serializers
from rest_framework import viewsets
from rest_framework import permissions

class ProjectSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Project
        fields = ['url', 'name', 'keycode']

class ProjectViewSet(viewsets.ModelViewSet):
    queryset = Project.objects.all().order_by('id')
    serializer_class = ProjectSerializer
    permission_classes = [permissions.AllowAny]

main.models.py

from django.contrib.auth.models import User as AuthUser
from django_tenants.models import TenantMixin, DomainMixin

# note, definition of custom "User" model which has an AuthUser 1 to 1 relationship

class Company(TenantMixin):
    name = models.CharField(max_length=100)
    subdomain = models.CharField(max_length=32)
    employees = models.ManyToManyField(User, related_name='companies')
    migration_id = models.IntegerField(null=True)

class Domain(DomainMixin):
    pass

company.models.py

from django.db import models

class Project(models.Model):
    name = models.CharField(max_length=100)
    keycode = models.CharField(max_length=8)

And the final detail is that I am not using a Django frontend but rather one created in React. The request which is going to the backend is just a standard request however it is coming from a subdomain and includes a JWT token (but I don't know if that is relevant), here is a shortened version of the request headers:

Request URL: http://localhost:8000/api/v1/project/
Request Method: GET
Authorization: Bearer <token here>
Origin: http://cbd.localhost:3000

The error that I am seeing on the backend is this:

relation "company_project" does not exist

My guess is that this is happening due to the fact that when this query is created in the ProjectViewSet:

queryset = Project.objects.all().order_by('id')

The request is not done within the context of a tenant. But my question is how exactly would that be done. I see in Django Tenants that there is a construct that looks like this:

with tenant_context(tenant):
    # All commands here are ran under the schema from the `tenant` object

But I have no idea how I would go about getting the 'tenant' parameter in the class definition where it seems to be needed.

Any ideas?


Solution

  • Well, I figured out one way to do it, but it's ugly.

    in my settings.py file I added a new constant:

    BASE_TENANT = "cbd_co"
    

    I set this to my first tenant (in my system there will be a default tenant schema which is not actually owned by a client but is a sort of template)

    Then, in my model viewset declaration i did this:

    class ProjectViewSet(viewsets.ModelViewSet):
        queryset = Project.objects.raw('SELECT * FROM "' + settings.BASE_TENANT + '"."company_project";')
        serializer_class = ProjectSerializer
        permission_classes = [permissions.AllowAny]
    
        def get_queryset(self):
            schema = self.request.META['HTTP_ORIGIN'].split("//")[1].split('.')[0] + '_co'
            qs = Project.objects.raw('SELECT * FROM "' + schema + '"."company_project" ORDER BY "company_project"."id" ASC;')
            return qs
    

    I'm still hoping someone suggests a better solution though...

    Notes on this solution. As it is, it is completely insecure. What I will actually have to do is get the authorization header from the request, verify the JWT token, get the user, get all the valid user companies (which also determines the valid subdomains and therefore the valid schemas. and only then would I return the queryset.