Unverified Commit 992b99a9 authored by Sanjay Boddu's avatar Sanjay Boddu Committed by GitHub

Merge pull request #3 from Ensembl/feat/user_authentication_api

User authentication API
parents 2028d0eb 00d4627c
......@@ -10,7 +10,7 @@ before_script:
variables:
# Configure mysql service (https://hub.docker.com/_/mysql/)
MYSQL_HOST: "mysql_db"
MYSQL_DATABASE: "test_thr_users"
MYSQL_DATABASE: "test_thr_db"
MYSQL_USER: "thr_dev"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
......
......@@ -77,7 +77,7 @@ Export the DB Configuration and turn on Debugging if necessary
```shell script
export DEBUG=1
export THR_DB_NAME=thr_users # The DB should already be created
export THR_DB_NAME=thr_db # The DB should already be created
export THR_DB_USER=user
export THR_DB_PASSWORD=password
export THR_HOST=localhost
......
......@@ -19,7 +19,7 @@ services:
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: "thr_users"
MYSQL_DATABASE: "thr_db"
MYSQL_USER: "thr_dev"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
\ No newline at end of file
asgiref==3.2.10
attrs==20.1.0
attrs==20.2.0
coverage==5.2.1
Django==3.1
Django==2.2
djangorestframework==3.11.1
iniconfig==1.0.1
more-itertools==8.5.0
mysqlclient==2.0.1
......
......@@ -9,7 +9,7 @@
<input type="submit" value="Login">
</form>
<a href="{% url 'thr-home' %}">Back to Home</a>
<a href="{% url 'thr_home' %}">Back to Home</a>
<a href="{% url 'password_reset' %}">Reset password</a>
<a href="{% url 'register' %}">Register</a>
......
......@@ -54,10 +54,18 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'thr_web.apps.ThrWebConfig',
'users.apps.UsersConfig',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......@@ -89,6 +97,9 @@ TEMPLATES = [
WSGI_APPLICATION = 'thr.wsgi.application'
# To uncomment later
# https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#substituting-a-custom-user-model
# AUTH_USER_MODEL = 'thr.users'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
......@@ -96,7 +107,7 @@ WSGI_APPLICATION = 'thr.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('THR_DB_NAME', 'thr_users'),
'NAME': os.environ.get('THR_DB_NAME', 'thr_db'),
'USER': os.environ.get('THR_DB_USER', 'thr_dev'),
'PASSWORD': os.environ.get('THR_DB_PASSWORD', 'password'),
'HOST': os.environ.get('THR_HOST', 'mysql_db'),
......@@ -152,7 +163,7 @@ STATICFILES_DIRS = [
]
LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'thr-home'
LOGOUT_REDIRECT_URL = 'thr_home'
LOGIN_URL = 'login'
EMAIL_HOST = "localhost"
......
......@@ -31,8 +31,15 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('thr_web.urls')),
path('', include('users.urls')),
path('user/', include('users.urls')),
# REST Framework URLs
path('api/user/', include('users.api.urls'), name='thr_users_api'),
# path('api/trackhub/', include('trackhubs.api.urls'), name='thr_trackhub_api'),
]
......@@ -13,9 +13,9 @@
"""
from django.urls import path
from . import views
from .views import HomeView, AboutView
urlpatterns = [
path('', views.home, name='thr-home'),
path('about/', views.about, name='thr-about')
path('', HomeView.as_view(), name='thr_home'),
path('about/', AboutView.as_view(), name='thr_about')
]
......@@ -12,12 +12,12 @@
limitations under the License.
"""
from django.shortcuts import render
from django.views.generic import TemplateView
def home(request):
return render(request, 'home.html')
class HomeView(TemplateView):
template_name = 'home.html'
def about(request):
return render(request, 'about.html')
class AboutView(TemplateView):
template_name = 'about.html'
from django import forms
from django.contrib.auth.models import User
from rest_framework import serializers
class RegistrationSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(style={'input_type': 'password'}, write_only=True)
class Meta:
model = User
fields = ['email', 'username', 'password', 'password2']
extra_kwargs = {
'password2': {'write_only': True}
}
def save(self):
user = User(
email=self.validated_data['email'],
username=self.validated_data['username'],
)
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2:
raise serializers.ValidationError({
'password': 'Passwords must match!'
})
user.set_password(password)
user.save()
return user
def validate(self, attrs):
if User.objects.filter(email=attrs['email']).exists():
raise serializers.ValidationError({
'email': 'Email already in use'
})
return super().validate(attrs)
"""
.. See the NOTICE file distributed with this work for additional information
regarding copyright ownership.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pytest
from django.urls import reverse
from rest_framework.authtoken.models import Token
@pytest.fixture
def api_client():
from rest_framework.test import APIClient
return APIClient()
@pytest.mark.django_db
def test_login_success(api_client, django_user_model):
"""
Test user login
:param api_client: the API client
:param django_user_model: a shortcut to the User model configured for use by the
current Django project
"""
username = 'user'
password = 'password'
django_user_model.objects.create_user(username=username, password=password)
url = reverse('login_api')
data = {
'username': username,
'password': password
}
response = api_client.post(url, data=data)
assert response.status_code == 200
@pytest.mark.django_db
@pytest.mark.parametrize(
'username, password, status_code', [
('', '', 400),
('', 'pass', 400),
('non_existing_user', 'pass', 400),
]
)
@pytest.mark.django_db
def test_login_fail(username, password, status_code, api_client):
"""
Test user login failure when the provided credential aren't correct
"""
url = reverse('login_api')
data = {
'username': username,
'password': password
}
response = api_client.post(url, data=data)
assert response.status_code == status_code
@pytest.mark.django_db
def test_logout_success(api_client, django_user_model):
"""
Log the user in and out while providing the access token
"""
user = django_user_model.objects.create_user(username='user', password='password')
token, _ = Token.objects.get_or_create(user=user)
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
url = reverse('logout_api')
response = api_client.post(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_logout_fail(api_client):
"""
Log the user in and out while providing the access token
"""
token = 'random14token77definitely895invalid'
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token)
url = reverse('logout_api')
response = api_client.post(url)
assert response.status_code == 401
@pytest.mark.django_db
@pytest.mark.parametrize(
'username, email, password, password2, status_code', [
('', '', '', '', 400),
('', '', 'test-pass', 'test-pass', 400),
('', 'user@example.com', '', '', 400),
('', 'user@example.com', 'pass', 'diff_pass', 400),
('user', 'invalid_email.com', 'test-pass', 'test-pass', 400),
('user', 'user@example.com', 'test-pass', 'test-pass', 201),
]
)
def test_registration(username, email, password, password2, status_code, api_client):
"""
Test user registration by providing different scenarios with the expected status_code
"""
url = reverse('register_api')
data = {
'email': email,
'username': username,
'password': password,
'password2': password2
}
response = api_client.post(url, data=data)
assert response.status_code == status_code
@pytest.mark.django_db
def test_unauthorized_request(api_client):
"""
Test unauthorized request, the user can't logout if he isn't logged in already
"""
url = reverse('logout_api')
response = api_client.post(url)
assert response.status_code == 401
@pytest.mark.django_db
def test_user_details_success(api_client, django_user_model):
"""
List the user details after providing the access token
"""
user = django_user_model.objects.create_user(username='user', password='password')
token, _ = Token.objects.get_or_create(user=user)
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
url = reverse('user_api')
response = api_client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_user_details_fail(api_client, django_user_model):
"""
List the user details after providing the access token
"""
token = 'another455random14token77definitely895invalid'
api_client.credentials(HTTP_AUTHORIZATION='Token ' + token)
url = reverse('user_api')
response = api_client.get(url)
assert response.status_code == 401
"""
.. See the NOTICE file distributed with this work for additional information
regarding copyright ownership.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
from .views import RegistrationViewAPI, LogoutViewAPI, UserDetailsView
urlpatterns = [
path('', UserDetailsView.as_view(), name='user_api'),
path('register', RegistrationViewAPI.as_view(), name='register_api'),
path('login', obtain_auth_token, name='login_api'),
path('logout', LogoutViewAPI.as_view(), name='logout_api'),
]
"""
.. See the NOTICE file distributed with this work for additional information
regarding copyright ownership.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from django.contrib.auth import logout
from rest_framework import status, authentication, permissions
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RegistrationSerializer
class RegistrationViewAPI(APIView):
"""
User registration endpoint, if the request is successful,
an HttpResponse is returned with the access token
:param request: the request
:returns: the data if the request was successful otherwise it return an error message
"""
def post(self, request):
serializer = RegistrationSerializer(data=request.data)
data = {}
if serializer.is_valid():
new_user = serializer.save()
data['response'] = 'User registered successfully!'
token = Token.objects.create(user=new_user).key
data['token'] = token
return Response(data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LogoutViewAPI(APIView):
"""
Log the users out if they are already logged in,
and delete the access token from the database
"""
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
"""
Returns the response message 200 or 401 (Invalid token)
"""
request.user.auth_token.delete()
logout(request)
return Response({"success": "Successfully logged out."}, status.HTTP_200_OK)
class UserDetailsView(APIView):
"""
Get the user details when providing a valid token
"""
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
return Response({
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name
})
......@@ -33,7 +33,7 @@ def test_superuser_create():
@pytest.mark.django_db
def test_view(client):
url = reverse('thr-home')
url = reverse('thr_home')
response = client.get(url)
assert response.status_code == 200
......
......@@ -12,14 +12,13 @@
limitations under the License.
"""
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.urls import path, include
from .views import dashboard, register
from django.contrib.auth import views as auth_views
from .views import DashboardView, RegistrationView
urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
path('dashboard/', dashboard, name='dashboard'),
path('register/', register, name='register'),
path('', include('django.contrib.auth.urls')),
path('dashboard/', login_required(DashboardView.as_view()), name='dashboard'),
path('register/', RegistrationView.as_view(), name='register'),
]
\ No newline at end of file
......@@ -12,26 +12,19 @@
limitations under the License.
"""
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.template import RequestContext
from .forms import CustomUserCreationForm
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, TemplateView
from .forms import CustomUserCreationForm
@login_required
def dashboard(request):
return render(request, 'user/dashboard.html')
class DashboardView(TemplateView):
template_name = 'user/dashboard.html'
def register(request):
if request.method == "POST":
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("dashboard")
else:
form = CustomUserCreationForm
return render(request, "user/register.html", {"form": form})
class RegistrationView(SuccessMessageMixin, CreateView):
template_name = 'user/register.html'
success_url = reverse_lazy('login')
form_class = CustomUserCreationForm
success_message = "Your profile was created successfully"
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment