diff --git a/composer.json b/composer.json
index 32c3524..495e411 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"type": "project",
"license": "proprietary",
"require": {
- "php": "^7.4|^8.0",
+ "php": "^8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.10",
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 5e80e77..893350e 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -12,7 +12,7 @@ doctrine:
mappings:
App:
is_bundle: false
- type: annotation
+ type: attribute
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
diff --git a/config/packages/reset_password.yaml b/config/packages/reset_password.yaml
index 796ff0c..037412d 100644
--- a/config/packages/reset_password.yaml
+++ b/config/packages/reset_password.yaml
@@ -1,2 +1,2 @@
symfonycasts_reset_password:
- request_password_repository: App\Repository\ResetPasswordRequestRepository
+ request_password_repository: App\Persistence\Repository\ResetPasswordRequestRepository
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index f58181d..f9c8db6 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -20,6 +20,9 @@ security:
form_login:
login_path: app_login
check_path: app_login
+ default_target_path: profile_show
+ post_only: true
+ use_referer: true
logout:
path: app_logout
# where to redirect after logout
@@ -42,5 +45,5 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- # - { path: ^/admin, roles: ROLE_ADMIN }
- # - { path: ^/profile, roles: ROLE_USER }
+ - { path: ^/admin, roles: ROLE_ADMIN }
+ - { path: ^/app, roles: ROLE_USER }
diff --git a/src/Attribute/FillDto.php b/src/Attribute/FillDto.php
new file mode 100644
index 0000000..1d69335
--- /dev/null
+++ b/src/Attribute/FillDto.php
@@ -0,0 +1,13 @@
+getAttributesOfType(
+ FillDto::class,
+ ArgumentMetadata::IS_INSTANCEOF
+ )[0] ?? null;
+
+ if (!$attribute) {
+ return [];
+ }
+
+ $fillDto = $argument->getAttributesOfType(FillDto::class, ArgumentMetadata::IS_INSTANCEOF)[0];
+
+ if ($argument->isVariadic()) {
+ throw new \LogicException(
+ sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())
+ );
+ }
+ $reflectionClass = new \ReflectionClass($argument->getType());
+ $dtoClass = $reflectionClass->newInstanceWithoutConstructor();
+
+ $queryParams = $request->query->all();
+ $requestParams = $request->request->all();
+ foreach (array_merge($queryParams, $requestParams) as $property => $value) {
+ $attribute = u($property)->camel();
+ if (property_exists($dtoClass, $attribute)) {
+ $reflectionProperty = $reflectionClass->getProperty($attribute);
+ $reflectionProperty->setValue($dtoClass, $value);
+ }
+ }
+ $files = $request->files->all();
+ foreach ($files as $fileKey => $file) {
+ $attribute = u($fileKey)->camel();
+ if (property_exists($dtoClass, $attribute)) {
+ $reflectionProperty = $reflectionClass->getProperty($attribute);
+ $reflectionProperty->setValue($dtoClass, $file);
+ }
+ }
+
+ return [$dtoClass];
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php
index f3d6546..e050737 100644
--- a/src/Controller/HomeController.php
+++ b/src/Controller/HomeController.php
@@ -5,14 +5,13 @@
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
-
+use Symfony\Component\Security\Http\Attribute\CurrentUser;
+use Symfony\Component\Security\Core\User\UserInterface;
class HomeController extends AbstractController
{
- /**
- * @Route("/home", name="home")
- * @IsGranted("IS_AUTHENTICATED_FULLY")
- */
- public function index()
+
+ #[Route("/", name: "home")]
+ public function index(#[CurrentUser] UserInterface $user)
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
diff --git a/src/Controller/SecurityController.php b/src/Controller/LoginController.php
similarity index 70%
rename from src/Controller/SecurityController.php
rename to src/Controller/LoginController.php
index 5d67316..8ee359c 100644
--- a/src/Controller/SecurityController.php
+++ b/src/Controller/LoginController.php
@@ -7,11 +7,9 @@
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
-class SecurityController extends AbstractController
+class LoginController extends AbstractController
{
- /**
- * @Route("/login", name="app_login")
- */
+ #[Route("/auth/login", name:"app_login")]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
@@ -25,12 +23,4 @@ public function login(AuthenticationUtils $authenticationUtils): Response
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
}
-
- /**
- * @Route("/logout", name="app_logout")
- */
- public function logout()
- {
- throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
- }
}
diff --git a/src/Controller/PasswordChangeController.php b/src/Controller/PasswordChangeController.php
index 4e25ad1..5e6a403 100644
--- a/src/Controller/PasswordChangeController.php
+++ b/src/Controller/PasswordChangeController.php
@@ -2,62 +2,51 @@
namespace App\Controller;
+use App\Service\Password\PasswordChangeRequest;
+use App\Service\Password\PasswordChangeService;
use Doctrine\ORM\EntityManagerInterface;
-use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
+use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
-use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
+use App\Attribute\FillDto;
+
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+use function Symfony\Component\Translation\t;
class PasswordChangeController extends AbstractController
{
- /**
- * @Route("/password/change", name="password_change",methods="GET|HEAD")
- * @IsGranted("IS_AUTHENTICATED_FULLY")
- */
- public function index()
- {
- return $this->render('password_change/index.html.twig');
+ public function __construct(
+ private PasswordChangeService $passwordChangeService,
+ private TranslatorInterface $translator
+ ) {
}
- /**
- * @Route("/password_save/save", name="password_save",methods="POST|PUT")
- * @param \Symfony\Component\HttpFoundation\Request $request
- * @param \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface $passwordEncoder
- * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
- *
- * @param \Doctrine\ORM\EntityManagerInterface $entityManager
- *
- * @return \Symfony\Component\HttpFoundation\Response|\Symfony\Component\HttpFoundation\RedirectResponse
- */
- public function change(Request $request, UserPasswordHasherInterface $passwordEncoder, ValidatorInterface $validator, EntityManagerInterface $entityManager)
- {
- $input = $request->request->all();
- $constraint = new Assert\Collection([
- 'old_password' => new UserPassword(["message" => "Wrong value for your current password"]),
- 'new_password' => new Assert\Length(['min' => 6]),
- 'confirm_new_password' => new Assert\EqualTo([
- 'value' => $request->get('new_password'),
- 'message' => 'Confirm password does not match.',
- ]),
- ]);
- $errors = $validator->validate($input, $constraint);
-
- if (count($errors) > 0) {
- return $this->render('password_change/index.html.twig', [
- 'errors' => $errors,
- ]);
+ #[Route("/app/password/change", name: "password_change", methods: ['GET', 'POST'])]
+ #[Template('password_change/index.html.twig')]
+ public function change(
+ #[FillDto] PasswordChangeRequest $passwordChangeRequest,
+ Request $request,
+ ValidatorInterface $validator
+ ) {
+ $errors = [];
+ if ($request->getMethod() === 'POST') {
+ $errors = $validator->validate($passwordChangeRequest);
+ if (count($errors) == 0) {
+ $this->passwordChangeService->execute($passwordChangeRequest);
+ $this->addFlash('message', $this->translator->trans('password.changed_successfully'));
+ return $this->redirectToRoute('home');
+ }
}
- $user = $this->getUser();
-
- $user->setPassword($passwordEncoder->hashPassword($user, $request->get('new_password')));
- $entityManager->persist($user);
- $entityManager->flush();
- $this->addFlash('message', 'Password changed successfully');
- return $this->redirectToRoute('home');
+ return ['errors' => $errors];
}
}
diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php
index b2774b8..e3292cd 100644
--- a/src/Controller/ProfileController.php
+++ b/src/Controller/ProfileController.php
@@ -2,8 +2,12 @@
namespace App\Controller;
+use App\Attribute\FillDto;
+use App\Service\User\UpdateProfileRequest;
+use App\Service\User\UpdateProfileService;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
+use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\Request;
@@ -12,101 +16,45 @@
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
+use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class ProfileController extends AbstractController
{
-
- /**
- * @Route("/profile", name="profile")
- * @param \Symfony\Component\Security\Core\User\UserInterface $user
- * @IsGranted("IS_AUTHENTICATED_FULLY")
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- */
- public function index(UserInterface $user)
+ public function __construct(private UpdateProfileService $profileService, private TranslatorInterface $translator)
{
- return $this->render('profile/index.html.twig', [
- 'user' => $user,
- ]);
}
- /**
- * @Route("/profile/update", name="profile_update")
- * @param \Symfony\Component\HttpFoundation\Request $request
- * @param \Doctrine\ORM\EntityManagerInterface $entityManager
- * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
- * @param \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface $csrfTokenManager
- *
- * @param \Symfony\Component\String\Slugger\SluggerInterface $slugger
- *
- * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
- * @IsGranted("IS_AUTHENTICATED_FULLY")
- *
- */
- public function store(Request $request, EntityManagerInterface $entityManager, ValidatorInterface $validator, CsrfTokenManagerInterface $csrfTokenManager, SluggerInterface $slugger)
- {
- $fileErrors = [];
- $token = new CsrfToken('authenticate', $request->get('_csrf_token'));
- if (!$csrfTokenManager->isTokenValid($token)) {
- throw new InvalidCsrfTokenException();
- }
-
- $user = $this->getUser();
- $user->setName($request->get('name'));
- $user->setUsername($request->get('username'));
- $user->setEmail($request->get('email'));
-
-
- if ($request->files->has('avatar')) {
- $avatarFile = $request->files->get('avatar');
-
- $constraint = new Assert\Collection([
- 'avatar' => new Assert\Image([
- 'maxSize' => 2048,
- ]),
- ]);
- $fileErrors = $validator->validate(['avatar' => $request->files->has('avatar')]);
- if (count($fileErrors) > 0) {
- return $this->render('profile/index.html.twig', [
- 'user' => $user,
- 'errors' => $fileErrors,
- ]);
+ #[Route("/app/profile", name: "profile_show", methods: ['GET', 'POST'])]
+ #[Template('profile/index.html.twig')]
+ public function update(
+ Request $request,
+ ValidatorInterface $validator,
+ CsrfTokenManagerInterface $csrfTokenManager,
+ #[CurrentUser] UserInterface $user,
+ #[FillDto] UpdateProfileRequest $updateProfileRequest,
+ ) {
+ $errors = [];
+ if ($request->getMethod() === 'POST') {
+ $fileErrors = [];
+ $token = new CsrfToken('authenticate', $request->get('_csrf_token'));
+ if (!$csrfTokenManager->isTokenValid($token)) {
+ throw new InvalidCsrfTokenException();
}
- $originalFilename = pathinfo($avatarFile->getClientOriginalName(), PATHINFO_FILENAME);
- // this is needed to safely include the file name as part of the URL
- $safeFilename = $slugger->slug($originalFilename);
- $newFilename = $safeFilename . '-' . uniqid() . '.' . $avatarFile->guessExtension();
- // Move the file to the directory where avatar are stored
- try {
- $avatarFile->move(
- 'images',
- $newFilename
- );
- $user->setAvatar('/images/' . $newFilename);
- } catch (FileException $e) {
- $this->addFlash('message', $e->getMessage());
- return $this->redirectToRoute('profile');
+ $errors = $validator->validate($updateProfileRequest);
+ if (count($errors) === 0) {
+ $this->profileService->execute($updateProfileRequest);
+ $this->addFlash('message', $this->translator->trans('user.profile_updated'));
}
}
- $errors = $validator->validate($user);
- if (count($errors) > 0) {
-
- return $this->render('profile/index.html.twig', [
- 'user' => $user,
- 'errors' => $errors,
- ]);
- }
-
- $entityManager->persist($user);
- $entityManager->flush();
-
- return $this->redirectToRoute('home');
-
+ return [
+ 'errors' => $errors,
+ 'user' => $user
+ ];
}
}
diff --git a/src/Controller/RegisterController.php b/src/Controller/RegisterController.php
index 24f6ce6..bcc8160 100644
--- a/src/Controller/RegisterController.php
+++ b/src/Controller/RegisterController.php
@@ -2,64 +2,45 @@
namespace App\Controller;
+use App\Attribute\FillDto;
use App\Entity\User;
use App\Events\UserRegisteredEvent;
-use Doctrine\ORM\EntityManagerInterface;
+use App\Service\User\CreateUserRequest;
+use App\Service\User\CreateUserService;
+use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
-use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class RegisterController extends AbstractController
{
- /**
- * @Route("/register", name="register_form",methods="GET|HEAD")
- */
- public function index()
+
+ public function __construct(private CreateUserService $userService, private TranslatorInterface $translator)
{
- return $this->render('register/index.html.twig', [
- 'errors' => [],
- 'user' => new User(),
- ]);
}
- /**
- * @Route("/register/store", name="register",methods="POST")
- * @param \Symfony\Component\HttpFoundation\Request $request
- * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
- * @param \Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface $passwordEncoder
- *
- * @param \Doctrine\ORM\EntityManagerInterface $entityManager
- *
- * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
- *
- * @return string
- */
- public function store(Request $request, ValidatorInterface $validator, UserPasswordHasherInterface $passwordEncoder, EntityManagerInterface $entityManager, EventDispatcherInterface $dispatcher)
- {
+ #[Route("/auth/register", name: "register", methods: ["POST", "GET"])]
+ #[Template('register/index.html.twig')]
+ public function store(
+ #[FillDto] CreateUserRequest $createUserRequest,
+ Request $request,
+ ValidatorInterface $validator,
+ ) {
$user = new User();
- $user->setName($request->get('name'));
- $user->setEmail($request->get('email'));
- $user->setUsername($request->get('username'));
- $errors = $validator->validate($user);
- if (count($errors) > 0) {
- return $this->render('register/index.html.twig',
- [
- 'errors' => $errors,
- 'user' => $user,
- ]
- );
+ if ($request->getMethod() === 'POST') {
+ $errors = $validator->validate($createUserRequest);
+ if (count($errors) === 0) {
+ $user = $this->userService->execute($createUserRequest);
+ $this->addFlash('message', $this->translator->trans('user.registration_successful'));
+ return $this->redirectToRoute('app_login');
+ }
}
-
- $user->setPassword($passwordEncoder->hashPassword($user, $request->get('password')));
- $entityManager->persist($user);
- $entityManager->flush();
-
- $dispatcher->dispatch(new UserRegisteredEvent($user), UserRegisteredEvent::NAME);
-
- return $this->redirectToRoute('app_login');
+ return [
+ 'errors' => $errors ?? [],
+ 'user' => $user
+ ];
}
}
diff --git a/src/Controller/ResetPasswordController.php b/src/Controller/ResetPasswordController.php
index 6b838fa..a99ef26 100644
--- a/src/Controller/ResetPasswordController.php
+++ b/src/Controller/ResetPasswordController.php
@@ -5,6 +5,8 @@
use App\Entity\User;
use App\Form\ChangePasswordFormType;
use App\Form\ResetPasswordRequestFormType;
+use App\Persistence\Repository\UserRepository;
+use App\Service\Password\SendResetPasswordEmailService;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -14,10 +16,13 @@
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
+use function Symfony\Component\Translation\t;
+
/**
* @Route("/reset-password")
*/
@@ -25,28 +30,27 @@ class ResetPasswordController extends AbstractController
{
use ResetPasswordControllerTrait;
- private $resetPasswordHelper;
- public function __construct(ResetPasswordHelperInterface $resetPasswordHelper)
- {
- $this->resetPasswordHelper = $resetPasswordHelper;
+ public function __construct(
+ private SendResetPasswordEmailService $sendResetPasswordEmailService,
+ private ResetPasswordHelperInterface $resetPasswordHelper,
+ private UserRepository $userRepository,
+ private TranslatorInterface $translator
+ ) {
}
- /**
- * Display & process form to request a password reset.
- *
- * @Route("", name="app_forgot_password_request")
- */
- public function request(Request $request, MailerInterface $mailer): Response
+ #[Route("/auth/password/forget", name: "app_forgot_password_request")]
+ public function request(Request $request): Response
{
$form = $this->createForm(ResetPasswordRequestFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
- return $this->processSendingPasswordResetEmail(
- $form->get('email')->getData(),
- $mailer
- );
+ $token = $this->sendResetPasswordEmailService->execute($form->get('email')->getData());
+ if ($token) {
+ $this->setTokenObjectInSession($token);
+ }
+ return $this->redirectToRoute('app_check_email');
}
return $this->render('reset_password/request.html.twig', [
@@ -54,15 +58,11 @@ public function request(Request $request, MailerInterface $mailer): Response
]);
}
- /**
- * Confirmation page after a user has requested a password reset.
- *
- * @Route("/check-email", name="app_check_email")
- */
+ #[Route("/auth/check-email", name: "app_check_email")]
public function checkEmail(): Response
{
// We prevent users from directly accessing this page
- if (!$this->canCheckEmail()) {
+ if (!$this->getTokenObjectFromSession()) {
return $this->redirectToRoute('app_forgot_password_request');
}
@@ -71,13 +71,12 @@ public function checkEmail(): Response
]);
}
- /**
- * Validates and process the reset URL that the user clicked in their email.
- *
- * @Route("/reset/{token}", name="app_reset_password")
- */
- public function reset(Request $request, UserPasswordHasherInterface $passwordEncoder, string $token = null): Response
- {
+ #[Route("/auth/reset/{token}", name: "app_reset_password")]
+ public function reset(
+ Request $request,
+ UserPasswordHasherInterface $passwordEncoder,
+ string $token = null
+ ): Response {
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
@@ -88,16 +87,16 @@ public function reset(Request $request, UserPasswordHasherInterface $passwordEnc
$token = $this->getTokenFromSession();
if (null === $token) {
- throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
+ throw $this->createNotFoundException($this->translator->trans('password.no_token_found'));
}
try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
- $this->addFlash('reset_password_error', sprintf(
- 'There was a problem validating your reset request - %s',
- $e->getReason()
- ));
+ $this->addFlash(
+ 'reset_password_error',
+ $this->translator->trans('password.reset_password_error', ['reason' => $e->getReason()])
+ );
return $this->redirectToRoute('app_forgot_password_request');
}
@@ -117,61 +116,16 @@ public function reset(Request $request, UserPasswordHasherInterface $passwordEnc
);
$user->setPassword($encodedPassword);
- $this->getDoctrine()->getManager()->flush();
+ $this->userRepository->upgradePassword($user, $encodedPassword);
// The session is cleaned up after the password has been changed.
$this->cleanSessionAfterReset();
- return $this->redirectToRoute('home');
+ return $this->redirectToRoute('app_login');
}
return $this->render('reset_password/reset.html.twig', [
'resetForm' => $form->createView(),
]);
}
-
- private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse
- {
- $user = $this->getDoctrine()->getRepository(User::class)->findOneBy([
- 'email' => $emailFormData,
- ]);
-
- // Marks that you are allowed to see the app_check_email page.
- $this->setCanCheckEmailInSession();
-
- // Do not reveal whether a user account was found or not.
- if (!$user) {
- return $this->redirectToRoute('app_check_email');
- }
-
- try {
- $resetToken = $this->resetPasswordHelper->generateResetToken($user);
- } catch (ResetPasswordExceptionInterface $e) {
- // If you want to tell the user why a reset email was not sent, uncomment
- // the lines below and change the redirect to 'app_forgot_password_request'.
- // Caution: This may reveal if a user is registered or not.
- //
- // $this->addFlash('reset_password_error', sprintf(
- // 'There was a problem handling your password reset request - %s',
- // $e->getReason()
- // ));
-
- return $this->redirectToRoute('app_check_email');
- }
-
- $email = (new TemplatedEmail())
- ->from(new Address('no-reply@tuhinbepari.com', 'Symfony Auth App'))
- ->to($user->getEmail())
- ->subject('Your password reset request')
- ->htmlTemplate('reset_password/email.html.twig')
- ->context([
- 'resetToken' => $resetToken,
- 'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(),
- ])
- ;
-
- $mailer->send($email);
-
- return $this->redirectToRoute('app_check_email');
- }
}
diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
index d00b730..dc3b60d 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -2,23 +2,14 @@
namespace App\Controller;
-use App\Repository\UserRepository;
-use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
+use App\Persistence\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
- /**
- * @Route("/user", name="user")
- * @IsGranted("ROLE_ADMIN")
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * @param \App\Repository\UserRepository $userRepository
- *
- * @return \Symfony\Component\HttpFoundation\Response
- */
+ #[Route("/user", name:"user")]
public function index(Request $request, UserRepository $userRepository)
{
return $this->render('user/index.html.twig', [
diff --git a/src/Entity/ResetPasswordRequest.php b/src/Entity/ResetPasswordRequest.php
index fdafca5..8c25c17 100644
--- a/src/Entity/ResetPasswordRequest.php
+++ b/src/Entity/ResetPasswordRequest.php
@@ -2,32 +2,27 @@
namespace App\Entity;
-use App\Repository\ResetPasswordRequestRepository;
+use App\Persistence\Repository\ResetPasswordRequestRepository;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
-/**
- * @ORM\Entity(repositoryClass=ResetPasswordRequestRepository::class)
- */
+#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
class ResetPasswordRequest implements ResetPasswordRequestInterface
{
use ResetPasswordRequestTrait;
- /**
- * @ORM\Id()
- * @ORM\GeneratedValue()
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id()]
+ #[ORM\GeneratedValue()]
+ #[ORM\Column(type:"integer")]
private $id;
- /**
- * @ORM\ManyToOne(targetEntity=User::class)
- * @ORM\JoinColumn(nullable=false)
- */
+ #[ORM\ManyToOne(targetEntity:User::class)]
+ #[ORM\JoinColumn(nullable:false)]
private $user;
- public function __construct(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
+ public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
{
$this->user = $user;
$this->initialize($expiresAt, $selector, $hashedToken);
@@ -38,7 +33,7 @@ public function getId(): ?int
return $this->id;
}
- public function getUser(): object
+ public function getUser(): User
{
return $this->user;
}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 72e5dba..fa05578 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -2,60 +2,42 @@
namespace App\Entity;
-use App\Repository\UserRepository;
+use App\Persistence\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
-/**
- * @ORM\Entity(repositoryClass=UserRepository::class)
- * @UniqueEntity("username")
- * @UniqueEntity("email")
- */
+#[ORM\Entity(repositoryClass: UserRepository::class)]
+#[UniqueEntity("username")]
+#[UniqueEntity("email")]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
- /**
- * @ORM\Id()
- * @ORM\GeneratedValue()
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id()]
+ #[ORM\GeneratedValue()]
+ #[ORM\Column(type: "integer")]
private $id;
- /**
- * @ORM\Column(type="string", length=191)
- * @Assert\NotBlank
- */
+ #[ORM\Column(type:"string", length:191)]
private $name;
- /**
- * @ORM\Column(type="string", length=180, unique=true)
- * @Assert\NotBlank
- */
+ #[ORM\Column(type:"string", length:180, unique:true)]
+ #[Assert\NotBlank]
private $username;
- /**
- * @ORM\Column(type="string", length=191,unique=true)
- * @Assert\Email
- * @Assert\NotBlank
- */
+ #[ORM\Column(type:"string", length:191,unique:true)]
+ #[Assert\Email]
+ #[Assert\NotBlank]
private $email;
- /**
- * @ORM\Column(type="string", length=255, nullable=true)
- */
+ #[ORM\Column(type:"string", length:255, nullable:true)]
private $avatar;
- /**
- * @ORM\Column(type="json")
- */
+ #[ORM\Column(type:"json")]
private $roles = [];
- /**
- * @var string The hashed password
- * @ORM\Column(type="string")
- */
+ #[ORM\Column(type:"string")]
private $password;
public function getId(): ?int
diff --git a/src/Enum/UserRole.php b/src/Enum/UserRole.php
new file mode 100644
index 0000000..d9b8083
--- /dev/null
+++ b/src/Enum/UserRole.php
@@ -0,0 +1,9 @@
+_em->flush();
}
+ public function save(User $user): User
+ {
+ $this->_em->persist($user);
+ $this->_em->flush();
+
+ return $user;
+ }
+
+ public function validate(string $field, mixed $value, Request $request, ?User $user): int
+ {
+ $builder = $this->createQueryBuilder('u')
+ ->select('count(u.id) as total')
+ ->andWhere("u.$field = :val")
+ ->setParameter('val', $value);
+
+ if ($user) {
+ $builder->andWhere('u.id !=:id')
+ ->setParameter('id', $user->getId());
+ }
+
+ return $builder->getQuery()->getSingleScalarResult();
+ }
/*
diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Service/ImageUploadService.php b/src/Service/ImageUploadService.php
new file mode 100644
index 0000000..361cb6d
--- /dev/null
+++ b/src/Service/ImageUploadService.php
@@ -0,0 +1,29 @@
+getClientOriginalName(), PATHINFO_FILENAME);
+ // this is needed to safely include the file name as part of the URL
+ $safeFilename = $this->slugger->slug($originalFilename);
+ $newFilename = $safeFilename . '-' . uniqid('user_', true) . '.' . $file->guessExtension();
+
+ // Move the file to the directory where avatar are stored
+ $file->move(
+ 'images',
+ $newFilename
+ );
+
+ return '/images/' . $newFilename;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Password/PasswordChangeRequest.php b/src/Service/Password/PasswordChangeRequest.php
new file mode 100644
index 0000000..73849ed
--- /dev/null
+++ b/src/Service/Password/PasswordChangeRequest.php
@@ -0,0 +1,20 @@
+ 6])]
+ #[Assert\NotCompromisedPassword]
+ #[Assert\PasswordStrength(minScore: Assert\PasswordStrength::STRENGTH_MEDIUM)]
+ public string $newPassword,
+ public string $confirmNewPassword
+ ) {
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Password/PasswordChangeService.php b/src/Service/Password/PasswordChangeService.php
new file mode 100644
index 0000000..9cd6787
--- /dev/null
+++ b/src/Service/Password/PasswordChangeService.php
@@ -0,0 +1,24 @@
+security->getUser();
+ $hash = $this->passwordEncoder->hashPassword($user, $request->newPassword);
+ $this->userRepository->upgradePassword($user, $hash);
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Password/ResetUserPasswordService.php b/src/Service/Password/ResetUserPasswordService.php
new file mode 100644
index 0000000..3c3f59b
--- /dev/null
+++ b/src/Service/Password/ResetUserPasswordService.php
@@ -0,0 +1,14 @@
+userRepository->findOneBy(['email' => $email,]);
+
+ // Marks that you are allowed to see the app_check_email page.
+
+ // Do not reveal whether a user account was found or not.
+ if (!$user) {
+ return false;
+
+ }
+
+ $resetToken = $this->resetPasswordHelper->generateResetToken($user);
+ $email = (new TemplatedEmail())
+ ->from(new Address('no-reply@tuhinbepari.com', 'Symfony Auth App'))
+ ->to($user->getEmail())
+ ->subject('Your password reset request')
+ ->htmlTemplate('reset_password/email.html.twig')
+ ->context([
+ 'resetToken' => $resetToken,
+ 'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(),
+ ]);
+
+ $this->mailer->send($email);
+
+ return $resetToken;
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Service/User/CreateUserRequest.php b/src/Service/User/CreateUserRequest.php
new file mode 100644
index 0000000..284b452
--- /dev/null
+++ b/src/Service/User/CreateUserRequest.php
@@ -0,0 +1,31 @@
+ 6])]
+ #[Assert\NotCompromisedPassword]
+ #[Assert\PasswordStrength(minScore: Assert\PasswordStrength::STRENGTH_MEDIUM)]
+ public string $password,
+ ) {
+ }
+}
\ No newline at end of file
diff --git a/src/Service/User/CreateUserService.php b/src/Service/User/CreateUserService.php
new file mode 100644
index 0000000..c0e48c0
--- /dev/null
+++ b/src/Service/User/CreateUserService.php
@@ -0,0 +1,35 @@
+setName($request->name);
+ $user->setUsername($request->username);
+ $user->setEmail($request->email);
+ $user->setPassword($this->passwordEncoder->hashPassword($user, $request->password));
+ $user->setRoles([UserRole::USER]);
+ $this->userRepository->save($user);
+
+ $this->dispatcher->dispatch(new UserRegisteredEvent($user), UserRegisteredEvent::NAME);
+
+ return $user;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/User/UpdateProfileRequest.php b/src/Service/User/UpdateProfileRequest.php
new file mode 100644
index 0000000..e26d32e
--- /dev/null
+++ b/src/Service/User/UpdateProfileRequest.php
@@ -0,0 +1,29 @@
+security->getUser();
+ $user->setName($request->name);
+ $user->setUsername($request->username);
+ $user->setEmail($request->email);
+ if($request->avatar){
+ $user->setAvatar($this->imageUploadService->upload($request->avatar));
+ }
+
+ return $this->userRepository->save($user);
+ }
+}
\ No newline at end of file
diff --git a/src/Validator/UniqueValue.php b/src/Validator/UniqueValue.php
new file mode 100644
index 0000000..606a20b
--- /dev/null
+++ b/src/Validator/UniqueValue.php
@@ -0,0 +1,23 @@
+em->getRepository($constraint->entity);
+ if ($method = $constraint->repositoryMethod) {
+ $user = $constraint->currentUser===true ? $this->security->getUser() : null;
+ $result = $repository->{$method}(
+ $constraint->field,
+ $value,
+ $this->requestStack->getCurrentRequest(),
+ $user
+ );
+ } else {
+ $result = $repository->count([$constraint->field => $value]);
+ }
+ if ($result === 0) {
+ return;
+ }
+ // the argument must be a string or an object implementing __toString()
+ $this->context->buildViolation($constraint->message)
+ ->setParameter('{{ string }}', $value)
+ ->addViolation();
+ // access your configuration options like this:
+ }
+}
\ No newline at end of file
diff --git a/symfony.lock b/symfony.lock
new file mode 100644
index 0000000..56758e2
--- /dev/null
+++ b/symfony.lock
@@ -0,0 +1,299 @@
+{
+ "dama/doctrine-test-bundle": {
+ "version": "6.7",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "main",
+ "version": "4.0",
+ "ref": "2c920f73a217f30bd4a37833c91071f4d3dc1ecd"
+ },
+ "files": [
+ "config/packages/test/dama_doctrine_test_bundle.yaml"
+ ]
+ },
+ "doctrine/annotations": {
+ "version": "1.14",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.10",
+ "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
+ }
+ },
+ "doctrine/doctrine-bundle": {
+ "version": "2.10",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.10",
+ "ref": "e025a6cb69b195970543820b2f18ad21724473fa"
+ },
+ "files": [
+ "config/packages/doctrine.yaml",
+ "src/Entity/.gitignore",
+ "src/Repository/.gitignore"
+ ]
+ },
+ "doctrine/doctrine-fixtures-bundle": {
+ "version": "3.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
+ },
+ "files": [
+ "src/DataFixtures/AppFixtures.php"
+ ]
+ },
+ "doctrine/doctrine-migrations-bundle": {
+ "version": "3.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.1",
+ "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
+ },
+ "files": [
+ "config/packages/doctrine_migrations.yaml",
+ "migrations/.gitignore"
+ ]
+ },
+ "sensio/framework-extra-bundle": {
+ "version": "6.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.2",
+ "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
+ },
+ "files": [
+ "config/packages/sensio_framework_extra.yaml"
+ ]
+ },
+ "symfony/console": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
+ },
+ "files": [
+ "bin/console"
+ ]
+ },
+ "symfony/debug-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
+ },
+ "files": [
+ "config/packages/debug.yaml"
+ ]
+ },
+ "symfony/flex": {
+ "version": "1.20",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
+ },
+ "files": [
+ ".env"
+ ]
+ },
+ "symfony/framework-bundle": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.2",
+ "ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3"
+ },
+ "files": [
+ "config/packages/cache.yaml",
+ "config/packages/framework.yaml",
+ "config/preload.php",
+ "config/routes/framework.yaml",
+ "config/services.yaml",
+ "public/index.php",
+ "src/Controller/.gitignore",
+ "src/Kernel.php"
+ ]
+ },
+ "symfony/mailer": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "4.3",
+ "ref": "2bf89438209656b85b9a49238c4467bff1b1f939"
+ },
+ "files": [
+ "config/packages/mailer.yaml"
+ ]
+ },
+ "symfony/maker-bundle": {
+ "version": "1.51",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
+ }
+ },
+ "symfony/monolog-bundle": {
+ "version": "3.8",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.7",
+ "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578"
+ },
+ "files": [
+ "config/packages/monolog.yaml"
+ ]
+ },
+ "symfony/notifier": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.0",
+ "ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
+ },
+ "files": [
+ "config/packages/notifier.yaml"
+ ]
+ },
+ "symfony/phpunit-bridge": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "6df817da0ca2b8cddbb6e690051d7f2b45530d06"
+ },
+ "files": [
+ ".env.test",
+ "bin/phpunit",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
+ "symfony/routing": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.2",
+ "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6"
+ },
+ "files": [
+ "config/packages/routing.yaml",
+ "config/routes.yaml"
+ ]
+ },
+ "symfony/security-bundle": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.0",
+ "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48"
+ },
+ "files": [
+ "config/packages/security.yaml"
+ ]
+ },
+ "symfony/translation": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.3",
+ "ref": "64fe617084223633e1dedf9112935d8c95410d3e"
+ },
+ "files": [
+ "config/packages/translation.yaml",
+ "translations/.gitignore"
+ ]
+ },
+ "symfony/twig-bundle": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.3",
+ "ref": "b7772eb20e92f3fb4d4fe756e7505b4ba2ca1a2c"
+ },
+ "files": [
+ "config/packages/twig.yaml",
+ "templates/base.html.twig"
+ ]
+ },
+ "symfony/validator": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c"
+ },
+ "files": [
+ "config/packages/validator.yaml"
+ ]
+ },
+ "symfony/web-profiler-bundle": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.1",
+ "ref": "e42b3f0177df239add25373083a564e5ead4e13a"
+ },
+ "files": [
+ "config/packages/web_profiler.yaml",
+ "config/routes/web_profiler.yaml"
+ ]
+ },
+ "symfony/webpack-encore-bundle": {
+ "version": "1.17",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.10",
+ "ref": "eff2e505d4557c967b6710fe06bd947ba555cae5"
+ },
+ "files": [
+ "assets/app.js",
+ "assets/bootstrap.js",
+ "assets/controllers.json",
+ "assets/controllers/hello_controller.js",
+ "assets/styles/app.css",
+ "config/packages/webpack_encore.yaml",
+ "package.json",
+ "webpack.config.js"
+ ]
+ },
+ "symfonycasts/reset-password-bundle": {
+ "version": "1.18",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "97c1627c0384534997ae1047b93be517ca16de43"
+ },
+ "files": [
+ "config/packages/reset_password.yaml"
+ ]
+ },
+ "twig/extra-bundle": {
+ "version": "v3.7.1"
+ }
+}
diff --git a/templates/app.html.twig b/templates/app.html.twig
index 91f2f71..60dcffb 100644
--- a/templates/app.html.twig
+++ b/templates/app.html.twig
@@ -25,9 +25,9 @@
@@ -48,7 +48,7 @@
{% if app.request.hasPreviousSession %}
{% for message in app.flashes('message') %}
-
{{ message }}
+
{{ message }}
{% endfor %}
{% endif %}
diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig
index f80445f..65fc435 100644
--- a/templates/home/index.html.twig
+++ b/templates/home/index.html.twig
@@ -1,10 +1,11 @@
-{% extends 'app.html.twig' %}
+{% extends 'base.html.twig' %}
{% block title %}Dashboard{% endblock %}
{% block body %}
-
Your Dashboard
-
+
Your Front Page
+
Login
+
Register
{% endblock %}
diff --git a/templates/password_change/index.html.twig b/templates/password_change/index.html.twig
index 4eae4bd..a23d1b8 100644
--- a/templates/password_change/index.html.twig
+++ b/templates/password_change/index.html.twig
@@ -5,7 +5,7 @@
{% endblock %}
{% block body %}
-