From 0e897644480342acb1e2a6542fa4c893e124cc41 Mon Sep 17 00:00:00 2001 From: DeadCoder Date: Fri, 17 Sep 2021 07:55:35 +0430 Subject: [PATCH 1/3] Adding auth and user app --- BaseDRF/asgi.py | 2 +- BaseDRF/auth_backends.py | 33 +++++++ BaseDRF/settings.py | 87 ++++++++++------- BaseDRF/urls.py | 26 ++---- BaseDRF/wsgi.py | 2 +- api/__init__.py | 0 api/admin.py | 3 + api/apps.py | 5 + api/migrations/__init__.py | 0 api/models.py | 3 + api/tests.py | 3 + api/views.py | 3 + auth_user/__init__.py | 0 auth_user/admin.py | 3 + auth_user/apps.py | 5 + auth_user/migrations/__init__.py | 0 auth_user/models.py | 3 + auth_user/serializers.py | 7 ++ auth_user/services.py | 17 ++++ auth_user/tests.py | 3 + auth_user/urls.py | 9 ++ auth_user/views.py | 46 +++++++++ docs/Base DRF.postman_collection.json | 72 ++++++++++++++ manage.py | 4 +- user/__init__.py | 0 user/admin.py | 12 +++ user/apps.py | 5 + user/migrations/0001_initial.py | 74 +++++++++++++++ user/migrations/0002_auto_20210917_0137.py | 23 +++++ user/migrations/0003_auto_20210917_0141.py | 21 +++++ user/migrations/__init__.py | 0 user/models.py | 104 +++++++++++++++++++++ user/serializers.py | 58 ++++++++++++ user/tests.py | 3 + user/views.py | 11 +++ 35 files changed, 591 insertions(+), 56 deletions(-) create mode 100644 BaseDRF/auth_backends.py create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/apps.py create mode 100644 api/migrations/__init__.py create mode 100644 api/models.py create mode 100644 api/tests.py create mode 100644 api/views.py create mode 100755 auth_user/__init__.py create mode 100755 auth_user/admin.py create mode 100755 auth_user/apps.py create mode 100755 auth_user/migrations/__init__.py create mode 100755 auth_user/models.py create mode 100755 auth_user/serializers.py create mode 100644 auth_user/services.py create mode 100755 auth_user/tests.py create mode 100755 auth_user/urls.py create mode 100755 auth_user/views.py create mode 100644 docs/Base DRF.postman_collection.json create mode 100755 user/__init__.py create mode 100755 user/admin.py create mode 100755 user/apps.py create mode 100644 user/migrations/0001_initial.py create mode 100644 user/migrations/0002_auto_20210917_0137.py create mode 100644 user/migrations/0003_auto_20210917_0141.py create mode 100755 user/migrations/__init__.py create mode 100755 user/models.py create mode 100755 user/serializers.py create mode 100755 user/tests.py create mode 100755 user/views.py diff --git a/BaseDRF/asgi.py b/BaseDRF/asgi.py index bdb8255..4c0a012 100644 --- a/BaseDRF/asgi.py +++ b/BaseDRF/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") application = get_asgi_application() diff --git a/BaseDRF/auth_backends.py b/BaseDRF/auth_backends.py new file mode 100644 index 0000000..128a914 --- /dev/null +++ b/BaseDRF/auth_backends.py @@ -0,0 +1,33 @@ +from user.models import User +from rest_framework import authentication +from rest_framework import exceptions + + +class EmailAuthentication(authentication.BaseAuthentication): + def authenticate(self, request): + email = request.data.get("username") + password = request.data.get("password") + + try: + user = User.objects.get(email=email) + if not user.check_password(password): + return None, None + except User.DoesNotExist: + return None, None + + return user, None + + +class PhoneNoAuthentication(authentication.BaseAuthentication): + def authenticate(self, request): + phone_no = request.data.get("username") + password = request.data.get("password") + + try: + user = User.objects.get(email=phone_no) + if not user.check_password(password): + return None, None + except User.DoesNotExist: + return None, None + + return user, None diff --git a/BaseDRF/settings.py b/BaseDRF/settings.py index f2a5c3d..fe86f9a 100644 --- a/BaseDRF/settings.py +++ b/BaseDRF/settings.py @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '^=+i9yxmh1++h22rq$by&nfb3wco-u92#iuosw0i7l5zl@a_)j' +SECRET_KEY = "^=+i9yxmh1++h22rq$by&nfb3wco-u92#iuosw0i7l5zl@a_)j" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,52 +31,54 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework_simplejwt", + "user", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'BaseDRF.urls' +ROOT_URLCONF = "BaseDRF.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'BaseDRF.wsgi.application' +WSGI_APPLICATION = "BaseDRF.wsgi.application" # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -86,16 +88,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -103,9 +105,9 @@ # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -117,4 +119,19 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" + + +AUTH_USER_MODEL = "user.User" +AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "BaseDRF.auth_backends.EmailAuthentication", + # 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + +OTP_EXPIRE_TIME = 60 + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/BaseDRF/urls.py b/BaseDRF/urls.py index 39fb3d9..b60ceef 100644 --- a/BaseDRF/urls.py +++ b/BaseDRF/urls.py @@ -1,21 +1,13 @@ -"""BaseDRF URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin -from django.urls import path +from django.urls import path, include +from rest_framework_simplejwt.views import ( + TokenRefreshView, +) +from user.views import MyTokenObtainPairView urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), + path("auth/", include("auth_user.urls")), + path("api/token/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), ] diff --git a/BaseDRF/wsgi.py b/BaseDRF/wsgi.py index d4e0edd..eba3a8c 100644 --- a/BaseDRF/wsgi.py +++ b/BaseDRF/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") application = get_wsgi_application() diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..14b89a8 --- /dev/null +++ b/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = "api" diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/auth_user/__init__.py b/auth_user/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/auth_user/admin.py b/auth_user/admin.py new file mode 100755 index 0000000..8c38f3f --- /dev/null +++ b/auth_user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/auth_user/apps.py b/auth_user/apps.py new file mode 100755 index 0000000..45d3337 --- /dev/null +++ b/auth_user/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthUserConfig(AppConfig): + name = "auth_user" diff --git a/auth_user/migrations/__init__.py b/auth_user/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/auth_user/models.py b/auth_user/models.py new file mode 100755 index 0000000..71a8362 --- /dev/null +++ b/auth_user/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/auth_user/serializers.py b/auth_user/serializers.py new file mode 100755 index 0000000..cfa440b --- /dev/null +++ b/auth_user/serializers.py @@ -0,0 +1,7 @@ +from django.db.models.fields import EmailField +from rest_framework import serializers, exceptions +from rest_framework.authtoken.serializers import AuthTokenSerializer + + +class SendOTPSerializer(serializers.Serializer): + username = serializers.CharField(required=True) diff --git a/auth_user/services.py b/auth_user/services.py new file mode 100644 index 0000000..fc8fd27 --- /dev/null +++ b/auth_user/services.py @@ -0,0 +1,17 @@ +from user.models import User + + +def get_user(username): + user = None + + try: + if "@" in username: + user = User.objects.get(email=username) + elif username.isdigit(): + user = User.objects.get(phone_no=username) + else: + user = User.objects.get(username=username) + except User.DoesNotExist: + pass + + return user diff --git a/auth_user/tests.py b/auth_user/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/auth_user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/auth_user/urls.py b/auth_user/urls.py new file mode 100755 index 0000000..f8b4766 --- /dev/null +++ b/auth_user/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .views import AuthViewSet + +router = DefaultRouter() +router.register("", AuthViewSet, "auth-user") + +urlpatterns = router.urls diff --git a/auth_user/views.py b/auth_user/views.py new file mode 100755 index 0000000..34d848c --- /dev/null +++ b/auth_user/views.py @@ -0,0 +1,46 @@ +from auth_user.services import ( + get_user, +) +from django.conf import settings +from django.utils import timezone +from BaseDRF.settings import OTP_EXPIRE_TIME +from pyotp import TOTP +from rest_framework import response +from rest_framework.authtoken.views import Token +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from user.models import User + +from .serializers import ( + SendOTPSerializer, +) + + +class AuthViewSet(GenericViewSet): + permission_classes = [] + + @staticmethod + def generate_otp(user: User) -> str: + now = int(timezone.now().timestamp()) + print(OTP_EXPIRE_TIME) + print(TOTP(user.key, interval=OTP_EXPIRE_TIME).at(now)) + return TOTP(user.key, interval=OTP_EXPIRE_TIME).at(now) + + @action( + methods=["post"], + detail=False, + serializer_class=SendOTPSerializer, + ) + def send_otp(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user = get_user(serializer.validated_data["username"]) + print(user) + otp = self.__class__.generate_otp(user) + user.sendOTP(otp=otp) + if settings.DEBUG: + return Response({"otp": otp}) + return Response("sent") diff --git a/docs/Base DRF.postman_collection.json b/docs/Base DRF.postman_collection.json new file mode 100644 index 0000000..b77643b --- /dev/null +++ b/docs/Base DRF.postman_collection.json @@ -0,0 +1,72 @@ +{ + "info": { + "_postman_id": "71aa44b1-98f6-4662-a786-ce8d62e53524", + "name": "Base DRF", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Auth", + "item": [ + { + "name": "Login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"09388309605\",\n \"password\": \"518446\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8001/api/token/", + "host": [ + "localhost" + ], + "port": "8001", + "path": [ + "api", + "token", + "" + ] + } + }, + "response": [] + }, + { + "name": "Login Copy", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"09388309605\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8001/auth/send_otp/", + "host": [ + "localhost" + ], + "port": "8001", + "path": [ + "auth", + "send_otp", + "" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/manage.py b/manage.py index 6d8d992..e8675a3 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BaseDRF.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BaseDRF.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/user/__init__.py b/user/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/user/admin.py b/user/admin.py new file mode 100755 index 0000000..ffc5076 --- /dev/null +++ b/user/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.urls import path +from django.template.response import TemplateResponse +from django.urls import reverse +from django.utils.html import format_html +from .models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + pass diff --git a/user/apps.py b/user/apps.py new file mode 100755 index 0000000..1f2369a --- /dev/null +++ b/user/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + name = "user" diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..6c51c03 --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 3.1 on 2021-09-17 01:35 + +from django.db import migrations, models +import django.utils.timezone +import pyotp + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("username", models.CharField(max_length=45, unique=True)), + ("email", models.CharField(max_length=45, unique=True)), + ("phone_no", models.CharField(max_length=45, unique=True)), + ( + "key", + models.CharField( + blank=True, default=pyotp.random_base32, max_length=16 + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ("is_active", models.BooleanField(default=False)), + ("is_valid", models.BooleanField(default=False)), + ("is_superuser", models.BooleanField(default=False)), + ("is_staff", models.BooleanField(default=False)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "db_table": "users", + }, + ), + ] diff --git a/user/migrations/0002_auto_20210917_0137.py b/user/migrations/0002_auto_20210917_0137.py new file mode 100644 index 0000000..a6e49b7 --- /dev/null +++ b/user/migrations/0002_auto_20210917_0137.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2021-09-17 01:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="email", + field=models.CharField(blank=True, max_length=45, null=True, unique=True), + ), + migrations.AlterField( + model_name="user", + name="phone_no", + field=models.CharField(blank=True, max_length=45, null=True, unique=True), + ), + ] diff --git a/user/migrations/0003_auto_20210917_0141.py b/user/migrations/0003_auto_20210917_0141.py new file mode 100644 index 0000000..5a5a847 --- /dev/null +++ b/user/migrations/0003_auto_20210917_0141.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1 on 2021-09-17 01:41 + +from django.db import migrations, models +import pyotp + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0002_auto_20210917_0137"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="key", + field=models.CharField( + blank=True, default=pyotp.random_base32, max_length=40 + ), + ), + ] diff --git a/user/migrations/__init__.py b/user/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/user/models.py b/user/models.py new file mode 100755 index 0000000..508ba57 --- /dev/null +++ b/user/models.py @@ -0,0 +1,104 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils import timezone +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.contrib.auth.base_user import BaseUserManager +from django.core.mail import send_mail +from BaseDRF.settings import OTP_EXPIRE_TIME + +from pyotp import TOTP, random_base32 + + +class UserManager(BaseUserManager): + def create_user(self, username, email, phone_no, password, **extra_fields): + if not email and not phone_no: + raise ValueError("Eigther email or phone_no must be set") + + if not email: + email = None + if not phone_no: + phone_no = None + + if not username: + if email: + username = email + if phone_no: + username = phone_no + + user = self.model( + username=username, email=email, phone_no=phone_no, **extra_fields + ) + user.set_password(password) + user.save() + return user + + def create_superuser(self, username, email, phone_no, password, **extra_fields): + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_active", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") + + return self.create_user(username, email, phone_no, password, **extra_fields) + + +class User(AbstractBaseUser, PermissionsMixin): + id = models.AutoField(primary_key=True) + + username = models.CharField(unique=True, max_length=45, null=True, blank=True) + email = models.CharField(unique=True, max_length=45, null=True, blank=True) + phone_no = models.CharField(unique=True, max_length=45, null=True, blank=True) + + # used for hashing + key = models.CharField(max_length=40, blank=True, default=random_base32) + + date_joined = models.DateTimeField(default=timezone.now, editable=False) + is_active = models.BooleanField(default=False) + is_valid = models.BooleanField(default=False) + + is_superuser = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + + USERNAME_FIELD = "username" + REQUIRED_FIELDS = ["email", "phone_no"] + + objects = UserManager() + + class Meta: + db_table = "users" + + def clean(self): + cleaned_data = super().clean() + if not self.email and not self.phone_no: + raise ValidationError( + { + "email": "Eigther email or phone_no must be set", + "phone_no": "Eigther email or phone_no must be set", + } + ) + + def verify_otp(self, otp): + if not self.key or not otp: + return False + + try: + delta = self.otp.delta_time + except: + delta = 0 + + return TOTP(self.key, interval=OTP_EXPIRE_TIME).verify( + otp, for_time=int(timezone.now().timestamp()) - delta + ) + + def sendOTP(self, otp, **params): + if self.email: + send_mail( + "BaseDRF activation code", + f"your activation code is {otp}", + "bshadmehr76@gmail.com", + [self.email], + fail_silently=False, + ) diff --git a/user/serializers.py b/user/serializers.py new file mode 100755 index 0000000..dbde565 --- /dev/null +++ b/user/serializers.py @@ -0,0 +1,58 @@ +from rest_framework import serializers, exceptions +from django.contrib.auth import authenticate +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from .models import User +from django.contrib.auth.models import update_last_login +from rest_framework_simplejwt.settings import api_settings as simplejwt_api_settings + + +class MyTokenObtainPairSerializer(TokenObtainPairSerializer): + def validate(self, attrs): + try: + request = self.context["request"] + except KeyError: + pass + + try: + if "@" in request.data.get("username"): + self.user = User.objects.get(email=request.data.get("username")) + elif request.data.get("username").isdigit(): + self.user = User.objects.get(phone_no=request.data.get("username")) + else: + self.user = User.objects.get(username=request.data.get("username")) + except User.DoesNotExist: + raise exceptions.AuthenticationFailed( + self.error_messages["no_active_account"], + "no_active_account", + ) + + if self.user.check_password(request.data.get("password")): + print(1) + pass + elif self.user.verify_otp(request.data.get("password")): + print(2) + pass + else: + print(3) + raise exceptions.AuthenticationFailed( + self.error_messages["no_active_account"], + "no_active_account", + ) + + if not simplejwt_api_settings.USER_AUTHENTICATION_RULE(self.user): + raise exceptions.AuthenticationFailed( + self.error_messages["no_active_account"], + "no_active_account", + ) + + data = {} + + refresh = self.get_token(self.user) + + data["refresh"] = str(refresh) + data["access"] = str(refresh.access_token) + + if simplejwt_api_settings.UPDATE_LAST_LOGIN: + update_last_login(None, self.user) + + return data diff --git a/user/tests.py b/user/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/user/views.py b/user/views.py new file mode 100755 index 0000000..1d007a6 --- /dev/null +++ b/user/views.py @@ -0,0 +1,11 @@ +from rest_framework_simplejwt.views import ( + TokenObtainPairView, +) +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from .serializers import MyTokenObtainPairSerializer +from django.shortcuts import render + + +class MyTokenObtainPairView(TokenObtainPairView): + def get_serializer_class(self): + return MyTokenObtainPairSerializer From 147285a41d598b22649f81aa31c5b0fa96ad5299 Mon Sep 17 00:00:00 2001 From: DeadCoder Date: Mon, 15 Nov 2021 21:27:54 +0330 Subject: [PATCH 2/3] Finilizing auth --- BaseDRF/auth_backends.py | 33 --------------------------------- BaseDRF/settings.py | 7 +++---- auth_user/views.py | 12 +++++++++--- user/serializers.py | 9 +++------ 4 files changed, 15 insertions(+), 46 deletions(-) delete mode 100644 BaseDRF/auth_backends.py diff --git a/BaseDRF/auth_backends.py b/BaseDRF/auth_backends.py deleted file mode 100644 index 128a914..0000000 --- a/BaseDRF/auth_backends.py +++ /dev/null @@ -1,33 +0,0 @@ -from user.models import User -from rest_framework import authentication -from rest_framework import exceptions - - -class EmailAuthentication(authentication.BaseAuthentication): - def authenticate(self, request): - email = request.data.get("username") - password = request.data.get("password") - - try: - user = User.objects.get(email=email) - if not user.check_password(password): - return None, None - except User.DoesNotExist: - return None, None - - return user, None - - -class PhoneNoAuthentication(authentication.BaseAuthentication): - def authenticate(self, request): - phone_no = request.data.get("username") - password = request.data.get("password") - - try: - user = User.objects.get(email=phone_no) - if not user.check_password(password): - return None, None - except User.DoesNotExist: - return None, None - - return user, None diff --git a/BaseDRF/settings.py b/BaseDRF/settings.py index fe86f9a..56c5e62 100644 --- a/BaseDRF/settings.py +++ b/BaseDRF/settings.py @@ -126,10 +126,9 @@ AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ( - "BaseDRF.auth_backends.EmailAuthentication", - # 'rest_framework_simplejwt.authentication.JWTAuthentication', - ) + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework_simplejwt.authentication.JWTAuthentication", + ] } OTP_EXPIRE_TIME = 60 diff --git a/auth_user/views.py b/auth_user/views.py index 34d848c..cbc7cb6 100755 --- a/auth_user/views.py +++ b/auth_user/views.py @@ -24,8 +24,6 @@ class AuthViewSet(GenericViewSet): @staticmethod def generate_otp(user: User) -> str: now = int(timezone.now().timestamp()) - print(OTP_EXPIRE_TIME) - print(TOTP(user.key, interval=OTP_EXPIRE_TIME).at(now)) return TOTP(user.key, interval=OTP_EXPIRE_TIME).at(now) @action( @@ -38,9 +36,17 @@ def send_otp(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) user = get_user(serializer.validated_data["username"]) - print(user) otp = self.__class__.generate_otp(user) user.sendOTP(otp=otp) if settings.DEBUG: return Response({"otp": otp}) return Response("sent") + + @action( + methods=["post"], + detail=False, + permission_classes=[IsAuthenticated], + ) + def sample_auth(self, request, *args, **kwargs): + print(request.user.phone_no) + return Response("sent") diff --git a/user/serializers.py b/user/serializers.py index dbde565..f237373 100755 --- a/user/serializers.py +++ b/user/serializers.py @@ -27,17 +27,14 @@ def validate(self, attrs): ) if self.user.check_password(request.data.get("password")): - print(1) pass elif self.user.verify_otp(request.data.get("password")): - print(2) pass else: - print(3) raise exceptions.AuthenticationFailed( - self.error_messages["no_active_account"], - "no_active_account", - ) + self.error_messages["no_active_account"], + "no_active_account", + ) if not simplejwt_api_settings.USER_AUTHENTICATION_RULE(self.user): raise exceptions.AuthenticationFailed( From a407d01d97b3679eb31902aab474eb4a9acb0c90 Mon Sep 17 00:00:00 2001 From: DeadCoder Date: Mon, 15 Nov 2021 21:33:04 +0330 Subject: [PATCH 3/3] Adding grappelli to project --- BaseDRF/settings.py | 1 + BaseDRF/urls.py | 1 + requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/BaseDRF/settings.py b/BaseDRF/settings.py index 56c5e62..1288ccd 100644 --- a/BaseDRF/settings.py +++ b/BaseDRF/settings.py @@ -31,6 +31,7 @@ # Application definition INSTALLED_APPS = [ + "grappelli", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/BaseDRF/urls.py b/BaseDRF/urls.py index b60ceef..304c073 100644 --- a/BaseDRF/urls.py +++ b/BaseDRF/urls.py @@ -6,6 +6,7 @@ from user.views import MyTokenObtainPairView urlpatterns = [ + path("grappelli/", include("grappelli.urls")), # grappelli URLS path("admin/", admin.site.urls), path("auth/", include("auth_user.urls")), path("api/token/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"), diff --git a/requirements.txt b/requirements.txt index 1c0373d..70a09b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ asgiref==3.2.10 Django==3.1 pytz==2021.1 sqlparse==0.4.1 +django-grappelli