diff --git a/backend/focusty/focusty/settings.py b/backend/focusty/focusty/settings.py index cc6fc0c..0a20335 100644 --- a/backend/focusty/focusty/settings.py +++ b/backend/focusty/focusty/settings.py @@ -1,7 +1,7 @@ # settings.py import os from pathlib import Path -from decouple import config +from decouple import config, Csv # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -12,11 +12,9 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config("SECRET_KEY") +DEBUG = config("DEBUG", default=False, cast=bool) +ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv()) -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ["trisdev.pythonanywhere.com", "127.0.0.1"] INSTALLED_APPS = [ "django.contrib.admin", @@ -123,3 +121,10 @@ # Path where media is stored MEDIA_ROOT = os.path.join(BASE_DIR, "media/") + + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), +} diff --git a/backend/focusty/focusty_app/serializers.py b/backend/focusty/focusty_app/serializers.py index faffaac..6114df4 100644 --- a/backend/focusty/focusty_app/serializers.py +++ b/backend/focusty/focusty_app/serializers.py @@ -1,18 +1,30 @@ -#serializers.py +# serializers.py from rest_framework import serializers from .models import User, Task, Pomodoro + class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = fields = ('id', 'username', 'email', 'password', 'date_joined', 'birthday', 'profile_picture', 'country') + fields = fields = ( + "id", + "username", + "email", + "password", + "date_joined", + "birthday", + "profile_picture", + "country", + ) + class TaskSerializer(serializers.ModelSerializer): class Meta: model = Task - fields = '__all__' + fields = "__all__" + class PomodoroSerializer(serializers.ModelSerializer): class Meta: model = Pomodoro - fields = ['date', 'minutes'] \ No newline at end of file + fields = ["date", "minutes"] diff --git a/backend/focusty/focusty_app/urls.py b/backend/focusty/focusty_app/urls.py index e50f52b..b7f786f 100644 --- a/backend/focusty/focusty_app/urls.py +++ b/backend/focusty/focusty_app/urls.py @@ -6,6 +6,7 @@ TaskListCreate, TaskDetail, login_view, + RegisterView, PomodoroListCreate, PomodoroDetail, tasks_count, @@ -18,6 +19,7 @@ path("users//tasks//", TaskDetail.as_view()), path("users//tasks/all/", tasks_count, name="tasks count"), path("login/", login_view, name="login"), + path("register/", RegisterView.as_view(), name="register"), path("users//pomodoros/", PomodoroListCreate.as_view()), path("users//pomodoros//", PomodoroDetail.as_view()), ] diff --git a/backend/focusty/focusty_app/views.py b/backend/focusty/focusty_app/views.py index 2a0a0b7..969e353 100644 --- a/backend/focusty/focusty_app/views.py +++ b/backend/focusty/focusty_app/views.py @@ -1,6 +1,5 @@ # views.py -import uuid -from datetime import datetime +import uuid, json from rest_framework import generics from .models import User, Task, Pomodoro from .serializers import TaskSerializer, UserSerializer, PomodoroSerializer @@ -9,7 +8,10 @@ from rest_framework.response import Response from rest_framework import status from rest_framework.decorators import api_view -import json +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.tokens import RefreshToken + + from django.db.models import Count, Sum from .task_repeat import repeat_task @@ -22,6 +24,83 @@ class UserList(generics.ListCreateAPIView): class UserDetail(generics.RetrieveUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_object(self): + obj = super().get_object() + if obj != self.request.user: + return JsonResponse( + {"error": "You do not have permission to access this data."} + ) + return obj + + +class RegisterView(generics.CreateAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + + def create(self, request, *args, **kwargs): + response = super().create(request, *args, **kwargs) + + user = self.get_user_from_response(response.data) + + if user: + token = self.get_token(user) + response.set_cookie(key="jwt", value=str(token), httponly=True) + response.data["token"] = str(token) + return response + + def get_token(self, user): + + refresh = RefreshToken.for_user(user) + return refresh.access_token + + def get_user_from_response(self, data): + try: + user_id = data.get("id") + return User.objects.get(id=user_id) + except User.DoesNotExist: + return None + + +@csrf_exempt +def login_view(request): + if request.method == "POST": + data = json.loads(request.body) + email = data.get("email") + password = data.get("password") + + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + return JsonResponse( + {"success": False, "message": "User does not exist"}, status=404 + ) + + if password == user.password: + # Generate token + refresh = RefreshToken.for_user(user) + + return JsonResponse( + { + "success": True, + "user": { + "id": user.id, + "username": user.username, + "email": user.email, + }, + "access": str(refresh.access_token), + "refresh": str(refresh), + } + ) + else: + return JsonResponse( + {"success": False, "message": "Invalid email or password"}, status=400 + ) + else: + return JsonResponse( + {"success": False, "message": "Method not allowed"}, status=405 + ) class TaskListCreate(generics.ListCreateAPIView): @@ -177,42 +256,6 @@ def tasks_count(request, user_id): return Response(tasks_count_by_date) -@csrf_exempt -def login_view(request): - if request.method == "POST": - data = json.loads(request.body) - email = data.get("email") - password = data.get("password") - - try: - user = User.objects.get(email=email) - except User.DoesNotExist: - return JsonResponse( - {"success": False, "message": "User does not exist"}, status=404 - ) - if password == user.password: - print(user) - return JsonResponse( - { - "success": True, - "user": { - "id": user.id, - "username": user.username, - "email": user.email, - }, - } - ) - - else: - return JsonResponse( - {"success": False, "message": "Invalid email or password"}, status=400 - ) - else: - return JsonResponse( - {"success": False, "message": "Method not allowed"}, status=405 - ) - - class PomodoroDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Pomodoro.objects.all() serializer_class = PomodoroSerializer diff --git a/frontend/.gitignore b/frontend/.gitignore index 8ee54e8..3c5cb03 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -13,7 +13,7 @@ dist dist-ssr coverage *.local - +*.env /cypress/videos/ /cypress/screenshots/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e70344a..6b3d4e8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "axios": "^1.7.7", "chart.js": "^4.4.5", "dayjs": "^1.11.13", + "js-cookie": "^3.0.5", "pinia": "^2.2.4", "sortablejs": "^1.15.3", "uuid": "^10.0.0", @@ -3641,7 +3642,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, "engines": { "node": ">=14" } diff --git a/frontend/package.json b/frontend/package.json index 66c04bc..318fb2f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "axios": "^1.7.7", "chart.js": "^4.4.5", "dayjs": "^1.11.13", + "js-cookie": "^3.0.5", "pinia": "^2.2.4", "sortablejs": "^1.15.3", "uuid": "^10.0.0", diff --git a/frontend/src/main.js b/frontend/src/main.js index a865c67..4f2e767 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,6 +1,7 @@ import './styles.scss' import { createApp } from 'vue' import { createPinia } from 'pinia' +import Cookies from 'js-cookie' import axios from 'axios' import App from './App.vue' import router from './router' @@ -9,8 +10,22 @@ import VueDatePicker from '@vuepic/vue-datepicker' import '@vuepic/vue-datepicker/dist/main.css' import 'v-calendar/style.css' -//axios.defaults.baseURL = 'https://trisdev.pythonanywhere.com' -axios.defaults.baseURL = 'http://127.0.0.1:8000/' +//axios +axios.defaults.baseURL = import.meta.env.VITE_BASE_URL +axios.defaults.headers['Content-Type'] = 'application/json' +axios.interceptors.request.use( + (config) => { + const token = Cookies.get('token') + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + // font awesome icons import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/frontend/src/views/user/log-in.vue b/frontend/src/views/user/log-in.vue index 9453a01..8c7ace7 100644 --- a/frontend/src/views/user/log-in.vue +++ b/frontend/src/views/user/log-in.vue @@ -3,6 +3,7 @@ import { ref } from 'vue' import axios from 'axios' import { useRouter } from 'vue-router' import { useStore } from '@/stores' +import Cookies from 'js-cookie' const store = useStore() const router = useRouter() @@ -17,6 +18,11 @@ const login = async () => { }) // Handle successful login localStorage.setItem('userId', response.data.user.id) + const [token, refresh] = [response.data.access, response.data.refresh] + if (token) { + Cookies.set('token', token, { expires: 7, secure: true, sameSite: 'Strict' }) + Cookies.set('refresh', refresh, { expires: 7, secure: true, sameSite: 'Strict' }) + } store.setUser({ id: response.data.user.id, username: response.data.user.username, diff --git a/frontend/src/views/user/sign-up.vue b/frontend/src/views/user/sign-up.vue index c643e3f..cb19272 100644 --- a/frontend/src/views/user/sign-up.vue +++ b/frontend/src/views/user/sign-up.vue @@ -1,5 +1,6 @@