I have a working multitenant app on Django with isolated databases that currently uses a subdomain for each tenant and threading. The subdomain prefix is used to make a connection to the corresponding database, so:
- client1.mysite.com access the database client1
- client2.mysite.com access the database client2
And so on. This is the current code I have:
middleware:
import threading
from app.utils import tenant_db_from_the_request
Thread_Local = threading.local()
class AppMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
db = tenant_db_from_the_request(request)
setattr(Thread_Local, 'DB', db)
response = self.get_response(request)
return response
def get_current_db_name():
return getattr(Thread_Local, 'DB', None)
utils:
def hostname_from_the_request(request):
return request.get_host().split(':')[0].lower()
def tenant_db_from_the_request(request):
hostname = hostname_from_the_request(request)
tenants_map = get_tenants_map()
return tenants_map.get(hostname)
def get_tenants_map():
return dict(Tenants.objects.values_list('subdomain', 'database',))
routers:
class AppRouter:
def db_for_read(self, model, **hints):
return get_current_db_name()
def db_for_write(self, model, **hints):
return get_current_db_name()
def allow_relation(self, *args, **kwargs):
return True
def allow_syncdb(self, *args, **kwargs):
return None
def allow_migrate(self, *args, **kwargs):
return None
middleware section on settings.py:
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'app.middleware.AppMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Using subdomains is not exactly ideal, so I’m trying to switch the subdomain prefix to a suffix that will be added to the username of each user on the tenant. With that, every user will access the same URL:
mysite.com, for example
And the suffix will work like that:
- user@client1 access the database client1
- user@client2 access the database client2
Since I already have this working with subdomains, my thought process was to change just the middleware to retrieve the suffix of the username and use session to store the tenant alias that will be used on each database request. With that in mind, I changed just my utils file to this:
def hostname_from_the_request(request):
# changed the request to get the 'db_alias' from the session
return request.session.get('db_alias', None)
def get_tenants_map():
# change the mapping to retrieve the database based on an alias (which is a column on the tenants table), instead of subdomain
return dict(Tenants.objects.values_list('alias', 'database',))
And added these 3 lines of code before the authentication process in my login view:
if request.method == "POST":
if form.is_valid():
# retrieve the alias from the username
complete_username = form.cleaned_data.get('username')
db_alias = complete_username.split('@')[1]
request.session['db_alias'] = db_alias
# from here on, the code is unchanged
user = authenticate(
username=form.cleaned_data.get('username'),
password=form.cleaned_data.get('password'),
)
# rest of the code
This works, but only after the second login attempt. The first one always return a failed authentication error. With print statements and breakpoints I confirmed that the session is being created before the authentication and the name of the database is being retrieved correctly, so after the second login attempt, the login is successful and each request is processed on the corresponding database.
I’ve tried to use the session data directly instead of threading, but the problem with that is that the router doesn’t accept a request parameter, or at least it is what I understood from the docs, but I could be wrong.
Maybe I’m missing something obvious here, but I don’t understand why this is not working on the first login attempt. I appreciate any help.
Thanks!