From 98924d9e6dd28c3e0875d6ce8feb9b808ac6b53c Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Sat, 9 Mar 2024 13:08:14 +0100 Subject: [PATCH] fix: make middleware async-compatible to fix asgi --- .gitignore | 1 + hub/middleware.py | 95 +++++++++++++++++++++++++----- local_intelligence_hub/settings.py | 33 +---------- poetry.lock | 17 +----- pyproject.toml | 1 - 5 files changed, 86 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 237ee55b2..6d69fb54f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv +__pycache__ .env .env.local .static/**/* diff --git a/hub/middleware.py b/hub/middleware.py index c4e1ac6f4..6dd39c5a9 100644 --- a/hub/middleware.py +++ b/hub/middleware.py @@ -1,32 +1,99 @@ +import logging from datetime import timedelta +from inspect import isawaitable +from django.http import HttpRequest +from django.utils.decorators import sync_and_async_middleware from django.utils.timezone import now +from asgiref.sync import iscoroutinefunction, sync_to_async +from gqlauth.core.middlewares import USER_OR_ERROR_KEY, UserOrError +from gqlauth.core.middlewares import django_jwt_middleware as _django_jwt_middleware +from gqlauth.core.types_ import GQLAuthError, GQLAuthErrors +from whitenoise.middleware import WhiteNoiseMiddleware + from hub.models import UserProperties +logger = logging.getLogger(__name__) -class RecordLastSeenMiddleware: - one_day = timedelta(hours=24) - def __init__(self, get_response): - self.get_response = get_response +@sync_and_async_middleware +def record_last_seen_middleware(get_response): + one_day = timedelta(hours=24) - def __call__(self, request): + def process_request(request): if request.user.is_authenticated: user = request.user - if not hasattr(user, "userproperties"): - UserProperties.objects.create(user=user) - + props = UserProperties.objects.get_or_create(user=user) last_seen = request.session.get("last_seen", None) - - yesterday = now().replace(hour=0, minute=0) - self.one_day - + yesterday = now().replace(hour=0, minute=0) - one_day if last_seen is None or last_seen < yesterday.timestamp(): - props = user.userproperties props.last_seen = now() request.session["last_seen"] = props.last_seen.timestamp() props.save() - response = self.get_response(request) + if iscoroutinefunction(get_response): + async def middleware(request: HttpRequest): + await sync_to_async(process_request)(request) + return await get_response(request) + + else: + def middleware(request: HttpRequest): + process_request(request) + return get_response(request) + + return middleware + + +@sync_and_async_middleware +def async_whitenoise_middleware(get_response): + def logic(request): + return WhiteNoiseMiddleware(get_response)(request) + + if iscoroutinefunction(get_response): + async def middleware(request: HttpRequest): + response = await sync_to_async(logic)(request) + if isawaitable(response): + response = await response + return response + + else: + def middleware(request: HttpRequest): + return logic(request) + + return middleware + + +@sync_and_async_middleware +def django_jwt_middleware(get_response): + """ + Wrap the gqlauth jwt middleware in an exception + handler (initially added because if a user is + deleted, the middleware throws an error, + causing a 500 instead of a 403). + """ + gqlauth_middleware = _django_jwt_middleware(get_response) + + def exception_handler(error: Exception, request: HttpRequest): + logger.warning(f"Gqlauth middleware error: {error}") + user_or_error = UserOrError() + user_or_error.error = GQLAuthError(code=GQLAuthErrors.UNAUTHENTICATED) + setattr(request, USER_OR_ERROR_KEY, user_or_error) + + if iscoroutinefunction(get_response): + async def middleware(request: HttpRequest): + try: + return await gqlauth_middleware(request) + except Exception: + exception_handler(request) + return await get_response(request) + + else: + def middleware(request: HttpRequest): + try: + return gqlauth_middleware(request) + except Exception as e: + exception_handler(request) + return get_response(request) - return response + return middleware diff --git a/local_intelligence_hub/settings.py b/local_intelligence_hub/settings.py index b96987956..075cf37ce 100644 --- a/local_intelligence_hub/settings.py +++ b/local_intelligence_hub/settings.py @@ -29,7 +29,6 @@ DEBUG=(bool, False), ALLOWED_HOSTS=(list, []), CORS_ALLOWED_ORIGINS=(list, ['http://localhost:3000']), - HIDE_DEBUG_TOOLBAR=(bool, False), GOOGLE_ANALYTICS=(str, ""), GOOGLE_SITE_VERIFICATION=(str, ""), TEST_AIRTABLE_BASE_ID=(str, ""), @@ -50,7 +49,6 @@ ALLOWED_HOSTS = env("ALLOWED_HOSTS") CORS_ALLOWED_ORIGINS = env("CORS_ALLOWED_ORIGINS") CACHE_FILE = env("CACHE_FILE") -HIDE_DEBUG_TOOLBAR = env("HIDE_DEBUG_TOOLBAR") MAPIT_URL = env("MAPIT_URL") MAPIT_API_KEY = env("MAPIT_API_KEY") GOOGLE_ANALYTICS = env("GOOGLE_ANALYTICS") @@ -105,16 +103,16 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", + "hub.middleware.async_whitenoise_middleware", "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", - "gqlauth.core.middlewares.django_jwt_middleware", + "hub.middleware.django_jwt_middleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "hub.middleware.RecordLastSeenMiddleware", + "hub.middleware.record_last_seen_middleware", ] ROOT_URLCONF = "local_intelligence_hub.urls" @@ -235,31 +233,6 @@ EMAIL_PORT = env.str("EMAIL_PORT", 1025) DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", "webmaster@localhost") -if DEBUG and HIDE_DEBUG_TOOLBAR is False: # pragma: no cover - hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) - INTERNAL_IPS = [ip[:-1] + "1" for ip in ips] + ["127.0.0.1", "10.0.2.2"] - CSRF_TRUSTED_ORIGINS = ["https://*.preview.app.github.dev"] - - # debug toolbar has to come after django_hosts middleware - MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") - - INSTALLED_APPS += ("debug_toolbar",) - - DEBUG_TOOLBAR_PANELS = [ - "debug_toolbar.panels.versions.VersionsPanel", - "debug_toolbar.panels.timer.TimerPanel", - "debug_toolbar.panels.settings.SettingsPanel", - "debug_toolbar.panels.headers.HeadersPanel", - "debug_toolbar.panels.request.RequestPanel", - "debug_toolbar.panels.sql.SQLPanel", - "debug_toolbar.panels.staticfiles.StaticFilesPanel", - "debug_toolbar.panels.templates.TemplatesPanel", - "debug_toolbar.panels.cache.CachePanel", - "debug_toolbar.panels.signals.SignalsPanel", - "debug_toolbar.panels.logging.LoggingPanel", - "debug_toolbar.panels.redirects.RedirectsPanel", - ] - POSTCODES_IO_URL = "https://postcodes.commonknowledge.coop" POSTCODES_IO_BATCH_MAXIMUM = 100 diff --git a/poetry.lock b/poetry.lock index 895836a42..51991c836 100644 --- a/poetry.lock +++ b/poetry.lock @@ -495,21 +495,6 @@ files = [ asgiref = ">=3.6" Django = ">=3.2" -[[package]] -name = "django-debug-toolbar" -version = "3.8.1" -description = "A configurable set of panels that display various debug information about the current request/response." -optional = false -python-versions = ">=3.7" -files = [ - {file = "django_debug_toolbar-3.8.1-py3-none-any.whl", hash = "sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478"}, - {file = "django_debug_toolbar-3.8.1.tar.gz", hash = "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27"}, -] - -[package.dependencies] -django = ">=3.2.4" -sqlparse = ">=0.2" - [[package]] name = "django-environ" version = "0.9.0" @@ -2106,4 +2091,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "d06a2831fed444045e638c953c8b5eafc781a3c0399b41a32351c032924c497c" +content-hash = "753fc4b6469c26be052ef388e3134def37ed352f19c51e4ec49049683aa9f926" diff --git a/pyproject.toml b/pyproject.toml index 48ee8543f..cff165007 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ strawberry-graphql = {extras = ["asgi"], version = "^0.220.0"} pandas = "^2.2.1" [tool.poetry.dev-dependencies] -django-debug-toolbar = "^3.7.0" black = "^22.8.0" coverage = "^6.5.0" flake8 = "^5.0.4"