diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 79c63aff..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: 2.1 -orbs: - docker: circleci/docker@0.6.0 - -jobs: - test: - docker: - - image: circleci/python:3.8.0 - executor: docker/docker - steps: - - checkout - - run: - command: pip install --user tox coveralls - name: "Install Tox & Coverage" - - run: - command: tox - name: "Run Tox on All Supported Django and Python Versions" - - run: - command: | - mkdir -p test-results/ - tox -e coverage - name: "Generate Coverage Report" - - run: - command: COVERALLS_REPO_TOKEN=Q58WdUuZOi89XHyDeDsGE2lxUGQ2IfqP3 coveralls - name: "Send results to Coveralls" - - store_test_results: - path: test-results/ - build: - docker: - - image: circleci/python:3.8.0 - executor: docker/docker - steps: - - checkout - - run: - command: python3 setup.py sdist - name: Build - - store_artifacts: - path: dist/ - -workflows: - main: - jobs: - - test - - build: - requires: - - test diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..ef305e59 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +per-file-ignores = + dj_rest_auth/tests/test_serializers.py:E501,F401 + dj_rest_auth/serializers.py:E501 + dj_rest_auth/jwt_auth.py:E501 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39feedd1..44c79c7e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,14 +1,89 @@ -name: Release to PyPi -on: [push] +name: Lint, Build and Test +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Lint + run: flake8 dj_rest_auth/ --append-config ./.flake8 build: - name: Publish runs-on: ubuntu-latest + name: Build + needs: [lint] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + - name: Build + run: python3 setup.py sdist + - name: Store artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + test: + runs-on: ubuntu-20.04 + name: Test Python ${{ matrix.python-version }} + Django ~= ${{ matrix.django-version }} + needs: [build] + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + django-version: ['3.2', '4.2', '5.0'] + exclude: + - python-version: '3.8' + django-version: '5.0' + - python-version: '3.9' + django-version: '5.0' steps: - - name: Publish package - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install -r dj_rest_auth/tests/requirements.pip + pip install "Django~=${{ matrix.django-version }}.0" + - name: Run Tests + run: | + echo "$(python --version) / Django $(django-admin --version)" + coverage run ./runtests.py + - name: Generate Coverage Report + run: | + mkdir -p test-results/ + coverage report + coverage xml + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage.xml + - name: Store test results + uses: actions/upload-artifact@v4 with: - user: __token__ - password: ${{ secrets.pypi_password }} + name: results-${{ matrix.python-version }}-${{ matrix.django-version }} + path: test-results/ diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 7cd3279e..00000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: 'Automatically close stale issues and PRs' -on: - schedule: - - cron: '0 0 * * *' - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v5 - with: - # Number of days of inactivity before an issue becomes stale - daysUntilStale: 60 - # Number of days of inactivity before a stale issue is closed - daysUntilClose: 7 - # Issues with these labels will never be considered stale - exemptLabels: - - pinned - - security - # Label to use when marking an issue as stale - staleLabel: wontfix - # Comment to post when marking an issue as stale. Set to `false` to disable - markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. - # Comment to post when closing a stale issue. Set to `false` to disable - closeComment: false diff --git a/README.md b/README.md index d99e7e58..3ba816fd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Dj-Rest-Auth -[![](https://circleci.com/gh/iMerica/dj-rest-auth.svg?style=svg)](https://app.circleci.com/pipelines/github/iMerica/dj-rest-auth) +[![](https://github.com/iMerica/dj-rest-auth/actions/workflows/main.yml/badge.svg)](https://github.com/iMerica/dj-rest-auth/actions/workflows/main.yml/) Drop-in API endpoints for handling authentication securely in Django Rest Framework. Works especially well with SPAs (e.g., React, Vue, Angular), and Mobile applications. ## Requirements -- Django 2, 3, or 4 (See Unit Test Coverage in CI) -- Python 3 +- Django 3, 4 and 5 (See Unit Test Coverage in CI) +- Python >= 3.8 ## Quick Setup diff --git a/dj_rest_auth/__version__.py b/dj_rest_auth/__version__.py index 43316ab2..c282a187 100644 --- a/dj_rest_auth/__version__.py +++ b/dj_rest_auth/__version__.py @@ -1,7 +1,7 @@ __title__ = 'dj-rest-auth' __description__ = 'Authentication and Registration in Django Rest Framework.' __url__ = 'http://github.com/iMerica/dj-rest-auth' -__version__ = '5.1.0' +__version__ = '6.0.0' __author__ = '@iMerica https://github.com/iMerica' __author_email__ = 'imichael@pm.me' __license__ = 'MIT' diff --git a/dj_rest_auth/app_settings.py b/dj_rest_auth/app_settings.py index d04f4b01..920f7089 100644 --- a/dj_rest_auth/app_settings.py +++ b/dj_rest_auth/app_settings.py @@ -35,7 +35,7 @@ 'JWT_AUTH_SECURE': False, 'JWT_AUTH_HTTPONLY': True, 'JWT_AUTH_SAMESITE': 'Lax', - 'JWT_AUTH_COOKIE_DOMAIN' : None, + 'JWT_AUTH_COOKIE_DOMAIN': None, 'JWT_AUTH_RETURN_EXPIRATION': False, 'JWT_AUTH_COOKIE_USE_CSRF': False, 'JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED': False, @@ -59,7 +59,7 @@ ) # List of settings that have been removed -REMOVED_SETTINGS = ( ) +REMOVED_SETTINGS = [] class APISettings(_APISettings): # pragma: no cover diff --git a/dj_rest_auth/forms.py b/dj_rest_auth/forms.py index 4c0efa57..78b66e0b 100644 --- a/dj_rest_auth/forms.py +++ b/dj_rest_auth/forms.py @@ -73,8 +73,7 @@ def save(self, request, **kwargs): 'uid': uid, } if ( - allauth_account_settings.AUTHENTICATION_METHOD - != allauth_account_settings.AuthenticationMethod.EMAIL + allauth_account_settings.AUTHENTICATION_METHOD != allauth_account_settings.AuthenticationMethod.EMAIL ): context['username'] = user_username(user) get_adapter(request).send_mail( diff --git a/dj_rest_auth/jwt_auth.py b/dj_rest_auth/jwt_auth.py index 0d71bbba..67bc6dc5 100644 --- a/dj_rest_auth/jwt_auth.py +++ b/dj_rest_auth/jwt_auth.py @@ -18,7 +18,6 @@ def set_jwt_access_cookie(response, access_token): cookie_samesite = api_settings.JWT_AUTH_SAMESITE cookie_domain = api_settings.JWT_AUTH_COOKIE_DOMAIN - if cookie_name: response.set_cookie( cookie_name, @@ -139,7 +138,7 @@ def authenticate(self, request): if header is None: if cookie_name: raw_token = request.COOKIES.get(cookie_name) - if api_settings.JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED: #True at your own risk + if api_settings.JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED: # True at your own risk self.enforce_csrf(request) elif raw_token is not None and api_settings.JWT_AUTH_COOKIE_USE_CSRF: self.enforce_csrf(request) diff --git a/dj_rest_auth/models.py b/dj_rest_auth/models.py index c85169aa..8e4ef920 100644 --- a/dj_rest_auth/models.py +++ b/dj_rest_auth/models.py @@ -4,6 +4,7 @@ from .app_settings import api_settings + def get_token_model(): token_model = api_settings.TOKEN_MODEL session_login = api_settings.SESSION_LOGIN @@ -15,8 +16,7 @@ def get_token_model(): 'more of `TOKEN_MODEL`, `USE_JWT` or `SESSION_LOGIN`' ) if ( - token_model == DefaultTokenModel - and 'rest_framework.authtoken' not in settings.INSTALLED_APPS + token_model == DefaultTokenModel and 'rest_framework.authtoken' not in settings.INSTALLED_APPS ): raise ImproperlyConfigured( 'You must include `rest_framework.authtoken` in INSTALLED_APPS ' @@ -24,4 +24,5 @@ def get_token_model(): ) return token_model + TokenModel = get_token_model() diff --git a/dj_rest_auth/serializers.py b/dj_rest_auth/serializers.py index c8c7c3bb..aa519563 100644 --- a/dj_rest_auth/serializers.py +++ b/dj_rest_auth/serializers.py @@ -110,8 +110,7 @@ def validate_auth_user_status(user): def validate_email_verification_status(user, email=None): from allauth.account import app_settings as allauth_account_settings if ( - allauth_account_settings.EMAIL_VERIFICATION == allauth_account_settings.EmailVerificationMethod.MANDATORY - and not user.emailaddress_set.filter(email=user.email, verified=True).exists() + allauth_account_settings.EMAIL_VERIFICATION == allauth_account_settings.EmailVerificationMethod.MANDATORY and not user.emailaddress_set.filter(email=user.email, verified=True).exists() ): raise serializers.ValidationError(_('E-mail is not verified.')) diff --git a/dj_rest_auth/tests/requirements.pip b/dj_rest_auth/tests/requirements.pip index ec63f0cf..214895e0 100644 --- a/dj_rest_auth/tests/requirements.pip +++ b/dj_rest_auth/tests/requirements.pip @@ -1,7 +1,6 @@ coveralls==1.11.1 django-allauth==0.61.1 -django>=2.2,<5.0 -djangorestframework-simplejwt==4.6.0 +djangorestframework-simplejwt>=5.3.1 flake8==3.8.4 responses==0.12.1 unittest-xml-reporting==3.0.4 diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index b2fbd407..571669ed 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -501,7 +501,7 @@ def test_registration_allowed_with_custom_no_password_serializer(self): self.assertEqual(new_user.username, payload['username']) self.assertFalse(new_user.has_usable_password()) - ## Also check that regular registration also works + # Also check that regular registration also works user_count = get_user_model().objects.all().count() # test empty payload @@ -514,7 +514,6 @@ def test_registration_allowed_with_custom_no_password_serializer(self): new_user = get_user_model().objects.latest('id') self.assertEqual(new_user.username, self.REGISTRATION_DATA['username']) - @override_api_settings(USE_JWT=True) def test_registration_with_jwt(self): user_count = get_user_model().objects.all().count() @@ -837,7 +836,7 @@ def test_wo_csrf_enforcement(self): self.assertTrue('jwt-auth' in list(client.cookies.keys())) self.assertEquals(resp.status_code, 200) - ## TEST WITH JWT AUTH HEADER + # TEST WITH JWT AUTH HEADER jwtclient = APIClient(enforce_csrf_checks=True) token = resp.data['access'] resp = jwtclient.get('/protected-view/', HTTP_AUTHORIZATION='Bearer ' + token) @@ -845,7 +844,7 @@ def test_wo_csrf_enforcement(self): resp = jwtclient.post('/protected-view/', {}, HTTP_AUTHORIZATION='Bearer ' + token) self.assertEquals(resp.status_code, 200) - ## TEST WITH COOKIES + # TEST WITH COOKIES resp = client.get('/protected-view/') self.assertEquals(resp.status_code, 200) @@ -883,7 +882,7 @@ def test_csrf_wo_login_csrf_enforcement(self): self.assertTrue('csrftoken' in list(client.cookies.keys())) self.assertEquals(resp.status_code, 200) - ## TEST WITH JWT AUTH HEADER + # TEST WITH JWT AUTH HEADER jwtclient = APIClient(enforce_csrf_checks=True) token = resp.data['access'] resp = jwtclient.get('/protected-view/') @@ -909,7 +908,7 @@ def test_csrf_wo_login_csrf_enforcement(self): @override_api_settings(USE_JWT=True) @override_api_settings(JWT_AUTH_COOKIE='jwt-auth') @override_api_settings(JWT_AUTH_COOKIE_USE_CSRF=True) - @override_api_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk + @override_api_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk @override_settings( REST_FRAMEWORK=dict( DEFAULT_AUTHENTICATION_CLASSES=[ @@ -942,9 +941,9 @@ def test_csrf_w_login_csrf_enforcement(self): self.assertTrue('csrftoken' in list(client.cookies.keys())) self.assertEquals(resp.status_code, 200) - ## TEST WITH JWT AUTH HEADER does not make sense + # TEST WITH JWT AUTH HEADER does not make sense - ## TEST WITH COOKIES + # TEST WITH COOKIES resp = client.get('/protected-view/') self.assertEquals(resp.status_code, 200) # fail w/o csrftoken in payload @@ -958,7 +957,7 @@ def test_csrf_w_login_csrf_enforcement(self): @override_api_settings(USE_JWT=True) @override_api_settings(JWT_AUTH_COOKIE='jwt-auth') @override_api_settings(JWT_AUTH_COOKIE_USE_CSRF=False) - @override_api_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk + @override_api_settings(JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED=True) # True at your own risk @override_settings( REST_FRAMEWORK=dict( DEFAULT_AUTHENTICATION_CLASSES=[ @@ -1064,7 +1063,7 @@ def test_custom_token_refresh_view(self): # Ensure access keys are provided in response self.assertIn('access', refresh_resp.data) self.assertIn('access_expiration', refresh_resp.data) - + @override_api_settings(JWT_AUTH_RETURN_EXPIRATION=True) @override_api_settings(USE_JWT=True) @override_api_settings(JWT_AUTH_COOKIE='xxx') diff --git a/dj_rest_auth/tests/test_serializers.py b/dj_rest_auth/tests/test_serializers.py index 93e6c15a..74f9281c 100644 --- a/dj_rest_auth/tests/test_serializers.py +++ b/dj_rest_auth/tests/test_serializers.py @@ -2,7 +2,7 @@ from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter from allauth.socialaccount.providers.facebook.views import FacebookProvider from allauth.socialaccount.models import SocialApp -from allauth.exceptions import ImmediateHttpResponse +from allauth.core.exceptions import ImmediateHttpResponse from django.contrib.auth import get_user_model from django.urls import reverse from django.core.exceptions import ValidationError diff --git a/dj_rest_auth/tests/test_social.py b/dj_rest_auth/tests/test_social.py index e2b20f14..492b7b01 100644 --- a/dj_rest_auth/tests/test_social.py +++ b/dj_rest_auth/tests/test_social.py @@ -3,7 +3,6 @@ import responses from allauth.socialaccount.models import SocialApp from allauth.socialaccount.providers.facebook.provider import GRAPH_API_URL -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.sites.models import Site from django.test import TestCase diff --git a/dj_rest_auth/tests/urls.py b/dj_rest_auth/tests/urls.py index 7ea09a06..c9794e5f 100644 --- a/dj_rest_auth/tests/urls.py +++ b/dj_rest_auth/tests/urls.py @@ -71,6 +71,7 @@ def validate_password1(self, password): return super().validate_password1(password) return None + class NoPasswordRegisterView(RegisterView): serializer_class = NoPassowrdRegisterSerializer diff --git a/dj_rest_auth/tests/utils.py b/dj_rest_auth/tests/utils.py index 6094cc66..5ae7871f 100644 --- a/dj_rest_auth/tests/utils.py +++ b/dj_rest_auth/tests/utils.py @@ -40,4 +40,4 @@ def override_api_settings(**settings): try: delattr(api_settings, k) except AttributeError: - pass \ No newline at end of file + pass diff --git a/dj_rest_auth/views.py b/dj_rest_auth/views.py index de5915ba..d37c3245 100644 --- a/dj_rest_auth/views.py +++ b/dj_rest_auth/views.py @@ -189,13 +189,13 @@ def logout(self, request): token = RefreshToken(request.COOKIES[api_settings.JWT_AUTH_REFRESH_COOKIE]) except KeyError: response.data = {'detail': _('Refresh token was not included in cookie data.')} - response.status_code =status.HTTP_401_UNAUTHORIZED + response.status_code = status.HTTP_401_UNAUTHORIZED else: try: token = RefreshToken(request.data['refresh']) except KeyError: response.data = {'detail': _('Refresh token was not included in request data.')} - response.status_code =status.HTTP_401_UNAUTHORIZED + response.status_code = status.HTTP_401_UNAUTHORIZED token.blacklist() except (TokenError, AttributeError, TypeError) as error: diff --git a/docs/index.rst b/docs/index.rst index 249e377a..f13ea485 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,11 +7,8 @@ Welcome to dj-rest-auth's documentation! ============================================ -.. note:: dj-rest-auth version 1.0.0 now uses Django Simple JWT. - - -.. image:: https://circleci.com/gh/iMerica/dj-rest-auth.svg?style=svg - :target: https://circleci.com/gh/iMerica/dj-rest-auth +.. image:: https://github.com/iMerica/dj-rest-auth/actions/workflows/main.yml/badge.svg + :target: https://github.com/iMerica/dj-rest-auth/actions/workflows/main.yml Contents -------- diff --git a/setup.py b/setup.py index f6df8d4a..91f50c4b 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ keywords='django rest auth registration rest-framework django-registration api', zip_safe=False, install_requires=[ - 'Django>=3.2,<5.0', + 'Django>=3.2,<6.0', 'djangorestframework>=3.13.0', ], extras_require={ @@ -43,7 +43,7 @@ ], test_suite='runtests.runtests', include_package_data=True, - python_requires='>=3.6', + python_requires='>=3.8', classifiers=[ 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/tox.ini b/tox.ini index 30b42fe6..a6e69be6 100644 --- a/tox.ini +++ b/tox.ini @@ -10,16 +10,24 @@ [tox] skipsdist = true envlist = - python{3.6,3.7,3.8,3.9}-django3 - python{3.8,3.9,3.10,3.11}-django4 + python{3.8,3.9}-django{3,4} + python{3.10,3.11}-django{4,5} + +[gh-actions] +python = + 3.8: python3.8-django3, python3.8-django4 + 3.9: python3.9-django3, python3.9-django4 + 3.10: python3.10-django4, python3.10-django5 + 3.11: python3.11-django4, python3.11-django5 [testenv] commands = python ./runtests.py deps = -rdj_rest_auth/tests/requirements.pip - django3: Django>=3.2 - django4: Django>=4.0,<4.3 + django3: Django>=3.2,<4.0 + django4: Django>=4.0,<5.0 + django5: Django>=5.0,<6.0 # Configuration for coverage and flake8 is being set in `./setup.cfg` [testenv:coverage]