diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 623ac4a..4116b1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: max-parallel: 4 matrix: # move to 3.11 soon - python-version: [ 3.8 ] + python-version: [ 3.8, 3.11 ] steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2b4a2d..0ec3eac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,5 @@ exclude: .+/migrations/.+\.py repos: - - repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: @@ -19,3 +14,14 @@ repos: hooks: - id: isort args: [ "--profile", "black" ] + + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.15.0 + hooks: + - id: django-upgrade + args: [ --target-version, "4.2" ] + + - repo: https://github.com/psf/black + rev: 23.11.0 + hooks: + - id: black diff --git a/HisabKitab/custom_settings.py b/HisabKitab/custom_settings.py index 5f3eaf0..ae127e5 100644 --- a/HisabKitab/custom_settings.py +++ b/HisabKitab/custom_settings.py @@ -71,7 +71,7 @@ REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), - "DEFAULT_AUTHENTICATION_CLASSES": ("drfaddons.auth.JSONWebTokenAuthenticationQS",), + "DEFAULT_AUTHENTICATION_CLASSES": ("core.auth.JSONWebTokenAuthenticationQS",), "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", @@ -122,15 +122,6 @@ STATIC_ROOT = "static" -JET_THEMES = [ - {"theme": "default", "color": "#47bac1", "title": "Default"}, - {"theme": "green", "color": "#44b78b", "title": "Green"}, - {"theme": "light-green", "color": "#2faa60", "title": "Light Green"}, - {"theme": "light-violet", "color": "#a464c4", "title": "Light Violet"}, - {"theme": "light-blue", "color": "#5EADDE", "title": "Light Blue"}, - {"theme": "light-gray", "color": "#222", "title": "Light Gray"}, -] - # Sentry # ------------------------------------------------------------------------------ SENTRY_DSN = env("SENTRY_DSN") diff --git a/HisabKitab/settings.py b/HisabKitab/settings.py index 71fd1d4..89d788a 100644 --- a/HisabKitab/settings.py +++ b/HisabKitab/settings.py @@ -22,8 +22,6 @@ # Application definition INSTALLED_APPS = [ - "jet.dashboard", - "jet", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -88,4 +86,4 @@ USE_I18N = True -USE_L10N = True +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/HisabKitab/test_settings.py b/HisabKitab/test_settings.py index 19dfe28..14703ce 100644 --- a/HisabKitab/test_settings.py +++ b/HisabKitab/test_settings.py @@ -6,8 +6,6 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) INSTALLED_APPS = ( - "jet.dashboard", - "jet", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -107,7 +105,7 @@ REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), - "DEFAULT_AUTHENTICATION_CLASSES": ("drfaddons.auth.JSONWebTokenAuthenticationQS",), + "DEFAULT_AUTHENTICATION_CLASSES": ("core.auth.JSONWebTokenAuthenticationQS",), "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", diff --git a/HisabKitab/urls.py b/HisabKitab/urls.py index 4f56f8f..169036d 100644 --- a/HisabKitab/urls.py +++ b/HisabKitab/urls.py @@ -43,17 +43,13 @@ schema_view.without_ui(cache_timeout=None), name="schema-json", ), - re_path( - "swagger/$", + path( + "swagger/", schema_view.with_ui("swagger", cache_timeout=None), name="schema-swagger-ui", ), - re_path( - "redoc/$", schema_view.with_ui("redoc", cache_timeout=None), name="schema-redoc" - ), - path("jet/", include("jet.urls", "jet")), # Django JET URLSz path( - "jet/dashboard/", include("jet.dashboard.urls", "jet-dashboard") - ), # Django JET dashboard URLS + "redoc/", schema_view.with_ui("redoc", cache_timeout=None), name="schema-redoc" + ), path("", admin.site.urls), ] diff --git a/core/auth.py b/core/auth.py new file mode 100644 index 0000000..d656ea7 --- /dev/null +++ b/core/auth.py @@ -0,0 +1,146 @@ +import jwt +from django.conf import settings +from django.contrib.auth import get_user_model +from django.utils.encoding import smart_str +from django.utils.translation import gettext as _ +from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework.authentication import BaseAuthentication +from rest_framework_jwt.settings import api_settings +from six import text_type + +jwt_decode_handler = api_settings.JWT_DECODE_HANDLER +jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER + + +class JSONWebTokenAuthenticationQS(BaseAuthentication): + """ + Token based authentication using the JSON Web Token standard. + + This is a custom JWT Authentication class. The traditional one + can only authenticate from Header with a specific key only. + + This model will first look into HEADER and if the key is not found + there, it looks for key in the body. + Key is also changeable and can be set in Django settings as + JWT_AUTH_KEY with default value of Authorization. + + """ + + key = getattr(settings, "JWT_AUTH_KEY", "Authorization") + header_key = "HTTP_" + key.upper() + prefix = api_settings.JWT_AUTH_HEADER_PREFIX + cookie = api_settings.JWT_AUTH_COOKIE + + def get_authorization(self, request): + """ + This function extracts the authorization JWT string. It first + looks for specified key in header and then looks + for the same in body part. + + Parameters + ---------- + request: HttpRequest + This is the raw request that user has sent. + + Returns + ------- + auth: str + Return request's 'JWT_AUTH_KEY:' content from body or + Header, as a bytestring. + + Hide some test client ickyness where the header can be unicode. + """ + + auth = request.META.get(self.header_key, b"") + + if isinstance(auth, text_type): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + return auth + + def get_jwt_value(self, request): + """ + This function has been overloaded and it returns the proper JWT + auth string. + Parameters + ---------- + request: HttpRequest + This is the request that is received by DJango in the view. + Returns + ------- + str + This returns the extracted JWT auth token string. + + """ + + auth = self.get_authorization(request).split() + auth_header_prefix = self.prefix.lower() or "" + + if not auth: + return request.COOKIES.get(self.cookie) if self.cookie else None + if auth_header_prefix is None or len(auth_header_prefix) < 1: + auth.append("") + auth.reverse() + + if smart_str(auth[0].lower()) != auth_header_prefix: + return None + + if len(auth) == 1: + msg = _("Invalid Authorization header. No credentials provided.") + raise exceptions.AuthenticationFailed(msg) + + elif len(auth) > 2: + msg = _( + "Invalid Authorization header. Credentials string " + "should not contain spaces." + ) + raise exceptions.AuthenticationFailed(msg) + + return auth[1] + + def authenticate(self, request): + """ + Returns a two-tuple of `User` and token if a valid signature has been + supplied using JWT-based authentication. Otherwise returns `None`. + """ + jwt_value = self.get_jwt_value(request) + if jwt_value is None: + return None + + try: + payload = jwt_decode_handler(jwt_value) + except jwt.ExpiredSignature: + msg = _("Signature has expired.") + raise exceptions.AuthenticationFailed(msg) + except jwt.DecodeError: + msg = _("Error decoding signature.") + raise exceptions.AuthenticationFailed(msg) + except jwt.InvalidTokenError: + raise exceptions.AuthenticationFailed() + + user = self.authenticate_credentials(payload) + + return (user, jwt_value) + + def authenticate_credentials(self, payload): + """ + Returns an active user that matches the payload's user id and email. + """ + User = get_user_model() + username = jwt_get_username_from_payload(payload) + + if not username: + msg = _("Invalid payload.") + raise exceptions.AuthenticationFailed(msg) + + try: + user = User.objects.get_by_natural_key(username) + except User.DoesNotExist: + msg = _("Invalid signature.") + raise exceptions.AuthenticationFailed(msg) + + if not user.is_active: + msg = _("User account is disabled.") + raise exceptions.AuthenticationFailed(msg) + + return user diff --git a/drf_account/__init__.py b/drf_account/__init__.py index 7e6b8f1..4d5042c 100644 --- a/drf_account/__init__.py +++ b/drf_account/__init__.py @@ -2,5 +2,3 @@ __version__ = "0.0.2" __author__ = "Civil Machines Technologies Private Limited" __license__ = "GPLv3" - -default_app_config = "drf_account.apps.DRFAccountConfig" diff --git a/drf_contact/__init__.py b/drf_contact/__init__.py index 9139f4f..7a75b87 100644 --- a/drf_contact/__init__.py +++ b/drf_contact/__init__.py @@ -2,5 +2,3 @@ __version__ = "0.0.2" __author__ = "Civil Machines Technologies Private Limited" __license__ = "GPLv3" - -default_app_config = "drf_contact.apps.DRFContactConfig" diff --git a/drf_contact/urls.py b/drf_contact/urls.py index a02bc24..54a2a0a 100644 --- a/drf_contact/urls.py +++ b/drf_contact/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from . import views @@ -7,7 +7,7 @@ urlpatterns = [ # ex: api/contacts/show/ - url(r"^show/", views.ShowContacts.as_view(), name="show_contact"), + re_path(r"^show/", views.ShowContacts.as_view(), name="show_contact"), # ex: api/contacts/add/ - url(r"^add/", views.AddContacts.as_view(), name="add_contact"), + re_path(r"^add/", views.AddContacts.as_view(), name="add_contact"), ] diff --git a/drf_transaction/__init__.py b/drf_transaction/__init__.py index de8e326..19fe119 100644 --- a/drf_transaction/__init__.py +++ b/drf_transaction/__init__.py @@ -2,5 +2,3 @@ __version__ = "0.0.3" __author__ = "Civil Machines Technologies Private Limited" __license__ = "GPLv3" - -default_app_config = "drf_transaction.apps.DRFTransactionConfig" diff --git a/requirements-dev.txt b/requirements-dev.txt index 7dbe1f6..623f825 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -r requirements.txt -coverage==7.0.5 -factory_boy==3.2.1 -fakeredis[lua]==2.7.1 +coverage==7.3.2 +factory_boy==3.3.0 +fakeredis[lua]==2.20.0 diff --git a/requirements.txt b/requirements.txt index 02386bf..8e4248a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,18 @@ -Django==2.2.28 -django-braces==1.13.0 -django-cors-headers==3.2.1 +Django==4.2.7 +django-braces==1.15.0 +django-cors-headers==4.3.1 django-cors-middleware==1.5.0 -django-environ==0.4.5 -django-jet==1.0.8 -django-sendsms==0.3.1 -djangorestframework==3.11.2 +django-environ==0.11.2 +django-sendsms==0.5 +djangorestframework==3.14.0 djangorestframework-jwt==1.11.0 -drf-user==0.0.8 -drf-yasg==1.17.1 +drf-user==1.1.0 +drf-yasg==1.21.7 drfaddons==0.1.4 -huey==2.4.4 -pre-commit==2.20.0 -psycopg2-binary==2.8.6 +huey==2.5.0 +pre-commit==3.5.0 +psycopg2-binary==2.9.9 python-dateutil==2.8.2 -redis==4.4.4 -sentry-sdk==1.14.0 +redis==5.0.1 +sentry-sdk==1.38.0 six==1.16.0 diff --git a/users/admin.py b/users/admin.py index 0daf19c..061aca1 100644 --- a/users/admin.py +++ b/users/admin.py @@ -3,10 +3,10 @@ from .models import * +@admin.register(User) class AuthorAdmin(admin.ModelAdmin): readonly_fields = ("password",) -admin.site.register(User, AuthorAdmin) admin.site.register(OTPValidation) admin.site.register(AuthTransaction)