Skip to Content
CoreMiddleware

Middleware System

Django-CFG ships a suite of middleware classes covering authentication enhancement, real IP detection, timezone handling, user activity tracking, pagination, connection pool management, public endpoint bypass, admin security notifications, and development debugging.

Overview

ClassModuleActive When
JWTAuthenticationWithLastLoginauthenticationAlways (JWT auth enabled)
RealIPMiddlewarereal_ipAlways (must be first)
TimezoneMiddlewaretimezoneAlways
UserActivityMiddlewareuser_activityAlways (accounts app active)
PublicEndpointsMiddlewarepublic_endpointsAlways
ConnectionPoolCleanupMiddlewarepool_cleanupAlways (must be last, WSGI)
AsyncConnectionPoolCleanupMiddlewarepool_cleanupAlways (must be last, ASGI)
RouteDebugMiddlewareroute_debugDEBUG=True only
DefaultPagination / variantspaginationPer-view configuration
extract_paginated_components hookpagination_schema_hookOpenAPI schema generation
Admin notifications (signals)admin_notificationsAlways (Telegram configured)

JWTAuthenticationWithLastLogin

Purpose: Extends rest_framework_simplejwt.authentication.JWTAuthentication to automatically update last_login on every successful JWT authentication, and validates that the user account is still active.

Active when: JWT authentication is enabled (set as default authentication class).

Extends standard JWT authentication with:

  • Checks user.is_active on every token validation — deactivated accounts cannot use existing tokens.
  • Updates last_login via a direct UPDATE query (no signals, no save() overhead).
  • Throttles DB writes with an in-process cache: updates only if last_login is None or older than UPDATE_INTERVAL (default 5 minutes).
  • Auto-cleans the cache when it exceeds MAX_CACHE_SIZE (1000 entries) to prevent memory leaks.

Also patches DRF Request._authenticate at import time so that non-JWT backends receiving a JWT Bearer token skip gracefully instead of raising AuthenticationFailed, allowing mixed authentication setups without changing each backend.

# Add to REST_FRAMEWORK settings REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'django_cfg.middleware.authentication.JWTAuthenticationWithLastLogin', ] }
# Optional: explicit mixin for API-key backends (not required when using django_cfg) from django_cfg.middleware.authentication import JWTAwareAuthenticationMixin class MyAPIKeyBackend(JWTAwareAuthenticationMixin, BaseAuthentication): """Skips authentication when request carries a JWT Bearer token.""" ...

Cache statistics for monitoring:

from django_cfg.middleware.authentication import JWTAuthenticationWithLastLogin stats = JWTAuthenticationWithLastLogin.get_cache_stats() # { # 'cache_size': 42, # 'max_cache_size': 1000, # 'update_interval_seconds': 300, # 'update_interval_minutes': 5.0, # }

RealIPMiddleware

Purpose: Overwrites REMOTE_ADDR with the real client IP extracted from proxy headers so that all downstream code sees the correct IP.

Active when: Always. Must be placed first in MIDDLEWARE, before any security or auth middleware.

Headers are checked in priority order:

PriorityHeaderProxy
1CF-Connecting-IPCloudflare
2X-Forwarded-Fornginx, traefik, AWS ALB
3X-Real-IPnginx alternative

For X-Forwarded-For with a chain of IPs (client, proxy1, proxy2), the first IP (original client) is used.

MIDDLEWARE = [ 'django_cfg.middleware.real_ip.RealIPMiddleware', # Must be first 'django.middleware.security.SecurityMiddleware', # ... rest of middleware ]

Without this middleware, all downstream code (django-axes, DRF throttling, rate limiting, audit logging) sees the proxy IP instead of the real client address.


TimezoneMiddleware

Purpose: Activates the user’s local timezone for each request by reading the X-Timezone header, and persists the timezone to the user profile when it changes.

Active when: Always.

  • Reads X-Timezone header (sent by the generated TS SDK auth interceptor) and calls django.utils.timezone.activate().
  • Parses X-Client-Time (ISO-8601) and attaches it as request.client_time.
  • After the response, persists the timezone to CustomUser.timezone via a single UPDATE query if the value changed.
  • Uses an in-process cache (up to 2000 entries, keyed by user_pk + tz string) to avoid repeated DB writes within the same worker lifetime.
MIDDLEWARE = [ # ... 'django_cfg.middleware.timezone.TimezoneMiddleware', # ... ]

Request headers used:

HeaderPurpose
X-TimezoneIANA timezone name (e.g. America/New_York)
X-Client-TimeClient’s current time in ISO-8601 format

UserActivityMiddleware

Purpose: Tracks authenticated user activity by updating last_login on API requests. Complements JWTAuthenticationWithLastLogin by also covering session-authenticated users.

Active when: When the accounts app is enabled.

  • Only updates for authenticated users.
  • Only tracks API requests (JSON content type, DRF format params, REST verbs, /api/ or /cfg/ prefixes).
  • Updates last_login at most once every 5 minutes per user (in-process cache, up to 1000 entries).
  • Uses User.objects.filter(pk=...).update(last_login=now) — no signals fired.
from django_cfg.middleware.user_activity import UserActivityMiddleware stats = middleware.get_activity_stats() # { # 'tracked_users': 42, # 'update_interval': 300, # seconds # 'api_only': True, # }

PublicEndpointsMiddleware

Purpose: Prevents JWT authentication errors on AllowAny endpoints by temporarily removing the Authorization header for requests that match configured public endpoint patterns.

Active when: Always (configured via PUBLIC_ENDPOINT_PATTERNS setting).

Problem solved: When a client with an expired or invalid JWT token hits a public endpoint (health check, OTP, token refresh), DRF tries to authenticate and returns 401 — even though the endpoint does not require auth. This middleware strips the header before DRF sees it and restores it after the response.

Default public endpoint patterns:

/api/accounts/otp/ # OTP request/verify /cfg/accounts/otp/ # CFG OTP endpoints /api/accounts/token/refresh/ # Token refresh /cfg/accounts/token/refresh/ # CFG token refresh /api/health/ # Health checks /cfg/api/health/ # CFG health checks /admin/login/ # Django admin login /api/schema/ # API schema /api/docs/ # API docs

Custom patterns via Django settings:

# settings.py PUBLIC_ENDPOINT_PATTERNS = [ r'^/api/public/', r'^/api/newsletter/subscribe/$', ]

Middleware statistics:

# Accessed via the middleware instance stats = middleware.get_stats() # { # 'requests_processed': 1500, # 'tokens_ignored': 42, # 'public_endpoints_hit': 200, # 'public_patterns_count': 9, # }

ConnectionPoolCleanupMiddleware

Purpose: Ensures database connections are returned to the pool after every request, including error cases, to prevent connection leaks in production.

Active when: Always. Must be placed last in MIDDLEWARE.

Two variants are provided:

# WSGI deployments 'django_cfg.middleware.pool_cleanup.ConnectionPoolCleanupMiddleware' # ASGI deployments 'django_cfg.middleware.pool_cleanup.AsyncConnectionPoolCleanupMiddleware'

Both variants:

  • Close connections in a finally block after every request (successful or not).
  • Roll back uncommitted transactions when a view raises an exception.
  • Iterate all database aliases (connections) so multi-database setups are covered.
  • Add less than 1ms of overhead per request.

This middleware is critical when using connection pooling with ATOMIC_REQUESTS=False. Without it, connections can leak and exhaust the pool under load.

MIDDLEWARE = [ 'django_cfg.middleware.real_ip.RealIPMiddleware', # ... all other middleware ... 'django_cfg.middleware.pool_cleanup.ConnectionPoolCleanupMiddleware', # Last ]

RouteDebugMiddleware

Purpose: Logs 404 responses with URL pattern context to help diagnose misconfigured URL routers in development.

Active when: DEBUG=True only. The middleware checks settings.DEBUG on every 404 — it is safe to leave in MIDDLEWARE in all environments, but only produces output in debug mode.

When a 404 occurs, the logger django_cfg.debug_404 emits a WARNING containing:

  • HTTP method and full request path
  • Referer header (if present)
  • Up to 10 candidate URL patterns that share the first two path segments with the 404 path
# Auto-enabled in DEBUG mode via django-cfg config # Logger: django_cfg.debug_404 # Manual addition to MIDDLEWARE: 'django_cfg.middleware.route_debug.RouteDebugMiddleware'

Example log output:

WARNING django_cfg.debug_404: 404 GET /api/projects/123/characters/ Referer: http://localhost:3000/scenes/ Partial matches (2 patterns share prefix): api/projects/<pk>/ → project-detail api/projects/<pk>/scenes/ → scene-list

Pagination Classes

Purpose: Enhanced page-number pagination with a richer response format and full OpenAPI schema support.

Active when: Configured per view or globally in REST_FRAMEWORK['DEFAULT_PAGINATION_CLASS'].

Four classes are provided:

Classpage_sizemax_page_sizeUse case
DefaultPagination1001000General purpose
LargePagination5002000Large datasets
SmallPagination20100Detailed views
NoPaginationDisable pagination

All paginated classes share the same response envelope:

{ "count": 150, "page": 2, "pages": 15, "page_size": 10, "has_next": true, "has_previous": true, "next_page": 3, "previous_page": 1, "results": [...] }

Query parameters:

ParameterDefaultDescription
page1Page number (1-based)
page_sizeclass defaultItems per page (up to max_page_size)
# Global configuration REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'django_cfg.middleware.pagination.DefaultPagination', 'PAGE_SIZE': 100, } # Per-view override from django_cfg.middleware.pagination import SmallPagination class MyViewSet(viewsets.ModelViewSet): pagination_class = SmallPagination

Pagination Schema Hook (extract_paginated_components)

Purpose: drf-spectacular post-processing hook that promotes inline paginated response schemas to named components/schemas entries, enabling TypeScript codegen to produce typed Zod validators for every paginated endpoint.

Active when: Wired into SPECTACULAR_SETTINGS['POSTPROCESSING_HOOKS'] (done automatically by django_cfg.models.api.drf.SpectacularConfig).

Without this hook:

  • drf-spectacular embeds the pagination wrapper inline in every list endpoint.
  • TypeScript codegen cannot produce a named Zod schema for inline responses.
  • The generated SWR hooks skip runtime validation for paginated responses.

The hook identifies paginated responses by checking for the characteristic field set (count, page, pages, has_next, results), then extracts them to PaginatedXList named components. It is idempotent.

Manual configuration:

SPECTACULAR_SETTINGS = { 'POSTPROCESSING_HOOKS': [ 'drf_spectacular.hooks.postprocess_schema_enums', 'django_cfg.middleware.pagination_schema_hook.extract_paginated_components', ], }

Admin Notifications (Signal Handlers)

Purpose: Sends Telegram notifications for Django admin security events: successful admin logins, failed login attempts (via django-axes), and account lockouts.

Active when: Imported at startup (done automatically by django_cfg/middleware/__init__.py). Notifications are only sent when DjangoTelegram is configured.

This module registers Django signal receivers rather than a middleware class:

SignalTriggerNotification level
user_logged_inStaff or superuser logs into /admin/Info
user_login_failed (axes)Failed login on /admin/Warning
user_locked_out (axes)Account locked after multiple failuresError

django-axes is an optional dependency. If it is not installed, failed login and lockout notifications are silently skipped.

The get_client_ip() helper reads REMOTE_ADDR, which is already corrected by RealIPMiddleware — always place RealIPMiddleware first.

Notifications fire only for /admin/ paths and only for staff or superuser accounts. Errors in the notification path are caught and logged without interrupting the login flow.


MIDDLEWARE = [ # 1. Real IP — must be first so all middleware below see the correct IP 'django_cfg.middleware.real_ip.RealIPMiddleware', # 2. Standard Django security 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 3. Timezone — after auth so request.user is available for persistence 'django_cfg.middleware.timezone.TimezoneMiddleware', # 4. Public endpoints — before DRF processes auth 'django_cfg.middleware.public_endpoints.PublicEndpointsMiddleware', # 5. User activity — after auth so request.user is populated 'django_cfg.middleware.user_activity.UserActivityMiddleware', # 6. Route debug (only active when DEBUG=True) 'django_cfg.middleware.route_debug.RouteDebugMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 7. Connection pool cleanup — must be last 'django_cfg.middleware.pool_cleanup.ConnectionPoolCleanupMiddleware', ]

Logging

All middleware classes use standard Python logging under the django_cfg.middleware namespace:

LOGGING = { 'loggers': { 'django_cfg.middleware': {'level': 'INFO'}, 'django_cfg.debug_404': {'level': 'WARNING'}, 'django_cfg.middleware.pool_cleanup': {'level': 'WARNING'}, } }
  • ./routing — URL routing and extension URL patterns
  • ./startup — Startup validation hooks
  • ./mixins — API access mixins for ViewSets

TAGS: middleware, authentication, real-ip, timezone, pagination, pool-cleanup, public-endpoints, route-debug, admin-notifications DEPENDS_ON: [authentication, accounts, routing] USED_BY: [all-apps, api-generation, monitoring]

Last updated on