From f3c0d397f2742facf12069f31bd756490d2b1dcf Mon Sep 17 00:00:00 2001 From: Josix Date: Sun, 2 Oct 2022 02:16:51 +0800 Subject: [PATCH 1/7] feat(swagger): add drf-yasg to automatically generate swagger in dev env --- requirements/dev.txt | 5 ++++- src/pycontw2016/settings/base.py | 3 ++- src/pycontw2016/urls.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 025172814..fef4f122f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -50,4 +50,7 @@ pytest-xdist==1.31.0 cssselect==1.1.0 # PostgreSQL database adapter for the Python -psycopg2-binary==2.8.5 \ No newline at end of file +psycopg2-binary==2.8.5 + +# Automated generation of real Swagger/OpenAPI 2.0 schemas from Django REST Framework code. +drf-yasg==1.20.0 diff --git a/src/pycontw2016/settings/base.py b/src/pycontw2016/settings/base.py index 29de1d683..dc9638709 100644 --- a/src/pycontw2016/settings/base.py +++ b/src/pycontw2016/settings/base.py @@ -103,7 +103,8 @@ 'sorl.thumbnail', 'registry', 'corsheaders', - 'rest_framework' + 'rest_framework', + 'drf_yasg', ) LOCAL_APPS = ( diff --git a/src/pycontw2016/urls.py b/src/pycontw2016/urls.py index e596e215e..d11760ea1 100644 --- a/src/pycontw2016/urls.py +++ b/src/pycontw2016/urls.py @@ -4,11 +4,28 @@ from django.conf.urls.static import static from django.contrib import admin from django.views.i18n import set_language +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi from core.views import error_page, flat_page, index from users.views import user_dashboard +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + + urlpatterns = i18n_patterns( # Add top-level URL patterns here. @@ -45,3 +62,8 @@ if settings.DEBUG: import debug_toolbar urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls))] + urlpatterns += [ + url(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + ] From 4ef0894f3d44174a2e973341e3890cfce9962b1d Mon Sep 17 00:00:00 2001 From: "Wei-Hsiang (Matt) Wang" Date: Wed, 14 Dec 2022 14:16:39 +0800 Subject: [PATCH 2/7] fix: indent --- src/pycontw2016/urls.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pycontw2016/urls.py b/src/pycontw2016/urls.py index d11760ea1..d0987e96f 100644 --- a/src/pycontw2016/urls.py +++ b/src/pycontw2016/urls.py @@ -13,16 +13,16 @@ schema_view = get_schema_view( - openapi.Info( - title="Snippets API", - default_version='v1', - description="Test description", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="contact@snippets.local"), - license=openapi.License(name="BSD License"), - ), - public=True, - permission_classes=(permissions.AllowAny,), + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), ) From ec0984dbf77d17eac9d7c5d9da0cd17508cfe1dc Mon Sep 17 00:00:00 2001 From: dawn yang Date: Wed, 17 May 2023 21:21:08 +0800 Subject: [PATCH 3/7] fix auth for swagger --- src/attendee/api/views.py | 4 +-- src/core/authentication.py | 45 ++++++++++++++++++++++++-- src/core/models.py | 4 +-- src/events/api/views.py | 12 +++---- src/pycontw2016/settings/base.py | 33 +++++++++++++++++++ src/pycontw2016/urls.py | 3 +- src/sponsors/api/views.py | 6 ++-- src/users/api/__init__.py | 0 src/users/api/serializers.py | 0 src/users/api/urls.py | 8 +++++ src/users/api/views.py | 54 ++++++++++++++++++++++++++++++++ 11 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 src/users/api/__init__.py create mode 100644 src/users/api/serializers.py create mode 100644 src/users/api/urls.py create mode 100644 src/users/api/views.py diff --git a/src/attendee/api/views.py b/src/attendee/api/views.py index 1bc7a7c1b..9dd91d4aa 100644 --- a/src/attendee/api/views.py +++ b/src/attendee/api/views.py @@ -4,12 +4,12 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from core.authentication import TokenAuthentication +from core.authentication import BearerAuthentication from attendee.models import Attendee class AttendeeAPIView(views.APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] model = Attendee diff --git a/src/core/authentication.py b/src/core/authentication.py index 0ea33615f..7f1467ab6 100644 --- a/src/core/authentication.py +++ b/src/core/authentication.py @@ -1,7 +1,48 @@ from rest_framework.authentication import TokenAuthentication - +from rest_framework import exceptions +from rest_framework import HTTP_HEADER_ENCODING, exceptions +from django.utils.translation import gettext_lazy as _ from .models import Token -class TokenAuthentication(TokenAuthentication): +class BearerAuthentication(TokenAuthentication): + keyword = 'Bearer' model = Token + def get_model(self): + if self.model is not None: + return self.model + from rest_framework.authtoken.models import Token + return Token + + def get_authorization_header(self,request): + """ + Return request's 'Authorization:' header, as a bytestring. + Hide some test client ickyness where the header can be unicode. + """ + auth = request.META.get('HTTP_AUTHORIZATION', b'') + if isinstance(auth, str): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + return auth + + def authenticate(self, request): + auth = self.get_authorization_header(request).split() + + if not auth : + return None + + token = auth[0].decode() + + return self.authenticate_credentials(token) + + def authenticate_credentials(self, key): + model = self.get_model() + try: + token = model.objects.select_related('user').get(key=key) + except model.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + + if not token.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + return (token.user, token) diff --git a/src/core/models.py b/src/core/models.py index c4d290275..062a181ac 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -270,8 +270,8 @@ class Token(models.Model): """ key = models.CharField(_("Key"), max_length=40, primary_key=True) user = BigForeignKey( - to=settings.AUTH_USER_MODEL, - related_name='auth_token', + to=settings.AUTH_USER_MODEL, + related_name='%(app_label)s_auth_token', verbose_name=_('user'), on_delete=models.CASCADE, ) diff --git a/src/events/api/views.py b/src/events/api/views.py index 3494265c0..5211dd811 100644 --- a/src/events/api/views.py +++ b/src/events/api/views.py @@ -11,7 +11,7 @@ from django.http import Http404 from django.utils.timezone import make_naive -from core.authentication import TokenAuthentication +from core.authentication import BearerAuthentication from events.models import ( CustomEvent, Location, ProposedTalkEvent, ProposedTutorialEvent, SponsoredEvent, Time, KeynoteEvent @@ -54,7 +54,7 @@ def get_queryset(self): class SpeechListAPIView(APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): @@ -81,7 +81,7 @@ def get(self, request, *args, **kwargs): class SpeechListByCategoryAPIView(APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): @@ -110,7 +110,7 @@ class TutorialDetailAPIView(RetrieveAPIView): class SpeechDetailAPIView(APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): @@ -263,7 +263,7 @@ def display(self): class ScheduleAPIView(APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] event_querysets = [ @@ -344,7 +344,7 @@ def get(self, request): class KeynoteEventListAPIView(ListAPIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] queryset = KeynoteEvent.objects.all() diff --git a/src/pycontw2016/settings/base.py b/src/pycontw2016/settings/base.py index dc9638709..522d6ab6c 100644 --- a/src/pycontw2016/settings/base.py +++ b/src/pycontw2016/settings/base.py @@ -104,6 +104,7 @@ 'registry', 'corsheaders', 'rest_framework', + 'rest_framework.authtoken', 'drf_yasg', ) @@ -118,6 +119,38 @@ 'attendee' ) +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + 'core.authentication.BearerAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES':{ + 'rest_framework.permissions.IsAuthenticated' + + } + +} + +SWAGGER_SETTINGS = { + 'SHOW_REQUEST_HEADERS': True, + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header', + }, + }, + 'USE_SESSION_AUTH': True, + 'JSON_EDITOR': True, + 'SUPPORTED_SUBMIT_METHODS': [ + 'get', + 'post', + 'put', + 'delete', + 'patch' + ], +} + INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # Enable Postgres-specific things if we are using it. diff --git a/src/pycontw2016/urls.py b/src/pycontw2016/urls.py index d0987e96f..baea6a991 100644 --- a/src/pycontw2016/urls.py +++ b/src/pycontw2016/urls.py @@ -52,7 +52,8 @@ url(r'^api/events/', include('events.api.urls', namespace="events")), url(r'^set-language/$', set_language, name='set_language'), url(r'^admin/', admin.site.urls), - url(r'^api/attendee/', include('attendee.api.urls')) + url(r'^api/attendee/', include('attendee.api.urls')), + url(r'^api/users/', include('users.api.urls')), ] # User-uploaded files like profile pics need to be served in development. diff --git a/src/sponsors/api/views.py b/src/sponsors/api/views.py index 8436aea32..4b5a700b6 100644 --- a/src/sponsors/api/views.py +++ b/src/sponsors/api/views.py @@ -4,12 +4,12 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from core.authentication import TokenAuthentication +from core.authentication import BearerAuthentication from sponsors.models import Sponsor, OpenRole class SponsorAPIView(views.APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] def get(self, request): @@ -42,7 +42,7 @@ def get(self, request): class JobAPIView(views.APIView): - authentication_classes = [TokenAuthentication] + authentication_classes = [BearerAuthentication] permission_classes = [IsAuthenticated] def get(self, request): diff --git a/src/users/api/__init__.py b/src/users/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/users/api/serializers.py b/src/users/api/serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/users/api/urls.py b/src/users/api/urls.py new file mode 100644 index 000000000..37d7e1212 --- /dev/null +++ b/src/users/api/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from users.api.views import CustomAuthToken + + +urlpatterns = [ + path("api-token-auth/", CustomAuthToken.as_view()), + +] diff --git a/src/users/api/views.py b/src/users/api/views.py new file mode 100644 index 000000000..63ee979f0 --- /dev/null +++ b/src/users/api/views.py @@ -0,0 +1,54 @@ +from rest_framework.response import Response +from rest_framework.authtoken.views import ObtainAuthToken + +from core.models import Token +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from rest_framework import exceptions +from django.contrib.auth import get_user_model +from users.models import User +from datetime import datetime, timedelta + + +class CustomAuthToken(ObtainAuthToken): + @swagger_auto_schema( + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['username','password'], + order=['username', 'password'], + properties={ + 'username':openapi.Schema(type=openapi.TYPE_STRING), + 'password':openapi.Schema(type=openapi.TYPE_STRING) + }, + ), + operation_description='Get account token' + ) + + def post(self, request): + username = request.data['username'] + try: + user = get_user_model().objects.get(email=username) + except User.DoesNotExist: + raise exceptions.AuthenticationFailed(('User matching query does not exist')) + + tokens = Token.objects.filter(user=user) + if len(tokens)== 0: + Token.objects.create(user=user) + + token = Token.objects.get(user=user) + token = str(token) + + token_create_time = Token.objects.get(key=token).created + pre_week_day = datetime.now(token_create_time.tzinfo) + timedelta(days=-7) + if token_create_time < pre_week_day: + Token.objects.get(key=token).delete() + Token.objects.create(user=user) + + serializer = self.serializer_class(data=request.data, context={'request': request}) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + + return Response({ + 'username': user.email, + 'token': token + }) From bd15fa30eee1d5814e2338ca7a233bff0eef5894 Mon Sep 17 00:00:00 2001 From: dawn yang Date: Sun, 11 Jun 2023 17:42:47 +0800 Subject: [PATCH 4/7] fix code style error --- src/core/authentication.py | 14 +++++++------- src/core/models.py | 2 +- src/pycontw2016/settings/base.py | 2 +- src/users/api/views.py | 23 +++++++++++------------ 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/core/authentication.py b/src/core/authentication.py index 7f1467ab6..0689d16bb 100644 --- a/src/core/authentication.py +++ b/src/core/authentication.py @@ -1,5 +1,4 @@ from rest_framework.authentication import TokenAuthentication -from rest_framework import exceptions from rest_framework import HTTP_HEADER_ENCODING, exceptions from django.utils.translation import gettext_lazy as _ from .models import Token @@ -8,13 +7,14 @@ class BearerAuthentication(TokenAuthentication): keyword = 'Bearer' model = Token + def get_model(self): if self.model is not None: return self.model from rest_framework.authtoken.models import Token return Token - - def get_authorization_header(self,request): + + def get_authorization_header(self, request): """ Return request's 'Authorization:' header, as a bytestring. Hide some test client ickyness where the header can be unicode. @@ -24,13 +24,13 @@ def get_authorization_header(self,request): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth - + def authenticate(self, request): auth = self.get_authorization_header(request).split() - if not auth : + if not auth: return None - + token = auth[0].decode() return self.authenticate_credentials(token) @@ -45,4 +45,4 @@ def authenticate_credentials(self, key): if not token.user.is_active: raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) - return (token.user, token) + return (token.user, token) diff --git a/src/core/models.py b/src/core/models.py index 062a181ac..e51459ffa 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -270,7 +270,7 @@ class Token(models.Model): """ key = models.CharField(_("Key"), max_length=40, primary_key=True) user = BigForeignKey( - to=settings.AUTH_USER_MODEL, + to=settings.AUTH_USER_MODEL, related_name='%(app_label)s_auth_token', verbose_name=_('user'), on_delete=models.CASCADE, diff --git a/src/pycontw2016/settings/base.py b/src/pycontw2016/settings/base.py index 522d6ab6c..755540f9e 100644 --- a/src/pycontw2016/settings/base.py +++ b/src/pycontw2016/settings/base.py @@ -124,7 +124,7 @@ 'rest_framework.authentication.TokenAuthentication', 'core.authentication.BearerAuthentication', ], - 'DEFAULT_PERMISSION_CLASSES':{ + 'DEFAULT_PERMISSION_CLASSES': { 'rest_framework.permissions.IsAuthenticated' } diff --git a/src/users/api/views.py b/src/users/api/views.py index 63ee979f0..d2ca6fc13 100644 --- a/src/users/api/views.py +++ b/src/users/api/views.py @@ -4,7 +4,7 @@ from core.models import Token from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi -from rest_framework import exceptions +from rest_framework import exceptions from django.contrib.auth import get_user_model from users.models import User from datetime import datetime, timedelta @@ -14,40 +14,39 @@ class CustomAuthToken(ObtainAuthToken): @swagger_auto_schema( request_body=openapi.Schema( type=openapi.TYPE_OBJECT, - required=['username','password'], + required=['username', 'password'], order=['username', 'password'], properties={ - 'username':openapi.Schema(type=openapi.TYPE_STRING), - 'password':openapi.Schema(type=openapi.TYPE_STRING) + 'username': openapi.Schema(type=openapi.TYPE_STRING), + 'password': openapi.Schema(type=openapi.TYPE_STRING) }, ), - operation_description='Get account token' + operation_description='Get account token' ) - def post(self, request): username = request.data['username'] try: user = get_user_model().objects.get(email=username) except User.DoesNotExist: raise exceptions.AuthenticationFailed(('User matching query does not exist')) - + tokens = Token.objects.filter(user=user) - if len(tokens)== 0: + if len(tokens) == 0: Token.objects.create(user=user) - + token = Token.objects.get(user=user) token = str(token) - + token_create_time = Token.objects.get(key=token).created pre_week_day = datetime.now(token_create_time.tzinfo) + timedelta(days=-7) if token_create_time < pre_week_day: Token.objects.get(key=token).delete() - Token.objects.create(user=user) + Token.objects.create(user=user) serializer = self.serializer_class(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] - + return Response({ 'username': user.email, 'token': token From 396da1f53b47dbe7977f5bc8782aafd5cc4bcf60 Mon Sep 17 00:00:00 2001 From: dawn yang Date: Sun, 11 Jun 2023 17:48:32 +0800 Subject: [PATCH 5/7] fix code style error --- src/core/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/authentication.py b/src/core/authentication.py index 0689d16bb..5c23fb6ff 100644 --- a/src/core/authentication.py +++ b/src/core/authentication.py @@ -7,7 +7,7 @@ class BearerAuthentication(TokenAuthentication): keyword = 'Bearer' model = Token - + def get_model(self): if self.model is not None: return self.model From 9a9c08d53d93fecf8afbfd60b153782ad6d9904a Mon Sep 17 00:00:00 2001 From: dawn yang Date: Sat, 17 Jun 2023 16:34:14 +0800 Subject: [PATCH 6/7] fix test error --- src/events/tests/api/test_list_speeches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/tests/api/test_list_speeches.py b/src/events/tests/api/test_list_speeches.py index ae5069403..f0426ef67 100644 --- a/src/events/tests/api/test_list_speeches.py +++ b/src/events/tests/api/test_list_speeches.py @@ -38,7 +38,7 @@ def test_list_speeches_by_category(category, bare_user, drf_api_client): url = reverse("events:speeches-category", kwargs={"category": category}) token = Token.objects.get_or_create(user=bare_user) - drf_api_client.credentials(HTTP_AUTHORIZATION="Token " + str(token[0])) + drf_api_client.credentials(HTTP_AUTHORIZATION=str(token[0])) response = drf_api_client.get(url) for event in response.json(): From 287fc55f166266171e970a0795d8241536b982c3 Mon Sep 17 00:00:00 2001 From: dawn yang Date: Sat, 17 Jun 2023 17:27:32 +0800 Subject: [PATCH 7/7] fix permission error --- src/pycontw2016/settings/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pycontw2016/settings/base.py b/src/pycontw2016/settings/base.py index 755540f9e..f18073081 100644 --- a/src/pycontw2016/settings/base.py +++ b/src/pycontw2016/settings/base.py @@ -122,12 +122,12 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', - 'core.authentication.BearerAuthentication', + 'core.authentication.BearerAuthentication' ], - 'DEFAULT_PERMISSION_CLASSES': { - 'rest_framework.permissions.IsAuthenticated' - } + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated' + ] }