-
Notifications
You must be signed in to change notification settings - Fork 486
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5597 from christianbeeznest/ofaj-21546
Internal: Make password edition a single independent page - refs BT#21546
- Loading branch information
Showing
8 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,17 +7,23 @@ | |
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; | ||
use Chamilo\CoreBundle\ServiceHelper\UserHelper; | ||
use Chamilo\CoreBundle\Settings\SettingsManager; | ||
use Chamilo\CoreBundle\Traits\ControllerTrait; | ||
use Security; | ||
use Symfony\Component\HttpFoundation\RedirectResponse; | ||
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; | ||
use Symfony\Contracts\Translation\TranslatorInterface; | ||
|
||
/** | ||
* @author Julio Montoya <[email protected]> | ||
|
@@ -29,6 +35,7 @@ class AccountController extends BaseController | |
|
||
public function __construct( | ||
private readonly UserHelper $userHelper, | ||
private readonly TranslatorInterface $translator | ||
) {} | ||
|
||
#[Route('/edit', name: 'chamilo_core_account_edit', methods: ['GET', 'POST'])] | ||
|
@@ -69,4 +76,78 @@ public function edit(Request $request, UserRepository $userRepository, Illustrat | |
'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($this->translator->trans('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($this->translator->trans('Current password is incorrect.'))); | ||
} elseif ($newPassword !== $confirmPassword) { | ||
$form->get('confirmPassword')->addError(new FormError($this->translator->trans('Passwords do not match.'))); | ||
} else { | ||
$errors = $this->validatePassword($newPassword); | ||
if (count($errors) > 0) { | ||
foreach ($errors as $error) { | ||
$form->get('newPassword')->addError(new FormError($error)); | ||
} | ||
} else { | ||
$user->setPlainPassword($newPassword); | ||
$userRepository->updateUser($user); | ||
$this->addFlash('success', $this->translator->trans('Password changed successfully.')); | ||
return $this->redirectToRoute('chamilo_core_account_home'); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return $this->render('@ChamiloCore/Account/change_password.html.twig', [ | ||
'form' => $form->createView(), | ||
]); | ||
} | ||
|
||
/** | ||
* Validate the password against the same requirements as the client-side validation. | ||
*/ | ||
private function validatePassword(string $password): array | ||
{ | ||
$errors = []; | ||
$minRequirements = Security::getPasswordRequirements()['min']; | ||
|
||
if (strlen($password) < $minRequirements['length']) { | ||
$errors[] = $this->translator->trans('Password must be at least %length% characters long.', ['%length%' => $minRequirements['length']]); | ||
} | ||
if ($minRequirements['lowercase'] > 0 && !preg_match('/[a-z]/', $password)) { | ||
$errors[] = $this->translator->trans('Password must contain at least %count% lowercase characters.', ['%count%' => $minRequirements['lowercase']]); | ||
} | ||
if ($minRequirements['uppercase'] > 0 && !preg_match('/[A-Z]/', $password)) { | ||
$errors[] = $this->translator->trans('Password must contain at least %count% uppercase characters.', ['%count%' => $minRequirements['uppercase']]); | ||
} | ||
if ($minRequirements['numeric'] > 0 && !preg_match('/[0-9]/', $password)) { | ||
$errors[] = $this->translator->trans('Password must contain at least %count% numerical (0-9) characters.', ['%count%' => $minRequirements['numeric']]); | ||
} | ||
if ($minRequirements['specials'] > 0 && !preg_match('/[\W]/', $password)) { | ||
$errors[] = $this->translator->trans('Password must contain at least %count% special characters.', ['%count%' => $minRequirements['specials']]); | ||
} | ||
|
||
return $errors; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* For licensing terms, see /license.txt */ | ||
|
||
namespace Chamilo\CoreBundle\Form; | ||
|
||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\Extension\Core\Type\PasswordType; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class ChangePasswordType extends AbstractType | ||
Check failure on line 14 in src/CoreBundle/Form/ChangePasswordType.php GitHub Actions / PHP 8.1 Test on ubuntu-latestMissingTemplateParam
|
||
{ | ||
public function buildForm(FormBuilderInterface $builder, array $options): void | ||
{ | ||
$builder | ||
->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', | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
src/CoreBundle/Resources/views/Account/change_password.html.twig
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
{% extends "@ChamiloCore/Layout/layout_one_col.html.twig" %} | ||
|
||
{% block content %} | ||
<section id="change-password" class="py-8"> | ||
<div class="mx-auto w-full"> | ||
<h2 class="text-2xl font-semibold text-center mb-6">{{ "Change Password"|trans }}</h2> | ||
|
||
{{ form_start(form, {'attr': {'class': 'bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4'}}) }} | ||
|
||
{% for message in app.flashes('success') %} | ||
<div class="alert alert-success"> | ||
{{ message }} | ||
</div> | ||
{% endfor %} | ||
|
||
{% if form.vars.errors|length > 0 %} | ||
<div class="alert alert-danger" id="server-errors"> | ||
{{ form_errors(form) }} | ||
</div> | ||
{% endif %} | ||
|
||
<div class="mb-4 relative"> | ||
{{ 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', 'id': 'change_password_currentPassword'}}) }} | ||
<span class="toggle-password absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer" data-target="#change_password_currentPassword"> | ||
<i class="mdi mdi-eye-outline text-gray-700"></i> | ||
</span> | ||
{{ form_errors(form.currentPassword) }} | ||
</div> | ||
|
||
<div class="mb-4 relative"> | ||
{{ 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', 'id': 'change_password_newPassword'}}) }} | ||
<span class="toggle-password absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer" data-target="#change_password_newPassword"> | ||
<i class="mdi mdi-eye-outline text-gray-700"></i> | ||
</span> | ||
<ul id="password-requirements" class="text-sm text-red-500 mt-2" style="display: none;"></ul> | ||
<div id="new-password-errors"> | ||
{{ form_errors(form.newPassword) }} | ||
</div> | ||
</div> | ||
|
||
<div class="mb-4 relative"> | ||
{{ 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', 'id': 'change_password_confirmPassword'}}) }} | ||
<span class="toggle-password absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer" data-target="#change_password_confirmPassword"> | ||
<i class="mdi mdi-eye-outline text-gray-700"></i> | ||
</span> | ||
{{ form_errors(form.confirmPassword) }} | ||
</div> | ||
|
||
<div class="flex items-center justify-center"> | ||
<input type="hidden" name="_token" value="{{ csrf_token('change_password') }}"> | ||
<button type="submit" class="btn btn--primary mt-4">{{ "Change Password"|trans }}</button> | ||
</div> | ||
|
||
{{ form_end(form) }} | ||
</div> | ||
</section> | ||
|
||
{{ password_checker_js('#change_password_newPassword') }} | ||
|
||
<script> | ||
document.addEventListener('DOMContentLoaded', function() { | ||
const togglePasswordButtons = document.querySelectorAll('.toggle-password'); | ||
togglePasswordButtons.forEach(button => { | ||
button.addEventListener('click', function() { | ||
const input = document.querySelector(this.getAttribute('data-target')); | ||
if (input) { | ||
const type = input.getAttribute('type') === 'password' ? 'text' : 'password'; | ||
input.setAttribute('type', type); | ||
this.querySelector('i').classList.toggle('mdi-eye-outline'); | ||
this.querySelector('i').classList.toggle('mdi-eye-off-outline'); | ||
} | ||
}); | ||
}); | ||
const newPasswordInput = document.querySelector('#change_password_newPassword'); | ||
const newPasswordErrors = document.querySelector('#new-password-errors'); | ||
const serverErrors = document.querySelector('#server-errors'); | ||
newPasswordInput.addEventListener('input', function() { | ||
if (serverErrors) { | ||
serverErrors.style.display = 'none'; | ||
} | ||
if (newPasswordErrors) { | ||
newPasswordErrors.style.display = 'none'; | ||
} | ||
}); | ||
}); | ||
</script> | ||
{% endblock %} |
Oops, something went wrong.