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
| Class | Module | Active When |
|---|---|---|
JWTAuthenticationWithLastLogin | authentication | Always (JWT auth enabled) |
RealIPMiddleware | real_ip | Always (must be first) |
TimezoneMiddleware | timezone | Always |
UserActivityMiddleware | user_activity | Always (accounts app active) |
PublicEndpointsMiddleware | public_endpoints | Always |
ConnectionPoolCleanupMiddleware | pool_cleanup | Always (must be last, WSGI) |
AsyncConnectionPoolCleanupMiddleware | pool_cleanup | Always (must be last, ASGI) |
RouteDebugMiddleware | route_debug | DEBUG=True only |
DefaultPagination / variants | pagination | Per-view configuration |
extract_paginated_components hook | pagination_schema_hook | OpenAPI schema generation |
| Admin notifications (signals) | admin_notifications | Always (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_activeon every token validation — deactivated accounts cannot use existing tokens. - Updates
last_loginvia a directUPDATEquery (no signals, nosave()overhead). - Throttles DB writes with an in-process cache: updates only if
last_loginisNoneor older thanUPDATE_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:
| Priority | Header | Proxy |
|---|---|---|
| 1 | CF-Connecting-IP | Cloudflare |
| 2 | X-Forwarded-For | nginx, traefik, AWS ALB |
| 3 | X-Real-IP | nginx 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-Timezoneheader (sent by the generated TS SDK auth interceptor) and callsdjango.utils.timezone.activate(). - Parses
X-Client-Time(ISO-8601) and attaches it asrequest.client_time. - After the response, persists the timezone to
CustomUser.timezonevia a singleUPDATEquery 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:
| Header | Purpose |
|---|---|
X-Timezone | IANA timezone name (e.g. America/New_York) |
X-Client-Time | Client’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_loginat 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 docsCustom 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
finallyblock 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-listPagination 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:
| Class | page_size | max_page_size | Use case |
|---|---|---|---|
DefaultPagination | 100 | 1000 | General purpose |
LargePagination | 500 | 2000 | Large datasets |
SmallPagination | 20 | 100 | Detailed views |
NoPagination | — | — | Disable 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:
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number (1-based) |
page_size | class default | Items 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 = SmallPaginationPagination 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:
| Signal | Trigger | Notification level |
|---|---|---|
user_logged_in | Staff or superuser logs into /admin/ | Info |
user_login_failed (axes) | Failed login on /admin/ | Warning |
user_locked_out (axes) | Account locked after multiple failures | Error |
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.
Recommended Middleware Order
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'},
}
}Related
- ./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]