From a8e2384ee96f700429be0c6634b67f37793bd10a Mon Sep 17 00:00:00 2001 From: punkyoon Date: Sun, 26 May 2019 01:31:45 +0900 Subject: [PATCH 1/2] feat: add email auth backend --- accounts/backends.py | 16 ++++++++++++++++ accounts/models.py | 2 -- accounts/urls.py | 0 accounts/views.py | 2 +- mock_server/settings/settings_base.py | 1 + 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 accounts/backends.py create mode 100644 accounts/urls.py diff --git a/accounts/backends.py b/accounts/backends.py new file mode 100644 index 0000000..89672c0 --- /dev/null +++ b/accounts/backends.py @@ -0,0 +1,16 @@ +from django.contrib.auth.backends import ModelBackend + +from accounts.models import MockUser + + +class EmailAuthenticationBackend(ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + try: + user = MockUser.objects.get(email=username) + except MockUser.DoesNotExist: + return None + else: + if user.check_password(password): + return user + + return None diff --git a/accounts/models.py b/accounts/models.py index 5c10d28..422d4b0 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -91,5 +91,3 @@ def create(cls, mock_user, language='', country='', locale=''): mock_user=mock_user, language=language, country=country, locale=locale ) return mock_profile - -# TODO: add custom authentication backend to login with email diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..b588925 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,3 @@ from django.shortcuts import render -# Create your views here. + diff --git a/mock_server/settings/settings_base.py b/mock_server/settings/settings_base.py index 92c2d6e..2d16a8d 100644 --- a/mock_server/settings/settings_base.py +++ b/mock_server/settings/settings_base.py @@ -55,6 +55,7 @@ WSGI_APPLICATION = 'mock_server.wsgi.application' AUTH_USER_MODEL = 'accounts.MockUser' +AUTHENTICATION_BACKENDS = ['accounts.backends.EmailAuthenticationBackend', ] # Password validation AUTH_PASSWORD_VALIDATORS = [ From 2f0c40b50413cdd4a245c03b722e3101a3920e6a Mon Sep 17 00:00:00 2001 From: punkyoon Date: Sun, 26 May 2019 18:35:36 +0900 Subject: [PATCH 2/2] feat: modify jwt auth module and add register/fetch user profile endpoint --- accounts/exceptions.py | 8 +++++ accounts/serializers.py | 27 +++++++++++++++ accounts/urls.py | 24 ++++++++++++++ accounts/views.py | 24 +++++++++++++- mock_server/__init__.py | 4 +++ mock_server/models.py | 3 -- mock_server/settings/settings_base.py | 20 +++++++----- mock_server/urls.py | 5 +-- poetry.lock | 47 +++++++++++++++++++-------- pyproject.toml | 3 +- 10 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 accounts/exceptions.py create mode 100644 accounts/serializers.py diff --git a/accounts/exceptions.py b/accounts/exceptions.py new file mode 100644 index 0000000..d2c5017 --- /dev/null +++ b/accounts/exceptions.py @@ -0,0 +1,8 @@ +from rest_framework import status +from rest_framework.exceptions import APIException + + +class ProfileDoesNotExist(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = 'User profile does not exist' + default_code = '400_PROFILE_DOES_NOT_EXIST' diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..bec6c46 --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,27 @@ +from django_countries.serializer_fields import CountryField +from rest_framework import serializers +from rest_framework_serializer_extensions.serializers import SerializerExtensionsMixin + +from accounts.models import MockUser, MockProfile + + +class MockUserSerializer(SerializerExtensionsMixin, serializers.ModelSerializer): + language = serializers.CharField(write_only=True, max_length=2) + country = CountryField(write_only=True) + locale = serializers.CharField(write_only=True, max_length=5) + + def create(self, validated_data): + # validated_data: email, password, language, country, locale + return MockUser.create(**validated_data) + + class Meta: + model = MockUser + fields = ('email', 'password', 'locale', 'country', 'language', ) + write_only_fields = ('email', 'password', ) + + +class MockProfileSerializer(SerializerExtensionsMixin, serializers.ModelSerializer): + class Meta: + model = MockProfile + fields = ('nickname', 'locale', 'country', 'language', ) + read_only_fields = ('nickname', 'locale', 'country', 'language', ) diff --git a/accounts/urls.py b/accounts/urls.py index e69de29..6306339 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -0,0 +1,24 @@ +from django.urls import path + +from rest_auth.views import LogoutView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView +from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token, verify_jwt_token + +from accounts.views import RegisterMockUserView, FetchMockProfileView + + +app_name = 'accounts' +urlpatterns = [ + path(r'login', obtain_jwt_token, name='login'), + path(r'logout', LogoutView.as_view(), name='logout'), + path('register', RegisterMockUserView.as_view(), name='register'), + path(r'profile', FetchMockProfileView.as_view(), name='profile'), + + path(r'password/change', PasswordChangeView.as_view(), name='password_change'), + path(r'password/reset', PasswordResetView.as_view(), name='password_reset'), + path(r'password/reset/confirm', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + + path('token/refresh', refresh_jwt_token), + path('token/verify', verify_jwt_token), +] + +# More details: https://django-rest-auth.readthedocs.io/en/latest/api_endpoints.html#basic diff --git a/accounts/views.py b/accounts/views.py index b588925..e7a0956 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,25 @@ -from django.shortcuts import render +from rest_framework.generics import CreateAPIView, RetrieveAPIView +from rest_framework.permissions import AllowAny, IsAuthenticated +from accounts.exceptions import ProfileDoesNotExist +from accounts.models import MockUser, MockProfile +from accounts.serializers import MockUserSerializer, MockProfileSerializer + +class RegisterMockUserView(CreateAPIView): + model = MockUser + permission_classes = (AllowAny, ) + serializer_class = MockUserSerializer + + +class FetchMockProfileView(RetrieveAPIView): + permission_classes = (IsAuthenticated, ) + serializer_class = MockProfileSerializer + + def get_object(self): + try: + mock_profile = MockProfile.objects.get(mock_user=self.request.user) + except MockProfile.DoesNotExist: + raise ProfileDoesNotExist() + else: + return mock_profile diff --git a/mock_server/__init__.py b/mock_server/__init__.py index e69de29..c5c23e6 100644 --- a/mock_server/__init__.py +++ b/mock_server/__init__.py @@ -0,0 +1,4 @@ +import hashids + + +HASH_IDS = hashids.Hashids(salt='MOCK-SERVER-SALT') diff --git a/mock_server/models.py b/mock_server/models.py index b4d6549..38bd9f2 100644 --- a/mock_server/models.py +++ b/mock_server/models.py @@ -5,6 +5,3 @@ class BaseModel(SafeDeleteModel, TimeStampedModel): class Meta: abstract = True - - -# TODO: https://django-rest-framework-serializer-extensions.readthedocs.io/en/latest/usage-hashids/ diff --git a/mock_server/settings/settings_base.py b/mock_server/settings/settings_base.py index 2d16a8d..28ca532 100644 --- a/mock_server/settings/settings_base.py +++ b/mock_server/settings/settings_base.py @@ -10,7 +10,6 @@ SECRET_KEY = '&6fbttu7hb^=-v!84htgi=eh$1bc7ov$d-!2fuzmb2sxyktu+!' - DEFAULT_DJANGO_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -20,8 +19,7 @@ 'django.contrib.staticfiles', ] PROJECT_APPS = ['accounts', ] -EXTERNAL_APPS = ['rest_framework', 'safedelete', ] - +EXTERNAL_APPS = ['rest_auth', 'rest_framework', 'rest_framework.authtoken', 'safedelete', ] INSTALLED_APPS = DEFAULT_DJANGO_APPS + PROJECT_APPS + EXTERNAL_APPS MIDDLEWARE = [ @@ -65,15 +63,19 @@ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] +REST_USE_JWT = True REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ] + 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated', ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ), + 'SERIALIZER_EXTENSIONS': {'USE_HASH_IDS': True, 'HASH_IDS_SOURCE': 'mock_server.HASH_IDS'} } +JWT_AUTH = {'JWT_AUTH_HEADER_PREFIX': 'Bearer', } + # Internationalization LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' diff --git a/mock_server/urls.py b/mock_server/urls.py index d85c01d..26688db 100644 --- a/mock_server/urls.py +++ b/mock_server/urls.py @@ -1,13 +1,10 @@ from django.contrib import admin from django.urls import path, include -from rest_framework_simplejwt import views - urlpatterns = [ path('admin/', admin.site.urls), path('admin-api/', include('rest_framework.urls')), - path('api/token/', views.TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('api/token/refresh/', views.TokenRefreshView.as_view(), name='token_refresh'), + path('accounts/', include('accounts.urls')), ] diff --git a/poetry.lock b/poetry.lock index 6188483..9865674 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,6 +29,19 @@ version = "3.1.2" [package.dependencies] Django = ">=1.8" +[[package]] +category = "main" +description = "Create a set of REST API endpoints for Authentication and Registration" +name = "django-rest-auth" +optional = false +python-versions = "*" +version = "0.9.5" + +[package.dependencies] +Django = ">=1.8.0" +djangorestframework = ">=3.1.3" +six = ">=1.9.0" + [[package]] category = "main" description = "Mask your objects instead of deleting them from your database." @@ -50,27 +63,25 @@ version = "3.9.4" [[package]] category = "main" -description = "Extensions to DRY up Django Rest Framework serializers" -name = "djangorestframework-serializer-extensions" +description = "JSON Web Token based authentication for Django REST framework" +name = "djangorestframework-jwt" optional = false python-versions = "*" -version = "1.0.0" +version = "1.11.0" [package.dependencies] -hashids = ">1.0.0" +PyJWT = ">=1.5.2,<2.0.0" [[package]] category = "main" -description = "A minimal JSON Web Token authentication plugin for Django REST Framework" -name = "djangorestframework-simplejwt" +description = "Extensions to DRY up Django Rest Framework serializers" +name = "djangorestframework-serializer-extensions" optional = false -python-versions = ">=3.5,<4" -version = "4.3.0" +python-versions = "*" +version = "1.0.0" [package.dependencies] -django = "*" -djangorestframework = "*" -pyjwt = "*" +hashids = ">1.0.0" [[package]] category = "main" @@ -96,6 +107,14 @@ optional = false python-versions = "*" version = "2019.1" +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + [[package]] category = "main" description = "Non-validating SQL parser" @@ -105,18 +124,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.3.0" [metadata] -content-hash = "c43f0bd50249004be4901cfff7d61980e159550b962628442aa238ecde8a549e" +content-hash = "c0f5a0f9049ddba7e9ea83ea427cbf1bbad9fd9788ae51e45b069dea8eda7747" python-versions = "^3.6" [metadata.hashes] django = ["6fcc3cbd55b16f9a01f37de8bcbe286e0ea22e87096557f1511051780338eaea", "bb407d0bb46395ca1241f829f5bd03f7e482f97f7d1936e26e98dacb201ed4ec"] django-countries = ["5307a61172eee5740720e44ea08721858b7d8bf8509ec7701ccd7a8d21120b9a", "e4eaaec9bddb9365365109f833d1fd0ecc0cfee3348bf5441c0ccefb2d6917cd"] django-model-utils = ["2c057f3bf0859aba27f04389f0cedd2d48f8c9b3848acb86fd9970794e58f477", "8cd377744aa45f9f131d652ec460c57d1aaa88d3e9b586c8e27eb709341b9084"] +django-rest-auth = ["f11e12175dafeed772f50d740d22caeab27e99a3caca24ec65e66a8d6de16571"] django-safedelete = ["24037eb9d0e581d1aab350c2622db1398bbc47118da5d8a84c434aad20446a68"] djangorestframework = ["376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", "c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb"] +djangorestframework-jwt = ["5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7", "ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c"] djangorestframework-serializer-extensions = ["c383f18bf38ee1277cc8cb738f4667b8b9b6bd9a2f98ca938f8ac8c703a68ae4", "f44c5592dfd1289af1bd6afb6ba9b2afe0881c5cd9c242e8e76f08a9fcab16cb"] -djangorestframework-simplejwt = ["4cb1a51bf9b098b983e1326a1cfc65fb0b3c5a2aa0fa43effdbf05dd6da5e8cb", "d025211806622c53020aa4f66029f1f7305e38de6439116b4a842661fc02cf36"] hashids = ["6539b892a426e75747a9c0ad69409e9566f9c21b79310fc3424b5b6726f28da6"] pyjwt = ["5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", "8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"] pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] +six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] sqlparse = ["40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", "7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"] diff --git a/pyproject.toml b/pyproject.toml index 741f5c8..c548f9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ django-safedelete = "^0.5.1" django-model-utils = "^3.1" djangorestframework-serializer-extensions = "^1.0" django-countries = "^5.3" -djangorestframework_simplejwt = "^4.3" +django-rest-auth = "^0.9.5" +djangorestframework-jwt = "^1.11" [tool.poetry.dev-dependencies]