diff --git a/.gitignore b/.gitignore index b26ab7e..4403a54 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ dmypy.json # Cython debug symbols cython_debug/ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index f08cd8f..1e4b731 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,4 @@ To make sure Docker automatically starts on boot, run `$ sudo systemctl enable docker` -this shouldn't be necessary, but you may as well run it just to be safe. \ No newline at end of file +this shouldn't be necessary, but you may as well run it just to be safe. diff --git a/django/authentication/__init__.py b/django/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/authentication/admin.py b/django/authentication/admin.py new file mode 100644 index 0000000..47e8131 --- /dev/null +++ b/django/authentication/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +# Register your models here. +from .models import User + +admin.site.register(User) \ No newline at end of file diff --git a/django/authentication/apps.py b/django/authentication/apps.py new file mode 100644 index 0000000..9635c9d --- /dev/null +++ b/django/authentication/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + name = 'authentication' diff --git a/django/authentication/migrations/0001_initial.py b/django/authentication/migrations/0001_initial.py new file mode 100644 index 0000000..e866bf1 --- /dev/null +++ b/django/authentication/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 3.0.8 on 2020-08-01 19:32 + +from django.db import migrations, models +import django.db.models.manager + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(db_index=True, max_length=90, unique=True)), + ('email', models.EmailField(db_index=True, max_length=70, unique=True)), + ('is_verified', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('created_date', models.DateTimeField(auto_now=True)), + ('updated_date', models.DateTimeField(auto_now_add=True)), + ('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={ + 'abstract': False, + }, + managers=[ + ('object', django.db.models.manager.Manager()), + ], + ), + ] diff --git a/django/authentication/migrations/0002_auto_20200801_2142.py b/django/authentication/migrations/0002_auto_20200801_2142.py new file mode 100644 index 0000000..4b10557 --- /dev/null +++ b/django/authentication/migrations/0002_auto_20200801_2142.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.8 on 2020-08-01 21:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ], + ), + ] diff --git a/django/authentication/migrations/0003_auto_20200802_0944.py b/django/authentication/migrations/0003_auto_20200802_0944.py new file mode 100644 index 0000000..1e25a9e --- /dev/null +++ b/django/authentication/migrations/0003_auto_20200802_0944.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-08-02 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0002_auto_20200801_2142'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='updated_date', + new_name='created_at', + ), + migrations.RenameField( + model_name='user', + old_name='created_date', + new_name='updated_at', + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(db_index=True, max_length=255, unique=True), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(db_index=True, max_length=255, unique=True), + ), + ] diff --git a/django/authentication/migrations/__init__.py b/django/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/authentication/models.py b/django/authentication/models.py new file mode 100644 index 0000000..5b90006 --- /dev/null +++ b/django/authentication/models.py @@ -0,0 +1,57 @@ +from django.db import models + +# Create your models here. +from django.contrib.auth.models import ( + AbstractBaseUser, BaseUserManager, PermissionsMixin) + +from django.db import models +from rest_framework_simplejwt.tokens import RefreshToken + + +class UserManager(BaseUserManager): + + def create_user(self, username, email, password=None): + if username is None: + raise TypeError('Users should have a username') + if email is None: + raise TypeError('Users should have a Email') + + user = self.model(username=username, email=self.normalize_email(email)) + user.set_password(password) + user.save() + return user + + def create_superuser(self, username, email, password=None): + if password is None: + raise TypeError('Password should not be none') + + user = self.create_user(username, email, password) + user.is_superuser = True + user.is_staff = True + user.save() + return user + + +class User(AbstractBaseUser, PermissionsMixin): + username = models.CharField(max_length=255, unique=True, db_index=True) + email = models.EmailField(max_length=255, unique=True, db_index=True) + is_verified = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + objects = UserManager() + + def __str__(self): + return self.email + + def tokens(self): + refresh = RefreshToken.for_user(self) + return { + 'refresh': str(refresh), + 'access': str(refresh.access_token) + } \ No newline at end of file diff --git a/django/authentication/renderers.py b/django/authentication/renderers.py new file mode 100644 index 0000000..73b851c --- /dev/null +++ b/django/authentication/renderers.py @@ -0,0 +1,13 @@ +from rest_framework import renderers +import json +class UserRenderer(renderers.JSONRenderer): #This pre-fixes responses with keywords, this ensures consitent responses in the API + charset = 'utf-8' + + def render(self, data, accepted_media_type=None, renderer_context=None): + response = '' + + if 'ErrorDetail' in str(data): + response= json.dumps({'errors':data}) + else: + response= json.dumps({'data':data}) + return response \ No newline at end of file diff --git a/django/authentication/serializers.py b/django/authentication/serializers.py new file mode 100644 index 0000000..36f1ba2 --- /dev/null +++ b/django/authentication/serializers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers +from .models import User +from django.contrib import auth +from rest_framework.exceptions import AuthenticationFailed + +class RegisterSerializer(serializers.ModelSerializer):#Added Registeration Serializer, with conditions + password = serializers.CharField( + max_length=255, min_length=8, write_only=True) + + class Meta: + model = User + fields = ['username', 'email', 'password' + ] + + def validate(self, attrs): + email = attrs.get('email', '') + username = attrs.get('username', '') + if not username.isalnum(): + raise serializers.ValidationError('This username should only contain alphanumeric characters') + return attrs + + def create(self, validated_data): + return User.objects.create_user(**validated_data) + +class EmailVerificationSerializer(serializers.ModelSerializer): #Email Verification Serializer + token = serializers.CharField(max_length=555) + + class Meta: + model = User + fields = ['token'] + +class LoginSerializer(serializers.ModelSerializer):# Created Login Serializer Linking refresh tokens from jwt + email = serializers.EmailField( + max_length=255, min_length=8) + password = serializers.CharField( + max_length=68, min_length=3, write_only=True) + username = serializers.CharField( + max_length=255, min_length=3, read_only=True) + tokens = serializers.CharField( + max_length=68, min_length=3,read_only=True) + + class Meta: + model=User + fields=['email','password','username','tokens'] + + + def validate(self, attrs): + email = attrs.get('email','') + password = attrs.get('password','') + + user = auth.authenticate(email=email, password=password) + if not user: + raise AuthenticationFailed('Invalid Credentials, try again') + if not user.is_active: + raise AuthenticationFailed('Account Disabled, contact Admin') + if not user.is_verified: + raise AuthenticationFailed('Email is not Verified') + + + return{ + 'email':user.email, + 'username':user.username, + 'tokens':user.tokens + } + return super().validate(attrs) + + \ No newline at end of file diff --git a/django/authentication/tests.py b/django/authentication/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django/authentication/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/authentication/urls.py b/django/authentication/urls.py new file mode 100644 index 0000000..e664c03 --- /dev/null +++ b/django/authentication/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import RegisterView,VerifyEmail,LoginApiView + + +urlpatterns = [ + path('register/',RegisterView.as_view(), name="register"), + path('login/',LoginApiView.as_view(), name="login"), + path('email-verify/',VerifyEmail.as_view(), name="email-verify"), +] diff --git a/django/authentication/utils.py b/django/authentication/utils.py new file mode 100644 index 0000000..9e81a28 --- /dev/null +++ b/django/authentication/utils.py @@ -0,0 +1,9 @@ +from django.core.mail import EmailMessage + +class Util: + @staticmethod + def send_email(data): + + email = EmailMessage( + subject=data['email_subject'],body=data['email_body'], to=[data['to_email']]) + email.send() diff --git a/django/authentication/views.py b/django/authentication/views.py new file mode 100644 index 0000000..229cbeb --- /dev/null +++ b/django/authentication/views.py @@ -0,0 +1,65 @@ +from django.shortcuts import render +from rest_framework import generics, status, views +from .serializers import RegisterSerializer,EmailVerificationSerializer, LoginSerializer +from rest_framework.response import Response +from django.conf import settings +from rest_framework.generics import GenericAPIView +from rest_framework_simplejwt.tokens import RefreshToken +from .models import User +from .utils import Util +from django.contrib.sites.shortcuts import get_current_site +from django.urls import reverse +import jwt +from django.conf import settings +from .renderers import UserRenderer +# Create your views here. + +class RegisterView(generics.GenericAPIView): + serializer_class = RegisterSerializer + renderer_classes = (UserRenderer,) + + def post(self, request): + user = request.data + serializer = self.serializer_class(data=user) + serializer.is_valid(raise_exception=True) + serializer.save() + user_data = serializer.data + user = User.objects.get(email=user_data['email']) + + token = RefreshToken.for_user(user).access_token + + current_site = get_current_site(request).domain + relativeLink = reverse('email-verify') + + absurl = 'http://'+current_site+relativeLink+"?token="+str(token) + email_body = 'Hi '+user.username+ ' Use link below to verify your Email \n' + absurl + data ={'email_body':email_body,'to_email':user.email, 'email_subject': 'Verify Your Email'} + Util.send_email(data) + + return Response(user_data, status=status.HTTP_201_CREATED ) + +class VerifyEmail(views.APIView): + serializer_class = EmailVerificationSerializer + + def get(self, request): + token = request.GET.get('token') + try: + payload = jwt.decode(token, settings.SECRET_KEY) + user = User.objects.get(id=payload['user_id']) + if not user.is_verified: + user.is_verified = True + user.save() + return Response({'email':'Successfully Verified'}, status=status.HTTP_200_OK ) + + except jwt.ExpiredSignatureError as identifier: + return Response({'error':'Activation Link Expired'}, status=status.HTTP_400_BAD_REQUEST) + except jwt.exceptions.DecodeError as identifier: + return Response({'error':'Invalid Token'}, status=status.HTTP_400_BAD_REQUEST) + +class LoginApiView(generics.GenericAPIView): + serializer_class=LoginSerializer + def post(self,request): + serializer=self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + return Response(serializer.data, status=status.HTTP_200_OK) + diff --git a/django/webapp/settings.py b/django/webapp/settings.py index 56d8608..cf348bc 100644 --- a/django/webapp/settings.py +++ b/django/webapp/settings.py @@ -32,6 +32,8 @@ else: ALLOWED_HOSTS = [] +AUTH_USER_MODEL = 'authentication.User' + # Application definition INSTALLED_APPS = [ @@ -43,8 +45,16 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'authentication', ] +REST_FRAMEWORK = { + 'NON_FIELD_ERRORS_KEY':'error', + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -128,3 +138,9 @@ # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' + +EMAIL_USE_TLS = True +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_HOST_USER = 'lmvptest@gmail.com' +EMAIL_HOST_PASSWORD = 'Lmvp@test123' \ No newline at end of file diff --git a/django/webapp/urls.py b/django/webapp/urls.py index a6c67d2..ba227f6 100644 --- a/django/webapp/urls.py +++ b/django/webapp/urls.py @@ -16,7 +16,9 @@ from django.contrib import admin from django.urls import path, include + urlpatterns = [ path('admin/', admin.site.urls), path('', include('lmvpinterface.urls')), + path('auth/', include('authentication.urls')), ] diff --git a/requirements.txt b/requirements.txt index c72ed93..09da823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django>=3.0,<4.0 psycopg2>=2.8,<3.0 djangorestframework>=3.9,<4.0 +djangorestframework-simplejwt==4.4.0