From 88da73b49b05ed12bec8e60dd08e7b611e719ea4 Mon Sep 17 00:00:00 2001 From: DHRUMIL PATEL <123137675+dhrumilp12@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:05:23 -0400 Subject: [PATCH] Allow editting required fields #12 - Done A user's profile must contain required registration fields: 8.a Name 8.b Email Address 8.c Password --- client/src/Components/passwordUpdateTab.jsx | 111 ++++++++++++++++++++ client/src/Components/userContext.jsx | 30 +++++- client/src/Components/userProfile.jsx | 55 +++++++++- server/agents/mental_health_agent.py | 6 ++ server/models/user.py | 18 ++++ server/routes/user.py | 34 ++++++ 6 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 client/src/Components/passwordUpdateTab.jsx diff --git a/client/src/Components/passwordUpdateTab.jsx b/client/src/Components/passwordUpdateTab.jsx new file mode 100644 index 00000000..2645cfc8 --- /dev/null +++ b/client/src/Components/passwordUpdateTab.jsx @@ -0,0 +1,111 @@ +import React, { useContext, useState } from 'react'; +import { UserContext } from './userContext'; // Adjust the import path as necessary +import { useParams } from 'react-router-dom'; +import { TextField, Button, Box, Typography, Container, Snackbar, Alert } from '@mui/material'; +import { createTheme, ThemeProvider} from '@mui/material/styles'; +import VpnKeyIcon from '@mui/icons-material/VpnKey'; +import LockIcon from '@mui/icons-material/Lock'; + +const theme = createTheme({ + palette: { + primary: { + main: '#3F51B5', + }, + secondary: { + main: '#F6AE2D', + }, + }, + }); + + +const PasswordUpdateTab = () => { + const { changePassword } = useContext(UserContext); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + const [snackbarType, setSnackbarType] = useState('success'); // 'success' or 'error' + + const { userId } = useParams(); + const handleSubmit = async (e) => { + e.preventDefault(); + const result = await changePassword(userId, currentPassword, newPassword); + setSnackbarMessage(result.message); + setSnackbarType(result.success ? 'success' : 'error'); + setSnackbarOpen(true); +}; + + + return ( + + + + + Update Password + +
+ setCurrentPassword(e.target.value)} + InputProps={{ + startAdornment: ( + + ), + }} + /> + setNewPassword(e.target.value)} + InputProps={{ + startAdornment: ( + + ), + }} + /> + + + setSnackbarOpen(false)}> + setSnackbarOpen(false)} severity={snackbarType} sx={{ width: '100%' }}> + {snackbarMessage} + + +
+
+
+ ); +}; + +export default PasswordUpdateTab; diff --git a/client/src/Components/userContext.jsx b/client/src/Components/userContext.jsx index b8d0e3d1..f7f9bbc4 100644 --- a/client/src/Components/userContext.jsx +++ b/client/src/Components/userContext.jsx @@ -7,6 +7,7 @@ export const UserProvider = ({ children }) => { const [voiceEnabled, setVoiceEnabled] = useState(false); const [user, setUser] = useState(null); const navigate = useNavigate(); + const logout = useCallback(async () => { try { const token = localStorage.getItem('token'); @@ -32,8 +33,35 @@ export const UserProvider = ({ children }) => { } }, [navigate]); + const changePassword = async (userId, currentPassword, newPassword) => { + try { + const token = localStorage.getItem('token'); + const response = await axios.patch(`/api/user/change_password/${userId}`, { + current_password: currentPassword, + new_password: newPassword + }, { + headers: { + Authorization: `Bearer ${token}`, // Make sure the token is being managed in your context or retrieved from somewhere secure + } + }); + + if (response.status === 200) { + return { success: true, message: 'Password updated successfully!' }; + } else { + return { success: false, message: response.data.message || 'Update failed!' }; + } + } catch (error) { + if(error.response.status === 403) { + return { success: false, message: error.response.data.message || 'Incorrect current password' }; + } + else { + return { success: false, message: error.response?.data?.message || 'Network error' }; + } + } + }; + return ( - + {children} ); diff --git a/client/src/Components/userProfile.jsx b/client/src/Components/userProfile.jsx index f94fac39..25504a79 100644 --- a/client/src/Components/userProfile.jsx +++ b/client/src/Components/userProfile.jsx @@ -1,8 +1,10 @@ import React, { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import PasswordUpdateTab from './passwordUpdateTab'; import axios from 'axios'; + import { - TextField, Button, Container, Typography, Paper, CssBaseline, Snackbar, Alert, FormControl, InputLabel, Select, MenuItem,IconButton + TextField, Button, Container, Typography, Paper, CssBaseline, Snackbar, Alert, FormControl, InputLabel, Select, MenuItem,IconButton,Tabs,Tab,Box } from '@mui/material'; import { createTheme, ThemeProvider, styled } from '@mui/material/styles'; import EmailIcon from '@mui/icons-material/Email'; @@ -13,6 +15,37 @@ import HomeIcon from '@mui/icons-material/Home'; // Icon for place of residence import WorkIcon from '@mui/icons-material/Work'; import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import UpdateIcon from '@mui/icons-material/Update'; + +const CustomTabs = styled(Tabs)({ + background: '#fff', // Set the background color you prefer + borderRadius: '8px', // Optional: rounded corners + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', // Optional: adds a subtle shadow for depth + margin: '20px 0', // Adds margin around the tabs for spacing + maxWidth: '100%', // Ensures it doesn't overflow its container + overflow: 'hidden', // Prevents any internal content from overflowing +}); + +const CustomTab = styled(Tab)({ + fontSize: '1rem', // Sets the font size to 16px + fontWeight: 'bold', // Makes the font weight bold + color: '#3F51B5', // Uses the primary color defined in the theme + marginRight: '4px', // Adds space between tabs + marginLeft: '4px', // Adds space between tabs + flex: 1, // Each tab flexes to fill available space + maxWidth: 'none', // Allows the tab to grow as needed + '&.Mui-selected': { // Styles for the selected tab + color: '#F6AE2D', // Changes text color when selected + background: '#e0e0e0', // Light grey background on selection + }, + '&:hover': { // Styles for hover state + background: '#f4f4f4', // Lighter grey background on hover + transition: 'background-color 0.3s', // Smooth transition for background color + }, + '@media (max-width: 720px)': { // Responsive adjustment for smaller screens + padding: '6px 12px', // Reduces padding on smaller screens + fontSize: '0.8rem', // Reduces font size to fit on smaller devices +}}); + const theme = createTheme({ palette: { primary: { @@ -57,12 +90,12 @@ const theme = createTheme({ }); const StyledForm = styled(Paper)(({ theme }) => ({ - marginTop: theme.spacing(4), + marginTop: theme.spacing(2), padding: theme.spacing(4), display: 'flex', flexDirection: 'column', alignItems: 'center', - gap: theme.spacing(3), + gap: theme.spacing(2), boxShadow: theme.shadows[3], // Subtle shadow for depth })); @@ -77,6 +110,11 @@ function UserProfile() { placeOfResidence: '', fieldOfWork: '' }); + const [tabValue, setTabValue] = useState(0); // To control the active tab + + const handleTabChange = (event, newValue) => { + setTabValue(newValue); + }; const [message, setMessage] = useState(''); const [open, setOpen] = useState(false); const [severity, setSeverity] = useState('info'); @@ -136,6 +174,12 @@ function UserProfile() { + + + + + + {tabValue === 0 && ( {user.username} Update Profile - + )} + {tabValue === 1 && ( + + )} {message} diff --git a/server/agents/mental_health_agent.py b/server/agents/mental_health_agent.py index 3071d532..b36a1128 100644 --- a/server/agents/mental_health_agent.py +++ b/server/agents/mental_health_agent.py @@ -439,7 +439,13 @@ def get_initial_greeting(self, user_id): except StopIteration: last_turn = {} + # Check if 'SessionId' is in last_turn and extract old_chat_id + if 'SessionId' in last_turn: old_chat_id = int(last_turn["SessionId"].split('-')[-1]) + else: + # Initialize old_chat_id if SessionId is not found + old_chat_id = 0 + new_chat_id = old_chat_id + 1 response = self.run( diff --git a/server/models/user.py b/server/models/user.py index 4bcd0276..154be002 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, EmailStr, Field, field_validator, validator from services.azure_mongodb import MongoDBClient +from bson import ObjectId import re class User(BaseModel): @@ -24,6 +25,23 @@ def find_by_username(cls, username): db_client = MongoDBClient.get_client() db = db_client[MongoDBClient.get_db_name()] user_data = db.users.find_one({"username": username}) # 'users' is the collection name + if user_data: + user_data['id'] = str(user_data['_id']) + return cls(**user_data) + return None + + @classmethod + def update_password(cls, username, new_hashed_password): + db_client = MongoDBClient.get_client() + db = db_client[MongoDBClient.get_db_name()] + result = db.users.update_one({"username": username}, {"$set": {"password": new_hashed_password}}) + return result.modified_count == 1 + + @classmethod + def find_by_id(cls, user_id): + db_client = MongoDBClient.get_client() + db = db_client[MongoDBClient.get_db_name()] + user_data = db.users.find_one({"_id": ObjectId(user_id)}) if user_data: user_data['id'] = str(user_data['_id']) return cls(**user_data) diff --git a/server/routes/user.py b/server/routes/user.py index 92d8a416..c59c1a5b 100644 --- a/server/routes/user.py +++ b/server/routes/user.py @@ -123,6 +123,40 @@ def update_profile_fields(user_id): return jsonify({"message": "User has been updated successfully."}), 200 +@user_routes.patch('/user/change_password/') +def change_password(user_id): + try: + # Authenticate user + user_data = request.get_json() + current_password = user_data['current_password'] + new_password = user_data['new_password'] + + user = UserModel.find_by_id(user_id) + if not user: + logging.error("User not found") + return jsonify({"error": "User not found"}), 404 + + # Verify current password + if not check_password_hash(user.password, current_password): + logging.error("Incorrect current password") + return jsonify({"error": "Incorrect current password"}), 403 + + # Update to new password + new_password_hash = generate_password_hash(new_password) + if user.update_password(user.username, new_password_hash): + logging.info("Password updated successfully") + return jsonify({"message": "Password successfully updated"}), 200 + else: + logging.error("Password update failed") + return jsonify({"error": "Password update failed"}), 500 + except KeyError as e: + logging.error(f"Missing data: {str(e)}") + return jsonify({"error": "Missing data"}), 400 + except Exception as e: + logging.error(f"Error changing password: {str(e)}") + return jsonify({"error": str(e)}), 500 + + @user_routes.post('/user/log_mood') @jwt_required() def log_mood():