diff --git a/backend/core/settings.py b/backend/core/settings.py index f06c5ac..c795b7c 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -79,11 +79,14 @@ REST_FRAMEWORK = { REST_USE_JWT = True +GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') +GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') + SOCIALACCOUNT_PROVIDERS = { 'google': { 'APP': { - 'client_id': config('GOOGLE_CLIENT_ID', default='fake-client-id'), - 'secret': config('GOOGLE_CLIENT_SECRET', default='fake-client-secret'), + 'client_id': GOOGLE_CLIENT_ID, + 'secret': GOOGLE_CLIENT_SECRET, 'key': '' }, "SCOPE": [ diff --git a/backend/users/migrations/0002_customuser_refresh_token.py b/backend/users/migrations/0002_customuser_refresh_token.py new file mode 100644 index 0000000..25d9215 --- /dev/null +++ b/backend/users/migrations/0002_customuser_refresh_token.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-01 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='refresh_token', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index e473572..64d976d 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -19,6 +19,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # Custom manager objects = CustomAccountManager() + # Google API + refresh_token = models.CharField(max_length=255, blank=True, null=True) + # Fields for authentication USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'first_name'] diff --git a/backend/users/urls.py b/backend/users/urls.py index fd03be1..28f77da 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -9,5 +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()) + 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 cb63de2..591c609 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,6 +3,7 @@ import json import requests +from django.conf import settings from django.contrib.auth.hashers import make_password from rest_framework import status @@ -16,6 +17,8 @@ from allauth.socialaccount.providers.oauth2.client import OAuth2Client from dj_rest_auth.registration.views import SocialLoginView +from google_auth_oauthlib.flow import InstalledAppFlow + from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer from .managers import CustomAccountManager from .models import CustomUser @@ -96,17 +99,28 @@ class GoogleRetrieveUserInfo(APIView): Retrieve user information from Google and create a user if not exists. """ permission_classes = (AllowAny,) + client_config = {"web":{"client_id": settings.GOOGLE_CLIENT_ID, + "project_id":"turtask","auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_secret": settings.GOOGLE_CLIENT_SECRET, + } + } + scopes = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/calendar.readonly', + ] 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) + code = request.data.get("code") + payload = self.exchange_authorization_code(code=code) + if 'error' in payload: + return Response({'error': payload['error']}) + user_info = self.call_google_api(api_url='https://www.googleapis.com/oauth2/v2/userinfo?alt=json', + access_token=payload['access_token']) + payload['email'] = user_info['email'] + user = self.get_or_create_user(payload) token = RefreshToken.for_user(user) response = { @@ -117,19 +131,52 @@ class GoogleRetrieveUserInfo(APIView): 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) + def get(self, request): + """Get authorization url.""" + flow = InstalledAppFlow.from_client_config(client_config=self.client_config, + scopes=self.scopes) + flow.redirect_uri = 'http://localhost:5173/' + authorization_url, state = flow.authorization_url( + access_type='offline', + # include_granted_scopes='true', + ) + return Response({'url': authorization_url}) + + def exchange_authorization_code(self, code): + """Exchange authorization code for access, id, refresh token.""" + url = 'https://oauth2.googleapis.com/token' + payload = { + 'code': code, + 'client_id': settings.GOOGLE_CLIENT_ID, + 'client_secret': settings.GOOGLE_CLIENT_SECRET, + 'redirect_uri': 'postmessage', + 'grant_type': 'authorization_code', + } + response = requests.post(url, data=payload) return json.loads(response.text) def get_or_create_user(self, user_info): + """Get or create a user based on email.""" try: user = CustomUser.objects.get(email=user_info['email']) + user.refresh_token = user_info['refresh_token'] + user.save() except CustomUser.DoesNotExist: user = CustomUser() user.username = user_info['email'] user.password = make_password(CustomAccountManager().make_random_password()) user.email = user_info['email'] + user.refresh_token = user_info['refresh_token'] user.save() - return user \ No newline at end of file + return user + + def call_google_api(self, api_url, access_token): + """Call Google API with access token.""" + headers = { + 'Authorization': f'Bearer {access_token}' + } + + response = requests.get(api_url, headers=headers) + if response.status_code == 200: + return response.json() + raise Exception('Google API Error', response) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8c7d50d..7333c4a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import IconSideNav from './components/IconSideNav'; import AuthenticantionPage from './components/authentication/AuthenticationPage'; import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; +import Home from './components/Home'; const App = () => { @@ -14,7 +15,7 @@ const App = () => {
-

This is Home page!

} /> + }/> }/> }/> }/> diff --git a/frontend/src/api/axiosapi.jsx b/frontend/src/api/axiosapi.jsx index 211cf14..313d6f1 100644 --- a/frontend/src/api/axiosapi.jsx +++ b/frontend/src/api/axiosapi.jsx @@ -67,7 +67,7 @@ const googleLogin = async (token) => { let res = await axios.post( "http://localhost:8000/api/auth/google/", { - token: token, + code: token, } ); // console.log('service google login res: ', res); diff --git a/frontend/src/components/Home.jsx b/frontend/src/components/Home.jsx new file mode 100644 index 0000000..3089df5 --- /dev/null +++ b/frontend/src/components/Home.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function HomePage() { + return ( +
+

Welcome to My Website

+
+ ); +} + +export default HomePage; diff --git a/frontend/src/components/Nav/Navbar.jsx b/frontend/src/components/Nav/Navbar.jsx index 331ab9e..f88d821 100644 --- a/frontend/src/components/Nav/Navbar.jsx +++ b/frontend/src/components/Nav/Navbar.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import IsAuthenticated from '../authentication/IsAuthenticated'; import axiosapi from '../../api/axiosapi'; import AppBar from '@mui/material/AppBar'; @@ -28,6 +28,7 @@ const settings = { }; function NavBar() { + const Navigate = useNavigate(); const [anchorElNav, setAnchorElNav] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); @@ -51,7 +52,7 @@ function NavBar() { const logout = () => { // Log out the user, clear tokens, and navigate to the "/testAuth" route axiosapi.apiUserLogout(); - Navigate('/testAuth'); + Navigate('/'); } return ( diff --git a/frontend/src/components/authentication/AuthenticationPage.jsx b/frontend/src/components/authentication/AuthenticationPage.jsx index 791cdd9..0955342 100644 --- a/frontend/src/components/authentication/AuthenticationPage.jsx +++ b/frontend/src/components/authentication/AuthenticationPage.jsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react'; +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'; @@ -17,6 +16,7 @@ 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'; @@ -24,8 +24,8 @@ function Copyright(props) { return ( {'Copyright © '} - - Your Website + + TurTask {' '} {new Date().getFullYear()} {'.'} @@ -40,6 +40,12 @@ export default function SignInSide() { const Navigate = useNavigate(); + useEffect(() => { + if (!refreshAccessToken()) { + Navigate("/"); + } + }, []); + const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -76,27 +82,12 @@ export default function SignInSide() { }); } - 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); - + flow: 'auth-code', + redirect_uri: 'postmessage', + onSuccess: async (response) => { try { - const loginResponse = await axiosapi.googleLogin(response.access_token); + const loginResponse = await axiosapi.googleLogin(response.code); if (loginResponse && loginResponse.data) { const { access_token, refresh_token } = loginResponse.data; diff --git a/frontend/src/components/authentication/IsAuthenticated.jsx b/frontend/src/components/authentication/IsAuthenticated.jsx index e3691c8..48322de 100644 --- a/frontend/src/components/authentication/IsAuthenticated.jsx +++ b/frontend/src/components/authentication/IsAuthenticated.jsx @@ -1,48 +1,19 @@ import { useState, useEffect } from 'react'; -import axiosapi from '../../api/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'); + const access_token = localStorage.getItem('access_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(); + if (access_token) { + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + } }, []); - const checkIfAccessTokenExpired = (accessToken) => { - // Need to change logic again! - return !accessToken; - }; - return isAuthenticated; } -export default IsAuthenticated; +export default IsAuthenticated; \ No newline at end of file diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx index 3e33a86..7739016 100644 --- a/frontend/src/components/authentication/SignUpPage.jsx +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -21,8 +21,8 @@ function Copyright(props) { return ( {'Copyright © '} - - Your Website + + TurTask {' '} {new Date().getFullYear()} {'.'} diff --git a/frontend/src/components/authentication/refreshAcessToken.jsx b/frontend/src/components/authentication/refreshAcessToken.jsx new file mode 100644 index 0000000..89204d5 --- /dev/null +++ b/frontend/src/components/authentication/refreshAcessToken.jsx @@ -0,0 +1,37 @@ +import axios from 'axios'; + +async function refreshAccessToken() { + const refresh_token = localStorage.getItem('refresh_token'); + const access_token = localStorage.getItem('access_token'); + + if (access_token) { + return true; + } + + if (!refresh_token) { + return false; + } + + const refreshUrl = 'http://127.0.0.1:8000/api/token/refresh/'; + + try { + const response = await axios.post(refreshUrl, { refresh: refresh_token }); + + if (response.status === 200) { + // Successful refresh - save the new access token and refresh token + const newAccessToken = response.data.access; + const newRefreshToken = response.data.refresh; + + localStorage.setItem('access_token', newAccessToken); + localStorage.setItem('refresh_token', newRefreshToken); + + return true; + } else { + return false; + } + } catch (error) { + return false; + } +} + +export default refreshAccessToken;