diff --git a/assets/vue/components/social/UserProfileCard.vue b/assets/vue/components/social/UserProfileCard.vue index fcc773b7ee..c542e57a1a 100644 --- a/assets/vue/components/social/UserProfileCard.vue +++ b/assets/vue/components/social/UserProfileCard.vue @@ -104,6 +104,14 @@ type="primary" @click="editProfile" /> + @@ -140,6 +148,10 @@ const editProfile = () => { window.location = "/account/edit" } +const changePassword = () => { + window.location = "/account/change-password" +} + async function fetchUserProfile(userId) { try { const { data } = await axios.get(`/social-network/user-profile/${userId}`) diff --git a/src/CoreBundle/Controller/AccountController.php b/src/CoreBundle/Controller/AccountController.php index 3c32ed7bcb..13e931ebbe 100644 --- a/src/CoreBundle/Controller/AccountController.php +++ b/src/CoreBundle/Controller/AccountController.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace Chamilo\CoreBundle\Controller; use Chamilo\CoreBundle\Entity\User; +use Chamilo\CoreBundle\Form\ChangePasswordType; use Chamilo\CoreBundle\Form\ProfileType; use Chamilo\CoreBundle\Repository\Node\IllustrationRepository; use Chamilo\CoreBundle\Repository\Node\UserRepository; @@ -18,6 +19,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; /** * @author Julio Montoya @@ -69,4 +73,44 @@ class AccountController extends BaseController 'user' => $user, ]); } + + #[Route('/change-password', name: 'chamilo_core_account_change_password', methods: ['GET', 'POST'])] + public function changePassword(Request $request, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response + { + $user = $this->getUser(); + + if (!\is_object($user) || !$user instanceof UserInterface) { + throw $this->createAccessDeniedException('This user does not have access to this section'); + } + + $form = $this->createForm(ChangePasswordType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $submittedToken = $request->request->get('_token'); + + if (!$csrfTokenManager->isTokenValid(new CsrfToken('change_password', $submittedToken))) { + $form->addError(new FormError('CSRF token is invalid. Please try again.')); + } else { + $currentPassword = $form->get('currentPassword')->getData(); + $newPassword = $form->get('newPassword')->getData(); + $confirmPassword = $form->get('confirmPassword')->getData(); + + if (!$userRepository->isPasswordValid($user, $currentPassword)) { + $form->get('currentPassword')->addError(new FormError('Current password is incorrect.')); + } elseif ($newPassword !== $confirmPassword) { + $form->get('confirmPassword')->addError(new FormError('Passwords do not match.')); + } else { + $user->setPlainPassword($newPassword); + $userRepository->updateUser($user); + $this->addFlash('success', 'Password changed successfully.'); + return $this->redirectToRoute('chamilo_core_account_home'); + } + } + } + + return $this->render('@ChamiloCore/Account/change_password.html.twig', [ + 'form' => $form->createView(), + ]); + } } diff --git a/src/CoreBundle/Form/ChangePasswordType.php b/src/CoreBundle/Form/ChangePasswordType.php new file mode 100644 index 0000000000..a2887fc02d --- /dev/null +++ b/src/CoreBundle/Form/ChangePasswordType.php @@ -0,0 +1,43 @@ +add('currentPassword', PasswordType::class, [ + 'label' => 'Current Password', + 'required' => true, + ]) + ->add('newPassword', PasswordType::class, [ + 'label' => 'New Password', + 'required' => true, + ]) + ->add('confirmPassword', PasswordType::class, [ + 'label' => 'Confirm New Password', + 'required' => true, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_token_id' => 'change_password', + ]); + } +} diff --git a/src/CoreBundle/Repository/Node/UserRepository.php b/src/CoreBundle/Repository/Node/UserRepository.php index 96a279fd57..40707c78ba 100644 --- a/src/CoreBundle/Repository/Node/UserRepository.php +++ b/src/CoreBundle/Repository/Node/UserRepository.php @@ -105,6 +105,11 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter } } + public function isPasswordValid(User $user, string $plainPassword): bool + { + return $this->hasher->isPasswordValid($user, $plainPassword); + } + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { /** @var User $user */ diff --git a/src/CoreBundle/Resources/config/services.yml b/src/CoreBundle/Resources/config/services.yml index b35bcc83bd..00540aeb2b 100644 --- a/src/CoreBundle/Resources/config/services.yml +++ b/src/CoreBundle/Resources/config/services.yml @@ -5,6 +5,9 @@ services: public: true autoconfigure: true + csrf.token_manager: + class: Symfony\Component\Security\Csrf\CsrfTokenManager + chamilo_core.translation.loader.po: class: Symfony\Component\Translation\Loader\PoFileLoader tags: diff --git a/src/CoreBundle/Resources/views/Account/change_password.html.twig b/src/CoreBundle/Resources/views/Account/change_password.html.twig new file mode 100644 index 0000000000..177a38cb04 --- /dev/null +++ b/src/CoreBundle/Resources/views/Account/change_password.html.twig @@ -0,0 +1,48 @@ +{% extends "@ChamiloCore/Layout/layout_one_col.html.twig" %} + +{% block content %} +
+
+

{{ "Change Password"|trans }}

+ + {{ form_start(form, {'attr': {'class': 'bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4'}}) }} + + {% for message in app.flashes('success') %} +
+ {{ message }} +
+ {% endfor %} + + {% if form.vars.errors|length > 0 %} +
+ {{ form_errors(form) }} +
+ {% endif %} + +
+ {{ form_label(form.currentPassword) }} + {{ form_widget(form.currentPassword, {'attr': {'class': 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'}}) }} + {{ form_errors(form.currentPassword) }} +
+ +
+ {{ form_label(form.newPassword) }} + {{ form_widget(form.newPassword, {'attr': {'class': 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'}}) }} + {{ form_errors(form.newPassword) }} +
+ +
+ {{ form_label(form.confirmPassword) }} + {{ form_widget(form.confirmPassword, {'attr': {'class': 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline'}}) }} + {{ form_errors(form.confirmPassword) }} +
+ +
+ + +
+ + {{ form_end(form) }} +
+
+{% endblock %}