mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 05:54:07 +01:00
Merge pull request #7 from TurTaskProject/iteration1
Iteration1 Finish React+DRF Authentication System+PostGres Backend Integration
This commit is contained in:
commit
8da076542b
@ -10,7 +10,9 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from decouple import config, Csv
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@ -20,16 +22,23 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-47)&+$r648bswzins@+y)*!qhs2kshz4h09bb5-%zl@yay!n07'
|
SECRET_KEY = config('SECRET_KEY')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
SITE_ID = 4
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
'allauth.account.auth_backends.AuthenticationBackend',
|
||||||
|
)
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@ -37,9 +46,57 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
'users',
|
||||||
|
'rest_framework',
|
||||||
|
'corsheaders',
|
||||||
|
|
||||||
|
'django.contrib.sites',
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
'allauth.socialaccount.providers.google',
|
||||||
|
|
||||||
|
'dj_rest_auth',
|
||||||
|
'dj_rest_auth.registration',
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
|
||||||
|
],
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
REST_USE_JWT = True
|
||||||
|
|
||||||
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
|
'google': {
|
||||||
|
'APP': {
|
||||||
|
'client_id': config('GOOGLE_CLIENT_ID'),
|
||||||
|
'secret': config('GOOGLE_CLIENT_SECRET'),
|
||||||
|
'key': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = False
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://127.0.0.1:8000",
|
||||||
|
"http://localhost:5173",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@ -47,6 +104,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
"allauth.account.middleware.AccountMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'core.urls'
|
ROOT_URLCONF = 'core.urls'
|
||||||
@ -54,7 +112,9 @@ ROOT_URLCONF = 'core.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [
|
||||||
|
os.path.join(BASE_DIR, 'templates')
|
||||||
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@ -75,8 +135,12 @@ WSGI_APPLICATION = 'core.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': config('DB_NAME'),
|
||||||
|
'USER': config('DB_USER'),
|
||||||
|
'PASSWORD': config('DB_PASSWORD'),
|
||||||
|
'HOST': config('DB_HOST'),
|
||||||
|
'PORT': config('DB_PORT'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,3 +185,14 @@ STATIC_URL = 'static/'
|
|||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
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"
|
||||||
@ -15,8 +15,10 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
]
|
path('api/', include('users.urls')),
|
||||||
|
path('accounts/', include('allauth.urls')),
|
||||||
|
]
|
||||||
10
backend/sample.env
Normal file
10
backend/sample.env
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
SECRET_KEY=your_secret_key
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=*.ku.th, localhost, 127.0.0.1, ::1
|
||||||
|
DB_NAME=your_DB_NAME
|
||||||
|
DB_USER=your_DB_USER
|
||||||
|
DB_PASSWORD=your_DB_PASSWORD
|
||||||
|
DB_HOST=your_DB_HOST
|
||||||
|
DB_PORT=your_DB_PORT
|
||||||
|
GOOGLE_CLIENT_ID=your_GOOGLE_CLIENT_ID
|
||||||
|
GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET
|
||||||
0
backend/users/__init__.py
Normal file
0
backend/users/__init__.py
Normal file
32
backend/users/admin.py
Normal file
32
backend/users/admin.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from users.models import CustomUser
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.forms import Textarea
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class UserAdminConfig(UserAdmin):
|
||||||
|
model = CustomUser
|
||||||
|
|
||||||
|
search_fields = ('email', 'username', 'first_name',)
|
||||||
|
list_filter = ('email', 'username', 'first_name', 'is_active', 'is_staff')
|
||||||
|
ordering = ('-start_date',)
|
||||||
|
list_display = ('email', 'username', 'first_name', 'is_active', 'is_staff')
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email', 'username', 'first_name',)}),
|
||||||
|
('Permissions', {'fields': ('is_staff', 'is_active')}),
|
||||||
|
('Personal', {'fields': ('about',)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Formfield overrides for 'about' field
|
||||||
|
formfield_overrides = {
|
||||||
|
models.TextField: {'widget': Textarea(attrs={'rows': 20, 'cols': 60})},
|
||||||
|
}
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'username', 'first_name', 'password1', 'password2', 'is_active', 'is_staff')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(CustomUser, UserAdminConfig)
|
||||||
6
backend/users/apps.py
Normal file
6
backend/users/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'users'
|
||||||
31
backend/users/managers.py
Normal file
31
backend/users/managers.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.contrib.auth.models import BaseUserManager
|
||||||
|
|
||||||
|
class CustomAccountManager(BaseUserManager):
|
||||||
|
def create_superuser(self, email, username, first_name, password, **other_fields):
|
||||||
|
"""
|
||||||
|
Create a superuser with the given email, username, first_name, and password.
|
||||||
|
"""
|
||||||
|
other_fields.setdefault('is_staff', True)
|
||||||
|
other_fields.setdefault('is_superuser', True)
|
||||||
|
other_fields.setdefault('is_active', True)
|
||||||
|
|
||||||
|
if other_fields.get('is_staff') is not True:
|
||||||
|
raise ValueError('Superuser must be assigned to is_staff=True.')
|
||||||
|
if other_fields.get('is_superuser') is not True:
|
||||||
|
raise ValueError('Superuser must be assigned to is_superuser=True.')
|
||||||
|
|
||||||
|
return self.create_user(email, username, first_name, password, **other_fields)
|
||||||
|
|
||||||
|
def create_user(self, email, username, first_name, password, **other_fields):
|
||||||
|
"""
|
||||||
|
Create a user with the given email, username, first_name, and password.
|
||||||
|
"""
|
||||||
|
if not email:
|
||||||
|
raise ValueError(_('You must provide an email address'))
|
||||||
|
|
||||||
|
email = self.normalize_email(email)
|
||||||
|
user = self.model(email=email, username=username, first_name=first_name, **other_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
37
backend/users/migrations/0001_initial.py
Normal file
37
backend/users/migrations/0001_initial.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-27 13:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
|
||||||
|
('username', models.CharField(max_length=150, unique=True)),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150)),
|
||||||
|
('start_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('about', models.TextField(blank=True, max_length=500, verbose_name='about')),
|
||||||
|
('is_staff', models.BooleanField(default=False)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/users/migrations/__init__.py
Normal file
0
backend/users/migrations/__init__.py
Normal file
28
backend/users/models.py
Normal file
28
backend/users/models.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
|
||||||
|
from .managers import CustomAccountManager
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
# User fields
|
||||||
|
email = models.EmailField(_('email address'), unique=True)
|
||||||
|
username = models.CharField(max_length=150, unique=True)
|
||||||
|
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)
|
||||||
|
is_staff = models.BooleanField(default=False)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
# Custom manager
|
||||||
|
objects = CustomAccountManager()
|
||||||
|
|
||||||
|
# Fields for authentication
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = ['username', 'first_name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# String representation of the user
|
||||||
|
return self.username
|
||||||
39
backend/users/serializers.py
Normal file
39
backend/users/serializers.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||||
|
@classmethod
|
||||||
|
def get_token(cls, user):
|
||||||
|
"""
|
||||||
|
Get the token for the user and add custom claims, such as 'username'.
|
||||||
|
"""
|
||||||
|
token = super(MyTokenObtainPairSerializer, cls).get_token(user)
|
||||||
|
token['username'] = user.username
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for CustomUser model.
|
||||||
|
"""
|
||||||
|
email = serializers.EmailField(required=True)
|
||||||
|
username = serializers.CharField(required=True)
|
||||||
|
password = serializers.CharField(min_length=8, write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = ('email', 'username', 'password')
|
||||||
|
extra_kwargs = {'password': {'write_only': True}}
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""
|
||||||
|
Create a CustomUser instance with validated data, including password hashing.
|
||||||
|
"""
|
||||||
|
password = validated_data.pop('password', None)
|
||||||
|
instance = self.Meta.model(**validated_data)
|
||||||
|
if password is not None:
|
||||||
|
instance.set_password(password)
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
3
backend/users/tests.py
Normal file
3
backend/users/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
12
backend/users/urls.py
Normal file
12
backend/users/urls.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from rest_framework_simplejwt import views as jwt_views
|
||||||
|
from .views import ObtainTokenPairWithCustomView, CustomUserCreate, GreetingView, GoogleLogin
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('user/create/', CustomUserCreate.as_view(), name="create_user"),
|
||||||
|
path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'),
|
||||||
|
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
|
path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'),
|
||||||
|
path('hello/', GreetingView.as_view(), name='hello_world'),
|
||||||
|
path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"),
|
||||||
|
]
|
||||||
78
backend/users/views.py
Normal file
78
backend/users/views.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""This module defines API views for authentication, user creation, and a simple hello message."""
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
|
||||||
|
from dj_rest_auth.registration.views import SocialLoginView
|
||||||
|
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ObtainTokenPairWithCustomView(APIView):
|
||||||
|
"""
|
||||||
|
Custom Token Obtain Pair View.
|
||||||
|
Allows users to obtain access and refresh tokens by providing credentials.
|
||||||
|
"""
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Issue access and refresh tokens in response to a valid login request.
|
||||||
|
"""
|
||||||
|
serializer = MyTokenObtainPairSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
token = serializer.validated_data
|
||||||
|
return Response(token, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreate(APIView):
|
||||||
|
"""
|
||||||
|
Custom User Creation View.
|
||||||
|
Allows users to create new accounts.
|
||||||
|
"""
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Create a new user account based on the provided data.
|
||||||
|
"""
|
||||||
|
serializer = CustomUserSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
user = serializer.save()
|
||||||
|
if user:
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class GreetingView(APIView):
|
||||||
|
"""
|
||||||
|
Hello World View.
|
||||||
|
Returns a greeting and user information for authenticated users.
|
||||||
|
"""
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
Retrieve a greeting message and user information.
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
user_info = {
|
||||||
|
"username": user.username,
|
||||||
|
}
|
||||||
|
response_data = {
|
||||||
|
"message": "Hello, world!",
|
||||||
|
"user_info": user_info,
|
||||||
|
}
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleLogin(SocialLoginView):
|
||||||
|
"""
|
||||||
|
Google Login View.
|
||||||
|
Handles Google OAuth2 authentication.
|
||||||
|
"""
|
||||||
|
permission_classes = (AllowAny,)
|
||||||
|
adapter_class = GoogleOAuth2Adapter
|
||||||
@ -10,8 +10,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
||||||
|
"axios": "^1.5.1",
|
||||||
|
"bootstrap": "^5.3.2",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"gapi-script": "^1.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-bootstrap": "^2.9.1",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-google-login": "^5.2.2",
|
||||||
|
"react-router-dom": "^6.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
frontend/sample.env
Normal file
1
frontend/sample.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
|
||||||
@ -1,42 +1,38 @@
|
|||||||
#root {
|
.App {
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.App-logo {
|
||||||
height: 6em;
|
height: 40vmin;
|
||||||
padding: 1.5em;
|
pointer-events: none;
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-spin {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@ -1,35 +1,52 @@
|
|||||||
import { useState } from 'react'
|
// import './App.css';
|
||||||
import reactLogo from './assets/react.svg'
|
// import { Routes, Route, Link } from "react-router-dom";
|
||||||
import viteLogo from '/vite.svg'
|
// import Login from "./components/login";
|
||||||
import './App.css'
|
// import TestAuth from './components/testAuth';
|
||||||
|
|
||||||
function App() {
|
// function App() {
|
||||||
const [count, setCount] = useState(0)
|
// return (
|
||||||
|
// <div className="App">
|
||||||
|
// <nav>
|
||||||
|
// <Link className={"nav-link"} to={"/"}>Home</Link>
|
||||||
|
// <Link className={"nav-link"} to={"/login"}>Login</Link>
|
||||||
|
// <Link className={"nav-link"} to={"/testAuth"}>testAuth</Link>
|
||||||
|
// </nav>
|
||||||
|
// <Routes>
|
||||||
|
// <Route exact path={"/login"} element={Login} />
|
||||||
|
// <Route exact path={"/testAuth"} element={TestAuth} />
|
||||||
|
// <Route path={"/"} render={() => <h1>This is Home page!</h1>} />
|
||||||
|
// </Routes>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
// export default App;
|
||||||
<>
|
|
||||||
<div>
|
import './App.css';
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
import {BrowserRouter, Route, Routes, Link} from "react-router-dom";
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
import Login from "./components/login";
|
||||||
</a>
|
import TestAuth from './components/testAuth';
|
||||||
<a href="https://react.dev" target="_blank">
|
import Signup from './components/signup';
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
const App = () => {
|
||||||
</div>
|
return (
|
||||||
<h1>Vite + React</h1>
|
<BrowserRouter>
|
||||||
<div className="card">
|
<div className="App">
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
<nav>
|
||||||
count is {count}
|
<Link className={"nav-link"} to={"/"}>Home</Link>
|
||||||
</button>
|
<Link className={"nav-link"} to={"/login"}>Login</Link>
|
||||||
<p>
|
<Link className={"nav-link"} to={"/signup"}>Signup</Link>
|
||||||
Edit <code>src/App.jsx</code> and save to test HMR
|
<Link className={"nav-link"} to={"/testAuth"}>testAuth</Link>
|
||||||
</p>
|
</nav>
|
||||||
</div>
|
<Routes>
|
||||||
<p className="read-the-docs">
|
<Route path={"/"} render={() => <h1>This is Home page!</h1>} />
|
||||||
Click on the Vite and React logos to learn more
|
<Route path="/login" element={<Login/>}/>
|
||||||
</p>
|
<Route path="/signup" element={<Signup/>}/>
|
||||||
</>
|
<Route path="/testAuth" element={<TestAuth/>}/>
|
||||||
)
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
113
frontend/src/api/axiosapi.jsx
Normal file
113
frontend/src/api/axiosapi.jsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Create an Axios instance with common configurations
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a response interceptor to handle 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 Unauthorized (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) => {
|
||||||
|
// Update access and refresh tokens
|
||||||
|
localStorage.setItem('access_token', response.data.access);
|
||||||
|
localStorage.setItem('refresh_token', response.data.refresh);
|
||||||
|
|
||||||
|
// Update the authorization header with the new access token
|
||||||
|
axiosInstance.defaults.headers['Authorization'] = "Bearer " + response.data.access;
|
||||||
|
originalRequest.headers['Authorization'] = "Bearer " + response.data.access;
|
||||||
|
|
||||||
|
return axiosInstance(originalRequest); // Retry the original request
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('Interceptors error: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function for user login
|
||||||
|
const apiUserLogin = (data) => {
|
||||||
|
return axiosInstance
|
||||||
|
.post('token/obtain/', data)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.statusText);
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('apiUserLogin error: ', error);
|
||||||
|
return error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function for user logout
|
||||||
|
const apiUserLogout = () => {
|
||||||
|
axiosInstance.defaults.headers['Authorization'] = ""; // Clear authorization header
|
||||||
|
localStorage.removeItem('access_token'); // Remove access token
|
||||||
|
localStorage.removeItem('refresh_token'); // Remove refresh token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function for Google login
|
||||||
|
const googleLogin = async (accessToken) => {
|
||||||
|
let res = await axios.post(
|
||||||
|
"http://localhost:8000/api/dj-rest-auth/google/",
|
||||||
|
{
|
||||||
|
access_token: accessToken,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return await res;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get 'hello' data
|
||||||
|
const getGreeting = () => {
|
||||||
|
return axiosInstance
|
||||||
|
.get('hello')
|
||||||
|
.then((response) => {
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
return error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to register
|
||||||
|
const createUser = async (formData) => {
|
||||||
|
try {
|
||||||
|
axios.defaults.withCredentials = true
|
||||||
|
const resposne = axios.post("http://localhost:8000/api/user/create/", formData);
|
||||||
|
// const response = await axiosInstance.post('/user/create/', formData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Export the functions and Axios instance
|
||||||
|
export default {
|
||||||
|
axiosInstance,
|
||||||
|
apiUserLogin,
|
||||||
|
apiUserLogout,
|
||||||
|
getGreeting: getGreeting,
|
||||||
|
googleLogin,
|
||||||
|
createUser
|
||||||
|
};
|
||||||
160
frontend/src/components/login.jsx
Normal file
160
frontend/src/components/login.jsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import Container from '@material-ui/core/Container';
|
||||||
|
import axiosapi from '../api/axiosapi';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { GoogleLogin } from 'react-google-login';
|
||||||
|
|
||||||
|
|
||||||
|
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
// Styles for various elements
|
||||||
|
paper: {
|
||||||
|
marginTop: theme.spacing(8),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
margin: theme.spacing(3, 0, 2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const history = useNavigate();
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleUsernameChange = (event) => {
|
||||||
|
// Update the 'username' state when the input field changes
|
||||||
|
setUsername(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmailChange = (event) => {
|
||||||
|
// Update the 'email' state when the email input field changes
|
||||||
|
setEmail(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordChange = (event) => {
|
||||||
|
// Update the 'password' state when the password input field changes
|
||||||
|
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;
|
||||||
|
history.push('/testAuth');
|
||||||
|
}).catch(err => {
|
||||||
|
console.log('Login failed'); // Handle login failure
|
||||||
|
console.log(err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseGoogle = async (response) => {
|
||||||
|
// Handle Google login response
|
||||||
|
let googleResponse = await axiosapi.googleLogin(response.accessToken);
|
||||||
|
console.log('Google Response:\n', googleResponse);
|
||||||
|
|
||||||
|
if (googleResponse.status === 200) {
|
||||||
|
// Store Google login tokens and set the authorization header on success
|
||||||
|
localStorage.setItem('access_token', googleResponse.data.access_token);
|
||||||
|
localStorage.setItem('refresh_token', googleResponse.data.refresh_token);
|
||||||
|
axiosapi.axiosInstance.defaults.headers['Authorization'] = "Bearer " + googleResponse.data.access_token;
|
||||||
|
history.push('/testAuth');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main" maxWidth="xs">
|
||||||
|
<CssBaseline />
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Avatar className={classes.avatar} />
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Sign in
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} noValidate onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="email"
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
autoFocus
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="username"
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
|
autoFocus
|
||||||
|
onChange={handleUsernameChange}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<GoogleLogin
|
||||||
|
clientId={GOOGLE_CLIENT_ID}
|
||||||
|
buttonText="Login"
|
||||||
|
onSuccess={responseGoogle}
|
||||||
|
onFailure={responseGoogle}
|
||||||
|
cookiePolicy={'single_host_origin'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
frontend/src/components/signup.jsx
Normal file
116
frontend/src/components/signup.jsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import axiosapi from '../api/axiosapi';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
|
import Container from '@material-ui/core/Container';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
// Styles for various elements
|
||||||
|
paper: {
|
||||||
|
marginTop: theme.spacing(8),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
margin: theme.spacing(3, 0, 2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Signup = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
axiosapi.createUser(formData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user:', error);
|
||||||
|
setError('Registration failed. Please try again.'); // Set an error message
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({ ...formData, [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main" maxWidth="xs">
|
||||||
|
<CssBaseline />
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Sign Up
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
fullWidth
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
label="Email"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
fullWidth
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
label="Username"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
fullWidth
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
label="Password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Signup;
|
||||||
42
frontend/src/components/testAuth.jsx
Normal file
42
frontend/src/components/testAuth.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axiosapi from '../api/axiosapi';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
function TestAuth() {
|
||||||
|
let history = useNavigate();
|
||||||
|
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch the "hello" data from the server when the component mounts
|
||||||
|
axiosapi.getGreeting().then(res => {
|
||||||
|
console.log(res.data);
|
||||||
|
setMessage(res.data.user);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
setMessage("");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
||||||
|
axiosapi.apiUserLogout();
|
||||||
|
history('/testAuth');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{message !== "" && (
|
||||||
|
<div>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
<h2>{message}</h2>
|
||||||
|
<Button variant="contained" onClick={logout}>Logout</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{message === "" && <h1>Need to sign in</h1>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestAuth;
|
||||||
@ -1,69 +1,19 @@
|
|||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
place-items: center;
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
min-width: 320px;
|
sans-serif;
|
||||||
min-height: 100vh;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
code {
|
||||||
font-size: 3.2em;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
line-height: 1.1;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.nav-link{
|
||||||
border-radius: 8px;
|
color:black;
|
||||||
border: 1px solid transparent;
|
/* border: 1px solid white; */
|
||||||
padding: 0.6em 1.2em;
|
padding: 1em;
|
||||||
font-size: 1em;
|
}
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.jsx'
|
import './index.css';
|
||||||
import './index.css'
|
import App from './App';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(
|
||||||
<React.StrictMode>
|
document.getElementById("root"),
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
)
|
||||||
|
.render(
|
||||||
|
<App />
|
||||||
|
);
|
||||||
|
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Loading…
Reference in New Issue
Block a user