diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 1896a3e..0cc3f29 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -2,13 +2,12 @@ name: Django CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: build: - runs-on: ubuntu-latest services: @@ -24,20 +23,21 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.11 - uses: actions/setup-python@v2 - with: - python-version: 3.11 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run migrations - run: | - cd backend - python manage.py migrate - - name: Run tests - run: | - cd backend - python manage.py test + - uses: actions/checkout@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - name: Install dependencies + run: | + cd backend + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run migrations + run: | + cd backend + python manage.py migrate + - name: Run tests + run: | + cd backend + python manage.py test diff --git a/.gitignore b/.gitignore index d6a7e2b..a40fc56 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ cover/ # Django stuff: *.log -local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/backend/authentications/urls.py b/backend/authentications/urls.py index 32965a4..75c5914 100644 --- a/backend/authentications/urls.py +++ b/backend/authentications/urls.py @@ -1,6 +1,6 @@ from django.urls import path from rest_framework_simplejwt import views as jwt_views -from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo +from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo, CheckAccessTokenAndRefreshToken urlpatterns = [ path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), @@ -9,4 +9,5 @@ urlpatterns = [ path('hello/', GreetingView.as_view(), name='hello_world'), path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"), path('auth/google/', GoogleRetrieveUserInfo.as_view()), + path('auth/status/', CheckAccessTokenAndRefreshToken.as_view(), name='check_token_status') ] \ No newline at end of file diff --git a/backend/authentications/views.py b/backend/authentications/views.py index 9b5f249..167581b 100644 --- a/backend/authentications/views.py +++ b/backend/authentications/views.py @@ -1,6 +1,3 @@ -from django.shortcuts import render - -# Create your views here. """This module defines API views for authentication, user creation, and a simple hello message.""" import json @@ -14,6 +11,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.authentication import JWTAuthentication + from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter @@ -27,6 +26,31 @@ from users.managers import CustomAccountManager from users.models import CustomUser +class CheckAccessTokenAndRefreshToken(APIView): + permission_classes = (AllowAny,) + JWT_authenticator = JWTAuthentication() + + def post(self, request, *args, **kwargs): + access_token = request.data.get('access_token') + refresh_token = request.data.get('refresh_token') + # Check if the access token is valid + if access_token: + response = self.JWT_authenticator.authenticate(request) + if response is not None: + return Response({'status': 'true'}, status=status.HTTP_200_OK) + + # Check if the refresh token is valid + if refresh_token: + try: + refresh = RefreshToken(refresh_token) + access_token = str(refresh.access_token) + return Response({'access_token': access_token}, status=status.HTTP_200_OK) + except Exception as e: + return Response({'status': 'false'}, status=status.HTTP_401_UNAUTHORIZED) + + return Response({'status': 'false'}, status=status.HTTP_400_BAD_REQUEST) + + class ObtainTokenPairWithCustomView(APIView): """ Custom Token Obtain Pair View. @@ -165,4 +189,4 @@ class GoogleRetrieveUserInfo(APIView): response = requests.get(api_url, headers=headers) if response.status_code == 200: return response.json() - raise Exception('Google API Error', response) + raise Exception('Google API Error', response) \ No newline at end of file diff --git a/backend/core/local_settings.py b/backend/core/local_settings.py new file mode 100644 index 0000000..9e7903c --- /dev/null +++ b/backend/core/local_settings.py @@ -0,0 +1,269 @@ +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 4.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from datetime import timedelta +import os +from pathlib import Path +from decouple import config, Csv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config('DEBUG', default=False, cast=bool) + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) + + +# Application definition + +SITE_ID = 4 + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + + 'tasks', + 'users', + 'authentications', + 'dashboard', + 'boards', + + 'corsheaders', + 'drf_spectacular', + + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + + 'rest_framework', + 'dj_rest_auth', + 'dj_rest_auth.registration', + 'rest_framework.authtoken', +] + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + ], + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'TurTask API', + 'DESCRIPTION': 'API documentation for TurTask', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, +} + +REST_USE_JWT = True + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=3), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), +} + +GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') +GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') + +SOCIALACCOUNT_PROVIDERS = { + 'google': { + 'APP': { + 'client_id': GOOGLE_CLIENT_ID, + 'secret': GOOGLE_CLIENT_SECRET, + 'key': '' + }, + "SCOPE": [ + "profile", + "email", + ], + "AUTH_PARAMS": { + "access_type": "online", + } + } +} + + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8000", + "http://127.0.0.1:8000", + "http://localhost:5173", +] + +CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"] + +CORS_ORIGIN_WHITELIST = ["*"] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "allauth.account.middleware.AccountMiddleware", +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'templates') + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': config('DB_NAME', default='github_actions'), + 'USER': config('DB_USER', default='postgres'), + 'PASSWORD': config('DB_PASSWORD', default='postgres'), + 'HOST': config('DB_HOST', default='127.0.0.1'), + 'PORT': config('DB_PORT', default='5432'), + } +} + + +# Cache + +CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache" + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.db.DatabaseCache", + "LOCATION": CACHES_LOCATION, + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +] + +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' + +AUTH_USER_MODEL = "users.CustomUser" + +ACCOUNT_EMAIL_REQUIRED = True + +# Storages + +AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key') +AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key') +AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name') +AWS_DEFAULT_ACL = 'public-read' +AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com' +AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} + +MEDIA_URL = '/mediafiles/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') + +STORAGES = { + "default": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + }, + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} \ No newline at end of file diff --git a/backend/core/production_settings.py b/backend/core/production_settings.py new file mode 100644 index 0000000..dd9eeb3 --- /dev/null +++ b/backend/core/production_settings.py @@ -0,0 +1,267 @@ +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 4.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from datetime import timedelta +import os +from pathlib import Path +from decouple import config, Csv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config('DEBUG', default=False, cast=bool) + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) + + +# Application definition + +SITE_ID = 4 + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + + 'tasks', + 'users', + 'authentications', + 'dashboard', + 'boards', + + 'corsheaders', + 'drf_spectacular', + + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + + 'rest_framework', + 'dj_rest_auth', + 'dj_rest_auth.registration', + 'rest_framework.authtoken', +] + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + ], + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'TurTask API', + 'DESCRIPTION': 'API documentation for TurTask', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, +} + +REST_USE_JWT = True + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=3), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), +} + +GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') +GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') + +SOCIALACCOUNT_PROVIDERS = { + 'google': { + 'APP': { + 'client_id': GOOGLE_CLIENT_ID, + 'secret': GOOGLE_CLIENT_SECRET, + 'key': '' + }, + "SCOPE": [ + "profile", + "email", + ], + "AUTH_PARAMS": { + "access_type": "online", + } + } +} + + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = [ + "https://turtask.vercel.app", +] + +CSRF_TRUSTED_ORIGINS = ["https://turtask.vercel.app"] + +CORS_ORIGIN_WHITELIST = ["*"] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "allauth.account.middleware.AccountMiddleware", +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'templates') + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': config('PGDATABASE', default='github_actions'), + 'USER': config('PGUSER', default='postgres'), + 'PASSWORD': config('PGPASSWORD', default='postgres'), + 'HOST': config('PGHOST', default='127.0.0.1'), + 'PORT': config('PGPORT', default='5432'), + } +} + + +# Cache + +CACHES_LOCATION = "accesstokencache" + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.db.DatabaseCache", + "LOCATION": CACHES_LOCATION, + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +] + +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/' + +AUTH_USER_MODEL = "users.CustomUser" + +ACCOUNT_EMAIL_REQUIRED = True + +# Storages + +AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key') +AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key') +AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name') +AWS_DEFAULT_ACL = 'public-read' +AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com' +AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} + +MEDIA_URL = '/mediafiles/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') + +STORAGES = { + "default": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + }, + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} \ No newline at end of file diff --git a/backend/core/settings.py b/backend/core/settings.py index 9e7903c..7532f01 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -1,269 +1,6 @@ -""" -Django settings for core project. - -Generated by 'django-admin startproject' using Django 4.2.6. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" - -from datetime import timedelta import os -from pathlib import Path -from decouple import config, Csv -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5') - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config('DEBUG', default=False, cast=bool) - -ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv()) - - -# Application definition - -SITE_ID = 4 - -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', -) - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - - 'tasks', - 'users', - 'authentications', - 'dashboard', - 'boards', - - 'corsheaders', - 'drf_spectacular', - - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.google', - - 'rest_framework', - 'dj_rest_auth', - 'dj_rest_auth.registration', - 'rest_framework.authtoken', -] - -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', - - ], - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', - ], - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', -} - -SPECTACULAR_SETTINGS = { - 'TITLE': 'TurTask API', - 'DESCRIPTION': 'API documentation for TurTask', - 'VERSION': '1.0.0', - 'SERVE_INCLUDE_SCHEMA': False, -} - -REST_USE_JWT = True - -SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=3), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), -} - -GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') -GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') - -SOCIALACCOUNT_PROVIDERS = { - 'google': { - 'APP': { - 'client_id': GOOGLE_CLIENT_ID, - 'secret': GOOGLE_CLIENT_SECRET, - 'key': '' - }, - "SCOPE": [ - "profile", - "email", - ], - "AUTH_PARAMS": { - "access_type": "online", - } - } -} - - -CORS_ALLOW_CREDENTIALS = True -CORS_ALLOW_ALL_ORIGINS = False -CORS_ALLOWED_ORIGINS = [ - "http://localhost:8000", - "http://127.0.0.1:8000", - "http://localhost:5173", -] - -CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"] - -CORS_ORIGIN_WHITELIST = ["*"] - -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - "allauth.account.middleware.AccountMiddleware", -] - -ROOT_URLCONF = 'core.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates') - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'core.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': config('DB_NAME', default='github_actions'), - 'USER': config('DB_USER', default='postgres'), - 'PASSWORD': config('DB_PASSWORD', default='postgres'), - 'HOST': config('DB_HOST', default='127.0.0.1'), - 'PORT': config('DB_PORT', default='5432'), - } -} - - -# Cache - -CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache" - -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.db.DatabaseCache", - "LOCATION": CACHES_LOCATION, - } -} - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - - -SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', -] - -LOGIN_REDIRECT_URL = '/' -LOGOUT_REDIRECT_URL = '/' - -AUTH_USER_MODEL = "users.CustomUser" - -ACCOUNT_EMAIL_REQUIRED = True - -# Storages - -AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key') -AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key') -AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name') -AWS_DEFAULT_ACL = 'public-read' -AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com' -AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} - -MEDIA_URL = '/mediafiles/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') - -STORAGES = { - "default": { - "BACKEND": "storages.backends.s3.S3Storage", - "OPTIONS": { - }, - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, -} \ No newline at end of file +if os.environ.get("DJANGO_ENV") == "PRODUCTION": + from .production_settings import * +else: + from .local_settings import * \ No newline at end of file diff --git a/backend/core/wsgi.py b/backend/core/wsgi.py index f44964d..a08c895 100644 --- a/backend/core/wsgi.py +++ b/backend/core/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") application = get_wsgi_application() diff --git a/backend/railway.json b/backend/railway.json new file mode 100644 index 0000000..a99f1a8 --- /dev/null +++ b/backend/railway.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "NIXPACKS" + }, + "deploy": { + "startCommand": "python manage.py migrate && python manage.py createcachetable accesstokencache && gunicorn --timeout 500 core.wsgi", + "restartPolicyType": "NEVER", + "restartPolicyMaxRetries": 10 + } +} diff --git a/requirements.txt b/backend/requirements.txt similarity index 87% rename from requirements.txt rename to backend/requirements.txt index 344b980..95962f8 100644 --- a/requirements.txt +++ b/backend/requirements.txt @@ -15,4 +15,6 @@ google-auth-httplib2>=0.1 django-storages[s3]>=1.14 Pillow>=10.1 drf-spectacular>=0.26 -python-dateutil>=2.8 \ No newline at end of file +python-dateutil>=2.8 +gunicorn==21.2.0 +packaging==23.1 \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 4dcb439..1a8aa39 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,20 +1,17 @@ module.exports = { root: true, - env: { browser: true, es2020: true }, + env: { browser: true, es2020: true, node: true }, extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, + settings: { react: { version: "18.2" } }, + plugins: ["react-refresh"], rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, -} +}; diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..af4aef6 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "src/*": ["./src/*"] + } + } +} diff --git a/frontend/package.json b/frontend/package.json index e5ae2dd..f057dce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", "jwt-decode": "^4.0.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e4ccb3d..22fdadd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -89,6 +89,9 @@ dependencies: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + prop-types: + specifier: ^15.8.1 + version: 15.8.1 react: specifier: ^18.2.0 version: 18.2.0 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 521d8f2..1ab242f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ +import { useEffect } from "react"; import "./App.css"; -import { Route, Routes, useLocation } from "react-router-dom"; - +import { Route, Routes, Navigate } from "react-router-dom"; +import axios from "axios"; import TestAuth from "./components/testAuth"; import LoginPage from "./components/authentication/LoginPage"; import SignUpPage from "./components/authentication/SignUpPage"; @@ -8,23 +9,82 @@ import NavBar from "./components/navigations/Navbar"; import Calendar from "./components/calendar/calendar"; import KanbanPage from "./components/kanbanBoard/kanbanPage"; import IconSideNav from "./components/navigations/IconSideNav"; -import Eisenhower from "./components/eisenhowerMatrix/Eisenhower"; +import Eisenhower from "./components/EisenhowerMatrix/Eisenhower"; import PrivateRoute from "./PrivateRoute"; import ProfileUpdatePage from "./components/profilePage"; import Dashboard from "./components/dashboard/dashboard"; +import { LandingPage } from "./components/landingPage/LandingPage"; +import PublicRoute from "./PublicRoute"; +import { useAuth } from "./hooks/AuthHooks"; + +const baseURL = import.meta.env.VITE_BASE_URL; const App = () => { - const location = useLocation(); - const prevention = ["/login", "/signup"]; - const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_)); + const { isAuthenticated, setIsAuthenticated } = useAuth(); + useEffect(() => { + const checkLoginStatus = async () => { + const data = { + access_token: localStorage.getItem("access_token"), + refresh_token: localStorage.getItem("refresh_token"), + }; + + await axios + .post(`${baseURL}auth/status/`, data, { + headers: { + Authorization: "Bearer " + localStorage.getItem("access_token"), + }, + }) + .then((response) => { + if (response.status === 200) { + if (response.data.access_token) { + localStorage.setItem("access_token", response.data.access_token); + setIsAuthenticated(true); + } else { + setIsAuthenticated(true); + } + } else { + setIsAuthenticated(false); + } + }) + .catch((error) => { + console.error("Error checking login status:", error.message); + }); + }; + + checkLoginStatus(); + }, [setIsAuthenticated]); + + return
{isAuthenticated ? : }
; +}; + +const NonAuthenticatedComponents = () => { return ( -
- {!isLoginPageOrSignUpPage && } -
+
+ + }> + } /> + + }> + } /> + + }> + } /> + + } /> + +
+ ); +}; + +const AuthenticatedComponents = () => { + return ( +
+ +
-
+
} /> }> @@ -40,8 +100,7 @@ const App = () => { }> } /> - } /> - } /> + } />
diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx index defeaea..01afc6a 100644 --- a/frontend/src/PrivateRoute.jsx +++ b/frontend/src/PrivateRoute.jsx @@ -1,11 +1,9 @@ -import React from "react"; import { Navigate, Outlet } from "react-router-dom"; -import { useAuth } from "./hooks/authentication/IsAuthenticated"; +import { useAuth } from "src/hooks/AuthHooks"; const PrivateRoute = () => { - const { isAuthenticated, setIsAuthenticated } = useAuth(); - const auth = isAuthenticated; - return auth ? : ; + const { isAuthenticated } = useAuth(); + return isAuthenticated ? : ; }; -export default PrivateRoute; \ No newline at end of file +export default PrivateRoute; diff --git a/frontend/src/PublicRoute.jsx b/frontend/src/PublicRoute.jsx new file mode 100644 index 0000000..ffdc39c --- /dev/null +++ b/frontend/src/PublicRoute.jsx @@ -0,0 +1,9 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "src/hooks/AuthHooks"; + +const PublicRoute = () => { + const { isAuthenticated } = useAuth(); + return isAuthenticated ? : ; +}; + +export default PublicRoute; diff --git a/frontend/src/api/AuthenticationApi.jsx b/frontend/src/api/AuthenticationApi.jsx index d4b6486..bca81d5 100644 --- a/frontend/src/api/AuthenticationApi.jsx +++ b/frontend/src/api/AuthenticationApi.jsx @@ -1,15 +1,18 @@ import axios from "axios"; -import axiosInstance from "./configs/AxiosConfig"; +import axiosInstance from "./AxiosConfig"; + +const baseURL = import.meta.env.VITE_BASE_URL; // Function for user login -const apiUserLogin = data => { +const apiUserLogin = (data) => { return axiosInstance .post("token/obtain/", data) - .then(response => { + .then((response) => { console.log(response.statusText); + return response; }) - .catch(error => { + .catch((error) => { console.log("apiUserLogin error: ", error); return error; }); @@ -23,9 +26,9 @@ const apiUserLogout = () => { }; // Function for Google login -const googleLogin = async token => { +const googleLogin = async (token) => { axios.defaults.withCredentials = true; - let res = await axios.post("http://localhost:8000/api/auth/google/", { + let res = await axios.post(`${baseURL}auth/google/`, { code: token, }); // console.log('service google login res: ', res); @@ -36,29 +39,23 @@ const googleLogin = async token => { const getGreeting = () => { return axiosInstance .get("hello") - .then(response => { + .then((response) => { return response; }) - .catch(error => { + .catch((error) => { return error; }); }; -const config = { - headers: { - "Content-Type": "application/json", - }, -}; - // Function to register -const createUser = async formData => { +const createUser = async (formData) => { try { axios.defaults.withCredentials = true; - const resposne = axios.post("http://localhost:8000/api/user/create/", formData); + const response = axios.post(`${baseURL}user/create/`, formData); // const response = await axiosInstance.post('/user/create/', formData); return response.data; - } catch (error) { - throw error; + } catch (e) { + console.log(e); } }; diff --git a/frontend/src/api/AxiosConfig.jsx b/frontend/src/api/AxiosConfig.jsx new file mode 100644 index 0000000..b037386 --- /dev/null +++ b/frontend/src/api/AxiosConfig.jsx @@ -0,0 +1,47 @@ +import axios from "axios"; +import { redirect } from "react-router-dom"; + +const baseURL = import.meta.env.VITE_BASE_URL; + +const axiosInstance = axios.create({ + baseURL: baseURL, + timeout: 5000, + headers: { + Authorization: "Bearer " + localStorage.getItem("access_token"), + "Content-Type": "application/json", + accept: "application/json", + }, +}); + +// handling token refresh on 401 Unauthorized errors +axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + const originalRequest = error.config; + const refresh_token = localStorage.getItem("refresh_token"); + + // Check if the error is due to 401 and a refresh token is available + if (error.response && error.response.status === 401) { + if (refresh_token) { + return axiosInstance + .post("/token/refresh/", { refresh: refresh_token }) + .then((response) => { + localStorage.setItem("access_token", response.data.access); + + axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access; + originalRequest.headers["Authorization"] = "Bearer " + response.data.access; + + return axiosInstance(originalRequest); + }) + .catch((err) => { + console.log("Interceptors error: ", err); + }); + } else { + redirect("/login"); + } + } + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx index eee70d0..daad9c3 100644 --- a/frontend/src/api/TaskApi.jsx +++ b/frontend/src/api/TaskApi.jsx @@ -1,21 +1,21 @@ -import axiosInstance from "./configs/AxiosConfig"; +import axiosInstance from "src/api/AxiosConfig"; -const baseURL = ""; +const baseURL = import.meta.env.VITE_BASE_URL; export const createTask = (endpoint, data) => { return axiosInstance .post(`${baseURL}${endpoint}/`, data) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; -export const readTasks = endpoint => { +export const readTasks = (endpoint) => { return axiosInstance .get(`${baseURL}${endpoint}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -23,8 +23,8 @@ export const readTasks = endpoint => { export const readTaskByID = (endpoint, id) => { return axiosInstance .get(`${baseURL}${endpoint}/${id}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -32,8 +32,8 @@ export const readTaskByID = (endpoint, id) => { export const updateTask = (endpoint, id, data) => { return axiosInstance .put(`${baseURL}${endpoint}/${id}/`, data) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; @@ -41,16 +41,16 @@ export const updateTask = (endpoint, id, data) => { export const deleteTask = (endpoint, id) => { return axiosInstance .delete(`${baseURL}${endpoint}/${id}/`) - .then(response => response.data) - .catch(error => { + .then((response) => response.data) + .catch((error) => { throw error; }); }; // Create -export const createTodoTask = data => createTask("todo", data); -export const createRecurrenceTask = data => createTask("daily", data); -export const createHabitTask = data => createTask("habit", data); +export const createTodoTask = (data) => createTask("todo", data); +export const createRecurrenceTask = (data) => createTask("daily", data); +export const createHabitTask = (data) => createTask("habit", data); // Read export const readTodoTasks = () => readTasks("todo"); @@ -58,9 +58,9 @@ export const readRecurrenceTasks = () => readTasks("daily"); export const readHabitTasks = () => readTasks("habit"); // Read by ID -export const readTodoTaskByID = id => readTaskByID("todo", id); -export const readRecurrenceTaskByID = id => readTaskByID("daily", id); -export const readHabitTaskByID = id => readTaskByID("habit", id); +export const readTodoTaskByID = (id) => readTaskByID("todo", id); +export const readRecurrenceTaskByID = (id) => readTaskByID("daily", id); +export const readHabitTaskByID = (id) => readTaskByID("habit", id); // Update export const updateTodoTask = (id, data) => updateTask("todo", id, data); @@ -68,6 +68,6 @@ export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data); // Delete -export const deleteTodoTask = id => deleteTask("todo", id); -export const deleteRecurrenceTask = id => deleteTask("daily", id); -export const deleteHabitTask = id => deleteTask("habit", id); +export const deleteTodoTask = (id) => deleteTask("todo", id); +export const deleteRecurrenceTask = (id) => deleteTask("daily", id); +export const deleteHabitTask = (id) => deleteTask("habit", id); diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx index e7a1b17..6dfc050 100644 --- a/frontend/src/api/UserProfileApi.jsx +++ b/frontend/src/api/UserProfileApi.jsx @@ -1,8 +1,10 @@ import axios from "axios"; -const ApiUpdateUserProfile = async formData => { +const baseURL = import.meta.env.VITE_BASE_URL; + +const ApiUpdateUserProfile = async (formData) => { try { - const response = await axios.post("http://127.0.1:8000/api/user/update/", formData, { + const response = await axios.post(`${baseURL}user/update/`, formData, { headers: { Authorization: "Bearer " + localStorage.getItem("access_token"), "Content-Type": "multipart/form-data", diff --git a/frontend/src/api/configs/AxiosConfig.jsx b/frontend/src/api/configs/AxiosConfig.jsx deleted file mode 100644 index b0410d1..0000000 --- a/frontend/src/api/configs/AxiosConfig.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import axios from "axios"; -import { redirect } from "react-router-dom"; - -const axiosInstance = axios.create({ - baseURL: "http://127.0.0.1:8000/api/", - timeout: 5000, - headers: { - Authorization: "Bearer " + localStorage.getItem("access_token"), - "Content-Type": "application/json", - accept: "application/json", - }, -}); - -// handling token refresh on 401 Unauthorized errors -axiosInstance.interceptors.response.use( - response => response, - error => { - const originalRequest = error.config; - const refresh_token = localStorage.getItem("refresh_token"); - - // Check if the error is due to 401 and a refresh token is available - if ( - error.response.status === 401 && - error.response.statusText === "Unauthorized" && - refresh_token !== "undefined" - ) { - return axiosInstance - .post("/token/refresh/", { refresh: refresh_token }) - .then(response => { - localStorage.setItem("access_token", response.data.access); - - axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access; - originalRequest.headers["Authorization"] = "Bearer " + response.data.access; - - return axiosInstance(originalRequest); - }) - .catch(err => { - console.log("Interceptors error: ", err); - }); - } - return Promise.reject(error); - } -); - -export default axiosInstance; diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx index 374ac66..619dca9 100644 --- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx +++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx @@ -1,14 +1,14 @@ import React, { useState, useEffect } from "react"; import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi"; import { readTodoTasks } from "../../api/TaskApi"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import axiosInstance from "src/api/AxiosConfig"; function EachBlog({ name, colorCode, contentList, icon }) { const [tasks, setTasks] = useState(contentList); - const handleCheckboxChange = async index => { + const handleCheckboxChange = async (index) => { try { - setTasks(contentList) + setTasks(contentList); const updatedTasks = [...tasks]; const taskId = updatedTasks[index].id; @@ -60,12 +60,12 @@ function Eisenhower() { useEffect(() => { readTodoTasks() - .then(data => { + .then((data) => { console.log(data); - const contentList_ui = data.filter(task => task.priority === 1); - const contentList_uni = data.filter(task => task.priority === 2); - const contentList_nui = data.filter(task => task.priority === 3); - const contentList_nuni = data.filter(task => task.priority === 4); + const contentList_ui = data.filter((task) => task.priority === 1); + const contentList_uni = data.filter((task) => task.priority === 2); + const contentList_nui = data.filter((task) => task.priority === 3); + const contentList_nuni = data.filter((task) => task.priority === 4); setTasks({ contentList_ui, @@ -74,7 +74,7 @@ function Eisenhower() { contentList_nuni, }); }) - .catch(error => console.error("Error fetching tasks:", error)); + .catch((error) => console.error("Error fetching tasks:", error)); }, []); return ( diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx index 93f8f0b..dff40ab 100644 --- a/frontend/src/components/authentication/LoginPage.jsx +++ b/frontend/src/components/authentication/LoginPage.jsx @@ -1,18 +1,17 @@ -import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { useNavigate, redirect } from "react-router-dom"; import { useGoogleLogin } from "@react-oauth/google"; import { useCallback } from "react"; import Particles from "react-tsparticles"; import { loadFull } from "tsparticles"; -import refreshAccessToken from "./refreshAcesstoken"; +import refreshAccessToken from "./refreshAcessToken"; import axiosapi from "../../api/AuthenticationApi"; -import { useAuth } from "../../hooks/authentication/IsAuthenticated"; import { FcGoogle } from "react-icons/fc"; - +import { useAuth } from "src/hooks/AuthHooks"; function LoginPage() { + const { setIsAuthenticated } = useAuth(); const Navigate = useNavigate(); - const { isAuthenticated, setIsAuthenticated } = useAuth(); useEffect(() => { if (!refreshAccessToken()) { @@ -43,15 +42,13 @@ function LoginPage() { // On successful login, store tokens and set the authorization header localStorage.setItem("access_token", res.data.access); localStorage.setItem("refresh_token", res.data.refresh); - axiosapi.axiosInstance.defaults.headers["Authorization"] = - "Bearer " + res.data.access; + axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access; setIsAuthenticated(true); - Navigate("/"); + redirect("/"); }) .catch((err) => { console.log("Login failed"); console.log(err); - setIsAuthenticated(false); }); }; @@ -71,7 +68,6 @@ function LoginPage() { } } catch (error) { console.error("Error with the POST request:", error); - setIsAuthenticated(false); } }, onError: (errorResponse) => console.log(errorResponse), @@ -89,10 +85,7 @@ function LoginPage() { }, []); return ( -
+
{/* Particles Container */}
OR
{/* Login with Google Button */} - {/* Forgot Password Link */}
diff --git a/frontend/src/components/authentication/refreshAcessToken.jsx b/frontend/src/components/authentication/refreshAcessToken.jsx index 11a74fa..e07395b 100644 --- a/frontend/src/components/authentication/refreshAcessToken.jsx +++ b/frontend/src/components/authentication/refreshAcessToken.jsx @@ -1,5 +1,7 @@ import axios from "axios"; +const baseURL = import.meta.env.VITE_BASE_URL; + async function refreshAccessToken() { const refresh_token = localStorage.getItem("refresh_token"); const access_token = localStorage.getItem("access_token"); @@ -12,7 +14,7 @@ async function refreshAccessToken() { return false; } - const refreshUrl = "http://127.0.0.1:8000/api/token/refresh/"; + const refreshUrl = `${baseURL}token/refresh/`; try { const response = await axios.post(refreshUrl, { refresh: refresh_token }); diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index b4f8430..e4f1bdd 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -1,11 +1,11 @@ -import React, { useState } from "react"; +import React from "react"; import { formatDate } from "@fullcalendar/core"; import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import { getEvents, createEventId } from "./TaskDataHandler"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import axiosInstance from "src/api/AxiosConfig"; export default class Calendar extends React.Component { state = { @@ -83,7 +83,7 @@ export default class Calendar extends React.Component { }); }; - handleDateSelect = selectInfo => { + handleDateSelect = (selectInfo) => { let title = prompt("Please enter a new title for your event"); let calendarApi = selectInfo.view.calendar; @@ -100,20 +100,20 @@ export default class Calendar extends React.Component { } }; - handleEventClick = clickInfo => { + handleEventClick = (clickInfo) => { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { axiosInstance - .delete(`todo/${clickInfo.event.id}/`) - .then(response => { - clickInfo.event.remove(); - }) - .catch(error => { - console.error("Error deleting Task:", error); - }); + .delete(`todo/${clickInfo.event.id}/`) + .then((response) => { + clickInfo.event.remove(); + }) + .catch((error) => { + console.error("Error deleting Task:", error); + }); } }; - handleEvents = events => { + handleEvents = (events) => { this.setState({ currentEvents: events, }); diff --git a/frontend/src/components/dashboard/Areachart.jsx b/frontend/src/components/dashboard/Areachart.jsx index 8ede4d8..862fa21 100644 --- a/frontend/src/components/dashboard/Areachart.jsx +++ b/frontend/src/components/dashboard/Areachart.jsx @@ -1,103 +1,36 @@ import { AreaChart, Title } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { useState, useEffect } from "react"; +import axiosInstance from "src/api/AxiosConfig"; -const fetchAreaChartData = async () => { - let res = await axiosInstance.get("/dashboard/weekly/"); - console.log(res.data); - // const areaChartData = [ - // { - // date: "Mon", - // "This Week": res.data[0]["This Week"], - // "Last Week": res.data[0]["Last Week"], - // }, - // { - // date: "Tue", - // "This Week": res.data[1]["This Week"], - // "Last Week": res.data[1]["Last Week"], - // }, - // { - // date: "Wed", - // "This Week": res.data[2]["This Week"], - // "Last Week": res.data[2]["Last Week"], - // }, - // { - // date: "Th", - // "This Week": res.data[3]["This Week"], - // "Last Week": res.data[3]["Last Week"], - // }, - // { - // date: "Fri", - // "This Week": res.data[4]["This Week"], - // "Last Week": res.data[4]["Last Week"], - // }, - // { - // date: "Sat", - // "This Week": res.data[5]["This Week"], - // "Last Week": res.data[5]["Last Week"], - // }, - // { - // date: "Sun", - // "This Week": res.data[6]["This Week"], - // "Last Week": res.data[6]["Last Week"], - // }, - // ]; - const areaChartData = [ - { - date: "Mon", - "This Week": 1, - "Last Week": 2, - }, - { - date: "Tue", - "This Week": 5, - "Last Week": 2, - }, - { - date: "Wed", - "This Week": 7, - "Last Week": 9, - }, - { - date: "Th", - "This Week": 10, - "Last Week": 3, - }, - { - date: "Fri", - "This Week": 5, - "Last Week": 1, - }, - { - date: "Sat", - "This Week": 7, - "Last Week": 8, - }, - { - date: "Sun", - "This Week": 3, - "Last Week": 8, - }, - ]; - return areaChartData; -} - -const areaChartDataArray = await fetchAreaChartData(); export const AreaChartGraph = () => { - const [value, setValue] = React.useState(null); - return ( - <> - Number of tasks statistics vs. last week - setValue(v)} - showAnimation - /> - - ); -}; \ No newline at end of file + const [areaChartDataArray, setAreaChartDataArray] = useState([]); + + useEffect(() => { + const fetchAreaChartData = async () => { + try { + const response = await axiosInstance.get("/dashboard/weekly/"); + const areaChartData = response.data; + setAreaChartDataArray(areaChartData); + } catch (error) { + console.error("Error fetching area chart data:", error); + } + }; + + fetchAreaChartData(); + }, []); + + return ( + <> + Number of tasks statistics vs. last week + + + ); +}; diff --git a/frontend/src/components/dashboard/Barchart.jsx b/frontend/src/components/dashboard/Barchart.jsx index ab4d0b4..63a2c4f 100644 --- a/frontend/src/components/dashboard/Barchart.jsx +++ b/frontend/src/components/dashboard/Barchart.jsx @@ -1,88 +1,24 @@ import { BarChart, Title } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { useState, useEffect } from "react"; +import axiosInstance from "src/api/AxiosConfig"; -const fetchBarChartData = async () => { - let res = await axiosInstance.get("/dashboard/weekly/"); - // const barchartData = [ - // { - // date: "Mon", - // "This Week": res.data[0]["Completed This Week"], - // "Last Week": res.data[0]["Completed Last Week"], - // }, - // { - // date: "Tue", - // "This Week": res.data[1]["Completed This Week"], - // "Last Week": res.data[1]["Completed Last Week"], - // }, - // { - // date: "Wed", - // "This Week": res.data[2]["Completed This Week"], - // "Last Week": res.data[2]["Completed Last Week"], - // }, - // { - // date: "Th", - // "This Week": res.data[3]["Completed This Week"], - // "Last Week": res.data[3]["Completed Last Week"], - // }, - // { - // date: "Fri", - // "This Week": res.data[4]["Completed This Week"], - // "Last Week": res.data[4]["Completed Last Week"], - // }, - // { - // date: "Sat", - // "This Week": res.data[5]["Completed This Week"], - // "Last Week": res.data[5]["Completed Last Week"], - // }, - // { - // date: "Sun", - // "This Week": res.data[6]["Completed This Week"], - // "Last Week": res.data[6]["Completed Last Week"], - // }, - // ]; - const barchartData = [ - { - date: "Mon", - "This Week": 1, - "Last Week": 2, - }, - { - date: "Tue", - "This Week": 5, - "Last Week": 2, - }, - { - date: "Wed", - "This Week": 7, - "Last Week": 9, - }, - { - date: "Th", - "This Week": 10, - "Last Week": 3, - }, - { - date: "Fri", - "This Week": 5, - "Last Week": 1, - }, - { - date: "Sat", - "This Week": 7, - "Last Week": 8, - }, - { - date: "Sun", - "This Week": 3, - "Last Week": 8, - }, - ]; - return barchartData; -}; -const barchartDataArray = await fetchBarChartData(); export const BarChartGraph = () => { - const [value, setValue] = React.useState(null); + const [barchartDataArray, setBarChartDataArray] = useState([]); + + useEffect(() => { + const fetchAreaChartData = async () => { + try { + const response = await axiosInstance.get("/dashboard/weekly/"); + const barchartDataArray = response.data; + setBarChartDataArray(barchartDataArray); + } catch (error) { + console.error("Error fetching area chart data:", error); + } + }; + + fetchAreaChartData(); + }, []); + return ( <> Task completed statistics vs. last week @@ -93,7 +29,6 @@ export const BarChartGraph = () => { categories={["This Week", "Last Week"]} colors={["neutral", "indigo"]} yAxisWidth={30} - onValueChange={(v) => setValue(v)} showAnimation /> diff --git a/frontend/src/components/dashboard/DonutChart.jsx b/frontend/src/components/dashboard/DonutChart.jsx index c71b81d..ccab32f 100644 --- a/frontend/src/components/dashboard/DonutChart.jsx +++ b/frontend/src/components/dashboard/DonutChart.jsx @@ -1,40 +1,37 @@ import { DonutChart } from "@tremor/react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import axiosInstance from "src/api/AxiosConfig"; +import { useState, useEffect } from "react"; -const fetchDonutData = async () => { - try { - let res = await axiosInstance.get("/dashboard/stats/"); - // let todoCount = res.data.todo_count; - // let recurrenceCount = res.data.recurrence_count; - let todoCount = 10; - let recurrenceCount = 15; - if (todoCount === undefined) { - todoCount = 0; - } - if (recurrenceCount === undefined) { - recurrenceCount = 0; - } - const donutData = [ - { name: "Todo", count: todoCount }, - { name: "Recurrence", count: recurrenceCount }, - ]; - return donutData; - } catch (error) { - console.error("Error fetching donut data:", error); - return []; - } -}; - -const donutDataArray = await fetchDonutData(); export default function DonutChartGraph() { + const [donutData, setDonutData] = useState([]); + + useEffect(() => { + const fetchDonutData = async () => { + try { + const response = await axiosInstance.get("/dashboard/stats/"); + const todoCount = response.data.todo_count || 0; + const recurrenceCount = response.data.recurrence_count || 0; + + const donutData = [ + { name: "Todo", count: todoCount }, + { name: "Recurrence", count: recurrenceCount }, + ]; + + setDonutData(donutData); + } catch (error) { + console.error("Error fetching donut data:", error); + } + }; + fetchDonutData(); + }, []); + return ( setValue(v)} showAnimation radius={25} /> diff --git a/frontend/src/components/dashboard/KpiCard.jsx b/frontend/src/components/dashboard/KpiCard.jsx index 7ebb841..47c7162 100644 --- a/frontend/src/components/dashboard/KpiCard.jsx +++ b/frontend/src/components/dashboard/KpiCard.jsx @@ -1,42 +1,57 @@ - import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; - -const fetchKpiCardData = async () => { - let res = await axiosInstance.get("/dashboard/stats/"); - // let completedThisWeek = res.data["completed_this_week"]; - // let completedLastWeek = res.data["completed_last_week"]; - let completedThisWeek = 4; - let completedLastWeek = 23; - let percentage = (completedThisWeek / completedLastWeek)*100; - let incOrdec = undefined; - if (completedThisWeek <= completedLastWeek) { - incOrdec = "moderateDecrease"; - } - if (completedThisWeek > completedLastWeek) { - incOrdec = "moderateIncrease"; - } - return {completedThisWeek, completedLastWeek, incOrdec, percentage}; -} - -const {kpiCardDataArray, completedThisWeek ,completedLastWeek, incOrdec, percentage} = await fetchKpiCardData(); - +import { useEffect, useState } from "react"; +import axiosInstance from "src/api/AxiosConfig"; export default function KpiCard() { + const [kpiCardData, setKpiCardData] = useState({ + completedThisWeek: 0, + completedLastWeek: 0, + incOrdec: undefined, + percentage: 0, + }); + + useEffect(() => { + const fetchKpiCardData = async () => { + try { + const response = await axiosInstance.get("/dashboard/stats/"); + const completedThisWeek = response.data.completed_this_week || 0; + const completedLastWeek = response.data.completed_last_week || 0; + const percentage = (completedThisWeek / completedLastWeek) * 100; + let incOrdec = undefined; + + if (completedThisWeek <= completedLastWeek) { + incOrdec = "moderateDecrease"; + } + if (completedThisWeek > completedLastWeek) { + incOrdec = "moderateIncrease"; + } + + setKpiCardData({ + completedThisWeek, + completedLastWeek, + incOrdec, + percentage, + }); + } catch (error) { + console.error("Error fetching KPI card data:", error); + } + }; + + fetchKpiCardData(); + }, []); + return ( -
- {completedThisWeek} + {kpiCardData.completedThisWeek}
- {percentage.toFixed(0)}% + {kpiCardData.percentage.toFixed(0)}%
- vs. {completedLastWeek} (last week) + vs. {kpiCardData.completedLastWeek} (last week) - +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/dashboard/ProgressCircle.jsx b/frontend/src/components/dashboard/ProgressCircle.jsx index 4f8382b..c6d09b3 100644 --- a/frontend/src/components/dashboard/ProgressCircle.jsx +++ b/frontend/src/components/dashboard/ProgressCircle.jsx @@ -1,38 +1,39 @@ -import { Card, Flex, ProgressCircle, Text, } from "@tremor/react"; -import React from "react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { Card, Flex, ProgressCircle } from "@tremor/react"; +import { useState, useEffect } from "react"; +import axiosInstance from "src/api/AxiosConfig"; -const fetchProgressData = async () => { - try { - let res = await axiosInstance.get("/dashboard/stats/"); - // let completedLastWeek = res.data.completed_last_week; - // let assignLastWeek = res.data.tasks_assigned_last_week; - let completedLastWeek = 15; - let assignLastWeek = 35; - if (completedLastWeek === undefined) { - completedLastWeek = 0; - } - if (assignLastWeek === undefined) { - assignLastWeek = 0; - } - return (completedLastWeek / assignLastWeek) * 100; - } catch (error) { - console.error("Error fetching progress data:", error); - return 0; - } -}; - -const progressData = await fetchProgressData(); export default function ProgressCircleChart() { + const [progressData, setProgressData] = useState(0); + + useEffect(() => { + const fetchProgressData = async () => { + try { + const response = await axiosInstance.get("/dashboard/stats/"); + let completedLastWeek = response.data.completed_last_week || 0; + let assignLastWeek = response.data.tasks_assigned_last_week || 0; + + if (completedLastWeek === undefined) { + completedLastWeek = 0; + } + if (assignLastWeek === undefined) { + assignLastWeek = 0; + } + + const progress = (completedLastWeek / assignLastWeek) * 100; + + setProgressData(progress); + } catch (error) { + console.error("Error fetching progress data:", error); + } + }; + + fetchProgressData(); + }, []); + return ( - + {progressData.toFixed(0)} % diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 06f8c64..887fb6d 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -1,16 +1,4 @@ -import { - Card, - Grid, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, - Title, - Legend, - DateRangePicker, -} from "@tremor/react"; +import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react"; import KpiCard from "./KpiCard"; import { BarChartGraph } from "./Barchart"; import DonutChartGraph from "./DonutChart"; @@ -18,9 +6,6 @@ import { AreaChartGraph } from "./Areachart"; import ProgressCircleChart from "./ProgressCircle"; import { useState } from "react"; -const valueFormatter = (number) => - `$ ${new Intl.NumberFormat("us").format(number).toString()}`; - export default function Dashboard() { const [value, setValue] = useState({ from: new Date(2021, 0, 1), @@ -64,8 +49,7 @@ export default function Dashboard() { + colors={["indigo"]}> diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 2272444..a4ecd79 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -5,11 +5,11 @@ import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { createPortal } from "react-dom"; import TaskCard from "./taskCard"; import { AiOutlinePlusCircle } from "react-icons/ai"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import axiosInstance from "src/api/AxiosConfig"; function KanbanBoard() { const [columns, setColumns] = useState([]); - const columnsId = useMemo(() => columns.map(col => col.id), [columns]); + const columnsId = useMemo(() => columns.map((col) => col.id), [columns]); const [boardId, setBoardData] = useState(); const [tasks, setTasks] = useState([]); @@ -66,7 +66,7 @@ function KanbanBoard() { const tasksResponse = await axiosInstance.get("/todo"); // Transform - const transformedTasks = tasksResponse.data.map(task => ({ + const transformedTasks = tasksResponse.data.map((task) => ({ id: task.id, columnId: task.list_board, content: task.title, @@ -95,7 +95,7 @@ function KanbanBoard() { const columnsResponse = await axiosInstance.get("/lists"); // Transform - const transformedColumns = columnsResponse.data.map(column => ({ + const transformedColumns = columnsResponse.data.map((column) => ({ id: column.id, title: column.name, })); @@ -135,7 +135,7 @@ function KanbanBoard() {
- {columns.map(col => ( + {columns.map((col) => ( task.columnId === col.id)} + tasks={tasks.filter((task) => task.columnId === col.id)} /> ))} @@ -186,7 +186,7 @@ function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={tasks.filter(task => task.columnId === activeColumn.id)} + tasks={tasks.filter((task) => task.columnId === activeColumn.id)} /> )} {activeTask && } @@ -213,35 +213,34 @@ function KanbanBoard() { axiosInstance .post("todo/", newTaskData) - .then(response => { + .then((response) => { const newTask = { id: response.data.id, columnId, content: response.data.title, }; - }) - .catch(error => { + .catch((error) => { console.error("Error creating task:", error); }); - setTasks(tasks => [...tasks, newTask]); - } + setTasks((tasks) => [...tasks, newTask]); + } function deleteTask(id) { - const newTasks = tasks.filter(task => task.id !== id); + const newTasks = tasks.filter((task) => task.id !== id); axiosInstance .delete(`todo/${id}/`) - .then(response => { + .then((response) => { setTasks(newTasks); }) - .catch(error => { + .catch((error) => { console.error("Error deleting Task:", error); }); - setTasks(newTasks); + setTasks(newTasks); } function updateTask(id, content) { - const newTasks = tasks.map(task => { + const newTasks = tasks.map((task) => { if (task.id !== id) return task; return { ...task, content }; }); @@ -252,15 +251,15 @@ function KanbanBoard() { function createNewColumn() { axiosInstance .post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id }) - .then(response => { + .then((response) => { const newColumn = { id: response.data.id, title: response.data.name, }; - setColumns(prevColumns => [...prevColumns, newColumn]); + setColumns((prevColumns) => [...prevColumns, newColumn]); }) - .catch(error => { + .catch((error) => { console.error("Error creating ListBoard:", error); }); } @@ -268,22 +267,22 @@ function KanbanBoard() { function deleteColumn(id) { axiosInstance .delete(`lists/${id}/`) - .then(response => { - setColumns(prevColumns => prevColumns.filter(col => col.id !== id)); + .then((response) => { + setColumns((prevColumns) => prevColumns.filter((col) => col.id !== id)); }) - .catch(error => { + .catch((error) => { console.error("Error deleting ListBoard:", error); }); - const tasksToDelete = tasks.filter(t => t.columnId === id); + const tasksToDelete = tasks.filter((t) => t.columnId === id); - tasksToDelete.forEach(task => { + tasksToDelete.forEach((task) => { axiosInstance .delete(`todo/${task.id}/`) - .then(response => { - setTasks(prevTasks => prevTasks.filter(t => t.id !== task.id)); + .then((response) => { + setTasks((prevTasks) => prevTasks.filter((t) => t.id !== task.id)); }) - .catch(error => { + .catch((error) => { console.error("Error deleting Task:", error); }); }); @@ -293,10 +292,10 @@ function KanbanBoard() { // Update the column axiosInstance .patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements - .then(response => { - setColumns(prevColumns => prevColumns.map(col => (col.id === id ? { ...col, title } : col))); + .then((response) => { + setColumns((prevColumns) => prevColumns.map((col) => (col.id === id ? { ...col, title } : col))); }) - .catch(error => { + .catch((error) => { console.error("Error updating ListBoard:", error); }); } @@ -330,9 +329,9 @@ function KanbanBoard() { // Reorder columns if the dragged item is a column if (isActiveAColumn && isOverAColumn) { - setColumns(columns => { - const activeColumnIndex = columns.findIndex(col => col.id === activeId); - const overColumnIndex = columns.findIndex(col => col.id === overId); + setColumns((columns) => { + const activeColumnIndex = columns.findIndex((col) => col.id === activeId); + const overColumnIndex = columns.findIndex((col) => col.id === overId); const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex); @@ -342,9 +341,9 @@ function KanbanBoard() { // Reorder tasks within the same column if (isActiveATask && isOverATask) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); - const overIndex = tasks.findIndex(t => t.id === overId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); const reorderedTasks = arrayMove(tasks, activeIndex, overIndex); @@ -354,15 +353,15 @@ function KanbanBoard() { // Move tasks between columns and update columnId if (isActiveATask && isOverAColumn) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); tasks[activeIndex].columnId = overId; axiosInstance .put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 }) - .then(response => {}) - .catch(error => { + .then((response) => {}) + .catch((error) => { console.error("Error updating task columnId:", error); }); @@ -386,9 +385,9 @@ function KanbanBoard() { if (!isActiveATask) return; if (isActiveATask && isOverATask) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); - const overIndex = tasks.findIndex(t => t.id === overId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) { tasks[activeIndex].columnId = tasks[overIndex].columnId; @@ -402,8 +401,8 @@ function KanbanBoard() { const isOverAColumn = over.data.current?.type === "Column"; if (isActiveATask && isOverAColumn) { - setTasks(tasks => { - const activeIndex = tasks.findIndex(t => t.id === activeId); + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); tasks[activeIndex].columnId = overId; return arrayMove(tasks, activeIndex, activeIndex); diff --git a/frontend/src/components/landingPage/LandingPage.jsx b/frontend/src/components/landingPage/LandingPage.jsx new file mode 100644 index 0000000..1fe0e1e --- /dev/null +++ b/frontend/src/components/landingPage/LandingPage.jsx @@ -0,0 +1,58 @@ +export function LandingPage() { + return ( +
+ +
+ +
+
+
+

+ Manage your task with{" "} + + TurTask + + +

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio incidunt nam itaque sed eius modi error + totam sit illum. Voluptas doloribus asperiores quaerat aperiam. Quidem harum omnis beatae ipsum soluta! +

+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/navigations/Navbar.jsx b/frontend/src/components/navigations/Navbar.jsx index aeb0065..5a024af 100644 --- a/frontend/src/components/navigations/Navbar.jsx +++ b/frontend/src/components/navigations/Navbar.jsx @@ -1,18 +1,17 @@ -import * as React from "react"; import { useNavigate } from "react-router-dom"; import axiosapi from "../../api/AuthenticationApi"; -import { useAuth } from "../../hooks/authentication/IsAuthenticated"; +import { useAuth } from "src/hooks/AuthHooks"; const settings = { - Profile: '/profile', - Account: '/account', + Profile: "/profile", + Account: "/account", }; function NavBar() { const Navigate = useNavigate(); - const { isAuthenticated, setIsAuthenticated } = useAuth(); + console.log(isAuthenticated); const logout = () => { axiosapi.apiUserLogout(); setIsAuthenticated(false); diff --git a/frontend/src/components/signup/Signup.jsx b/frontend/src/components/signup/Signup.jsx index 3958feb..dd25136 100644 --- a/frontend/src/components/signup/Signup.jsx +++ b/frontend/src/components/signup/Signup.jsx @@ -1,39 +1,38 @@ -import React from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faGoogle, faGithub } from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faGoogle, faGithub } from "@fortawesome/free-brands-svg-icons"; function Signup() { - return ( -
-
-

Create your account

-

- Start spending more time on your own table. -

-
-
- -
+ return ( +
+
+

Create your account

+

Start spending more time on your own table.

+
+
+ +
-
- -
+
+ +
-
- -
-
-
+
+ +
- ); +
+
+ ); } export default Signup; diff --git a/frontend/src/contexts/AuthContextProvider.jsx b/frontend/src/contexts/AuthContextProvider.jsx new file mode 100644 index 0000000..fea0243 --- /dev/null +++ b/frontend/src/contexts/AuthContextProvider.jsx @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import PropTypes from "prop-types"; +import { createContext, useState } from "react"; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const accessToken = localStorage.getItem("access_token"); + if (accessToken) { + setIsAuthenticated(true); + } + }, []); + + const contextValue = { + isAuthenticated, + setIsAuthenticated, + }; + + return {children}; +}; + +AuthProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default AuthContext; diff --git a/frontend/src/hooks/AuthHooks.jsx b/frontend/src/hooks/AuthHooks.jsx new file mode 100644 index 0000000..27d3afb --- /dev/null +++ b/frontend/src/hooks/AuthHooks.jsx @@ -0,0 +1,36 @@ +import { useContext } from "react"; +import AuthContext from "src/contexts/AuthContextProvider"; + +/** + * useAuth - Custom React Hook for Accessing Authentication Context + * + * @returns {Object} An object containing: + * - {boolean} isAuthenticated: A boolean indicating whether the user is authenticated. + * - {function} setIsAuthenticated: A function to set the authentication status manually. + * + * @throws {Error} If used outside the context of an AuthProvider. + * + * @example + * // Import the hook + * import useAuth from './AuthHooks'; + * + * // Inside a functional component + * const { isAuthenticated, setIsAuthenticated } = useAuth(); + * + * // Check authentication status + * if (isAuthenticated) { + * // User is authenticated + * } else { + * // User is not authenticated + * } + * + * // Manually set authentication status + * setIsAuthenticated(true); + */ +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/frontend/src/hooks/authentication/IsAuthenticated.jsx b/frontend/src/hooks/authentication/IsAuthenticated.jsx deleted file mode 100644 index 8874645..0000000 --- a/frontend/src/hooks/authentication/IsAuthenticated.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; - -const AuthContext = createContext(); - -export const AuthProvider = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(() => { - const access_token = localStorage.getItem("access_token"); - return !!access_token; - }); - - useEffect(() => { - const handleTokenChange = () => { - const newAccessToken = localStorage.getItem("access_token"); - setIsAuthenticated(!!newAccessToken); - }; - - handleTokenChange(); - - window.addEventListener("storage", handleTokenChange); - - return () => { - window.removeEventListener("storage", handleTokenChange); - }; - }, []); - - return ( - - {children} - - ); -}; - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 38cca17..8ca8296 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { BrowserRouter } from "react-router-dom"; -import { AuthProvider } from "./hooks/authentication/IsAuthenticated"; +import { AuthProvider } from "./contexts/AuthContextProvider"; const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; @@ -12,7 +12,7 @@ ReactDOM.createRoot(document.getElementById("root")).render( - + diff --git a/frontend/vercel.json b/frontend/vercel.json new file mode 100644 index 0000000..59fd940 --- /dev/null +++ b/frontend/vercel.json @@ -0,0 +1,5 @@ +{ + "rewrites": [ + {"source": "/(.*)", "destination": "/"} + ] +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 5a33944..536af4a 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,14 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + resolve: { + alias: { + src: "/src", + }, + }, + define: { + __APP_ENV__: process.env.VITE_VERCEL_ENV, + }, +});