Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion mesh/accountSettings/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<int:account_id>", accountsettings_views.showSettings.as_view(), name="settings")
]
55 changes: 54 additions & 1 deletion mesh/accountSettings/views.py
Original file line number Diff line number Diff line change
@@ -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":
Expand All @@ -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)


12 changes: 7 additions & 5 deletions mesh/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,22 +46,27 @@
'mesh.conversation',
'mesh.notifications',
'mesh.tags',
'mesh.occupations'

'mesh.occupations',
'corsheaders',
]

# TODO: https://github.com/LetsMesh/Site/issues/202

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',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ALLOWED_HOSTS=[

]

CORS_ORIGIN_WHITELIST = [
'http://localhost:3000',
]
Expand Down
53 changes: 51 additions & 2 deletions mesh/tests/settings_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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'])
129 changes: 129 additions & 0 deletions meshapp/src/Settings/settings-page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider theme={theme}>
<FormControlLabel
control={<Switch checked={props.value} onChange={props.onChange} />}
label={<span style={{ color: 'white' }}>{props.label}</span>}
/>
</ThemeProvider>
);
}

/**
* 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 <div>Loading...</div>;
}

return (
<Container>
<Typography variant="h4" color="white" gutterBottom>
Account Settings
</Typography>
<SettingSwitch
label="Enable Two Factor Authentication"
value={settings.is2FAEnabled}
onChange={handleToggleChange('is2FAEnabled')}
/>
</Container>
);
}

export default SettingsPage;
10 changes: 10 additions & 0 deletions meshapp/src/Settings/tests/settings-examples.tsx
Original file line number Diff line number Diff line change
@@ -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,
}
8 changes: 8 additions & 0 deletions meshapp/src/Settings/types/account-settings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type AccountSettings = {
accountID: number;
isVerified: boolean;
verificationToken: string;
hasContentFilterEnabled: boolean;
displayTheme: char;
is2FAEnabled: boolean;
};
3 changes: 3 additions & 0 deletions meshapp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -30,6 +32,7 @@ root.render(
<ProfilePage {...exampleProfile} />
<LoggedInHome />
<SignUp />
<Settings {...exampleSettings}/>
</ThemeProvider>
</React.StrictMode>
);
Expand Down