From 80640d2580a42bf3c1750b86d861c5c8eb3b5717 Mon Sep 17 00:00:00 2001 From: Nell Date: Thu, 20 Mar 2025 10:32:04 +0100 Subject: [PATCH] init --- app/api/urls.py | 10 +++++ app/app/asgi.py | 6 ++- app/app/channels_middleware.py | 79 ++++++++++++++++++++++++++++++++++ app/app/settings.py | 15 +++++++ app/requirements.txt | 3 +- 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 app/app/channels_middleware.py diff --git a/app/api/urls.py b/app/api/urls.py index f6657be..347bc16 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -1,9 +1,19 @@ from django.urls import path, include +# Ajout du package manquant dans requirements.txt ou installez-le avec: +# pip install djangorestframework-simplejwt +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) + + from .routers import router app_name = "api" urlpatterns = [ path("", include(router.urls)), + path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ] diff --git a/app/app/asgi.py b/app/app/asgi.py index d31dee5..6ae490c 100644 --- a/app/app/asgi.py +++ b/app/app/asgi.py @@ -2,8 +2,12 @@ import os from django.core.asgi import get_asgi_application from channels.auth import AuthMiddlewareStack +from channels.sessions import SessionMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter +from app.channels_middleware import JwtOrSessionAuthMiddleware + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') django_asgi_app = get_asgi_application() @@ -12,5 +16,5 @@ from .ws_urls import websocket_urlpatterns application = ProtocolTypeRouter({ "http": django_asgi_app, - "websocket": AuthMiddlewareStack(websocket_urlpatterns) + "websocket": SessionMiddlewareStack(JwtOrSessionAuthMiddleware(websocket_urlpatterns)) }) diff --git a/app/app/channels_middleware.py b/app/app/channels_middleware.py new file mode 100644 index 0000000..f4a256d --- /dev/null +++ b/app/app/channels_middleware.py @@ -0,0 +1,79 @@ +from django.contrib.auth.models import AnonymousUser +from django.db import close_old_connections + +from channels.db import database_sync_to_async +from channels.middleware import BaseMiddleware +from channels.auth import AuthMiddlewareStack +from rest_framework_simplejwt.tokens import UntypedToken +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from jwt import decode as jwt_decode +from django.conf import settings +from urllib.parse import parse_qs +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +@database_sync_to_async +def get_user_from_token(token): + try: + # Vérifier que le token est valide + UntypedToken(token) + + # Décoder le token et obtenir l'ID de l'utilisateur + decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=["HS256"]) + user_id = decoded_data.get('user_id') + + if user_id: + return User.objects.get(id=user_id) + return AnonymousUser() + except (InvalidToken, TokenError, User.DoesNotExist): + return AnonymousUser() + + +@database_sync_to_async +def get_user(scope): + if "session" in scope: + session = scope["session"] + user_id = session.get("_auth_user_id") + if user_id: + try: + return User.objects.get(id=user_id) + except User.DoesNotExist: + pass + return AnonymousUser() + + +class JwtOrSessionAuthMiddleware(BaseMiddleware): + async def __call__(self, scope, receive, send): + # Fermer les connexions DB obsolètes pour éviter les problèmes + close_old_connections() + + # Par défaut, définir un utilisateur anonyme + scope['user'] = AnonymousUser() + + # Essayer d'abord l'authentification par session + if "session" in scope: + scope['user'] = await get_user(scope) + if not scope['user'].is_anonymous: + return await super().__call__(scope, receive, send) + + # Si l'utilisateur est toujours anonyme, essayer JWT + if scope['user'].is_anonymous and 'query_string' in scope: + # Extraire token des query parameters + query_params = parse_qs(scope['query_string'].decode('utf-8')) + token = query_params.get('token', [None])[0] + + # Si aucun token dans les query params, chercher dans les headers + if not token and 'headers' in scope: + headers = dict(scope['headers']) + auth_header = headers.get(b'authorization', b'') + if auth_header.startswith(b'Bearer '): + token = auth_header.decode('utf-8')[7:] + + # Authentifier avec le token si présent + if token: + scope['user'] = await get_user_from_token(token) + + return await super().__call__(scope, receive, send) diff --git a/app/app/settings.py b/app/app/settings.py index 633e481..49ef1ab 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -53,6 +53,7 @@ INSTALLED_APPS = [ 'corsheaders', 'channels', 'django_filters', + 'rest_framework_simplejwt', 'user', 'api', @@ -220,6 +221,7 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", + 'rest_framework_simplejwt.authentication.JWTAuthentication', ], "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", @@ -227,6 +229,19 @@ REST_FRAMEWORK = { ] } +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', +} + # Torrent related DOWNLOAD_BASE_DIR = Path("/transmission/downloads/complete") NGINX_ACCEL_BASE = "/dl/" diff --git a/app/requirements.txt b/app/requirements.txt index 24036d5..689f1fb 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,10 +1,9 @@ django django-vite django-cors-headers - djangorestframework django-filter - +djangorestframework-simplejwt channels[daphne] channels_redis