Skip to content

Commit

Permalink
Add subdomain middleware (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
danniel authored Dec 27, 2023
1 parent dcc5d59 commit ea5756a
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

DEBUG=True
ALLOWED_HOSTS=localhost
APEX_DOMAIN=redirectioneaza.ro
SECRET_KEY="replace-this-example-key"
SENTRY_DSN=""

Expand Down
Empty file added backend/partners/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions backend/partners/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin

from .models import Partner


@admin.register(Partner)
class PartnerAdmin(admin.ModelAdmin):
pass
6 changes: 6 additions & 0 deletions backend/partners/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class PartnersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "partners"
66 changes: 66 additions & 0 deletions backend/partners/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from django.conf import settings
from django.http import Http404

from .models import Partner


class InvalidSubdomain(Exception):
pass


class PartnerDomainMiddleware:
"""
Add the `request.partner` property based on the requested subdomain
"""

@staticmethod
def extract_subdomain(host: str, apex: str) -> str:
apex = apex.strip().lower()
dot_apex = "." + apex
host = host.strip().lower()

# Drop the port number (if present)
host = host.split(":", maxsplit=1)[0]

# Drop the www. prefix (if present)
if host[:4] == "www.":
host = host[4:]

# Extract the subdomain name
if host == apex:
subdomain = ""
elif host.endswith(dot_apex):
subdomain = host.split(dot_apex, maxsplit=1)[0]
else:
raise InvalidSubdomain

return subdomain

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
try:
subdomain = PartnerDomainMiddleware.extract_subdomain(request.get_host(), settings.APEX_DOMAIN)
except InvalidSubdomain:
raise Http404

if not subdomain:
partner = None
else:
try:
partner = Partner.objects.get(subdomain=subdomain)
except Partner.DoesNotExist:
partner = None

request.partner = partner

# Code to be executed for each request before
# the view (and later middleware) are called.

response = self.get_response(request)

# Code to be executed for each request/response after
# the view is called.

return response
35 changes: 35 additions & 0 deletions backend/partners/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.8 on 2023-12-27 14:48

from django.db import migrations, models
import django.db.models.functions.text


class Migration(migrations.Migration):
initial = True

dependencies = [
("donations", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="Partner",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(blank=True, db_index=True, max_length=100, verbose_name="name")),
("subdomain", models.CharField(max_length=100, unique=True, verbose_name="subdomain")),
("is_active", models.BooleanField(db_index=True, default=True, verbose_name="is active")),
("ngos", models.ManyToManyField(to="donations.ngo", verbose_name="NGOs")),
],
options={
"verbose_name": "Partner",
"verbose_name_plural": "Partners",
},
),
migrations.AddConstraint(
model_name="partner",
constraint=models.UniqueConstraint(
django.db.models.functions.text.Lower("subdomain"), name="subdomain_unique"
),
),
]
18 changes: 18 additions & 0 deletions backend/partners/migrations/0002_alter_partner_ngos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.8 on 2023-12-27 15:02

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("donations", "0001_initial"),
("partners", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="partner",
name="ngos",
field=models.ManyToManyField(blank=True, to="donations.ngo", verbose_name="NGOs"),
),
]
Empty file.
22 changes: 22 additions & 0 deletions backend/partners/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models
from django.db.models.functions import Lower
from django.utils.translation import gettext_lazy as _

from donations.models import Ngo


class Partner(models.Model):
name = models.CharField(verbose_name=_("name"), max_length=100, blank=True, null=False, db_index=True)
subdomain = models.CharField(verbose_name=_("subdomain"), max_length=100, blank=False, null=False, unique=True)
is_active = models.BooleanField(verbose_name=_("is active"), db_index=True, default=True)
ngos = models.ManyToManyField(verbose_name=_("NGOs"), to=Ngo, blank=True)

class Meta:
verbose_name = _("Partner")
verbose_name_plural = _("Partners")
constraints = [
models.UniqueConstraint(Lower("subdomain"), name="subdomain_unique"),
]

def __str__(self):
return f"{self.name}"
24 changes: 24 additions & 0 deletions backend/partners/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.test import TestCase

from .middleware import PartnerDomainMiddleware, InvalidSubdomain


class PartnerDomainMiddlewareTestCase(TestCase):
def setUp(self):
self.apex = "example.com"

def test_subdomain(self):
# Test standard subdomain extraction
subdom1 = PartnerDomainMiddleware.extract_subdomain("test1.example.com", self.apex)
self.assertEqual(subdom1, "test1")

# Test various capitalization subdomain extraction
subdom2 = PartnerDomainMiddleware.extract_subdomain("TesT1.exaMpLe.com", self.apex)
self.assertEqual(subdom2, "test1")

# Test apex domain
subdom3 = PartnerDomainMiddleware.extract_subdomain("exAmple.com", self.apex)
self.assertEqual(subdom3, "")

# Test invalid subdomain
self.assertRaises(InvalidSubdomain, PartnerDomainMiddleware.extract_subdomain, "test1.example.ORG", self.apex)
1 change: 1 addition & 0 deletions backend/partners/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your views here.
6 changes: 4 additions & 2 deletions backend/redirectioneaza/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
DATABASE_HOST=(str, "localhost"),
DATABASE_PORT=(str, "3306"),
# site settings
APEX_DOMAIN=(str, "redirectioneaza.ro"),
SITE_TITLE=(str, "redirectioneaza 3,5%"),
DONATIONS_LIMIT_DATE=(str, "2016-05-25"),
DONATIONS_LIMIT_TO_CURRENT_YEAR=(bool, True),
Expand Down Expand Up @@ -94,9 +95,8 @@
DJANGO_ADMIN_PASSWORD = env.str("DJANGO_ADMIN_PASSWORD", None)
DJANGO_ADMIN_EMAIL = env.str("DJANGO_ADMIN_EMAIL", None)

# Security settings

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
APEX_DOMAIN = env.str("APEX_DOMAIN")

CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN"
CSRF_COOKIE_NAME = "XSRF-TOKEN"
Expand Down Expand Up @@ -150,6 +150,7 @@
"django_q",
# custom apps:
"donations",
"partners",
"users",
]

Expand All @@ -161,6 +162,7 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"partners.middleware.PartnerDomainMiddleware",
]

ROOT_URLCONF = "redirectioneaza.urls"
Expand Down

0 comments on commit ea5756a

Please sign in to comment.