diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f..e9fb2c7 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1,13 @@ from django.contrib import admin +from .models import CustomUser +from .forms import CustomUserCreationForm, CustomUserChangeForm +from django.contrib.auth.admin import UserAdmin + + +@admin.register(CustomUser) +class CustomAdminUser(UserAdmin): + add_form = CustomUserCreationForm + form = CustomUserChangeForm + model = CustomUser + -# Register your models here. diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..17ba2f9 --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,14 @@ +from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from .models import CustomUser + + +class CustomUserCreationForm(UserCreationForm): + class Meta(UserCreationForm.Meta): + model = CustomUser + fields = ("email",) + + +class CustomUserChangeForm(UserChangeForm): + class Meta: + model = CustomUser + fields = ("email",) diff --git a/accounts/models.py b/accounts/models.py index 71a8362..caf361d 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,11 @@ from django.db import models +from django.contrib.auth.models import AbstractUser -# Create your models here. + +class CustomUser(AbstractUser): + email = models.EmailField(unique=True) + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + def __str__(self) -> str: + return self.email diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..10eed1d --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,52 @@ +from rest_framework import serializers +from accounts.models import CustomUser +from django.contrib.auth import authenticate + + +class CustomUserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ('id', 'username', 'email') + + +class UserRegistrationSerializer(serializers.ModelSerializer): + password1 = serializers.CharField(write_only=True) + password2 = serializers.CharField(write_only=True) + + class Meta: + model = CustomUser + fields = ('id', 'username', 'password1', 'password2', 'email') + extra_kwargs = {'password': {'write_only': True}} + + def validate(self, attrs): + if [attrs.password1] != [attrs.password2]: + raise serializers.ValidationError("Passwords must match") + password = attrs.get("password1", "") + if len(password) < 8: + raise serializers.ValidationError( + "Passwords must be at least 8 characters") + + return attrs + + def create(self, validated_data): + password = validated_data.pop("password1") + validated_data.pop("password2") + + return CustomUser.objects.create_user( + password=password, + **validated_data) + # user = RegisteredUser(**validated_data) + # user = set_password(validated_data['password']) + # user.save() + # return user + + +class UserLoginSerializer(serializers.ModelSerializer): + email = serializers.CharField() + password = serializers.CharField(write_only=True) + + def validate(self, data): + user = authenticate(**data) + if user and user.is_active: + return user + raise serializers.ValidationError("Credential Error") diff --git a/accounts/urls.py b/accounts/urls.py index df63034..4127210 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,10 +1,11 @@ -from django.urls import path -from .views import UserRegistrationView, UserLoginView, UserLogoutView +from django.urls import path, include +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from .views import * urlpatterns = [ - path('api/login/', UserLoginView.as_view(), name='login'), - path('api/logout/', UserLogoutView.as_view(), name='logout'), - path('api/register/', UserRegistrationView.as_view(), name='welcome'), - path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('api/token/refresh', TokenRefreshView.as_view(), name='token_fresh'), + path('login/', UserLoginView.as_view(), name='login'), + path('logout/', UserLogoutView.as_view(), name='logout'), + path('register/', UserRegistrationView.as_view(), name='register'), + path('token/', TokenObtainPairView.as_view(), name='token-obtain-pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh'), ] diff --git a/accounts/views.py b/accounts/views.py index e6ece4c..23b6459 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,30 +1,102 @@ # main/views.py from django.contrib.auth import authenticate, login, logout from rest_framework import status -from rest_framework.views import APIView +# from rest_framework.views import APIView +from rest_framework.generics import GenericAPIView from rest_framework.response import Response +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.models import User +from rest_framework.authtoken.models import Token +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.authentication import TokenAuthentication +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.tokens import RefreshToken +from .serializers import * +class UserRegistrationView(GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = UserRegistrationSerializer -class UserRegistrationView(APIView): - def post(self, request): + def post(self, request, *args, **kwargs): serializer = UserRegistrationSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.save() - return Response({"id": user.id, "username": user.username}, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.is_valid(raise_exception=True) + user = serializer.save() + token = RefreshToken.for_user(user) + data = serializer.data + + data["tokens"] = {"refresh": str(token), + "access": str(token.access_token)} + return Response(data, status=status.HTTP_201_CREATED) + # if serializer.is_valid(): + # user = serializer.save() + # return Response({"id": user.id, + # "username": user.username}, status=status.HTTP_201_CREATED) + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class UserLoginView(GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = UserLoginSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data = request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data + serializer = CustomUserSerializer(user) + token = {"refresh": str(token), + "access": str(token.access_token)} + return Response(data, status = status.HTTP_200_OK) +# class UserLoginView(APIView): +# authentication_classes = [JWTAuthentication] +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# content = {'message': 'Hello, World!'} +# return Response(content) + + +# class UserLoginView(APIView): +# authentication_classes = [JWTAuthentication] +# permission_classes = [IsAuthenticated] + +# def post(self, request): +# # Extract the token from the Authorization header +# print(request) +# auth_header = request.headers.get('Authorization') +# if auth_header is None: +# return Response({"error": +# "Authorization header missing"}, +# status=status.HTTP_401_UNAUTHORIZED) +# try: +# # The token is expected to be in the format "Bearer " +# token_key = auth_header.split(' ')[1] +# token = Token.objects.get(key=token_key) +# user = token.user +# return Response({"message": "Login successful", "user_id": user.id, "username": user.username}, status=status.HTTP_200_OK) +# except (Token.DoesNotExist, IndexError): +# return Response({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED) + + + +class UserLogoutView(GenericAPIView): + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + try: + refresh_token = request.data("refresh") + token = RefreshToken(refresh_token) + token.blacklist() + return Response( status = status.HTTP_205_RESET_CONTENT) + except Exception as e: + return Response( status = status.HTTP_400_BAD_REQUEST) + + + # def post(self, request): + # logout(request) + # return Response({"message": "Logout successful"}, status=status.HTTP_200_OK) + + + -class UserLoginView(APIView): - def post(self, request): - username = request.data.get('username') - password = request.data.get('password') - user = authenticate(request, username=username, password=password) - if user is not None: - login(request, user) - return Response({"message": "Login successful"}, status=status.HTTP_200_OK) - return Response({error: "Invalid credentials"}, status=status.HTTP_401_UNAUTHORIZED) -class UserLogoutView(APIView): - def post(self, request): - logout(request) - return Response({"message": "Logout successful"}, status=status.HTTP_200_OK) diff --git a/main/admin.py b/main/admin.py index 8c38f3f..8b13789 100644 --- a/main/admin.py +++ b/main/admin.py @@ -1,3 +1 @@ -from django.contrib import admin -# Register your models here. diff --git a/main/models.py b/main/models.py index 33438ee..8115376 100644 --- a/main/models.py +++ b/main/models.py @@ -1,15 +1,10 @@ from django.db import models # Create your models here. -from django.contrib.auth.models import User +#from django.contrib.auth.models import User from django.db import models from django.conf import settings -class RegisteredUser(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - def __str__(self): - return self.user.username - class Profile(models.Model): ROLE_CHOICES = [ ('Application Admin', 'Application Admin'), @@ -23,7 +18,7 @@ class Profile(models.Model): ('Project Initiator', 'Project Initiator'), ] - user = models.OneToOneField(User, on_delete=models.CASCADE) +# user = models.OneToOneField(User, on_delete=models.CASCADE) role = models.CharField(max_length=50, choices=ROLE_CHOICES) def __str__(self): diff --git a/main/serializers.py b/main/serializers.py index 9a252a8..164c21b 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,24 +1,19 @@ from rest_framework import serializers -from .models import Project, Task, RegisteredUser +from accounts.models import CustomUser +from .models import Project, Task +from django.contrib.auth import authenticate -class UserRegistrationSerializer(serializers.ModelSerializer): - class Meta: - model = RegisteredUser - fields = ('username', 'password', 'email') - extra_kwargs = {'password': {'write_only': True}} +from rest_framework import serializers +from .models import Project, Task - def create(self, validated_data): - user = RegisteredUser(**validated_data) - user = set_password(validated_data['password']) - user.save() - return user class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project fields = '__all__' + class TaskSerializer(serializers.ModelSerializer): class Meta: model = Task - fields = '__all__' \ No newline at end of file + fields = '__all__' diff --git a/main/urls.py b/main/urls.py index 85fa979..6d3bbba 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,18 +1,17 @@ # main/urls.py -from .views import welcome_page, create_project, project_dashboard, create_task +from .views import CreateProjectView, ProjectDashboardView, CreateTaskView from django.urls import path, include from rest_framework.routers import DefaultRouter from .api_views import ProjectViewSet, TaskViewSet -from django. urls import path + router = DefaultRouter() router.register(r'projects', ProjectViewSet) router.register(r'tasks', TaskViewSet) urlpatterns = [ - path ('', welcome_page, name='welcome_page'), path('api/', include(router.urls)), - path('projects/create/', create_project, name='create_project'), - path('projects//', project_dashboard, name='project_dashboard'), - path('projects//tasks/create/', create_task, name='create_task'), + path('projects/create/', CreateProjectView.as_view(), name='create_project'), + path('projects//', ProjectDashboardView.as_view(), name='project_dashboard'), + path('projects//tasks/create/', CreateTaskView.as_view(), name='create_task'), ] diff --git a/main/views.py b/main/views.py index c4e7fe0..58dd883 100644 --- a/main/views.py +++ b/main/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from .models import Project, Task +from accounts.models import CustomUser from .forms import ProjectForm, TaskForm from django.http import HttpResponse @@ -11,60 +12,142 @@ def welcome_page(request): template = loader.get_template('login.html') return HttpResponse(template.render()) +from django.views import View +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator +from django.core.mail import send_mail +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication -# create project -@login_required -def create_project(request): - if request.method == 'POST': + + + +class CreateProjectView(View): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request): + form = ProjectForm() + return render(request, 'create_project.html', {'form': form}) + + def post(self, request): form = ProjectForm(request.POST) if form.is_valid(): project = form.save(commit=False) project.initiator = request.user project.save() # Notify PMO Admins - pmo_admins = User.objects.filter(profile__role='PMO Admin') + pmo_admins = CustomUser.objects.filter(profile__role='PMO Admin') for admin in pmo_admins: send_mail( 'New Project Created', f'A new project "{project.name}" has been created and requires a project manager assignment.', - 'admin@example.com', + 'admin@domeitsolutions.com', [admin.email], - fail_silently=False, + fail_silently=True, ) return redirect('project_dashboard', project_id=project.id) - else: - form = ProjectForm() - return render(request, 'create_project.html', {'form': form}) + return render(request, 'create_project.html', {'form': form}) -# Load project as current focus -@login_required -def load_project(request): - if request.method == 'POST': + +class LoadProjectView(View): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request): + projects = Project.objects.all() + return render(request, 'load_project.html', {'projects': projects}) + + def post(self, request): project_id = request.POST['project_id'] return redirect('project_dashboard', project_id=project_id) - projects = Project.objects.all() - return render(request, 'load_project.html', {'projects': projects}) -# Active projects -@login_required -def project_dashboard(request, project_id): - project = get_object_or_404(Project, id=project_id) - tasks = project.tasks.all() - return render(request, 'project_dashboard.html', {'project': project, 'tasks': tasks}) +class ProjectDashboardView(View): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] -# Create task in current focus project - if no project chosen default project for user is focus -@login_required -def create_task(request, project_id): - project = get_object_or_404(Project, id=project_id) - if request.method == 'POST': + def get(self, request, project_id): + project = get_object_or_404(Project, id=project_id) + tasks = project.tasks.all() + return render(request, 'project_dashboard.html', {'project': project, 'tasks': tasks}) + + +class CreateTaskView(View): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, project_id): + project = get_object_or_404(Project, id=project_id) + form = TaskForm() + return render(request, 'create_task.html', {'form': form, 'project': project}) + + def post(self, request, project_id): + project = get_object_or_404(Project, id=project_id) form = TaskForm(request.POST) if form.is_valid(): task = form.save(commit=False) task.project = project task.save() return redirect('project_dashboard', project_id=project.id) - else: - form = TaskForm() - return render(request, 'create_task.html', {'form': form, 'project': project}) + return render(request, 'create_task.html', {'form': form, 'project': project}) + +# # create project +# @login_required +# def create_project(request): +# if request.method == 'POST': +# form = ProjectForm(request.POST) +# if form.is_valid(): +# project = form.save(commit=False) +# project.initiator = request.user +# project.save() +# # Notify PMO Admins +# pmo_admins = User.objects.filter(profile__role='PMO Admin') +# for admin in pmo_admins: +# send_mail( +# 'New Project Created', +# f'A new project "{project.name}" has been created and requires a project manager assignment.', +# 'admin@domeitsolutions.com', +# [admin.email], +# fail_silently=True, +# ) +# return redirect('project_dashboard', project_id=project.id) +# else: +# form = ProjectForm() +# return render(request, 'create_project.html', {'form': form}) + +# # Load project as current focus +# @login_required +# def load_project(request): +# if request.method == 'POST': +# project_id = request.POST['project_id'] +# return redirect('project_dashboard', project_id=project_id) +# projects = Project.objects.all() +# return render(request, 'load_project.html', {'projects': projects}) + + +# # Active projects +# @login_required +# def project_dashboard(request, project_id): +# project = get_object_or_404(Project, id=project_id) +# tasks = project.tasks.all() +# return render(request, 'project_dashboard.html', {'project': project, 'tasks': tasks}) + +# # Create task in current focus project - if no project chosen default project for user is focus +# @login_required +# def create_task(request, project_id): +# project = get_object_or_404(Project, id=project_id) +# if request.method == 'POST': +# form = TaskForm(request.POST) +# if form.is_valid(): +# task = form.save(commit=False) +# task.project = project +# task.save() +# return redirect('project_dashboard', project_id=project.id) +# else: +# form = TaskForm() +# return render(request, 'create_task.html', {'form': form, 'project': project}) diff --git a/project_management/settings.py b/project_management/settings.py index 6bc1df4..4edc7d8 100644 --- a/project_management/settings.py +++ b/project_management/settings.py @@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ from pathlib import Path - +from datetime import timedelta import os # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -22,19 +22,21 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get("SECRET_KEY") - +#SECRET_KEY = os.environ.get("SECRET_KEY") +SECRET_KEY = "express" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = bool(os.environ.get("DEBUG", default=0)) -ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") - +#ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") +ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'rest_framework', 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', + 'corsheaders', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -53,8 +55,10 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', ] + ROOT_URLCONF = 'project_management.urls' TEMPLATES = [ @@ -122,7 +126,45 @@ REST_FRAMEWORK = { 'rest_framework_simplejwt.authentication.JWTAuthentication', ] } +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + "UPDATE_LAST_LOGIN": False, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": "", + "AUDIENCE": None, + "ISSUER": None, + "JSON_ENCODER": None, + "JWK_URL": None, + "LEEWAY": 0, + + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", + "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", + + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", + "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", + + "JTI_CLAIM": "jti", + + "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", + "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), + "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), + + "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer", + "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer", + "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer", + "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer", + "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer", + "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", +} # Internationalization @@ -146,3 +188,9 @@ STATIC_URL = 'static/' # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = "accounts.CustomUser" + +CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = ['http://localhost:3000',] +CORS_ALLOWED_ORIGIN_REGEXES = ['http://localhost:3000',] \ No newline at end of file diff --git a/project_management/urls.py b/project_management/urls.py index 30d3758..3298baa 100644 --- a/project_management/urls.py +++ b/project_management/urls.py @@ -3,9 +3,8 @@ from django.contrib import admin from django.urls import path, include - urlpatterns = [ path('admin/', admin.site.urls), - path('accounts/', include('accounts.urls')), - path('', include('main.urls')), + path('api/', include('accounts.urls')), + path('ditswbs/', include('main.urls')), ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e9756dc..4b67432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ asgiref==3.8.1 Django==5.1.1 djangorestframework==3.15.2 +django-cors-headers==4.7.0 djangorestframework-simplejwt==5.4.0 psycopg2-binary==2.9.10 sqlparse==0.5.1