diff --git a/README.md b/README.md index 9ba1c2e6e..74538b693 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ The game must be responsive :white_check_mark: - *Major module*: Implement Two-Factor Authentication (2FA) and JWT :white_check_mark: - **Devops** - *Major module*: Infrastructure Setup for Log Management :x: - - *Minor module*: Monitoring system :x: + - *Minor module*: Monitoring system :white_check_mark: - *Major module*: Designing the Backend as Microservices :x: - **Graphics** - *Major module*: Use advanced 3D techniques :white_check_mark: diff --git a/backend/backend/settings.py b/backend/backend/settings.py index f5a43f41d..b90fb4b89 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -91,8 +91,8 @@ CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_ALLOW_ALL = True -# SESSION_COOKIE_SAMESITE = None -# SESSION_COOKIE_SECURE = False +SESSION_COOKIE_SAMESITE = None +SESSION_COOKIE_SECURE = True # SESSION_COOKIE_SAMESITE = 'Lax' REST_FRAMEWORK = { diff --git a/backend/user_api/views.py b/backend/user_api/views.py index 7992fb3f6..e45e463b6 100644 --- a/backend/user_api/views.py +++ b/backend/user_api/views.py @@ -23,6 +23,9 @@ from django_otp import match_token from email.mime.image import MIMEImage +from django.core.files.storage import default_storage +from django.core.files.images import ImageFile + from .authentication import account_activation_token, is_authenticated from .serializers import UserRegisterSerializer, UserLoginSerializer, UserSerializer from .validations import user_registration, is_valid_email, is_valid_password @@ -39,6 +42,8 @@ import os import qrcode import logging +from io import BytesIO +from PIL import Image logger = logging.getLogger(__name__) @@ -157,12 +162,15 @@ class updateProfile(APIView): def post(self, request): if request.user.is_authenticated: data = request.data - if data.get('profile_picture'): - request.user.profile_picture = data.get('profile_picture') + if 'profile_picture' in request.FILES: + if default_storage.exists(request.user.profile_picture.path): + default_storage.delete(request.user.profile_picture.path) + image = ImageFile(request.FILES['profile_picture']) + request.user.profile_picture.save(image.name, image, save=True) if data.get('email'): request.user.email = data.get('email') if data.get('username'): - request.user.username = data.get('username') + request.user.vusername = data.get('username') if data.get('title'): request.user.title = data.get('title') if data.get('AboutMe'): @@ -227,15 +235,20 @@ def get(self, request): username = user_response.json()["login"] email = user_response.json()["email"] + profile_picture_url = user_response.json()["image"]["versions"]["medium"] + response = requests.get(profile_picture_url) + img = Image.open(BytesIO(response.content)) + image_name = os.path.basename(profile_picture_url) + directory = 'profile_pictures/' + save_path = os.path.join(directory, image_name) + img.save(save_path) + intra_lvl = user_response.json()["cursus_users"][1]["level"] school = user_response.json()["campus"][0]["name"] ft_url = user_response.json()["url"], ft_user = True - # with open('output.json', 'w') as f: - # json.dump(user_response.json(), f, indent=4) - titles = user_response.json().get("titles", []) title = "" if titles: @@ -249,7 +262,7 @@ def get(self, request): 'username': username, 'email': email, 'title': title, - 'profile_picture': profile_picture_url, + 'profile_picture': save_path.replace('/app/backend/media', ''), 'intra_level': intra_lvl, 'school': school, 'ft_url': ft_url, @@ -308,53 +321,6 @@ def post(self, request): return response -class updateProfile(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (BlacklistCheckJWTAuthentication,) - ## - def post(self, request): - if request.user.is_authenticated: - data = request.data - if data.get('profile_picture'): - request.user.profile_picture = data.get('profile_picture') - if data.get('email'): - request.user.email = data.get('email') - if data.get('username'): - request.user.username = data.get('username') - if data.get('title'): - request.user.title = data.get('title') - if data.get('AboutMe'): - request.user.AboutMe = data.get('AboutMe') - if data.get('school'): - request.user.school = data.get('school') - if data.get('wins'): - request.user.wins = data.get('wins') - if data.get('losses'): - request.user.losses = data.get('losses') - if data.get('win_rate'): - if request.user.total_matches == 0: - request.user.win_rate = 0 - else: - request.user.win_rate = request.user.wins / request.user.total_matches - if data.get('total_matches'): - request.user.total_matches = data.get('total_matches') - if data.get('match_history'): - history = request.user.match_history - if history is None: - history = [] - history.append(data.get('match_history')) - request.user.match_history = history - if data.get('TwoFA'): - request.user.TwoFA = data.get('TwoFA') - if data.get('password'): - request.user.set_password(data.get('password')) - request.user.save() - - return Response({"detail": "Profile updated"}, status=status.HTTP_200_OK) - else: - return Response({"detail": "No active user session"}, status=status.HTTP_400_BAD_REQUEST) - - class activateTwoFa(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (BlacklistCheckJWTAuthentication,) @@ -407,21 +373,24 @@ def post(self, request): device = request.user.totpdevice_set.create(confirmed=True) current_site = get_current_site(request) - # Generate QR code img = qrcode.make(device.config_url) - # img.save("qrcode.png") mail_subject = 'DJANGO OTP DEMO' + byte_stream = BytesIO() + img.save(byte_stream, format='PNG') + byte_stream.seek(0) + + mail_subject = 'Iiinteernaaal Pooiinteeer Vaariaaablee' message = f"Hello {request.user},\n\nYour QR Code is: " to_email = request.user.email email = EmailMessage( mail_subject, message, to=[to_email] ) - # Attach image fp = open('qrcode.png', 'rb') msg_image = MIMEImage(fp.read()) fp.close() + msg_image = MIMEImage(byte_stream.getvalue()) msg_image.add_header('Content-ID', '') email.attach(msg_image) diff --git a/docker/frontend/frontend.Dockerfile b/docker/frontend/frontend.Dockerfile index 20912c77a..165e81067 100644 --- a/docker/frontend/frontend.Dockerfile +++ b/docker/frontend/frontend.Dockerfile @@ -10,6 +10,8 @@ RUN npm install -g serve RUN npm install +RUN npm ci + COPY . . EXPOSE 3000 diff --git a/docker/nginx/config/backend.conf b/docker/nginx/config/backend.conf index 5ab5dc5a5..bf28c3093 100644 --- a/docker/nginx/config/backend.conf +++ b/docker/nginx/config/backend.conf @@ -44,6 +44,10 @@ server { add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization' always; } + location /media/ { + alias /app/backend/media/; + } + location ~ ^/static/js/main\.[a-f0-9]+\.js$ { autoindex on; alias /app/frontend/build/static/js/; diff --git a/frontend/src/components/API.js b/frontend/src/components/API.js new file mode 100644 index 000000000..947e3b4c5 --- /dev/null +++ b/frontend/src/components/API.js @@ -0,0 +1,177 @@ +import axios from "axios"; +import Cookies from "js-cookie"; + +export const fetchUserDetails = async ( + setUserDetails, + setUsername, + setImageUrl, + redirectUri +) => { + const response = await getUserDetails({ redirectUri }); + setUserDetails(response.data.user); + setUsername(response.data.user.username); + if (response.data.user.profile_picture) { + let url = response.data.user.profile_picture; + setImageUrl(url); + } +}; + +export const getUserDetails = async ({ redirectUri }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.get(`${redirectUri}/api/profile`, { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + }); + } catch (error) { + console.log(error); + } + return response; +}; + +export const changeUsername = async ({ redirectUri, username }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.post( + `${redirectUri}/api/updateProfile`, + { username: username }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; + +export const changeAbout = async ({ redirectUri, about }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.post( + `${redirectUri}/api/updateProfile`, + { AboutMe: about }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; + +export const changePassword = async ({ redirectUri, password }) => { + let response = {}; + try { + const token = Cookies.get('access'); + response = await axios.post( + `${redirectUri}/api/updateProfile`, + { password: password }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; + }; + +export const changeAvatar = async ({ redirectUri, file }) => { + let response = {}; + try { + const token = Cookies.get('access'); + const formData = new FormData(); + formData.append('profile_picture', file); + + response = await axios.post( + `${redirectUri}/api/updateProfile`, + formData, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'multipart/form-data', + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; + +export const activate2FA = async ({ redirectUri }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.post( + `${redirectUri}/api/activateTwoFa`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; + +export const deactivate2FA = async ({ redirectUri }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.post( + `${redirectUri}/api/deactivateTwoFa`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; + +export const deleteAccount = async ({ redirectUri }) => { + let response = {}; + try { + const token = Cookies.get("access"); + response = await axios.post( + `${redirectUri}/api/accountDeletion`, + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + } catch (error) { + console.log(error); + } + return response; +}; diff --git a/frontend/src/components/home/Readme.js b/frontend/src/components/home/Readme.js index 6857123f2..cdacc41be 100644 --- a/frontend/src/components/home/Readme.js +++ b/frontend/src/components/home/Readme.js @@ -198,7 +198,7 @@ function Readme() { Infrastructure Setup for Log Management ❌
  • - Monitoring system ❌ + Monitoring system
  • Designing the Backend as Microservices ❌ diff --git a/frontend/src/components/profile/UserSettings.js b/frontend/src/components/profile/UserSettings.js index 49cd5d0fe..afdb21b75 100644 --- a/frontend/src/components/profile/UserSettings.js +++ b/frontend/src/components/profile/UserSettings.js @@ -3,75 +3,39 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { ButtonStyle } from "../buttons/ButtonStyle"; -import { getUserDetails } from "../../pages/Profile"; import { useNavigate } from "react-router-dom"; -import axios from "axios"; -import Cookies from "js-cookie"; - -const activate2FA = async ({ redirectUri }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.post( - `${redirectUri}/api/activateTwoFa`, - {}, - { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - } - ); - } catch (error) { - console.log(error); - } - return response; -}; - -const deactivate2FA = async ({ redirectUri }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.post( - `${redirectUri}/api/deactivateTwoFa`, - {}, - { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - } - ); - } catch (error) { - console.log(error); - } - return response; -}; - -const deleteAccount = async ({ redirectUri }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.post( - `${redirectUri}/api/accountDeletion`, - {}, - { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - } - ); - } catch (error) { - console.log(error); - } - return response; -}; +import { + getUserDetails, + activate2FA, + deactivate2FA, + deleteAccount, + changePassword, + changeAvatar, +} from "../API"; function UserSettings({ redirectUri }) { const { t } = useTranslation(); const navigate = useNavigate(); const [userDetails, setUserDetails] = useState({ TwoFA: false }); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + }; + + const handleConfirmPasswordChange = (e) => { + setConfirmPassword(e.target.value); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (password === confirmPassword) { + changePassword({ redirectUri, password }); + } else { + alert("Passwords do not match"); + } + }; useEffect(() => { const fetchUserDetails = async () => { @@ -82,10 +46,6 @@ function UserSettings({ redirectUri }) { fetchUserDetails(); }, [redirectUri]); - // useEffect(() => { - // console.log("userDetails:", userDetails); - // }, [userDetails]); - return (
    - -
    {!userDetails?.ft_user && ( - <> +
    - +
    - +
    - + +
    )}
    diff --git a/frontend/src/components/welcome_page/LoginVia42.js b/frontend/src/components/welcome_page/LoginVia42.js index 94b9833b8..edd10eb2c 100644 --- a/frontend/src/components/welcome_page/LoginVia42.js +++ b/frontend/src/components/welcome_page/LoginVia42.js @@ -9,11 +9,13 @@ const OAuth = async ({ navigate, redirect_uri }) => { window.onload = () => { const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get("token"); - Cookies.set("access", token, { - expires: 7, - sameSite: "Strict", - secure: true, - }); + if (token) { + Cookies.set("access", token, { + expires: 7, + sameSite: "None", + secure: true, + }); + } }; const LoginButton = ({ t, navigate, redirect_uri }) => { diff --git a/frontend/src/components/welcome_page/RegisterButton.js b/frontend/src/components/welcome_page/RegisterButton.js index 5bd92aa0a..2bc6cfbca 100644 --- a/frontend/src/components/welcome_page/RegisterButton.js +++ b/frontend/src/components/welcome_page/RegisterButton.js @@ -24,12 +24,14 @@ const RegisterButt = ({ t, redirectToHome, redirect_uri }) => { { withCredentials: true } ); const token = response.data.access; - Cookies.set("access", token, { - expires: 7, - sameSite: "Strict", - secure: true, - }); - if (response.data.access) redirectToHome(); + if (token) { + Cookies.set("access", token, { + expires: 7, + sameSite: "Strict", + secure: true, + }); + redirectToHome(); + } } catch (error) { if (error.response && error.response.data) { let errorMessage; diff --git a/frontend/src/components/welcome_page/SignInButton.js b/frontend/src/components/welcome_page/SignInButton.js index d84149d93..989246070 100644 --- a/frontend/src/components/welcome_page/SignInButton.js +++ b/frontend/src/components/welcome_page/SignInButton.js @@ -2,6 +2,7 @@ import React, { useState } from "react"; import axios from "axios"; import { WelcomeButtonStyle } from "../buttons/ButtonStyle"; import Cookies from "js-cookie"; +import { getUserDetails } from "../API"; const SignInButt = ({ t, redirectToHome, redirect_uri }) => { const [showFields, setShowFields] = useState(false); @@ -21,12 +22,19 @@ const SignInButt = ({ t, redirectToHome, redirect_uri }) => { { withCredentials: true } ); const token = response.data.access; - Cookies.set("access", token, { - expires: 7, - sameSite: "Strict", - secure: true, - }); - if (response.data.access) redirectToHome(); + if (token) { + Cookies.set("access", token, { + expires: 7, + sameSite: "Strict", + secure: true, + }); + const user = await getUserDetails({ redirectUri: redirect_uri }); + if (user.data.user.TwoFA === true) { + window.location.href = "/2fa"; + } else { + redirectToHome(); + } + } } catch (error) { if (error.response && error.response.data) { let errorMessage; diff --git a/frontend/src/pages/Profile.js b/frontend/src/pages/Profile.js index 23c6c502f..d66236332 100644 --- a/frontend/src/pages/Profile.js +++ b/frontend/src/pages/Profile.js @@ -4,7 +4,7 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { ButtonStyle } from "../components/buttons/ButtonStyle"; import UserSettings from "../components/profile/UserSettings"; -import axios from "axios"; +import { fetchUserDetails, changeUsername, changeAbout } from "../components/API"; import { CiEdit } from "react-icons/ci"; import Cookies from "js-cookie"; @@ -234,83 +234,6 @@ const defaultUserDetails = { "https://raw.githubusercontent.com/zstenger93/Transcendence/master/images/transcendence.webp", }; -export const getUserDetails = async ({ redirectUri }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.get(`${redirectUri}/api/profile`, { - headers: { - withCredentials: true, - Authorization: `Bearer ${token}`, - }, - }); - } catch (error) { - console.log(error); - } - return response; -}; - -const changeAbout = async ({ redirectUri, about }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.post( - `${redirectUri}/api/updateProfile`, - { AboutMe: about }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - } - ); - } catch (error) { - console.log(error); - } - return response; -}; - -const changeUsername = async ({ redirectUri, username }) => { - let response = {}; - try { - const token = Cookies.get('access'); - response = await axios.post( - `${redirectUri}/api/updateProfile`, - { username: username }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - withCredentials: true, - } - ); - } catch (error) { - console.log(error); - } - return response; -}; - -const fetchUserDetails = async ( - setUserDetails, - setUsername, - setImageUrl, - redirectUri -) => { - const response = await getUserDetails({ redirectUri }); - setUserDetails(response.data.user); - - console.log(response.data.user); - - setUsername(response.data.user.username); - - if (response.data.user.profile_picture) { - let url = decodeURIComponent( - response.data.user.profile_picture.replace("/media/", "") - ).replace(":", ":/"); - setImageUrl(url); - } -}; - function Profile({ redirectUri }) { const [userDetails, setUserDetails] = useState(null); const [imageUrl, setImageUrl] = useState(defaultUserDetails.profile_picture); @@ -319,10 +242,8 @@ function Profile({ redirectUri }) { ); useEffect(() => { - const wtf = Cookies.get('access'); - console.log("wtf1: ", wtf); - if (wtf) { - console.log("wtf2: ", wtf); + const token = Cookies.get('access'); + if (token) { fetchUserDetails(setUserDetails, setUsername, setImageUrl, redirectUri); } }, [redirectUri]); @@ -404,6 +325,8 @@ function Profile({ redirectUri }) { } }; + console.log(imageUrl); + return (