Merge pull request #22 from TurTaskProject/feature/user-profile-settings

Add profile update page and modify login page
This commit is contained in:
Sirin Puenggun 2023-11-05 19:13:03 +07:00 committed by GitHub
commit eb22d30a24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 465 additions and 491 deletions

View File

@ -10,6 +10,7 @@ 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
@ -80,6 +81,11 @@ REST_FRAMEWORK = {
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')
@ -226,4 +232,27 @@ LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
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",
},
}

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-04 19:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_customuser_refresh_token'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='profile_pic',
field=models.ImageField(blank=True, default='profile_pics/default.png', null=True, upload_to='profile_pics'),
),
]

View File

@ -13,6 +13,7 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
first_name = models.CharField(max_length=150, blank=True)
start_date = models.DateTimeField(default=timezone.now)
about = models.TextField(_('about'), max_length=500, blank=True)
profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png')
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)

View File

@ -7,12 +7,12 @@ class CustomUserSerializer(serializers.ModelSerializer):
Serializer for CustomUser model.
"""
email = serializers.EmailField(required=True)
username = serializers.CharField(required=True)
username = serializers.CharField()
password = serializers.CharField(min_length=8, write_only=True)
class Meta:
model = CustomUser
fields = ('email', 'password')
fields = ('email', 'username', 'password')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
@ -25,3 +25,25 @@ class CustomUserSerializer(serializers.ModelSerializer):
instance.set_password(password)
instance.save()
return instance
class UpdateProfileSerializer(serializers.ModelSerializer):
"""
Serializer for updating user profile.
"""
profile_pic = serializers.ImageField(required=False)
first_name = serializers.CharField(max_length=255, required=False)
about = serializers.CharField(required=False)
class Meta:
model = CustomUser
fields = ('profile_pic', 'first_name', 'about')
def update(self, instance, validated_data):
"""
Update an existing user's profile.
"""
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance

View File

@ -1,6 +1,7 @@
from django.urls import path
from users.views import CustomUserCreate
from users.views import CustomUserCreate, CustomUserProfileUpdate
urlpatterns = [
path('user/create/', CustomUserCreate.as_view(), name="create_user"),
path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user')
]

View File

@ -1,12 +1,14 @@
"""This module defines API views for user creation"""
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser
from .serializers import CustomUserSerializer
from users.serializers import CustomUserSerializer, UpdateProfileSerializer
from users.models import CustomUser
class CustomUserCreate(APIView):
"""
@ -25,3 +27,34 @@ class CustomUserCreate(APIView):
if user:
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CustomUserProfileUpdate(APIView):
"""
Custom User Profile Update View.
"""
parser_classes = (MultiPartParser,)
permission_classes = (IsAuthenticated,)
def get(self, request):
user = request.user
image_url = user.profile_pic.url
username = user.username
data = {
'image_url': image_url,
'username': username
}
return Response(data)
def post(self, request):
if not CustomUser.objects.filter(email=request.user.email).exists():
return Response ({
'error': 'User does not exist'
}, status=status.HTTP_404_NOT_FOUND)
serializer = UpdateProfileSerializer(request.user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -12,8 +12,6 @@
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@mui/icons-material": "^5.14.15",
"@mui/material": "^5.14.15",
"@mui/system": "^5.14.15",
@ -27,15 +25,16 @@
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0",
"react-google-login": "^5.2.2",
"react-icons": "^4.11.0",
"react-router-dom": "^6.17.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"daisyui": "^3.9.4",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",

View File

@ -11,12 +11,6 @@ dependencies:
'@emotion/styled':
specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.33)(react@18.2.0)
'@material-ui/core':
specifier: ^4.12.4
version: 4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@material-ui/icons':
specifier: ^4.11.3
version: 4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@mui/icons-material':
specifier: ^5.14.15
version: 5.14.15(@mui/material@5.14.15)(@types/react@18.2.33)(react@18.2.0)
@ -56,9 +50,6 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-google-login:
specifier: ^5.2.2
version: 5.2.2(react-dom@18.2.0)(react@18.2.0)
react-icons:
specifier: ^4.11.0
version: 4.11.0(react@18.2.0)
@ -67,6 +58,9 @@ dependencies:
version: 6.17.0(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.3.5)
'@types/react':
specifier: ^18.2.15
version: 18.2.33
@ -79,6 +73,9 @@ devDependencies:
autoprefixer:
specifier: ^10.4.16
version: 10.4.16(postcss@8.4.31)
daisyui:
specifier: ^3.9.4
version: 3.9.4
eslint:
specifier: ^8.45.0
version: 8.52.0
@ -364,10 +361,6 @@ packages:
stylis: 4.2.0
dev: false
/@emotion/hash@0.8.0:
resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
dev: false
/@emotion/hash@0.9.1:
resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==}
dev: false
@ -785,132 +778,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@material-ui/core@4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==}
engines: {node: '>=8.0.0'}
deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.
peerDependencies:
'@types/react': ^16.8.6 || ^17.0.0
react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@material-ui/styles': 4.11.5(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@material-ui/system': 4.12.2(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@material-ui/types': 5.1.0(@types/react@18.2.33)
'@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.33
'@types/react-transition-group': 4.4.8
clsx: 1.2.1
hoist-non-react-statics: 3.3.2
popper.js: 1.16.1-lts
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 16.13.1
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
dev: false
/@material-ui/icons@4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==}
engines: {node: '>=8.0.0'}
peerDependencies:
'@material-ui/core': ^4.0.0
'@types/react': ^16.8.6 || ^17.0.0
react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@material-ui/core': 4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.33
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@material-ui/styles@4.11.5(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==}
engines: {node: '>=8.0.0'}
deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.
peerDependencies:
'@types/react': ^16.8.6 || ^17.0.0
react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@emotion/hash': 0.8.0
'@material-ui/types': 5.1.0(@types/react@18.2.33)
'@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.33
clsx: 1.2.1
csstype: 2.6.21
hoist-non-react-statics: 3.3.2
jss: 10.10.0
jss-plugin-camel-case: 10.10.0
jss-plugin-default-unit: 10.10.0
jss-plugin-global: 10.10.0
jss-plugin-nested: 10.10.0
jss-plugin-props-sort: 10.10.0
jss-plugin-rule-value-function: 10.10.0
jss-plugin-vendor-prefixer: 10.10.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@material-ui/system@4.12.2(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==}
engines: {node: '>=8.0.0'}
peerDependencies:
'@types/react': ^16.8.6 || ^17.0.0
react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.33
csstype: 2.6.21
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@material-ui/types@5.1.0(@types/react@18.2.33):
resolution: {integrity: sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==}
peerDependencies:
'@types/react': '*'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.33
dev: false
/@material-ui/utils@4.11.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0
react-dom: ^16.8.0 || ^17.0.0
dependencies:
'@babel/runtime': 7.23.2
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 16.13.1
dev: false
/@mui/base@5.0.0-beta.21(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-eTKWx3WV/nwmRUK4z4K1MzlMyWCsi3WJ3RtV4DiXZeRh4qd4JCyp1Zzzi8Wv9xM4dEBmqQntFoei716PzwmFfA==}
engines: {node: '>=12.0.0'}
@ -1173,6 +1040,18 @@ packages:
tslib: 2.6.2
dev: false
/@tailwindcss/typography@0.5.10(tailwindcss@3.3.5):
resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 3.3.5
dev: true
/@types/babel__core@7.20.3:
resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==}
dependencies:
@ -1525,11 +1404,6 @@ packages:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/clsx@2.0.0:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
@ -1554,6 +1428,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
dev: true
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -1598,12 +1476,12 @@ packages:
which: 2.0.2
dev: true
/css-vendor@2.0.8:
resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==}
/css-selector-tokenizer@0.8.0:
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
dependencies:
'@babel/runtime': 7.23.2
is-in-browser: 1.1.3
dev: false
cssesc: 3.0.0
fastparse: 1.1.2
dev: true
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@ -1611,13 +1489,22 @@ packages:
hasBin: true
dev: true
/csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
dev: false
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/daisyui@3.9.4:
resolution: {integrity: sha512-fvi2RGH4YV617/6DntOVGcOugOPym9jTGWW2XySb5ZpvdWO4L7bEG77VHirrnbRUEWvIEVXkBpxUz2KFj0rVnA==}
engines: {node: '>=16.9.0'}
dependencies:
colord: 2.9.3
css-selector-tokenizer: 0.8.0
postcss: 8.4.31
postcss-js: 4.0.1(postcss@8.4.31)
tailwindcss: 3.3.5
transitivePeerDependencies:
- ts-node
dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -1995,6 +1882,10 @@ packages:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: true
/fastparse@1.1.2:
resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==}
dev: true
/fastq@1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies:
@ -2255,10 +2146,6 @@ packages:
react-is: 16.13.1
dev: false
/hyphenate-style-name@1.0.4:
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
dev: false
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@ -2384,10 +2271,6 @@ packages:
is-extglob: 2.1.1
dev: true
/is-in-browser@1.1.3:
resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==}
dev: false
/is-map@2.0.2:
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
dev: true
@ -2531,68 +2414,6 @@ packages:
hasBin: true
dev: true
/jss-plugin-camel-case@10.10.0:
resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==}
dependencies:
'@babel/runtime': 7.23.2
hyphenate-style-name: 1.0.4
jss: 10.10.0
dev: false
/jss-plugin-default-unit@10.10.0:
resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-global@10.10.0:
resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-nested@10.10.0:
resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
/jss-plugin-props-sort@10.10.0:
resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-rule-value-function@10.10.0:
resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
/jss-plugin-vendor-prefixer@10.10.0:
resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==}
dependencies:
'@babel/runtime': 7.23.2
css-vendor: 2.0.8
jss: 10.10.0
dev: false
/jss@10.10.0:
resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==}
dependencies:
'@babel/runtime': 7.23.2
csstype: 3.1.2
is-in-browser: 1.1.3
tiny-warning: 1.0.3
dev: false
/jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@ -2637,6 +2458,14 @@ packages:
p-locate: 5.0.0
dev: true
/lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
dev: true
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
@ -2872,10 +2701,6 @@ packages:
engines: {node: '>= 6'}
dev: true
/popper.js@1.16.1-lts:
resolution: {integrity: sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==}
dev: false
/postcss-import@15.1.0(postcss@8.4.31):
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@ -2925,6 +2750,14 @@ packages:
postcss-selector-parser: 6.0.13
dev: true
/postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
dev: true
/postcss-selector-parser@6.0.13:
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
engines: {node: '>=4'}
@ -3018,19 +2851,6 @@ packages:
scheduler: 0.23.0
dev: false
/react-google-login@5.2.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^16 || ^17
react-dom: ^16 || ^17
dependencies:
'@types/react': 18.2.33
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-icons@4.11.0(react@18.2.0):
resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==}
peerDependencies:
@ -3396,10 +3216,6 @@ packages:
any-promise: 1.3.0
dev: true
/tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}

View File

@ -2,12 +2,11 @@ import './App.css';
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';
import TestAuth from './components/testAuth';
import IconSideNav from './components/IconSideNav';
import AuthenticantionPage from './components/authentication/AuthenticationPage';
import LoginPage from './components/authentication/LoginPage';
import SignUpPage from './components/authentication/SignUpPage';
import NavBar from './components/Nav/Navbar';
import Home from './components/Home';
import ProfileUpdate from './components/ProfileUpdatePage'
const App = () => {
return (
@ -16,14 +15,12 @@ const App = () => {
<NavBar/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/login" element={<AuthenticantionPage/>}/>
<Route path="/login" element={<LoginPage/>}/>
<Route path="/signup" element={<SignUpPage/>}/>
<Route path="/testAuth" element={<TestAuth/>}/>
<Route path="/update_profile" element={<ProfileUpdate/>}/>
</Routes>
</div>
{/* <div>
<IconSideNav />
</div> */}
</BrowserRouter>
);
}

View File

@ -0,0 +1,21 @@
import axios from 'axios';
const ApiUpdateUserProfile = async (formData) => {
try {
const response = await axios.post('http://127.0.1:8000/api/user/update/', formData, {
headers: {
'Authorization': "Bearer " + localStorage.getItem('access_token'),
'Content-Type': 'multipart/form-data',
},
});
console.log(response.data);
return response.data;
} catch (error) {
console.error('Error updating user profile:', error);
throw error;
}
};
export { ApiUpdateUserProfile };

View File

@ -0,0 +1,107 @@
import React, { useState, useRef } from 'react';
import { ApiUpdateUserProfile } from '../api/UserProfileApi';
function ProfileUpdate() {
const [file, setFile] = useState(null);
const [username, setUsername] = useState('');
const [fullName, setFullName] = useState('');
const [about, setAbout] = useState('');
const defaultImage = 'https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg';
const fileInputRef = useRef(null);
const handleImageUpload = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setFile(selectedFile);
}
};
const handleSave = () => {
const formData = new FormData();
formData.append('profile_pic', file);
formData.append('first_name', username);
formData.append('about', about);
ApiUpdateUserProfile(formData);
};
return (
<div className="flex flex-col items-center mt-12 space-y-4">
{/* Profile Image */}
<div className="w-32 h-32 relative">
<label htmlFor="profileImage" className="absolute inset-0">
<input
type="file"
id="profileImage"
accept="image/*"
className="hidden"
onChange={handleFileChange}
ref={fileInputRef}
/>
</label>
<div
className="avatar w-32 h-32 cursor-pointer hover:blur"
onClick={handleImageUpload}
>
{file ? (
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
) : (
<>
<img src={defaultImage} alt="Default" className="rounded-full" />
<i className="fas fa-camera text-white text-2xl absolute bottom-0 right-0 mr-2 mb-2"></i>
<i className="fas fa-arrow-up text-white text-2xl absolute top-0 right-0 mr-2 mt-2"></i>
</>
)}
</div>
</div>
{/* Username Field */}
<div className="w-96">
<label className="block mb-2 text-gray-600">Username</label>
<input
type="text"
placeholder="Enter your username"
className="input w-full"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
{/* Full Name Field */}
<div className="w-96">
<label className="block mb-2 text-gray-600">Full Name</label>
<input
type="text"
placeholder="Enter your full name"
className="input w-full"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
</div>
{/* About Field */}
<div className="w-96">
<label className="block mb-2 text-gray-600">About Me</label>
<textarea
placeholder="Tell us about yourself"
className="textarea w-full h-32"
value={about}
onChange={(e) => setAbout(e.target.value)}
/>
</div>
{/* Save Button */}
<button className="btn btn-primary w-96" onClick={handleSave}>
Save
</button>
</div>
);
}
export default ProfileUpdate;

View File

@ -1,206 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useGoogleLogin } from '@react-oauth/google';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import Link from '@mui/material/Link';
import Divider from '@mui/material/Divider';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import refreshAccessToken from './refreshAcesstoken';
import axiosapi from '../../api/axiosapi';
function Copyright(props) {
return (
<Typography variant="body2" color="text.secondary" align="center" {...props}>
{'Copyright © '}
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
TurTask
</Link>{' '}
{new Date().getFullYear()}
{'.'}
</Typography>
);
}
const defaultTheme = createTheme();
export default function SignInSide() {
const Navigate = useNavigate();
useEffect(() => {
if (!refreshAccessToken()) {
Navigate("/");
}
}, []);
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleUsernameChange = (event) => {
setUsername(event.target.value);
}
const handleEmailChange = (event) => {
setEmail(event.target.value);
}
const handlePasswordChange = (event) => {
setPassword(event.target.value);
}
const handleSubmit = (event) => {
event.preventDefault();
// Send a POST request to the authentication API
axiosapi.apiUserLogin({
email: email,
username: username,
password: password
}).then(res => {
// 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;
Navigate('/');
}).catch(err => {
console.log('Login failed'); // Handle login failure
console.log(err)
});
}
const googleLoginImplicit = useGoogleLogin({
flow: 'auth-code',
redirect_uri: 'postmessage',
onSuccess: async (response) => {
try {
const loginResponse = await axiosapi.googleLogin(response.code);
if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data;
// Save the tokens in localStorage
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
Navigate('/');
}
} catch (error) {
console.error('Error with the POST request:', error);
}
},
onError: errorResponse => console.log(errorResponse),
});
return (
<ThemeProvider theme={defaultTheme}>
<Grid container component="main" sx={{ height: '100vh' }}>
<CssBaseline />
<Grid
item
xs={false}
sm={4}
md={7}
sx={{
backgroundImage: 'url(https://source.unsplash.com/random?wallpapers)',
backgroundRepeat: 'no-repeat',
backgroundColor: (t) =>
t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<Box
sx={{
my: 8,
mx: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
onChange={handleEmailChange}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={handlePasswordChange}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign In
</Button>
<Divider>OR</Divider>
<Box py={2}>
<Button
fullWidth
variant="outlined"
color="secondary"
onClick={() => googleLoginImplicit()}
>
Sign in with Google 🚀
</Button>
</Box>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
<Copyright sx={{ mt: 5 }} />
</Box>
</Box>
</Grid>
</Grid>
</ThemeProvider>
);
}

View File

@ -0,0 +1,143 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useGoogleLogin } from "@react-oauth/google"
import refreshAccessToken from './refreshAcesstoken';
import axiosapi from '../../api/axiosapi';
function LoginPage() {
const Navigate = useNavigate();
useEffect(() => {
if (!refreshAccessToken()) {
Navigate("/");
}
}, []);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleEmailChange = event => {
setEmail(event.target.value);
};
const handlePasswordChange = event => {
setPassword(event.target.value);
};
const handleSubmit = event => {
event.preventDefault();
// Send a POST request to the authentication API
axiosapi
.apiUserLogin({
email: email,
password: password,
})
.then(res => {
// 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;
Navigate("/");
})
.catch(err => {
console.log("Login failed");
console.log(err);
});
};
const googleLoginImplicit = useGoogleLogin({
flow: "auth-code",
redirect_uri: "postmessage",
onSuccess: async response => {
try {
const loginResponse = await axiosapi.googleLogin(response.code);
if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
Navigate("/");
}
} catch (error) {
console.error("Error with the POST request:", error);
}
},
onError: errorResponse => console.log(errorResponse),
});
return (
<html data-theme="night">
<div className="min-h-screen flex">
{/* Left Section (Login Box) */}
<div className="w-1/2 flex items-center justify-center">
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4">
<h2 className="text-2xl font-semibold text-left">Log in to your account</h2>
{/* Email Input */}
<div className="form-control">
<label className="label" htmlFor="email">
<p className="text-bold">
Email<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="email"
id="email"
placeholder="Enter your email"
onChange={handleEmailChange}
/>
</div>
{/* Password Input */}
<div className="form-control">
<label className="label" htmlFor="password">
<p className="text-bold">
Password<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="password"
id="password"
placeholder="Enter your password"
onChange={handlePasswordChange}
/>
</div>
{/* Login Button */}
<button className="btn btn-primary w-full" onClick={handleSubmit}>
Login
</button>
<div className="divider">OR</div>
{/* Login with Google Button */}
<button className="btn btn-outline btn-secondary w-full" onClick={() => googleLoginImplicit()}>
Login with Google
</button>
{/* Forgot Password Link */}
<div className="justify-left">
<a href="#" className="text-blue-500 text-sm text-left">
Forgot your password?
</a>
</div>
</div>
</div>
{/* Right Section (Blurred Image Background) */}
<div className="w-1/2 relative">
<div
className="w-full h-full bg-cover bg-center"
style={{
backgroundImage: 'url("https://th.bing.com/th/id/OIG.9byG0pWUCcbGL7Kly9tA?pid=ImgGn&w=1024&h=1024&rs=1")',
filter: "blur(2px) brightness(.5)",
}}></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-2xl font-semibold">
Text Overlay
</div>
</div>
</div>
</html>
);
}
export default LoginPage;

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import axiosapi from '../api/axiosapi';
import Button from '@material-ui/core/Button';
import { Button } from '@mui/material';
import { useNavigate } from 'react-router-dom';
function TestAuth() {

View File

@ -1,30 +1,21 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require('tailwindcss/defaultTheme')
export default {
content: ["./src/**/*.{js,jsx}"],
theme: {
colors: {
'blue': '#1fb6ff',
'purple': '#7e5bef',
'pink': '#ff49db',
'orange': '#ff7849',
'green': '#13ce66',
'yellow': '#ffc82c',
'gray-dark': '#273444',
'gray': '#8492a6',
'gray-light': '#d3dce6',
},
fontFamily: {
sans: ['Graphik', 'sans-serif'],
serif: ['Merriweather', 'serif'],
},
extend: {
spacing: {
'8xl': '96rem',
'9xl': '128rem',
fontFamily: {
'sans': ['"Proxima Nova"', ...defaultTheme.fontFamily.sans],
},
borderRadius: {
'4xl': '2rem',
}
}
},
},
plugins: [require("daisyui"),
require("@tailwindcss/typography"),
require("daisyui")
],
daisyui: {
themes: ["light", "night"],
},
}

View File

@ -11,4 +11,6 @@ djangorestframework-simplejwt>=5.3
django-cors-headers>=4.3
google_api_python_client>=2.1
google_auth_oauthlib>=1.1
google-auth-httplib2>=0.1
google-auth-httplib2>=0.1
django-storages[s3]>=1.14
Pillow>=10.1