I have a problem with django-tenants. I am still learning to program, so it is possible that I have made a beginner’s mistake somewhere.
I will explain what I am trying to achieve with a model example. Some procedures are quite awkward and serve mainly to identify the problem.
The problem is that the middleware likely does not switch the tenant. Specifically, I would expect that if /prefix/domain_idorsubfolder_id is in the URL, the middleware would automatically detect the prefix, subfolder ID, and set the corresponding schema as active. However, this is not happening, and the login to the tenant does not occur. Instead, the login happens in the “public” schema in the database.
Model example:
A user goes to http://127.0.0.1:8000/login/ and enters their email, which filters the appropriate tenant and redirects the user to /client/tenant_id/tenant/login.
Page not found (404)
Request Method: GET
Request URL: http://127.0.0.1:8000/client/test2f0d3775/tenant/login/
Using the URLconf defined in seo_app.tenant_urls_dynamically_tenant_prefixed, Django tried these URL patterns, in this order:
client/test2f0d3775/ basic-keyword-cleaning/ [name='basic_cleaned_keyword']
client/test2f0d3775/ ai-keyword-cleaning/ [name='auto_cleaned_keyword']
client/test2f0d3775/ keyword-group-creation/ [name='content_group']
client/test2f0d3775/ looking-for-link-oppurtunities/ [name='search_linkbuilding']
client/test2f0d3775/ url-pairing/ [name='url_pairing']
client/test2f0d3775/ creating-an-outline/ [name='article_outline']
client/test2f0d3775/ analyses/ [name='all_analyses']
client/test2f0d3775/ download/<str:model_type>/<int:file_id>/ [name='download_file']
client/test2f0d3775/ dashboard/ [name='dashboard']
client/test2f0d3775/ client/
The current path, client/test2f0d3775/tenant/login/, didn’t match any of these.
Notice that the authorization_app is not listed among the installed apps at all, which is strange because I reference it in urls.py, tenant_urls.py, and in the authorization_app’s own urls.py.
urls.py – public URLs in root_django_project
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("authorization_app.urls")),
path('', include('seo_app.tenant_urls')),
# path("client/", include('seo_app.tenant_urls_dynamically_tenant_prefixed')),
# path(r"", include("keyword_analysis_app.urls")),
# path(r"", include("linkbuilding_app.urls")),
# path(r"", include("content_app.urls")),
# path(r"", include("downloading_reports_app.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
tenant_urls.py – root_django_project
from django.urls import path, include
from django.conf import settings
app_name = "tenant"
urlpatterns = [
path(f"", include("keyword_analysis_app.urls")),
path(f"", include("linkbuilding_app.urls")),
path(f"", include("content_app.urls")),
path(f'', include("downloading_reports_app.urls")),
path(f'{settings.TENANT_SUBFOLDER_PREFIX}/', include("authorization_app.urls")),
# path("", include("keyword_analysis_app.urls")),
# path("", include("linkbuilding_app.urls")),
# path("", include("content_app.urls")),
# path('', include("downloading_reports_app.urls")),
# path('', include("authorization_app.urls")),
]
urls.py – authorization_app
from django.urls import path
from . import views
from django.contrib.auth.views import LogoutView
from .views import CustomLoginView, redirect_tenant_login
app_name = "authorization_app"
urlpatterns = [
path("register/", views.user_register, name="register"),
path("login/", views.redirect_tenant_login, name="redirect_tenant"),
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
path("<str:subfolder>/tenant/login/", CustomLoginView.as_view(), name="login_tenant"),
]
views.py – authorization_app
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.views import LoginView
from .forms import UserRegisterForm, RedirectTenantForm
from tenants_app.models import TenantModel, DomainModel
from django.db import transaction
from django.contrib.auth.decorators import login_required
from .models import UserModel
import uuid
import urllib.parse
import re
from seo_app import settings
import logging
from django_tenants.utils import schema_context
from django.contrib.auth.models import Group, Permission
from tenant_users.permissions.models import UserTenantPermissions
from django.db import connection
@transaction.atomic
def user_register(request):
if request.method == "POST":
form = UserRegisterForm(request.POST)
if form.is_valid():
new_user = form.save(commit=False) # Save new user to the database. Then add them as a foreign key to owner_id
new_user.is_active = True
new_user.save()
subdomain_username = form.cleaned_data.get("username").lower()
subdomain_username = re.sub(r'[^a-zA-Z0-9]', '', subdomain_username) # This regex cleans the user input for use in subdomain
unique_id = str(uuid.uuid4()).replace('-', '').lower()[:7]
subdomain_username = f"{subdomain_username}{unique_id}"
# Create tenant instance
new_user_tenant = TenantModel()
new_user_tenant.owner_id = new_user.id
new_user_tenant.name = subdomain_username
new_user_tenant.schema_name = subdomain_username
new_user_tenant.domain_subfolder = subdomain_username
new_user_tenant.save()
# Set domain instance for User
new_user_subdomain = DomainModel()
new_user_subdomain.domain = f"{subdomain_username}"
new_user_subdomain.tenant = new_user_tenant
new_user_subdomain.is_primary = True
new_user_subdomain.save()
return redirect("/")
else:
form = UserRegisterForm()
return render(request, "register_template.html", {"form": form})
def redirect_tenant_login(request):
if request.method == "POST":
form = RedirectTenantForm(request.POST)
if form.is_valid():
email = form.cleaned_data["tenant_email"]
user_instance = UserModel.objects.get(email=email)
tenant_instance = TenantModel.objects.get(owner_id=user_instance.id)
subdomain = tenant_instance.schema_name
print(f"User ID: {subdomain}")
return redirect(f"/client/{subdomain}/tenant/login/")
else:
form = RedirectTenantForm()
return render(request, "registration/redirect_tenant.html", {"form": form})
class CustomLoginView(LoginView): # Encode using urllib.parse
def get_success_url(self):
user = self.request.user
print(user)
try:
usermodel = UserModel.objects.get(email=user.email)
print(usermodel)
tenant_model = TenantModel.objects.get(owner_id=usermodel.id)
print(tenant_model)
tenant_name = tenant_model.schema_name
print(tenant_name)
tenant_name_normalized = urllib.parse.quote(tenant_name.lower()) # Safe encoding for URL
print(tenant_name_normalized)
return f'/client/{tenant_name_normalized}/dashboard/'
except UserModel.DoesNotExist:
print("Error 1")
return "/"
except AttributeError:
print("Error 2")
# Added to catch errors when 'tenant' does not exist for the given user
return "/"
def form_valid(self, form):
super().form_valid(form) # Standard login and get user
url = self.get_success_url()
return redirect(url)
At this URL http://127.0.0.1:8000/client/test2f0d3775/tenant/login/, I activated the Python manage.py shell and tested the connection, which returned the following:
>>> from django.db import connection
>>> print(connection.schema_name)
public
django – settings.py
from django.core.management.utils import get_random_secret_key
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = True
ALLOWED_HOSTS = ['localhost', '.localhost', '127.0.0.1', '.127.0.0.1', '*']
# Tenant Application definition
SHARED_APPS = (
'django_tenants',
'django.contrib.contenttypes',
"tenant_users.permissions",
"tenant_users.tenants",
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin',
'django.contrib.staticfiles',
'django.contrib.auth',
'authorization_app',
'tenants_app',
# everything below here is optional
)
TENANT_APPS = (
'django.contrib.auth',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.contenttypes',
"tenant_users.permissions",
'keyword_analysis_app',
'linkbuilding_app',
'content_app',
'downloading_reports_app',
'authorization_app',
)
TENANT_MODEL = "tenants_app.TenantModel" # app.Model
TENANT_DOMAIN_MODEL = "tenants_app.DomainModel" # app.Model
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
DEFAULT_SCHEMA_NAME = "public"
PUBLIC_SCHEMA_URLCONF = 'seo_app.urls' # points to public tenants in seo_app urls.py
# TENANT_URLCONF = 'seo_app.tenant_urls' # points to private tenants in seo_app urls.py
ROOT_URLCONF = 'seo_app.tenant_urls'
AUTH_USER_MODEL = "authorization_app.UserModel"
# Switch to authentication backend django-tenants-user
AUTHENTICATION_BACKENDS = (
"tenant_users.permissions.backend.UserBackend",
)
TENANT_USERS_DOMAIN = "127.0.0.1"
MIDDLEWARE = [
# 'seo_app.middleware.TenantSubfolderMiddleware',
'django_tenants.middleware.TenantSubfolderMiddleware', # Set tenants - subfolder
# 'django_tenants.middleware.main.TenantMainMiddleware', # Set tenants - subdomains
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
INTERNAL_IPS = [
'127.0.0.1',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request', # DJANGO TENANTS
'django.template.context_processors.debug',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'seo_app.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend', # TENANTS CONFIGURATION
'NAME': 'xxxxxx', # Name of your database
'USER': 'lukas_db', # Database user
'PASSWORD': 'xxxxxxxxx', # User password
'HOST': '127.0.0.1', # Server address (or IP address)
'PORT': '5432', # Port on which PostgreSQL is running (5432)
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'cs'
LOGOUT_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL = '/dashboard/'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# settings.py
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'
# asynchronous processing
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' # Uses Redis on localhost
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Prague'
CELERY_WORKER_POOL = 'prefork'
CELERY_WORKER_POOL = 'solo'
STATIC_URL = "/static/"
STATICFILES_DIRS = [
BASE_DIR / 'static',
# Add more paths if needed
]
# STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SESSION_COOKIE_DOMAIN = '127.0.0.1'
SESSION_COOKIE_NAME = 'sessionid_tenant'
CSRF_COOKIE_DOMAIN = "127.0.0.1"
SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
# TENANTS CONFIGURATION
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
TENANT_SUBFOLDER_PREFIX = "client"
Thank you very much for any advice you can offer. I’ve been working on this for several days without any success. Unfortunately, I couldn’t find any guidance in the documentation on how to implement a login system where the user can log in easily and then be redirected to their own schema, where they would use only their own database.
I tried writing new middleware.
I tried changing URL paths.
I tried using subdomain-based django-tenant. Unfortunately, there were also issues with cookies being lost. On MacOS, I couldn’t configure this in /etc/hosts. Authentication always occurs in the public schema, and when redirecting to the subdomain, all cookies are lost.
Several methods of logging and rewriting views. First, authentication and then redirection took place. In another attempt, I set the context using with schema_context. The latest approach, which you can see in the shared code, was supposed to identify the tenant based on the provided email, then find the corresponding subfolder in the database and create a path with /prefix/subfolder/tenant/login. The login to the tenant was supposed to happen on this page, but it doesn’t.
I also tried various configurations in settings.py, as it seems that some crucial information is missing from the documentation.
Thank you for any advice.
user25066324 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.