Skip to content

Commit

Permalink
Merge pull request #159 from HE-Arc/new-auth-system
Browse files Browse the repository at this point in the history
New auth system
  • Loading branch information
maelys-buhler authored Apr 21, 2024
2 parents 5d0ab1d + 5c764ef commit e16ec82
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 117 deletions.
9 changes: 7 additions & 2 deletions api/masteriq/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
'corsheaders',
'masteriqapp',
'rest_framework',

'rest_framework.authtoken',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -164,8 +164,13 @@
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
),
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}

AUTH_USER_MODEL='masteriqapp.CustomUser'

TOKEN_TIME_BEFORE_EXPIRATION_HOUR = 24
# end of file
12 changes: 7 additions & 5 deletions api/masteriq/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@
from django.urls.conf import include
from rest_framework.routers import DefaultRouter

from masteriqapp import views
from masteriqapp import views as masteriq_views
from rest_framework.authtoken import views


router = DefaultRouter()

router.register("category", views.IQView, basename="category")
router.register("question", views.QuestionView, basename="question")
router.register("rank", views.RankView, basename="rank")
router.register("user", views.AuthenticationView, basename="user")
router.register("category", masteriq_views.IQView, basename="category")
router.register("question", masteriq_views.QuestionView, basename="question")
router.register("rank", masteriq_views.RankView, basename="rank")
router.register("user", masteriq_views.AuthenticationView, basename="user")


urlpatterns = [
Expand Down
29 changes: 29 additions & 0 deletions api/masteriqapp/models/ExpiringToken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from datetime import datetime, timedelta

import pytz
from django.conf import settings
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed


# Source:
# https://stackoverflow.com/questions/14567586/token-authentication-for-restful-api-should-the-token-be-periodically-changed
class ExpiringTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')

if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')

# This is required for the time comparison
utc_now = datetime.utcnow()
utc_now = utc_now.replace(tzinfo=pytz.utc)

if token.created < utc_now - timedelta(hours=settings.TOKEN_TIME_BEFORE_EXPIRATION_HOUR):
raise exceptions.AuthenticationFailed('Token has expired')

return token.user, token
27 changes: 16 additions & 11 deletions api/masteriqapp/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,60 @@ def test_route(self):
print(response.status_code)
assert response.status_code == 201

response = c.get("/api/category/1/image/")
response = c.post("/api/user/token/", {"username":"test", "password":"test"})
token = "Token " + response.json()['token']
headers = {"Authorization":token}

response = c.get("/api/category/1/image/", headers=headers)
assert response.status_code == 200

response = c.get("/api/category/iq/")
response = c.get("/api/category/iq/", headers=headers)
assert response.status_code == 200
assert response.json()["1"]["category_name"] is not None
assert response.json()["1"]["user_iq"] is not None
assert response.status_code == 200

response = c.get("/api/question/1/new/")

response = c.get("/api/question/1/new/", headers=headers)
assert response.status_code == 200
assert response.json()['text'] is not None
assert response.json()['category'] is not None

response = c.post("/api/question/new_community/", {
"question": "How old is Harry Potter at the beginning of the first book?",
"options": ["11", "15", "He wasnt born"],
"answer": "1"})
"answer": "1"}, headers=headers)

response = c.get("/api/question/options/")
response = c.get("/api/question/options/", headers=headers)
assert response.status_code == 200
assert response.json()['question_id'] is not None
assert response.json()['number_of_options'] is not None
assert response.json()['options'] is not None
assert len(response.json()['options']) >= 2

response = c.get("/api/rank/1/leaderboard/")
response = c.get("/api/rank/1/leaderboard/", headers=headers)
assert response.status_code == 200
assert len(response.json()) > 0
assert response.json()[0]['user_id'] is not None
assert response.json()[0]['user_name'] is not None
assert response.json()[0]['user_iq'] is not None

response = c.get("/api/rank/global_leaderboard/")
response = c.get("/api/rank/global_leaderboard/", headers=headers)
assert response.status_code == 200
assert len(response.json()) > 0
assert response.json()[0]['user_id'] is not None
assert response.json()[0]['user_name'] is not None
assert response.json()[0]['user_iq'] is not None

response = c.get("/api/rank/1/user/")
response = c.get("/api/rank/1/user/", headers=headers)
assert response.status_code == 200
assert response.json()['user_rank'] is not None
assert response.json()['user_iq'] is not None

response = c.get("/api/rank/global_user/")
response = c.get("/api/rank/global_user/", headers=headers)
assert response.status_code == 200
assert response.json()['user_rank'] is not None
assert response.json()['user_iq'] is not None

response = c.get("/api/category/2/user_iq/")
response = c.get("/api/category/2/user_iq/", headers=headers)
assert response.status_code == 200
assert response.json()['user_iq'] is not None
49 changes: 30 additions & 19 deletions api/masteriqapp/views/AuthenticationView.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
from datetime import datetime, timedelta

import pytz
from django.apps import apps
from rest_framework import viewsets, status
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.auth import get_user_model
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from django.conf import settings

import masteriqapp.models.IQ

masteriq = apps.get_app_config("masteriqapp")


class AuthenticationView(viewsets.ViewSet):
class AuthenticationView(viewsets.ViewSet, ObtainAuthToken):
category_model = masteriq.get_model("Category")
iq_model = masteriq.get_model("IQ")

@action(detail=False, methods=['POST'], permission_classes=[AllowAny])
def login(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
login(request, user)
return Response({'message': 'Login successful'}, status=status.HTTP_200_OK)
else:
return Response({'message': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED)

@action(detail=False, methods=['POST'])
def logout(self, request):
logout(request)
return Response({'message': 'Logout successful'}, status=status.HTTP_200_OK)

@action(detail=False, methods=['POST'], permission_classes=[AllowAny])
def register(self, request):
username = request.data.get('username')
password = request.data.get('password')
if not get_user_model().objects.filter(username=username).exists():
user = get_user_model().objects.create_user(username=username, password=password)
self.create_iq_objects_for_new_user(user)
login(request, user)
return Response({'message': 'Register successful'}, status=status.HTTP_201_CREATED)
else:
return Response({'message': 'Username already exists'}, status=status.HTTP_400_BAD_REQUEST)

@action(detail=False, methods=['POST'], permission_classes=[AllowAny])
def token(self, request):
serializer = self.serializer_class(data=request.data,
context={'request': request})
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.validated_data['user'])
if not created:
# This is required for the time comparison
utc_now = datetime.utcnow()
utc_now = utc_now.replace(tzinfo=pytz.utc)

if token.created < utc_now - timedelta(hours=settings.TOKEN_TIME_BEFORE_EXPIRATION_HOUR):
token.delete()
token = Token.objects.create(user=serializer.validated_data['user'])

expiring_date = token.created + timedelta(hours=settings.TOKEN_TIME_BEFORE_EXPIRATION_HOUR)
return Response({
'token': token.key,
'expires': expiring_date
})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def create_iq_objects_for_new_user(self, user):
categories = self.category_model.objects.all()
for category in categories:
Expand Down
36 changes: 31 additions & 5 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,42 @@
import { RouterView } from 'vue-router'
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'
import router from './router'
import { getTokenFromCookie } from './api_client';
import { ref } from 'vue';
const isConnected = ref(false);
router.beforeEach(async (to, from, next) => {
const token = getTokenFromCookie();
// check if token exists
if (token !== undefined) {
isConnected.value = true;
} else {
isConnected.value = false;
}
// redirection if necessary
if (to.meta.requiresAuth) {
if (token !== undefined) {
next();
} else {
next('/'); // redirect to home page
}
} else {
next();
}
})
</script>

<template>
<Header />
<RouterView />
<Footer />
<Header :is-connected="isConnected" />
<RouterView />
<Footer />
</template>

<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Bevan:ital@0;1&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
</style>
Loading

0 comments on commit e16ec82

Please sign in to comment.