diff --git a/backend/videoflix/auth/__init__.py b/backend/videoflix/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/videoflix/auth/admin.py b/backend/videoflix/auth/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/videoflix/auth/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/videoflix/auth/apps.py b/backend/videoflix/auth/apps.py new file mode 100644 index 0000000..836fe02 --- /dev/null +++ b/backend/videoflix/auth/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'auth' diff --git a/backend/videoflix/auth/custom_backend.py b/backend/videoflix/auth/custom_backend.py new file mode 100644 index 0000000..f1997e1 --- /dev/null +++ b/backend/videoflix/auth/custom_backend.py @@ -0,0 +1,13 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model + +UserModel = get_user_model() + +class EmailBackend(ModelBackend): + def authenticate(self, request, email=None, password=None, **kwargs): + try: + user = UserModel.objects.get(email=email) + if user.check_password(password): + return user + except UserModel.DoesNotExist: + return None diff --git a/backend/videoflix/auth/migrations/__init__.py b/backend/videoflix/auth/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/videoflix/auth/models.py b/backend/videoflix/auth/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/videoflix/auth/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/videoflix/auth/serializer.py b/backend/videoflix/auth/serializer.py new file mode 100644 index 0000000..ad7ea93 --- /dev/null +++ b/backend/videoflix/auth/serializer.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +class LoginSerializer(serializers.Serializer): + email = serializers.EmailField(required=True) + password = serializers.CharField(write_only=True, required=True) + + def validate(self, data): + email = data.get('email') + password = data.get('password') + + if not email: + raise serializers.ValidationError("Email is required.") + + if not password: + raise serializers.ValidationError("Password is required.") + + return data diff --git a/backend/videoflix/auth/tests.py b/backend/videoflix/auth/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/videoflix/auth/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/videoflix/auth/views.py b/backend/videoflix/auth/views.py new file mode 100644 index 0000000..ac96a84 --- /dev/null +++ b/backend/videoflix/auth/views.py @@ -0,0 +1,27 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.authtoken.models import Token +from django.contrib.auth import authenticate +from .serializer import LoginSerializer + +class LoginView(APIView): + serializer_class = LoginSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + + if serializer.is_valid(): + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + user = authenticate(request, email=email, password=password) + + if user: + return self._create_token_response(user) + return Response({'detail': 'Unable to login with provided credentials.'}, status=status.HTTP_401_UNAUTHORIZED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def _create_token_response(self, user): + token, created = Token.objects.get_or_create(user=user) + return Response({token.key}, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/backend/videoflix/users/admin.py b/backend/videoflix/users/admin.py index 2616a02..c13af5a 100644 --- a/backend/videoflix/users/admin.py +++ b/backend/videoflix/users/admin.py @@ -4,19 +4,20 @@ from .forms import CustomUserCreationForm from django.contrib.auth.admin import UserAdmin @admin.register(CustomUser) -class CustomUserAdmin(admin.ModelAdmin): - add_form = CustomUserCreationForm +class CustomUserAdmin(UserAdmin): + add_form = CustomUserCreationForm - fieldsets = ( - *UserAdmin.fieldsets, - ( - 'Individual data', - { - 'fields': ( - 'custom', - 'phone', - 'address', - ) - } - ) - ) + fieldsets = ( + *UserAdmin.fieldsets, + ( + 'Individual data', + { + 'fields': ( + 'custom', + 'phone', + 'address', + ) + } + ) + ) + \ No newline at end of file diff --git a/backend/videoflix/users/models.py b/backend/videoflix/users/models.py index 8c6b5ca..fa9f6da 100644 --- a/backend/videoflix/users/models.py +++ b/backend/videoflix/users/models.py @@ -1,7 +1,7 @@ -from django.db import models from django.contrib.auth.models import AbstractUser +from django.db import models class CustomUser(AbstractUser): - custom = models.CharField(max_length=1000, default='') - phone = models.CharField(max_length=20, default='') - address = models.CharField(max_length=150, default='') + custom = models.CharField(max_length=1000, blank=True) + phone = models.CharField(max_length=20, blank=True) + address = models.CharField(max_length=150, blank=True) \ No newline at end of file diff --git a/backend/videoflix/users/views.py b/backend/videoflix/users/views.py index 28946e0..9008dda 100644 --- a/backend/videoflix/users/views.py +++ b/backend/videoflix/users/views.py @@ -1,12 +1,14 @@ from django.http import JsonResponse from django.shortcuts import render -from rest_framework.decorators import api_view +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status from .serializer import UserSerializer from .models import CustomUser @api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) def user_list(request): if request.method == 'GET': @@ -19,8 +21,10 @@ def user_list(request): if serializer.is_valid(): serializer.save return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET', 'PUT', 'DELETE']) +@permission_classes([IsAuthenticated]) def user_detail(request, id): try: @@ -35,7 +39,7 @@ def user_detail(request, id): elif request.method == 'PUT': serializer = UserSerializer(user, data=request.data) if serializer.is_valid(): - serializer().save() + serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/videoflix/videoflix/settings.py b/backend/videoflix/videoflix/settings.py index a72cffe..daeb408 100644 --- a/backend/videoflix/videoflix/settings.py +++ b/backend/videoflix/videoflix/settings.py @@ -32,7 +32,6 @@ CORS_ALLOWED_ORIGINS = [ ] # Application definition - INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -40,14 +39,12 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework.authtoken', 'rest_framework', 'corsheaders', 'users', ] -AUTH_USER_MODEL = 'users.CustomUser' - - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -57,7 +54,6 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware' ] ROOT_URLCONF = 'videoflix.urls' @@ -80,7 +76,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'videoflix.wsgi.application' - # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases @@ -132,3 +127,20 @@ STATIC_URL = 'static/' # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Auth + +AUTH_EMAIL_VERIFICATION = True +AUTH_USER_MODEL = 'users.CustomUser' + +# Rest Framework +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ], +} + +AUTHENTICATION_BACKENDS = ( + 'auth.custom_backend.EmailBackend', + 'django.contrib.auth.backends.ModelBackend', +) diff --git a/backend/videoflix/videoflix/urls.py b/backend/videoflix/videoflix/urls.py index 059b8d0..29f95c4 100644 --- a/backend/videoflix/videoflix/urls.py +++ b/backend/videoflix/videoflix/urls.py @@ -15,11 +15,14 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from users import views +from auth.views import LoginView urlpatterns = [ path('admin/', admin.site.urls), path('users/', views.user_list), path('users//', views.user_detail), + path('auth/login/', LoginView.as_view()), + ] diff --git a/frontend/src/app/components/auth/login/login.component.html b/frontend/src/app/components/auth/login/login.component.html index bb4f16e..780f938 100644 --- a/frontend/src/app/components/auth/login/login.component.html +++ b/frontend/src/app/components/auth/login/login.component.html @@ -28,7 +28,9 @@ } @else { @if (mail.touched && authData.mail.length > 0 && !isUserEmailValid(authData.mail.toLowerCase())) {

This is not a valid email format

- }} + }} @if ((errorService.error$ | async)) { +

{{ errorService.error$ | async }}

+ } { + const data = await lastValueFrom( + this.http.post(`${environment.baseUrl}/auth/login/`, body) + ); + const authToken = data.toString(); + localStorage.setItem('authToken', authToken); + } getAllUsers(): Observable { const headers = this.getAuthHeaders(); diff --git a/frontend/src/app/services/error.service.ts b/frontend/src/app/services/error.service.ts new file mode 100644 index 0000000..0d5507d --- /dev/null +++ b/frontend/src/app/services/error.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ErrorService { + private errorSubject = new BehaviorSubject(null); + error$ = this.errorSubject.asObservable(); + + setError(message: string | null) { + this.errorSubject.next(message); + } + + clearError() { + this.errorSubject.next(null); + } +}