diff --git a/backend/core/settings.py b/backend/core/settings.py index 7fa2954..f06c5ac 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -46,19 +46,19 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - - 'tasks', + 'django.contrib.sites', + 'tasks', 'users', - 'rest_framework', + 'corsheaders', - 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.google', + 'rest_framework', 'dj_rest_auth', 'dj_rest_auth.registration', 'rest_framework.authtoken', @@ -70,10 +70,10 @@ REST_FRAMEWORK = { ], 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.TokenAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication', 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', ] } @@ -105,7 +105,7 @@ CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", ] -CSRF_TRUSTED_ORIGINS = ["http://*"] +CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"] CORS_ORIGIN_WHITELIST = ["*"] diff --git a/backend/users/serializers.py b/backend/users/serializers.py index d6deaa5..14d96be 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -24,7 +24,7 @@ class CustomUserSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ('email', 'username', 'password') + fields = ('email', 'password') extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): diff --git a/backend/users/urls.py b/backend/users/urls.py index 309d11c..fd03be1 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,6 +1,6 @@ from django.urls import path from rest_framework_simplejwt import views as jwt_views -from .views import ObtainTokenPairWithCustomView, CustomUserCreate, GreetingView, GoogleLogin +from .views import ObtainTokenPairWithCustomView, CustomUserCreate, GreetingView, GoogleLogin, GoogleRetrieveUserInfo urlpatterns = [ path('user/create/', CustomUserCreate.as_view(), name="create_user"), @@ -9,4 +9,5 @@ urlpatterns = [ 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"), + path('auth/google/', GoogleRetrieveUserInfo.as_view()) ] \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index fd20a19..93e0ab0 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,16 +1,25 @@ """This module defines API views for authentication, user creation, and a simple hello message.""" -from django.shortcuts import render +import json +import requests + +from django.contrib.auth.hashers import make_password + 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 dj_rest_auth.registration.views import SocialLoginView -from allauth.socialaccount.providers.oauth2.client import OAuth2Client -from .adapter import CustomGoogleOAuth2Adapter -from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer +from rest_framework_simplejwt.tokens import RefreshToken from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter +from allauth.socialaccount.providers.oauth2.client import OAuth2Client + +from dj_rest_auth.registration.views import SocialLoginView + +from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer +from .managers import CustomAccountManager +from .models import CustomUser + class ObtainTokenPairWithCustomView(APIView): """ @@ -78,5 +87,47 @@ class GoogleLogin(SocialLoginView): """ # permission_classes = (AllowAny,) adapter_class = GoogleOAuth2Adapter - client_class = OAuth2Client - # callback_url = 'http://localhost:8000/accounts/google/login/callback/' \ No newline at end of file + # client_class = OAuth2Client + # callback_url = 'http://localhost:8000/accounts/google/login/callback/' + + +class GoogleRetrieveUserInfo(APIView): + """ + Retrieve user information from Google and create a user if not exists. + """ + def post(self, request): + access_token = request.data.get("token") + + user_info = self.get_google_user_info(access_token) + + if 'error' in user_info: + error_message = 'Wrong Google token or the token has expired.' + return Response({'message': error_message, 'error': user_info['error']}) + + user = self.get_or_create_user(user_info) + token = RefreshToken.for_user(user) + + response = { + 'username': user.username, + 'access_token': str(token.access_token), + 'refresh_token': str(token), + } + + return Response(response) + + def get_google_user_info(self, access_token): + url = 'https://www.googleapis.com/oauth2/v2/userinfo' + payload = {'access_token': access_token} + response = requests.get(url, params=payload) + return json.loads(response.text) + + def get_or_create_user(self, user_info): + try: + user = CustomUser.objects.get(email=user_info['email']) + except CustomUser.DoesNotExist: + user = CustomUser() + user.username = user_info['email'] + user.password = make_password(CustomAccountManager().make_random_password()) + user.email = user_info['email'] + user.save() + return user \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 98acb6d..7ff9906 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@material-ui/icons": "^4.11.3", "@mui/icons-material": "^5.14.15", "@mui/material": "^5.14.15", + "@mui/system": "^5.14.15", "@react-oauth/google": "^0.11.1", "axios": "^1.5.1", "bootstrap": "^5.3.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d085dbb..18613e9 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: '@mui/material': specifier: ^5.14.15 version: 5.14.15(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': + specifier: ^5.14.15 + version: 5.14.15(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.33)(react@18.2.0) '@react-oauth/google': specifier: ^0.11.1 version: 0.11.1(react-dom@18.2.0)(react@18.2.0) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 658d5e3..0ac97eb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,38 +1,18 @@ -// import './App.css'; -// import { Routes, Route, Link } from "react-router-dom"; -// import Login from "./components/login"; -// import TestAuth from './components/testAuth'; - -// function App() { -// return ( -//
-// -// -// -// -//

This is Home page!

} /> -//
-//
-// ); -// } - -// export default App; - import './App.css'; -import {BrowserRouter, Route, Routes, Link} from "react-router-dom"; -import Login from "./components/login"; +import { BrowserRouter, Route, Routes, Link } from 'react-router-dom'; + import TestAuth from './components/testAuth'; -import Signup from './components/signup'; -import IconSideNav from "./components/IconSideNav"; +import IconSideNav from './components/IconSideNav'; +import AuthenticantionPage from './components/authentication/AuthenticationPage'; +import SignUpPage from './components/authentication/SignUpPage'; +import NavBar from './components/Nav/Navbar'; + const App = () => { return (
+

This is Home page!

} /> - }/> - }/> + }/> + }/> }/>
-
+ {/*
-
+
*/}
); } diff --git a/frontend/src/api/axiosapi.jsx b/frontend/src/api/axiosapi.jsx index a96be14..211cf14 100644 --- a/frontend/src/api/axiosapi.jsx +++ b/frontend/src/api/axiosapi.jsx @@ -62,13 +62,12 @@ const apiUserLogout = () => { } // Function for Google login -const googleLogin = async (accesstoken) => { +const googleLogin = async (token) => { axios.defaults.withCredentials = true let res = await axios.post( - "http://localhost:8000/api/dj-rest-auth/google/", + "http://localhost:8000/api/auth/google/", { - access_token: accesstoken, - id_token: accesstoken, + token: token, } ); // console.log('service google login res: ', res); diff --git a/frontend/src/components/Nav/Navbar.jsx b/frontend/src/components/Nav/Navbar.jsx new file mode 100644 index 0000000..93cbd08 --- /dev/null +++ b/frontend/src/components/Nav/Navbar.jsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import Menu from '@mui/material/Menu'; +import MenuIcon from '@mui/icons-material/Menu'; +import Container from '@mui/material/Container'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; +import MenuItem from '@mui/material/MenuItem'; +import AdbIcon from '@mui/icons-material/Adb'; + +const pages = { + TestAuth: '/testAuth', + +}; +const settings = { + Profile: '/profile', + Account: '/account', + Dashboard: '/dashboard', + Logout: '/logout', + }; + +function NavBar() { + const [anchorElNav, setAnchorElNav] = React.useState(null); + const [anchorElUser, setAnchorElUser] = React.useState(null); + + const handleOpenNavMenu = (event) => { + setAnchorElNav(event.currentTarget); + }; + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + return ( + + + + + + LOGO + + + + + + + + {Object.entries(pages).map(([page, path]) => ( + + + {page} + + + ))} + + + + + LOGO + + + {Object.entries(pages).map(([page, path]) => ( + + ))} + + + + + + + + + + {Object.entries(settings).map(([setting, path]) => ( + + + {setting} + + + ))} + + + + + + ); +} +export default NavBar; \ No newline at end of file diff --git a/frontend/src/components/authentication/AuthenticationPage.jsx b/frontend/src/components/authentication/AuthenticationPage.jsx new file mode 100644 index 0000000..791cdd9 --- /dev/null +++ b/frontend/src/components/authentication/AuthenticationPage.jsx @@ -0,0 +1,215 @@ +import React, { useState } 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 axiosapi from '../../api/axiosapi'; + + +function Copyright(props) { + return ( + + {'Copyright © '} + + Your Website + {' '} + {new Date().getFullYear()} + {'.'} + + ); +} + + +const defaultTheme = createTheme(); + +export default function SignInSide() { + + const Navigate = useNavigate(); + + 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 responseGoogle = async (response) => { + // Handle Google login response + let googleResponse = await axiosapi.googleLogin(response.access_token); + 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; + Navigate('/'); + } + } + + const googleLoginImplicit = useGoogleLogin({ + // flow: 'auth-code', + onSuccess: async (response) => { + console.log(response); + + try { + const loginResponse = await axiosapi.googleLogin(response.access_token); + 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 ( + + + + + t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900], + backgroundSize: 'cover', + backgroundPosition: 'center', + }} + /> + + + + + + + Sign in + + + + + } + label="Remember me" + /> + + OR + + + + + + + Forgot password? + + + + + {"Don't have an account? Sign Up"} + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/authentication/IsAuthenticated.jsx b/frontend/src/components/authentication/IsAuthenticated.jsx new file mode 100644 index 0000000..48edd73 --- /dev/null +++ b/frontend/src/components/authentication/IsAuthenticated.jsx @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react'; +import axiosapi from './axiosapi'; + +function IsAuthenticated() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const checkAuthentication = async () => { + const access_token = localStorage.getItem('access_token'); + const refresh_token = localStorage.getItem('refresh_token'); + + if (access_token && refresh_token) { + const isAccessTokenExpired = checkIfAccessTokenExpired(access_token); + + if (!isAccessTokenExpired) { + setIsAuthenticated(true); + } else { + try { + // Attempt to refresh the access token using the refresh token + const response = await axiosapi.refreshAccessToken(refresh_token); + if (response.status === 200) { + const newAccessToken = response.data.access_token; + localStorage.setItem('access_token', newAccessToken); + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + } + } catch (error) { + setIsAuthenticated(false); + } + } + } else { + setIsAuthenticated(false); + } + }; + + checkAuthentication(); + }, []); + + const checkIfAccessTokenExpired = (accessToken) => { + // Need to change logic again! + return !accessToken; + }; + + return isAuthenticated; +} + +export default IsAuthenticated; diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx new file mode 100644 index 0000000..3e33a86 --- /dev/null +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axiosapi from '../../api/axiosapi'; + +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 Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import Typography from '@mui/material/Typography'; +import Container from '@mui/material/Container'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + + +function Copyright(props) { + return ( + + {'Copyright © '} + + Your Website + {' '} + {new Date().getFullYear()} + {'.'} + + ); +} + +const defaultTheme = createTheme(); + +export default function SignUp() { + + const Navigate = useNavigate(); + + 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.'); + } finally { + setIsSubmitting(false); + } + Navigate('/login'); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + return ( + + + + + + + + + Sign up + + + + + + + + + + + + + + } + label="I want to receive inspiration, marketing promotions and updates via email." + /> + + + + + + + Already have an account? Sign in + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/login.jsx b/frontend/src/components/login.jsx deleted file mode 100644 index b088523..0000000 --- a/frontend/src/components/login.jsx +++ /dev/null @@ -1,161 +0,0 @@ -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 { useGoogleLogin } from '@react-oauth/google'; - -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.access_token); - 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'); - } - } - - const googleLoginflow = useGoogleLogin({ - onSuccess: async tokenResponse => { - console.log(tokenResponse); - responseGoogle(tokenResponse); - }, - }) - - return ( - - -
- - - Sign in - -
- - - - - - - - -
-
- ); -} diff --git a/frontend/src/components/testAuth.jsx b/frontend/src/components/testAuth.jsx index 937a046..9677133 100644 --- a/frontend/src/components/testAuth.jsx +++ b/frontend/src/components/testAuth.jsx @@ -4,7 +4,7 @@ import Button from '@material-ui/core/Button'; import { useNavigate } from 'react-router-dom'; function TestAuth() { - let history = useNavigate(); + let Navigate = useNavigate(); const [message, setMessage] = useState(""); @@ -22,19 +22,19 @@ function TestAuth() { const logout = () => { // Log out the user, clear tokens, and navigate to the "/testAuth" route axiosapi.apiUserLogout(); - history('/testAuth'); + Navigate('/testAuth'); } return (
{message !== "" && (
-

Hello!

+

Login! Hello!

{message}

)} - {message === "" &&

Need to sign in

} + {message === "" &&

Need to sign in, No authentication found

}
); } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index af048e7..7aa4bf4 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,13 +1,12 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; -import { GoogleLogin, GoogleOAuthProvider} from '@react-oauth/google'; +import { GoogleOAuthProvider} from '@react-oauth/google'; const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID - ReactDOM.createRoot(document.getElementById("root")).render( - + ); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 81c4d2f..e5171c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,7 @@ djangorestframework>=3.14 markdown>=3.5 django-filter>=23.3 djangorestframework-simplejwt>=5.3 -django-cors-headers>=4.3 \ No newline at end of file +django-cors-headers>=4.3 +google_api_python_client>=2.1 +google_auth_oauthlib>=1.1 +google-auth-httplib2>=0.1 \ No newline at end of file