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..7ccfa2c 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,142 @@ 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. + + + +## JWT Tokens And Authentication + +> JWT is the JSON Web Token authentication plugin for the Django REST Framework. + +> JWT generates, access and refresh tokens for Users wanting to access information from the API + +>The command `permission_classes = (permissions.IsAuthenticated,)` located in `views.py` ensure that in order to access information of the API you must have logged in with your access token. + +* In order to get a JWT Token, you have to register a new user. + +* When a new user is created, email verification is sent to their email. You can try this by creating a user for yourself + +* After the User has been registered and has verified their email, they are able to login to the API (View Procedure to Login to the API Below) + +* Upon Logging In, when correct credentials are entered, you receive a 201 response from the API with the entered username, email and two tokens generated. + + +After you receive the tokens by logging in. You will notice that there is one that states, "Refresh" and another one that states "Access". + +* The Refresh token has a life span of 30 days (which can be changed in `settings.py` ) and is used to generate a new token when required. + +* The Acces token enables the users to have direct access to the API, it has a life span of 20 minutes (which can be changed in `settings.py` ) + +* When you have these tokens you can then login into the API interface and access all features. + +## How To Login A User + +> To be able to use the API and access all the information, a user has to login with generated access token + +To use the login feature and fully have access to the API, run these command in terminal: + + $ export DB_PASSWORD=arbitrarypassword + $ docker-compose run web python3 manage.py migrate + $ docker-compose up + +On your web browser access the API using http://localhost:8000 + + +* When the API has loaded up, you would see an interphace with various function as shown in the image below + +![Swagger Interface](/docs/images/SW-1.PNG?raw=true) + +* Click the button that states `(Authorize)` + +* Upon clicking Authorize you will see a pop-up requesting for API Key + +![Swagger Interface: Authorise Function](/docs/images/SW-2.PNG?raw=true) + +* At this point enter Bearer followed by the ACCESS token and click Authorize `Example:(Bearer **************)`. This will give you full access to the API and its functions + +## How To Use Login Feature For User And Get Tokens + +>To be able to use the API and access all the information, a user has to login to access the login tokens + +To use the login feature for a user follow the following steps, run these command in terminal: + + $ export DB_PASSWORD=arbitrarypassword + $ docker-compose run web python3 manage.py migrate + $ docker-compose up + +On your web browser access the API using http://localhost:8000 + +* When the API has loaded up, you would see an interphace with various function as shown in the image below + +![Swagger Interface](/docs/images/SW-3.PNG?raw=true) + +* This shows various options and function. Click the function that states `(“/auth/login/)` + +* Upon click `(“/auth/login/)`, you will see an option that states `Try it out`, this will show you an area where you will have to input some parameters (email and password) accordingly, to test use sample users listed below. + +* Once this has been entered click `EXECUTE` + +* Please note, if you have registered a user and not verifid the email sent, you will be unable to login. + +![Swagger Interface: Login Feature](/docs/images/SW-4.PNG?raw=true) + +* If the current credentials have been entered you will see a Response Code 200 (OK) with the username and email entered, and the two tokens (REFRESH AND ACCESS TOKENS) showing that the user has been successfully registered. + +![Swagger Interface: User Login Features With Tokens ](/docs/images/SW-5.PNG?raw=true) + +## How To Register A User + +> To be able to use the API and access all the information, a user has to be created to access the login tokens + +To create a user follow the following steps, run these command in terminal: + + $ export DB_PASSWORD=arbitrarypassword + $ docker-compose run web python3 manage.py migrate + $ docker-compose up + +On your web browser access the API using http://localhost:8000 + + +* When the API has loaded up, you would see an interphace with various function as shown in the image below + +![Swagger Interface](/docs/images/SW-6.PNG?raw=true) + +![Swagger Interface](/docs/images/SW-7.PNG?raw=true) + +* This shows various options and function. Click the function that states `(“/auth/register/)` + +* Upon click `(“/auth/register/)`, you will see an option that states try it out, this will show you an area where you will have to input some parameters (email, username and password) accordingly. + +* Once this has been entered click `EXECUTE` + +![Swagger Interface: Register Function](/docs/images/SW-8.PNG?raw=true) + +* After a few seconds you will see a Response Code 201 (CREATED) with the username and email entered, showing that the user has been successfully registered. + +![Swagger Interface: User Successfully Registered](/docs/images/SW-9.PNG?raw=true) + + +## How To Create Superuser + +>In order to log into the admin interface, you have to create a superuser. + +To create a super user follow the following steps, run these command in terminal: + + $ export DB_PASSWORD=arbitrarypassword + $ docker-compose run web python3 manage.py createsuperuser + $ Fill in the details accordingly + +## How To Log In To Admin Interface + +> In order to access the admin interface , you have to use your created superuser details. + +In order to acces the admin interphace follow the following steps, run these command in terminal: + + $ export DB_PASSWORD=arbitrarypassword + $ docker-compose run web python3 manage.py migrate + $ docker-compose up + +Access localhost http://localhost:8000/admin from your web browser + +Login in with superuser details diff --git a/django/authentication/Tests/__init__.py b/django/authentication/Tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/authentication/Tests/test_models.py b/django/authentication/Tests/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/django/authentication/Tests/test_setup.py b/django/authentication/Tests/test_setup.py new file mode 100644 index 0000000..3fd264d --- /dev/null +++ b/django/authentication/Tests/test_setup.py @@ -0,0 +1,24 @@ +from rest_framework.test import APITestCase +from django.urls import reverse + +#Reverse takes in a view name and gives us a path to the route +class TestSetUp(APITestCase): + + def setUp(self): + self.register_url = reverse('register') + self.register_url = reverse('login') + + self.user_data= { + 'email':"new_test@gmail.com", + 'username':"testemail", + 'password':"Password1", + } + + return super().setUp() + + def tearDown(self): + return super().tearDown() + + + + diff --git a/django/authentication/Tests/test_views.py b/django/authentication/Tests/test_views.py new file mode 100644 index 0000000..1aa6531 --- /dev/null +++ b/django/authentication/Tests/test_views.py @@ -0,0 +1,20 @@ +from .test_setup import TestSetUp + +class TestViews(TestSetUp): + def test_user_cannot_register_without_data(self): + res = self.client.post(self.register_url) + self.assertEqual(res.status_code , 400) + + def test_user_can_register_correctly(self): + res = self.client.post( + self.register_url, self.user_data, format="json") + #self.assertEqual(res.data['email'] ,self.user_data['email']) + #self.assertEqual(res.data['username'] ,self.user_data['username']) + self.assertEqual(res.status_code , 201) + + def test_user_cannot_login_with_unverified_email(self): + self.client.post( + self.register_url, self.user_data, format="json") + res = self.client.post(self.login_url, self.user_data, format="json") + self.assertEqual(res.status_code, 401) + \ No newline at end of file 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..e1c313b --- /dev/null +++ b/django/authentication/serializers.py @@ -0,0 +1,116 @@ +from rest_framework import serializers +from .models import User +from django.contrib import auth +from rest_framework.exceptions import AuthenticationFailed +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils.encoding import smart_str, force_str, smart_bytes, DjangoUnicodeDecodeError +from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode +from django.contrib.sites.shortcuts import get_current_site +from django.urls import reverse +from .utils import Util + +#We import force_str in order to retreive the human readable id +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) + + +class ResetPasswordRequestEmailSerializer(serializers.ModelSerializer): #Email Verification Serializer + token = serializers.CharField(max_length=555) + + class Meta: + model = User + fields = ['token'] + + +class ResetPasswordEmailRequestSerializer(serializers.Serializer): + email = serializers.EmailField(min_length=8) + class Meta: + fields = ['email'] + +class SetNewPasswordSerializer(serializers.Serializer): + password = serializers.CharField( + max_length=68, min_length=6, write_only=True) + uidb64 = serializers.CharField( + min_length=1, write_only=True) + token = serializers.CharField( + min_length=1,write_only=True) + + class Meta: + fields = ['password','token','uidb64'] + + def validate(self,attrs): + try: + password=attrs.get('password') + uidb64=attrs.get('uidb64') + token=attrs.get('token') + + id=force_str(urlsafe_base64_decode(uidb64)) + user=User.objects.get(id=id) + if not PasswordResetTokenGenerator().check_token(user, token): + raise AuthenticationFailed('The reset link is Invalid', 401) + + user.set_password(password) + user.save() + + return (user) + except Exception as e: + raise AuthenticationFailed('The reset link is Invalid', 401) + \ No newline at end of file diff --git a/django/authentication/urls.py b/django/authentication/urls.py new file mode 100644 index 0000000..e694ad3 --- /dev/null +++ b/django/authentication/urls.py @@ -0,0 +1,20 @@ +from django.urls import path +from .views import RegisterView,VerifyEmail,LoginApiView,PasswordTokenCheckAPI,RequestPasswordResetEmail,SetNewPasswordAPIView +from rest_framework_simplejwt.views import ( + TokenRefreshView, +) + + +urlpatterns = [ + path('register/',RegisterView.as_view(), name="register"), + path('login/',LoginApiView.as_view(), name="login"), + path('email-verify/',VerifyEmail.as_view(), name="email-verify"), + # This uses the JWT Package to enable users to generate access tokens when logged in by using their current refresh token + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('request-reset-email/',RequestPasswordResetEmail.as_view(), + name='request-reset-email'), + path('password-reset-complete', + SetNewPasswordAPIView.as_view(), name='password-reset-complete'), + path('password-reset///', + PasswordTokenCheckAPI.as_view(), name='password-reset-confirm') #This url coiolects the encoded user information and token sent to their email for resseting password +] 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..34df674 --- /dev/null +++ b/django/authentication/views.py @@ -0,0 +1,120 @@ +from django.shortcuts import render +from rest_framework import generics, status, views +from .serializers import RegisterSerializer,EmailVerificationSerializer, LoginSerializer, ResetPasswordEmailRequestSerializer, SetNewPasswordSerializer +from rest_framework.generics import GenericAPIView +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 +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from .renderers import UserRenderer +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils.encoding import smart_str, force_str, smart_bytes, DjangoUnicodeDecodeError +from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode +from django.contrib.sites.shortcuts import get_current_site +from django.urls import reverse +from .utils import Util +# Create your views here. + +class RegisterView(generics.GenericAPIView):#Allows for User Registeration + 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): # Verifies Email + serializer_class = EmailVerificationSerializer + + token_param_config = openapi.Parameter('token',in_=openapi.IN_QUERY,description='Description',type=openapi.TYPE_STRING) + + @swagger_auto_schema(manual_parameters=[token_param_config]) + 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): #Login Api View + 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) + +class RequestPasswordResetEmail(generics.GenericAPIView): #This sends suser email and validates if user is registered to the database + serializer_class=ResetPasswordEmailRequestSerializer + def post(self,request): + serializer = self.serializer_class(data=request.data) + email = request.data['email'] + if User.objects.filter(email=email).exists(): + user = User.objects.get(email=email) + uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) + token = PasswordResetTokenGenerator().make_token(user) + current_site = get_current_site( + request=request).domain + relativeLink = reverse('password-reset-confirm',kwargs={'uidb64':uidb64, 'token':token}) + absurl = 'http://'+current_site + relativeLink + email_body = 'Hi '+user.username+ ' Hello, \n Use link below to Reset you Password \n' + absurl + data ={'email_body':email_body,'to_email':user.email, 'email_subject': 'Reset Your Password'} + Util.send_email(data) + return Response({'Success':'We have sent you a link to reset your password'},status=status.HTTP_200_OK) + else: + return Response({'Error':'You do not have an account registered'},status=status.HTTP_400_BAD_REQUEST) + +class PasswordTokenCheckAPI(generics.GenericAPIView): #checks if Token is Valid + def get(self, request, uidb64, token): + try: + id = smart_str(urlsafe_base64_decode(uidb64)) + user = User.objects.get(id=id) + + if not PasswordResetTokenGenerator().check_token(user, token): # This ensures that user is not re-using the resetlink + return Response({'Error':'Token is not valid, please request a new one'},status=status.HTTP_401_UNAUTHORIZED) + + return Response({'Success':True, 'Message':'Credentials Valid','uidb64':uidb64, 'Token':token},status=status.HTTP_200_OK) + + + except DjangoUnicodeDecodeError as identifier: #This handles erros, for example if the user, tampers with the token. + return Response({'Error':'Token is not valid, please request a new one'},status=status.HTTP_401_UNAUTHORIZED) + +class SetNewPasswordAPIView(generics.GenericAPIView): + serializer_class = SetNewPasswordSerializer + + def patch(self,request):# We use patch, because we are changing the Users Password + serializer=self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + return Response({'Success':True, 'Message':'Password Reset Success'},status=status.HTTP_200_OK) diff --git a/django/lmvpinterface/views.py b/django/lmvpinterface/views.py index 980170e..a347800 100644 --- a/django/lmvpinterface/views.py +++ b/django/lmvpinterface/views.py @@ -1,19 +1,26 @@ from django.shortcuts import render from .models import * from rest_framework import viewsets +from rest_framework import permissions + # Create your views here. class UserViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) queryset = User.objects.all().order_by('username') serializer_class = UserSerializer class CommitViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) queryset = Commit.objects.all().order_by('created') # TODO add filtering by author and project serializer_class = CommitSerializer class ProjectViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) queryset = Project.objects.all().order_by('name') serializer_class = ProjectSerializer class TextMetricViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) queryset = TextMetric.objects.all().order_by('name') # TODO add filtering by project and commit serializer_class = TextMetricSerializer class NumericMetricViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) queryset = NumericMetric.objects.all().order_by('name') serializer_class = NumericMetricSerializer diff --git a/django/webapp/settings.py b/django/webapp/settings.py index 56d8608..55724bd 100644 --- a/django/webapp/settings.py +++ b/django/webapp/settings.py @@ -11,7 +11,7 @@ """ import os - +import datetime # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -32,6 +32,8 @@ else: ALLOWED_HOSTS = [] +AUTH_USER_MODEL = 'authentication.User' + # Application definition INSTALLED_APPS = [ @@ -43,8 +45,34 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'authentication', + 'drf_yasg' ] +SWAGGER_SETTINGS = { #This is used to intialise authorisation with API Key rather than Email and Password + 'SECURITY_DEFINITIONS':{ + 'Bearer':{ + 'type':'apiKey', + 'name':'Authorization', + 'in':'header' + } + } +} + +#In Order to use Authorize with API Key, you use the login model which produces an API key and input Bearer'API Key generated' to access + +REST_FRAMEWORK = { + 'NON_FIELD_ERRORS_KEY':'error', + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + +SIMPLE_JWT = { #This controls the lifetime of the access and refresh tokens + 'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=20), + 'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=30), +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -128,3 +156,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..055c2f6 100644 --- a/django/webapp/urls.py +++ b/django/webapp/urls.py @@ -15,8 +15,28 @@ """ from django.contrib import admin from django.urls import path, include +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="LMVP (Lightweight Model Versioning Platform) API", + default_version='v1', + description="Open source neural network versioning system that separates model management and training operations", + terms_of_service="(To Be Added)", + contact=openapi.Contact(email="hello@umassdsc.com"), + license=openapi.License(name="BSD-3-Clause License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + urlpatterns = [ path('admin/', admin.site.urls), + path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('', include('lmvpinterface.urls')), + path('auth/', include('authentication.urls')), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), ] diff --git a/docs/images/SW-1.png b/docs/images/SW-1.png new file mode 100644 index 0000000..79b93fb Binary files /dev/null and b/docs/images/SW-1.png differ diff --git a/docs/images/SW-2.png b/docs/images/SW-2.png new file mode 100644 index 0000000..2f0359e Binary files /dev/null and b/docs/images/SW-2.png differ diff --git a/docs/images/SW-3.png b/docs/images/SW-3.png new file mode 100644 index 0000000..79b93fb Binary files /dev/null and b/docs/images/SW-3.png differ diff --git a/docs/images/SW-4.png b/docs/images/SW-4.png new file mode 100644 index 0000000..6aa3d15 Binary files /dev/null and b/docs/images/SW-4.png differ diff --git a/docs/images/SW-5.png b/docs/images/SW-5.png new file mode 100644 index 0000000..a24c285 Binary files /dev/null and b/docs/images/SW-5.png differ diff --git a/docs/images/SW-6.png b/docs/images/SW-6.png new file mode 100644 index 0000000..79b93fb Binary files /dev/null and b/docs/images/SW-6.png differ diff --git a/docs/images/SW-7.png b/docs/images/SW-7.png new file mode 100644 index 0000000..434193a Binary files /dev/null and b/docs/images/SW-7.png differ diff --git a/docs/images/SW-8.png b/docs/images/SW-8.png new file mode 100644 index 0000000..5e3dbdb Binary files /dev/null and b/docs/images/SW-8.png differ diff --git a/docs/images/SW-9.png b/docs/images/SW-9.png new file mode 100644 index 0000000..1e94903 Binary files /dev/null and b/docs/images/SW-9.png differ diff --git a/requirements.txt b/requirements.txt index c72ed93..6ff25f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django>=3.0,<4.0 psycopg2>=2.8,<3.0 djangorestframework>=3.9,<4.0 +djangorestframework-simplejwt==4.4.0 +drf_yasg==1.17.1 \ No newline at end of file