diff --git a/MisplaceAI/user_dashboard/serializers.py b/MisplaceAI/user_dashboard/serializers.py index ef359568ce8d4d235b08a694ad5f7b412a9b5e97..a7fce826a9b2eb508b2c14e2d37df96f43ec14e2 100644 --- a/MisplaceAI/user_dashboard/serializers.py +++ b/MisplaceAI/user_dashboard/serializers.py @@ -26,4 +26,10 @@ class UserUpdateEmailSerializer(serializers.Serializer): class UserUpdateUsernameSerializer(serializers.Serializer): username = serializers.CharField(max_length=150) - password = serializers.CharField(write_only=True) \ No newline at end of file + password = serializers.CharField(write_only=True) + + +class UserUpdatePasswordSerializer(serializers.Serializer): + current_password = serializers.CharField(write_only=True) + new_password = serializers.CharField(write_only=True) + confirm_password = serializers.CharField(write_only=True) \ No newline at end of file diff --git a/MisplaceAI/user_dashboard/urls.py b/MisplaceAI/user_dashboard/urls.py index 3a063da5d825eba05740c0cc03a6a0a92d5c7b29..a730bd1b77bcbf947471500b953bf595c2e9dd69 100644 --- a/MisplaceAI/user_dashboard/urls.py +++ b/MisplaceAI/user_dashboard/urls.py @@ -1,6 +1,6 @@ # MisplaceAI/user_dashboard/urls.py from django.urls import path -from .views import UserDashboardView, UserUpdateView, UpdateEmailView,CurrentUserEmailView,CurrentUserUsernameView,UpdateUsernameView +from .views import UserDashboardView, UserUpdateView, UpdateEmailView,CurrentUserEmailView,CurrentUserUsernameView,UpdateUsernameView,UpdatePasswordView urlpatterns = [ path('dashboard/', UserDashboardView.as_view(), name='user-dashboard'), @@ -9,5 +9,6 @@ urlpatterns = [ path('email/', CurrentUserEmailView.as_view(), name='user-current-email'), path('username/', CurrentUserUsernameView.as_view(), name='user-current-username'), path('update_username/', UpdateUsernameView.as_view(), name='user-update-username'), + path('update_password/', UpdatePasswordView.as_view(), name='user-update-password'), ] diff --git a/MisplaceAI/user_dashboard/views.py b/MisplaceAI/user_dashboard/views.py index 416a509e6bc4be1a7b44b69d14945213092d009f..3a4904f38e0ea052a37badcd927bcdc63b33fc30 100644 --- a/MisplaceAI/user_dashboard/views.py +++ b/MisplaceAI/user_dashboard/views.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from .serializers import UserSerializer, UserUpdateSerializer, UserUpdateEmailSerializer, UserUpdateUsernameSerializer +from .serializers import UserSerializer, UserUpdateSerializer, UserUpdateEmailSerializer, UserUpdateUsernameSerializer, UserUpdatePasswordSerializer from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -78,3 +78,28 @@ class CurrentUserUsernameView(APIView): def get(self, request, *args, **kwargs): user = request.user return Response({'username': user.username}, status=status.HTTP_200_OK) + + +class UpdatePasswordView(APIView): + permission_classes = [IsAuthenticated] + + def put(self, request, *args, **kwargs): + user = request.user + serializer = UserUpdatePasswordSerializer(data=request.data) + + if serializer.is_valid(): + current_password = serializer.validated_data.get('current_password') + new_password = serializer.validated_data.get('new_password') + confirm_password = serializer.validated_data.get('confirm_password') + + if not user.check_password(current_password): + return Response({'error': 'Current password is incorrect'}, status=status.HTTP_400_BAD_REQUEST) + + if new_password != confirm_password: + return Response({'error': 'New passwords do not match'}, status=status.HTTP_400_BAD_REQUEST) + + user.set_password(new_password) + user.save() + return Response({'message': 'Password updated successfully'}, status=status.HTTP_200_OK) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/frontend/src/forms/UserProfile/ChangePasswordForm.js b/frontend/src/forms/UserProfile/ChangePasswordForm.js new file mode 100644 index 0000000000000000000000000000000000000000..737df3cbf39f4868993902cd7b454da62d6019ae --- /dev/null +++ b/frontend/src/forms/UserProfile/ChangePasswordForm.js @@ -0,0 +1,161 @@ +// src/forms/UserProfile/ChangePasswordForm.js + +import React, { useState, useEffect, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import api from '../../services/api'; +import FormContainer from '../../components/Common/Form/FormContainer'; +import PasswordInputField from '../../components/Common/Password/PasswordInputField'; +import ErrorMessage from '../../components/Common/Form/ErrorMessage'; +import SubmitButton from '../../components/Common/buttons/SubmitButton'; +import CancelButton from '../../components/Common/buttons/CancelButton'; +import ActionButton from '../../components/Common/buttons/ActionButton'; +import '../../styles/main.css'; + +const ChangePasswordForm = ({ onUpdatePassword }) => { + const [showForm, setShowForm] = useState(false); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [passwordMatchMessage, setPasswordMatchMessage] = useState(''); + const [isFormValid, setIsFormValid] = useState(false); + const [loading, setLoading] = useState(false); + + const validatePassword = useCallback(() => { + const requirements = { + length: /.{8,}/, + uppercase: /[A-Z]/, + lowercase: /[a-z]/, + number: /[0-9]/, + }; + + const value = newPassword; + let allValid = true; + + for (const key in requirements) { + if (!requirements[key].test(value)) { + allValid = false; + break; + } + } + + return allValid; + }, [newPassword]); + + const checkPasswordMatch = useCallback(() => { + if (validatePassword()) { + if (confirmPassword === '') { + setPasswordMatchMessage(''); + return false; + } + if (newPassword === confirmPassword) { + setPasswordMatchMessage('Passwords match'); + return true; + } else { + setPasswordMatchMessage('Passwords do not match'); + return false; + } + } else { + setPasswordMatchMessage(''); + return false; + } + }, [newPassword, confirmPassword, validatePassword]); + + const validateForm = useCallback(() => { + const isCurrentPasswordFilled = currentPassword.trim() !== ''; + const isNewPasswordValid = validatePassword(); + const isPasswordMatch = checkPasswordMatch(); + + setIsFormValid(isCurrentPasswordFilled && isNewPasswordValid && isPasswordMatch); + }, [currentPassword, validatePassword, checkPasswordMatch]); + + useEffect(() => { + validateForm(); + }, [currentPassword, newPassword, confirmPassword, validateForm]); + + const handleActionClick = () => { + setShowForm(!showForm); + }; + + const handleCancel = () => { + setShowForm(false); + setError(null); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (newPassword !== confirmPassword) { + setError("Passwords do not match"); + return; + } + setLoading(true); + try { + await api.put('/api/user_dashboard/update_password/', { + current_password: currentPassword, + new_password: newPassword, + confirm_password: confirmPassword, + }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setError(''); + setShowForm(false); + onUpdatePassword(); + } catch (error) { + setError('Password update failed'); + } + setLoading(false); + }; + + return ( + <div className="dashboard-card"> + <div className="card card-wide"> + <div className="card-body"> + <h3 className="card-title">Update Password</h3> + <ActionButton className="action-button" label={showForm ? 'Cancel' : 'Change Password'} onClick={handleActionClick} /> + </div> + </div> + {showForm && ( + <div className="card card-wide"> + <div className="card-body"> + <FormContainer onSubmit={handleSubmit}> + {error && <ErrorMessage message={error} />} + <PasswordInputField + label="Current Password" + value={currentPassword} + onChange={(e) => setCurrentPassword(e.target.value)} + required + /> + <PasswordInputField + label="New Password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} + showRequirements={true} + required + /> + <PasswordInputField + label="Confirm New Password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + required + /> + <small id="passwordMatchMessage" className={`form-text ${passwordMatchMessage.includes('match') ? 'text-success' : 'text-danger'}`}> + {passwordMatchMessage} + </small> + <div className="form-buttons"> + <SubmitButton className="submit-button" label={loading ? 'Updating...' : 'Update Password'} disabled={!isFormValid || loading} /> + <CancelButton className="action-button" label="Cancel" onClick={handleCancel} /> + </div> + </FormContainer> + </div> + </div> + )} + </div> + ); +}; + +ChangePasswordForm.propTypes = { + onUpdatePassword: PropTypes.func.isRequired, +}; + +export default ChangePasswordForm; diff --git a/frontend/src/forms/UserProfile/ChangeUsernameForm.js b/frontend/src/forms/UserProfile/ChangeUsernameForm.js index 07b515eb733bb87770728377aa08e4dce540530c..3ddda4367978423d867fc117bf43608e936d1fab 100644 --- a/frontend/src/forms/UserProfile/ChangeUsernameForm.js +++ b/frontend/src/forms/UserProfile/ChangeUsernameForm.js @@ -26,6 +26,7 @@ const ChangeUsernameForm = ({ currentUsername, onUpdateUsername }) => { try { const response = await updateUsername({ username: newInfo, password }); console.log('Username updated:', response); + localStorage.setItem('username', newInfo); // Update localStorage onUpdateUsername({ newInfo }); // Update the parent state setShowForm(false); } catch (err) { diff --git a/frontend/src/pages/UserProfile/UserProfile.js b/frontend/src/pages/UserProfile/UserProfile.js index 25370feb2ec3679396e0283a1e41bac529e56d59..ea77ab3427cdde652a86f4ae8498a0f5b6ef6867 100644 --- a/frontend/src/pages/UserProfile/UserProfile.js +++ b/frontend/src/pages/UserProfile/UserProfile.js @@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react'; import ChangeEmailForm from '../../forms/UserProfile/ChangeEmailForm'; import ChangeUsernameForm from '../../forms/UserProfile/ChangeUsernameForm'; +import ChangePasswordForm from '../../forms/UserProfile/ChangePasswordForm'; import { getCurrentUserEmail, getCurrentUserUsername } from '../../services/userApi'; import '../../styles/main.css'; import '../../styles/dashboard.css'; @@ -32,6 +33,11 @@ const UserProfile = () => { const handleUpdateUsername = async (data) => { setCurrentUsername(data.newInfo); + localStorage.setItem('username', data.newInfo); // Update localStorage + }; + + const handleUpdatePassword = () => { + console.log('Password updated successfully'); }; return ( @@ -45,6 +51,9 @@ const UserProfile = () => { currentUsername={currentUsername} onUpdateUsername={handleUpdateUsername} /> + <ChangePasswordForm + onUpdatePassword={handleUpdatePassword} + /> </div> ); }; diff --git a/frontend/src/services/userApi.js b/frontend/src/services/userApi.js index 787773780140cf5c3f424c3873c000028e29673c..b5df044da731b3ad40118875dd83c611b93b8120 100644 --- a/frontend/src/services/userApi.js +++ b/frontend/src/services/userApi.js @@ -26,3 +26,8 @@ export const getCurrentUserUsername = async () => { const response = await api.get('/api/user_dashboard/username/'); return response.data; }; + +export const updatePassword = async (passwordData) => { + const response = await api.put('/api/user_dashboard/update_password/', passwordData); + return response.data; +};