diff --git a/Fools_Arena/urls.py b/Fools_Arena/urls.py
index dd44f18..f8e673e 100644
--- a/Fools_Arena/urls.py
+++ b/Fools_Arena/urls.py
@@ -16,14 +16,20 @@
"""
from django.contrib import admin
-from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from django.http import HttpResponse
from django.shortcuts import redirect
+from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
+# UI
+ path('accounts/', include('accounts.urls')),
+
+ # API
+ path('api/accounts/', include('accounts.api_urls')),
+
]
# Add static files
diff --git a/accounts/api_urls.py b/accounts/api_urls.py
new file mode 100644
index 0000000..8d6dffa
--- /dev/null
+++ b/accounts/api_urls.py
@@ -0,0 +1,26 @@
+"""
+Authentication API routes for the Accounts app.
+
+This module defines the endpoints for user registration, login,
+profile retrieval, and logout. These routes are included in the
+project’s main urls.py under the prefix "api/accounts/", which means
+the final URLs are:
+
+ /api/accounts/auth/register/ → Register a new user
+ /api/accounts/auth/login/ → Log in an existing user
+ /api/accounts/auth/profile/ → Retrieve the authenticated user's profile
+ /api/accounts/auth/logout/ → Log out the current user
+
+Each path is mapped to a class-based API view defined in accounts/api_views.py.
+"""
+
+from django.urls import path
+from .api_views import RegistrationAPI, LoginAPI, ProfileAPI, LogoutAPI
+
+
+urlpatterns = [
+ path('auth/register/', RegistrationAPI.as_view(), name='api_register'),
+ path('auth/login/', LoginAPI.as_view(), name='api_login'),
+ path('auth/profile/', ProfileAPI.as_view(), name='api_profile'),
+ path('auth/logout/', LogoutAPI.as_view(), name='api_logout'),
+]
diff --git a/accounts/api_views.py b/accounts/api_views.py
new file mode 100644
index 0000000..6097b31
--- /dev/null
+++ b/accounts/api_views.py
@@ -0,0 +1,96 @@
+"""
+API views for the Accounts app.
+
+This module defines class-based views for handling user authentication
+via RESTful endpoints. It includes registration, login, profile retrieval,
+and logout functionality. These views are connected to the routes defined
+in accounts/api_urls.py and use serializers from accounts/serializers.py.
+
+Available API views:
+ - RegistrationAPI: create a new user and log them in automatically.
+ - LoginAPI: authenticate user credentials and start a session.
+ - ProfileAPI: return profile data for the authenticated user.
+ - LogoutAPI: end the current user session.
+"""
+
+from django.contrib.auth import login as auth_login, logout as auth_logout
+from rest_framework import generics, permissions, status
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from .serializers import RegistrationSerializer, LoginSerializer, ProfileSerializer
+
+class RegistrationAPI(generics.CreateAPIView):
+ """
+ API endpoint for user registration.
+
+ Handles the creation of a new user account using validated input.
+ Automatically logs in the newly created user to establish a session.
+ """
+ serializer_class = RegistrationSerializer
+ permission_classes = [permissions.AllowAny]
+
+ def perform_create(self, serializer):
+ """
+ Save the new user instance and log them in.
+
+ Overrides the default CreateAPIView behavior to attach the user
+ to the current session immediately after registration.
+ """
+ user = serializer.save()
+ auth_login(self.request, user)
+
+class LoginAPI(APIView):
+ """
+ API endpoint for user login.
+
+ Accepts username and password, authenticates the user,
+ and returns their profile data upon successful login.
+ """
+ permission_classes = [permissions.AllowAny]
+
+ def post(self, request):
+ """
+ Authenticate user credentials and start a session.
+
+ If credentials are valid, the user is logged in and their
+ profile data is returned in the response.
+ """
+ serializer = LoginSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ user = serializer.validated_data['user']
+ auth_login(request, user)
+ return Response(ProfileSerializer(user).data)
+
+class ProfileAPI(generics.RetrieveAPIView):
+ """
+ API endpoint for retrieving the authenticated user's profile.
+
+ Requires the user to be logged in. Returns basic profile information.
+ """
+ serializer_class = ProfileSerializer
+ permission_classes = [permissions.IsAuthenticated]
+
+ def get_object(self):
+ """
+ Return the current authenticated user.
+
+ Used by RetrieveAPIView to serialize and return profile data.
+ """
+ return self.request.user
+
+class LogoutAPI(APIView):
+ """
+ API endpoint for logging out the current user.
+
+ Requires authentication. Ends the session and returns a confirmation message.
+ """
+ permission_classes = [permissions.IsAuthenticated]
+
+ def post(self, request):
+ """
+ End the current user session.
+
+ Logs out the user and returns a success response.
+ """
+ auth_logout(request)
+ return Response({'detail': 'You are out of the system'}, status=status.HTTP_200_OK)
diff --git a/accounts/forms.py b/accounts/forms.py
new file mode 100644
index 0000000..945d67c
--- /dev/null
+++ b/accounts/forms.py
@@ -0,0 +1,41 @@
+"""
+Forms for the Accounts app.
+
+This module defines form classes used for user registration and login.
+They extend Django's built-in authentication forms to include additional
+fields or custom behavior where necessary.
+
+Available forms:
+ - RegistrationForm: extends UserCreationForm to include an email field.
+ - LoginForm: extends AuthenticationForm for user login.
+"""
+
+from django import forms
+from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+class RegistrationForm(UserCreationForm):
+ """
+ Form for user registration.
+
+ Extends Django's built-in UserCreationForm by adding
+ a required email field. Handles validation and creation
+ of a new user instance with username, email, and password.
+ """
+ email = forms.EmailField(required=True)
+
+ class Meta:
+ model = User
+ fields = ('username', 'email', 'password1', 'password2')
+
+class LoginForm(AuthenticationForm):
+ """
+ Form for user login.
+
+ Extends Django's built-in AuthenticationForm without
+ additional fields. Used to authenticate existing users
+ with their username and password.
+ """
+ pass
diff --git a/accounts/serializers.py b/accounts/serializers.py
new file mode 100644
index 0000000..979d751
--- /dev/null
+++ b/accounts/serializers.py
@@ -0,0 +1,76 @@
+"""
+Serializers for the Accounts app.
+
+This module defines serializers used for user authentication and profile
+management. They handle validation and transformation of input/output data
+between Django models and API views.
+
+Available serializers:
+ - RegistrationSerializer: validates and creates new user accounts.
+ - LoginSerializer: authenticates existing users with username/password.
+ - ProfileSerializer: returns basic profile information for authenticated users.
+"""
+
+from django.contrib.auth import authenticate, get_user_model
+from rest_framework import serializers
+
+User = get_user_model()
+
+class RegistrationSerializer(serializers.ModelSerializer):
+ """
+ Serializer for user registration.
+
+ Validates the provided username, email, and password.
+ Creates a new user instance with an encrypted password.
+ """
+ password = serializers.CharField(write_only=True, min_length=8)
+
+ class Meta:
+ model = User
+ fields = ('username', 'email', 'password')
+
+ def create(self, validated_data):
+ """
+ Create a new user with the given validated data.
+
+ Uses Django's built-in create_user method to ensure
+ the password is properly hashed before saving.
+ """
+ return User.objects.create_user(
+ username=validated_data['username'],
+ email=validated_data.get('email', ''),
+ password=validated_data['password'],
+ )
+
+class LoginSerializer(serializers.Serializer):
+ """
+ Serializer for user login.
+
+ Accepts username and password, and authenticates the user
+ using Django's built-in authentication system.
+ """
+ username = serializers.CharField()
+ password = serializers.CharField(write_only=True)
+
+ def validate(self, attrs):
+ """
+ Validate the provided credentials.
+
+ If authentication fails, raise a ValidationError.
+ On success, attach the authenticated user to attrs.
+ """
+ user = authenticate(username=attrs['username'], password=attrs['password'])
+ if not user:
+ raise serializers.ValidationError('Incorrect login details')
+ attrs['user'] = user
+ return attrs
+
+class ProfileSerializer(serializers.ModelSerializer):
+ """
+ Serializer for displaying user profile data.
+
+ Returns basic information about the authenticated user.
+ """
+ class Meta:
+ model = User
+ fields = ('id', 'username', 'email')
diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html
new file mode 100644
index 0000000..ca0bc63
--- /dev/null
+++ b/accounts/templates/accounts/login.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+
+{% block title %}Login{% endblock %}
+
+{% block content %}
+
Login
+
+
+ Don't have an account? Registration
+
+{% endblock %}
diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html
new file mode 100644
index 0000000..68191ef
--- /dev/null
+++ b/accounts/templates/accounts/profile.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Profile{% endblock %}
+
+{% block content %}
+Profile
+User name: {{ user.username }}
+Email: {{ user.email }}
+
+{% endblock %}
diff --git a/accounts/templates/accounts/registration.html b/accounts/templates/accounts/registration.html
new file mode 100644
index 0000000..52bbee1
--- /dev/null
+++ b/accounts/templates/accounts/registration.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Register{% endblock %}
+
+{% block content %}
+Registration
+
+Already exist? Login
+{% endblock %}
diff --git a/accounts/templates/base.html b/accounts/templates/base.html
new file mode 100644
index 0000000..61e5dd0
--- /dev/null
+++ b/accounts/templates/base.html
@@ -0,0 +1,27 @@
+
+
+
+
+ {% block title %}Fools Arena{% endblock %}
+
+
+
+
+
+ {% block content %}
+
+ {% endblock %}
+
+
+
+
+
diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py
new file mode 100644
index 0000000..c08c576
--- /dev/null
+++ b/accounts/tests/test_auth.py
@@ -0,0 +1,199 @@
+"""Authentication tests for both UI and REST API endpoints.
+
+This module contains test cases for verifying authentication flows in
+a Django application that provides both template-based (UI) and
+REST API endpoints. Tests cover registration, login, logout, and
+profile access behaviors.
+"""
+
+import pytest
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+User = get_user_model()
+
+
+@pytest.mark.django_db
+class TestTemplateAuth:
+ """Test suite for UI-based authentication using Django templates."""
+
+ def test_register_valid(self, client):
+ """Test successful registration through UI.
+
+ Sends valid registration data through a POST request to the 'register' view.
+ Ensures that the user is created and redirected to the profile page.
+
+ Args:
+ client (django.test.Client): Django test client fixture.
+ """
+ resp = client.post(reverse('register'), {
+ 'username': 'maksim',
+ 'email': 'm@example.com',
+ 'password1': 'StrongPass123',
+ 'password2': 'StrongPass123',
+ })
+ assert resp.status_code == 302
+ assert resp.url == reverse('profile')
+ assert User.objects.filter(username='maksim').exists()
+
+ def test_register_invalid_password_mismatch(self, client):
+ """Test registration with mismatched passwords.
+
+ Ensures that invalid password confirmation prevents user creation
+ and that the registration form is re-rendered with status 200.
+ """
+ resp = client.post(reverse('register'), {
+ 'username': 'bad',
+ 'email': 'b@example.com',
+ 'password1': 'StrongPass123',
+ 'password2': 'StrongPass124',
+ })
+ assert resp.status_code == 200
+ assert not User.objects.filter(username='bad').exists()
+
+ def test_login_valid(self, client, user_factory):
+ """Test successful login through UI.
+
+ Verifies that valid credentials redirect the user to the profile page.
+ """
+ user = user_factory(password="test123")
+ resp = client.post(reverse('login'), {
+ 'username': user.username,
+ 'password': 'test123',
+ })
+ assert resp.status_code == 302
+ assert resp.url == reverse('profile')
+
+ def test_login_invalid(self, client):
+ """Test login with invalid credentials.
+
+ Ensures the response remains on the login page (status 200) and does not redirect.
+ """
+ resp = client.post(reverse('login'), {
+ 'username': 'nope',
+ 'password': 'wrong'
+ })
+ assert resp.status_code == 200
+
+ def test_profile_requires_authentication(self, client):
+ """Test profile page access without authentication.
+
+ Ensures that unauthenticated users are redirected to the login page.
+ """
+ resp = client.get(reverse('profile'))
+ assert resp.status_code == 302
+ assert reverse('login') in resp.url
+
+ def test_logout(self, client, user_factory):
+ """Test logout functionality through UI.
+
+ Verifies that an authenticated user is logged out and redirected to the login page.
+ """
+ user = user_factory(password="test123")
+ client.post(reverse('login'), {'username': user.username, 'password': 'test123'})
+ resp = client.post(reverse('logout'))
+ assert resp.status_code == 302
+ assert resp.url == reverse('login')
+
+
+@pytest.mark.django_db
+class TestAPIAuth:
+ """Test suite for REST API authentication endpoints."""
+
+ def test_api_register_valid(self, api_client):
+ """Test successful API registration.
+
+ Sends valid user data to the registration endpoint and verifies
+ that a new user is created with HTTP 201 Created.
+ """
+ register_url = reverse("api_register")
+ resp = api_client.post(register_url, {
+ 'username': 'maksim_api',
+ 'email': 'mapi@example.com',
+ 'password': 'StrongPass123',
+ }, format='json')
+ assert resp.status_code == 201
+ assert User.objects.filter(username='maksim_api').exists()
+
+ def test_api_register_invalid(self, api_client):
+ """Test API registration with invalid data.
+
+ Ensures that malformed input returns HTTP 400 Bad Request.
+ """
+ register_url = reverse("api_register")
+ resp = api_client.post(register_url, {
+ 'username': '',
+ 'email': 'bad',
+ 'password': 'short',
+ }, format='json')
+ assert resp.status_code == 400
+
+ def test_api_login_valid(self, api_client, user_factory):
+ """Test successful API login.
+
+ Sends valid credentials to the login endpoint and verifies that
+ profile data is returned in the response.
+ """
+ user = user_factory(password="test123")
+ url = reverse("api_login")
+ resp = api_client.post(url, {
+ 'username': user.username,
+ 'password': 'test123'
+ }, format='json')
+ assert resp.status_code == 200
+ assert resp.data['username'] == user.username
+
+ def test_api_login_invalid(self, api_client):
+ """Test API login with invalid credentials.
+
+ Ensures that incorrect credentials return HTTP 400 Bad Request.
+ """
+ url = reverse("api_login")
+ resp = api_client.post(url, {
+ 'username': 'nope',
+ 'password': 'wrong'
+ }, format='json')
+ assert resp.status_code == 400
+
+ def test_api_profile_authenticated(self, api_client, user_factory):
+ """Test authenticated API profile access.
+
+ After logging in, verifies that the authenticated user can retrieve
+ their own profile data.
+ """
+ login_url = reverse("api_login")
+ user = user_factory(password="test123")
+ api_client.post(login_url, {
+ 'username': user.username,
+ 'password': 'test123'
+ }, format='json')
+ profile_url = reverse("api_profile")
+ resp = api_client.get(profile_url)
+ assert resp.status_code == 200
+ assert resp.data['username'] == user.username
+
+ def test_api_profile_unauthenticated(self, api_client):
+ """Test unauthenticated API profile access.
+
+ Ensures that accessing the profile endpoint without authentication
+ returns HTTP 403 Forbidden.
+ """
+ profile_url = reverse("api_profile")
+ resp = api_client.get(profile_url)
+ assert resp.status_code == 403
+
+ def test_api_logout(self, api_client, user_factory):
+ """Test API logout.
+
+ Verifies that an authenticated user can log out successfully,
+ receiving HTTP 200 OK in response.
+ """
+ login_url = reverse("api_login")
+ user = user_factory(password="test123")
+ api_client.post(login_url, {
+ 'username': user.username,
+ 'password': 'test123'
+ }, format='json')
+ logout_url = reverse("api_logout")
+ resp = api_client.post(logout_url)
+ assert resp.status_code == 200
diff --git a/accounts/urls.py b/accounts/urls.py
new file mode 100644
index 0000000..0444d0f
--- /dev/null
+++ b/accounts/urls.py
@@ -0,0 +1,25 @@
+"""
+Authentication template routes for the Accounts app.
+
+This module defines the endpoints for user registration, login,
+profile display, and logout. These routes are included in the
+project’s main urls.py under the prefix "accounts/", which means
+the final URLs are:
+
+ /accounts/register/ → Render the registration form and create a new user
+ /accounts/login/ → Render the login form and authenticate a user
+ /accounts/profile/ → Display the authenticated user's profile page
+ /accounts/logout/ → Log out the current user and redirect accordingly
+
+Each path is mapped to a function-based view defined in accounts/views.py.
+"""
+
+from django.urls import path
+from .views import register_view, login_view, profile_view, logout_view
+
+urlpatterns = [
+ path('register/', register_view, name='register'),
+ path('login/', login_view, name='login'),
+ path('profile/', profile_view, name='profile'),
+ path('logout/', logout_view, name='logout'),
+]
diff --git a/accounts/views.py b/accounts/views.py
index 91ea44a..55ce079 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,3 +1,84 @@
-from django.shortcuts import render
+"""
+Views for the Accounts app.
-# Create your views here.
+This module defines function-based views for handling user authentication
+through HTML templates. It includes registration, login, profile display,
+and logout functionality. These views are connected to the routes defined
+in accounts/urls.py and render templates located in accounts/templates/accounts/.
+
+Available views:
+ - register_view: render and process the registration form.
+ - login_view: render and process the login form.
+ - profile_view: display the authenticated user's profile page.
+ - logout_view: log out the current user and redirect accordingly.
+"""
+
+from django.contrib.auth import login as auth_login, logout as auth_logout
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import render, redirect
+from django.views.decorators.csrf import csrf_protect
+
+from .forms import RegistrationForm, LoginForm
+
+@csrf_protect
+def register_view(request):
+ """
+ Render and process the registration form.
+
+ If the request method is POST and the form is valid, a new user
+ is created and automatically logged in. On success, the user is
+ redirected to the profile page. Otherwise, the registration form
+ is re-rendered with validation errors.
+ """
+ if request.method == 'POST':
+ form = RegistrationForm(request.POST)
+ if form.is_valid():
+ user = form.save()
+ auth_login(request, user)
+ return redirect('profile')
+ else:
+ form = RegistrationForm()
+ return render(request, 'accounts/registration.html', {'form': form})
+
+@csrf_protect
+def login_view(request):
+ """
+ Render and process the login form.
+
+ If the request method is POST and the form is valid, the user
+ is authenticated and logged in. On success, the user is redirected
+ to the profile page. Otherwise, the login form is re-rendered with
+ validation errors.
+ """
+ if request.method == 'POST':
+ form = LoginForm(request, data=request.POST)
+ if form.is_valid():
+ auth_login(request, form.get_user())
+ return redirect('profile')
+ else:
+ form = LoginForm()
+ return render(request, 'accounts/login.html', {'form': form})
+
+@login_required
+def profile_view(request):
+ """
+ Display the authenticated user's profile page.
+
+ Requires the user to be logged in. If the user is not authenticated,
+ they will be redirected to the login page.
+ """
+ return render(request, 'accounts/profile.html')
+
+@csrf_protect
+def logout_view(request):
+ """
+ Log out the current user.
+
+ If the request method is POST, the user is logged out and redirected
+ to the login page. For non-POST requests, the user is redirected
+ back to the profile page.
+ """
+ if request.method == 'POST':
+ auth_logout(request)
+ return redirect('login')
+ return redirect('profile')
diff --git a/conftest.py b/conftest.py
index 23d949f..1a1b3b4 100644
--- a/conftest.py
+++ b/conftest.py
@@ -29,6 +29,7 @@ def test_basic_game_has_trump(basic_game, basic_cards):
django.setup()
from django.contrib.auth import get_user_model
+from rest_framework.test import APIClient
from game.models import (
CardSuit, CardRank, Card, Lobby, LobbySettings,
Game, GamePlayer, SpecialCard, SpecialRuleSet
@@ -357,3 +358,8 @@ def basic_rule_set(db):
description="Simple special cards for new players",
min_players=2
)
+
+@pytest.fixture
+def api_client():
+ """DRF APIClient for API-tests."""
+ return APIClient()