Skip to content

Added authentication endpoints #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ FROM python:3.10-slim

WORKDIR /app
COPY requirements.txt /app
# Installing gcc and libc6-dev because docker removes them after building Python,
# so it's impossible to build C extensions afterwards, wcwidth==0.2.6 and cwcwidth==0.1.8 in this case
RUN apt-get update && apt-get install -y \
gcc \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*
RUN pip install -r requirements.txt
COPY . /app

Expand Down
1 change: 1 addition & 0 deletions backend/accounts/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Module for managing admin functionality related to accounts.
"""

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
Expand Down
1 change: 1 addition & 0 deletions backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
path("api/posts/<int:pk>/", views.PostDetail.as_view(), name="post-detail"),
path("api/posts/delete/<int:pk>/", views.PostDelete.as_view(), name="post-delete"),
path("api/signup", views.SignUpView.as_view(), name="signup-view"),
path("api/auth/", include("authentication.urls")),
]
Empty file.
6 changes: 6 additions & 0 deletions backend/authentication/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AuthenticationConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "authentication"
36 changes: 36 additions & 0 deletions backend/authentication/authenticate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings

from rest_framework.authentication import CSRFCheck
from rest_framework import exceptions


def enforce_csrf(request):
"""
Enforce CSRF validation.
"""

def dummy_get_response(request):
return None

check = CSRFCheck(dummy_get_response)
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
raise exceptions.PermissionDenied("CSRF Failed: %s" % reason)


class CustomAuthentication(JWTAuthentication):

def authenticate(self, request):
header = self.get_header(request)

if header is None:
raw_token = request.COOKIES.get(settings.SIMPLE_JWT["AUTH_COOKIE"]) or None
else:
raw_token = self.get_raw_token(header)
if raw_token is None:
return None

validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
Empty file.
9 changes: 9 additions & 0 deletions backend/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import serializers

from accounts.models import CustomUser


class AuthUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ["username", "password"]
55 changes: 55 additions & 0 deletions backend/authentication/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from accounts.models import CustomUser
from django.conf import settings


class LoginViewTestCase(TestCase):
def setUp(self):
LOGIN_URL = "api/v2/auth/login/"
self.client = APIClient()
self.active_user = CustomUser.objects.create_user(
username="activeuser", password="password123"
)
self.active_user.is_active = True
self.active_user.save()

self.inactive_user = CustomUser.objects.create_user(
username="inactiveuser", password="password123"
)
self.inactive_user.is_active = False
self.inactive_user.save()

self.url = LOGIN_URL

def test_login_successful(self):
response = self.client.post(
self.url, {"username": "activeuser", "password": "password123"}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("access", response.data)
self.assertIn(settings.SIMPLE_JWT["AUTH_COOKIE"], response.cookies)

def test_login_inactive_user(self):
response = self.client.post(
self.url, {"username": "inactiveuser", "password": "password123"}
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {"details": "This account is not active."})

def test_login_invalid_credentials(self):
response = self.client.post(
self.url, {"username": "wronguser", "password": "wrongpassword"}
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data, {"details": "Account with given credentials not found."}
)

def test_login_missing_fields(self):
response = self.client.post(self.url, {"username": "activeuser"})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data, {"details": "Account with given credentials not found."}
)
7 changes: 7 additions & 0 deletions backend/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from . import views

urlpatterns = [
path("login/", views.LoginView.as_view(), name="login"),
path("logout/", views.LogoutView.as_view(), name="logout"),
]
72 changes: 72 additions & 0 deletions backend/authentication/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response
from django.contrib.auth import authenticate
from rest_framework import status, generics
from django.conf import settings

from authentication.serializers import AuthUserSerializer


def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)

return {
"refresh": str(refresh),
"access": str(refresh.access_token),
}


class LoginView(generics.GenericAPIView):
permission_classes = []
authentication_classes = []
serializer_class = AuthUserSerializer

def post(self, request, format=None):
data = request.data
response = Response()
username = data.get("username", None)
password = data.get("password", None)
user = authenticate(username=username, password=password)

if user is not None:
if user.is_active:
data = get_tokens_for_user(user)
response.set_cookie(
key=settings.SIMPLE_JWT["AUTH_COOKIE"],
value=data["access"],
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"],
httponly=settings.SIMPLE_JWT["AUTH_COOKIE_HTTP_ONLY"],
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"],
max_age=823396,
)
response.data = data
response.status_code = status.HTTP_200_OK
return response
else:
return Response(
{"details": "This account is not active."},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"details": "Account with given credentials not found."},
status=status.HTTP_400_BAD_REQUEST,
)


class LogoutView(generics.GenericAPIView):
permission_classes = []
authentication_classes = []
serializer_class = None

def post(self, request):
response = Response()
response.set_cookie(
key=settings.SIMPLE_JWT["AUTH_COOKIE"],
max_age=0,
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"],
expires="Thu, 01 Jan 1970 00:00:00 GMT",
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"],
)
response.data = {"detail": "Logout successful."}
return response
15 changes: 15 additions & 0 deletions backend/chitchat/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

from pathlib import Path
from datetime import timedelta

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
Expand Down Expand Up @@ -48,6 +49,7 @@
"api",
"accounts",
"core",
"authentication",
# Dev tools
"django_extensions",
"drf_yasg",
Expand Down Expand Up @@ -147,9 +149,22 @@
# "rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_AUTHENTICATION_CLASSES": (
"authentication.authenticate.CustomAuthentication",
),
}


CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
]

SIMPLE_JWT = {
"AUTH_COOKIE": "access_token", # Cookie name. Enables cookies if value is set.
"AUTH_COOKIE_DOMAIN": None, # A string like "example.com", or None for standard domain cookie.
"AUTH_COOKIE_SECURE": True, # Whether the auth cookies should be secure (https:// only).
"AUTH_COOKIE_HTTP_ONLY": True, # Http only cookie flag.It's not fetch by javascript.
"AUTH_COOKIE_PATH": "/", # The path of the auth cookie.
"AUTH_COOKIE_SAMESITE": "None", # Whether to set the flag restricting cookie leaks on cross-site requests.
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
}
1 change: 1 addition & 0 deletions backend/chitchat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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, include
from rest_framework import permissions
Expand Down
Binary file modified backend/requirements.txt
Binary file not shown.