diff --git a/mesh/accountSettings/urls.py b/mesh/accountSettings/urls.py index a0ddc08b..5d7ff09d 100644 --- a/mesh/accountSettings/urls.py +++ b/mesh/accountSettings/urls.py @@ -2,5 +2,9 @@ from . import views as accountsettings_views urlpatterns = [ - path("displayTheme", accountsettings_views.display_theme, name="display_theme") + path("displayTheme", accountsettings_views.display_theme, name="display_theme"), + + # GET /api/settings/:account_id + # PATCH /api/settings/:account_id + path("settings/", accountsettings_views.showSettings.as_view(), name="settings") ] diff --git a/mesh/accountSettings/views.py b/mesh/accountSettings/views.py index 3c07ac92..20e391e2 100644 --- a/mesh/accountSettings/views.py +++ b/mesh/accountSettings/views.py @@ -1,8 +1,12 @@ from django.core.exceptions import ObjectDoesNotExist -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse +from django.views import View +from django.core import serializers from mesh.accountSettings.models import Settings +from mesh.accounts.models import Account +import json def display_theme(request): if request.method == "GET": @@ -29,3 +33,52 @@ def display_theme(request): response.update({"status": "error"}) response.update({"message": "An account does not exist with this account ID."}) return JsonResponse(response) + +class showSettings(View): + """ + Handles HTTP requests related to Two Factor Authentication account settings. + Support Get request to retrieve setting and patch to change setting. + """ + def get(self, request, account_id, *args, **kwargs): + """ + Handles GET request + + Return JSON with Two Factor Authentication Settings for given account ID. + Returns a 404 error with error message if account does not exist. + """ + try: + settings = Settings.objects.get(accountID=account_id) + settings_detail = serializers.serialize('json', [settings]) + return JsonResponse(settings_detail, safe=False) + except Account.DoesNotExist: + return JsonResponse({'error': 'An account does not exist with this account ID.'}, status=404) + except Settings.DoesNotExist: + return JsonResponse({'error': 'Settings does not exist with this account ID.'}, status=404) + + + def patch(self, request, account_id, *args, **kwargs): + """ + Handles Patch request. + + Updates the 2FactAuth account setting information to either true or false. + + Returns 204 HTTP response upon success. + Returns 404 if Account or Settings is not found + """ + try: + settings = Settings.objects.get(accountID=account_id) + data = json.loads(request.body) + settings.accountID = Account.objects.get(accountID=data.get('accountID')) + settings.isVerified = data.get('isVerified') + settings.verificationToken = data.get('verificationToken') + settings.hasContentFilterEnabled = data.get('hasContentFilterEnabled') + settings.displayTheme = data.get('displayTheme') + settings.is2FAEnabled = data.get('is2FAEnabled') + settings.save() + return HttpResponse(status=204) + except Account.DoesNotExist: + return JsonResponse({'error': 'An account does not exist with this account ID.'}, status=404) + except Settings.DoesNotExist: + return JsonResponse({'error': 'Settings does not exist with this account ID.'}, status=404) + + diff --git a/mesh/settings.py b/mesh/settings.py index a307905c..21012ad2 100644 --- a/mesh/settings.py +++ b/mesh/settings.py @@ -12,9 +12,6 @@ from pathlib import Path import os -from dotenv import load_dotenv - -load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -49,8 +46,8 @@ 'mesh.conversation', 'mesh.notifications', 'mesh.tags', - 'mesh.occupations' - + 'mesh.occupations', + 'corsheaders', ] # TODO: https://github.com/LetsMesh/Site/issues/202 @@ -58,6 +55,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -65,6 +63,10 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +ALLOWED_HOSTS=[ + +] + CORS_ORIGIN_WHITELIST = [ 'http://localhost:3000', ] diff --git a/mesh/tests/settings_tests.py b/mesh/tests/settings_tests.py index d8c097d8..7060fd06 100644 --- a/mesh/tests/settings_tests.py +++ b/mesh/tests/settings_tests.py @@ -9,7 +9,7 @@ class SettingsTest(TestCase): def setUp(self): self.client = Client() - test_account = Account.objects.create( + self.test_account = Account.objects.create( email="settingstest@gmail.com", encryptedPass=bytes("password_test", "utf-8"), phoneNum="1234567890", @@ -19,7 +19,7 @@ def setUp(self): isMentee=True ) Settings.objects.create( - accountID=test_account, + accountID=self.test_account, isVerified=False, verificationToken=None, hasContentFilterEnabled=False, @@ -44,3 +44,52 @@ def test_no_account_display_theme(self): json_response = json.loads(response.content.decode("utf-8")) self.assertEquals(json_response.get("status"), "error") self.assertEquals(json_response.get("message"), "An account does not exist with this account ID.") + + """ + Settings Testing + """ + def test_get_settings(self): + """ + Test case to see if settings are retrieved from account settings + + GET request to /settings/settings endpoint. + Test passes if 200 response status code is returned + """ + test_user = Account.objects.get(email="settingstest@gmail.com") + response = self.client.get(f"/settings/settings/{test_user.accountID}/") + self.assertEqual(response.status_code, 200) + + def test_update_settings(self): + """ + Test case to see if settings are patched or update to account settings + + Patch request to /settings/settings endpoint. + Test passes if 204 response status code is returned + """ + test_user = Account.objects.get(email="settingstest@gmail.com") + test_user_settings = Settings.objects.get(accountID=test_user) + + print(f"Test User: {test_user}") + + updated_account_data = { + "accountID": test_user, + "isVerified": False, + "verificationToken": None, + "hasContentFilterEnabled": False, + "displayTheme": "0", + "is2FAEnabled": False, + } + + json_data = json.dumps(updated_account_data, default=str) + + response = self.client.patch(f'/settings/settings/{test_user_settings.accountID}/', json_data, content_type='application/json') + self.assertEqual(response.status_code, 204) + + updated_test_user = Account.objects.get(email="settingstest@gmail.com") + updated_test_user_settings = Settings.objects.get(accountID=updated_test_user) + self.assertEqual(updated_test_user_settings.accountID, updated_account_data['accountID']) + self.assertEqual(updated_test_user_settings.isVerified, updated_account_data['isVerified']) + self.assertEqual(updated_test_user_settings.verificationToken, updated_account_data['verificationToken']) + self.assertEqual(updated_test_user_settings.hasContentFilterEnabled, updated_account_data['hasContentFilterEnabled']) + self.assertEqual(updated_test_user_settings.displayTheme, updated_account_data['displayTheme']) + self.assertEqual(updated_test_user_settings.is2FAEnabled, updated_account_data['is2FAEnabled']) \ No newline at end of file diff --git a/meshapp/src/Settings/settings-page.tsx b/meshapp/src/Settings/settings-page.tsx new file mode 100644 index 00000000..978e0a8e --- /dev/null +++ b/meshapp/src/Settings/settings-page.tsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect } from 'react'; +import { Switch, FormControlLabel, Container, Typography, createTheme, ThemeProvider } from '@mui/material'; + +import { AccountSettings } from "./types/account-settings" +import { axiosInstance } from "../config/axiosConfig"; + +const theme = createTheme({ + palette: { + mode: "light", + primary: { + main: "#1cba9a", + }, + }, + typography: { + h1: { + fontFamily: "cocogoose", + fontWeight: "bold", + color: "#ffffff", + }, + }, +}); + +interface SettingsProps { + value: any, + label: string, + onChange: (event: any) => void, +} + +/** + * React component that represents a single setting + * Represented through a React Switch + * + * @param props - properties of the component + */ +const SettingSwitch = (props: SettingsProps) => { + return ( + + } + label={{props.label}} + /> + + ); +} + +/** + * React component to render settings page. + * Displays setting options for the account. + * + * @param props + * @param {number} props.accountID - ID for account that settings represent + * @param {boolean} props.isVerified - flag for is account if verified + * @param {string} props.verificationToken - Token for verification + * @param {boolean} props.hasContentFilterEnabled - flag for content filtering + * @param {char} props.displayTheme - Char for display theme + * @param {boolean} props.is2FAEnabled - flag for if account has TwoFactorAuthentication is enabled + * + */ +const SettingsPage = (props: AccountSettings) => { + const [settings, setSettings] = useState({ + accountID: props.accountID, + isVerified: props.isVerified, + verificationToken: props.verificationToken, + hasContentFilterEnabled: props.hasContentFilterEnabled, + displayTheme: props.displayTheme, + is2FAEnabled: false + }); + const [loading, setLoading] = useState(false); + + useEffect(() => { + // Make a GET request to update the settings page with user settings + axiosInstance.get("settings/settings/" + props.accountID + "/") // NOTE: settings/settings is old api, use accountSettings when merging + .then((response) => { + setLoading(false); + let settingsData = JSON.parse(response.data)[0]["fields"] + setSettings({...settings, + isVerified: settingsData.isVerified, + verificationToken: settingsData.verificationToken, + hasContentFilterEnabled: settingsData.hasContentFilterEnabled, + displayTheme: settingsData.displayTheme, + is2FAEnabled: settingsData.is2FAEnabled}); + }) + .catch((error) => { + console.error('Error patching account settings:', error); + setLoading(true); + }); + }, []); + + /** + * NOTE: Second useEffect for sending patch request to update account settings each time settings is modified. + * The patch request is made every time the setting switch is changed. + * This may want to be changed in the future to ensure that only one + * patch request is made for multiple settings. + */ + useEffect(() => { + // Make a PATCH request to the backend API to update account settings + axiosInstance.patch("settings/settings/" + props.accountID + "/", {...settings}) // NOTE: Use accountSettings api name when merging + .then(() => { + setLoading(false); + }) + .catch((error) => { + console.error('Error patching account settings:', error); + setLoading(true); + }); + }, [settings]) + + const handleToggleChange = (settingName: any) => (event: any) => { + setSettings({ ...settings, [settingName]: event.target.checked }); // TODO: Implement Confirm Authentication for 2FactAuth + }; + + if (loading) { + return
Loading...
; + } + + return ( + + + Account Settings + + + + ); +} + +export default SettingsPage; \ No newline at end of file diff --git a/meshapp/src/Settings/tests/settings-examples.tsx b/meshapp/src/Settings/tests/settings-examples.tsx new file mode 100644 index 00000000..3a965a66 --- /dev/null +++ b/meshapp/src/Settings/tests/settings-examples.tsx @@ -0,0 +1,10 @@ +import { AccountSettings } from "../types/account-settings" + +export const exampleSettings: AccountSettings = { + accountID: 2, + isVerified: false, + verificationToken: "", + hasContentFilterEnabled: false, + displayTheme: 0, + is2FAEnabled: true, +} \ No newline at end of file diff --git a/meshapp/src/Settings/types/account-settings.d.ts b/meshapp/src/Settings/types/account-settings.d.ts new file mode 100644 index 00000000..db848167 --- /dev/null +++ b/meshapp/src/Settings/types/account-settings.d.ts @@ -0,0 +1,8 @@ +export type AccountSettings = { + accountID: number; + isVerified: boolean; + verificationToken: string; + hasContentFilterEnabled: boolean; + displayTheme: char; + is2FAEnabled: boolean; +}; \ No newline at end of file diff --git a/meshapp/src/index.tsx b/meshapp/src/index.tsx index e256e22c..aa69e491 100644 --- a/meshapp/src/index.tsx +++ b/meshapp/src/index.tsx @@ -11,11 +11,13 @@ import { ThemeProvider } from "@emotion/react"; import ProfilePage from "./profile/profile-page"; import ForgotPassword from "./components/password-forms/forgot-password-form" import LoggedInHome from "./home/logged-in/LoggedInHome"; +import Settings from "./Settings/settings-page" import { exampleProfile, exampleProfile2, } from "./profile/tests/profile-examples"; +import { exampleSettings } from "./Settings/tests/settings-examples"; import SignUp from "./components/SignUp/SignUp"; const root = ReactDOM.createRoot(document.getElementById("root")!); root.render( @@ -30,6 +32,7 @@ root.render( + );