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 %} -
+
+
diff --git a/templates/register/index.html.twig b/templates/register/index.html.twig index def422d..d81fb4a 100644 --- a/templates/register/index.html.twig +++ b/templates/register/index.html.twig @@ -6,7 +6,7 @@

Register

- + diff --git a/templates/reset_password/check_email.html.twig b/templates/reset_password/check_email.html.twig index e0d0b9d..455391b 100644 --- a/templates/reset_password/check_email.html.twig +++ b/templates/reset_password/check_email.html.twig @@ -4,8 +4,9 @@ {% block body %}
-

An email has been sent that contains a link that you can click to reset your password. - This link will expire in {{ tokenLifetime|date('g') }} hour(s).

+

+ {{ 'password.email_sent'|trans({hour: tokenLifetime|date('g')}) }} +

If you don't receive an email please check your spam folder or try again.

diff --git a/templates/reset_password/request.html.twig b/templates/reset_password/request.html.twig index 0f1e9ca..217759a 100644 --- a/templates/reset_password/request.html.twig +++ b/templates/reset_password/request.html.twig @@ -1,23 +1,22 @@ {% extends 'base.html.twig' %} -{% block title %}Reset your password{% endblock %} +{% block title %}{{ 'Reset your password'|trans }}{% endblock %} {% block body %}
{% for flashError in app.flashes('reset_password_error') %} {% endfor %} -

Reset your password

+

{{'Reset your password'|trans}}

{{ form_start(requestForm) }} {{ form_row(requestForm.email) }}

- Enter your email address and we we will send you a - link to reset your password. + {{ 'password.enter_email'|trans }}

- + {{ form_end(requestForm) }}
diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 315340d..14dd7dc 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -11,7 +11,7 @@ {% if app.user %}
- You are logged in as {{ app.user.username }}, Logout + You are logged in as {{ app.user.username }}, Logout
{% endif %} @@ -47,7 +47,7 @@

Does not have an account? Register + href="{{ path('register') }}">Register Now

Forget your password? Click diff --git a/translations/messages+intl-icu.bn.yaml b/translations/messages+intl-icu.bn.yaml new file mode 100644 index 0000000..4338668 --- /dev/null +++ b/translations/messages+intl-icu.bn.yaml @@ -0,0 +1,21 @@ +password: + change: আপনার পাসওয়ার্ড পরিবর্তন করুন + old: পুরানো পাসওয়ার্ড + current: বর্তমান পাসওয়ার্ড + new: নতুন পাসওয়ার্ড + type_new: আপনার নতুন পাসওয়ার্ড টাইপ করুন + confirm: আপনার নতুন পাসওয়ার্ড নিশ্চিত করুন + length: পাসওয়ার্ড অবশ্যই ৬ সংখ্যার লম্বা হতে হবে + changed_successfully: পাসওয়ার্ড সফলভাবে পরিবর্তিত হয়েছে + enter_email: আপনার ইমেল ঠিকানা লিখুন এবং আমরা আপনাকে আপনার পাসওয়ার্ড রিসেট করার জন্য একটি লিঙ্ক পাঠাব। + email_sent: একটি ইমেল পাঠানো হয়েছে যাতে একটি লিঙ্ক রয়েছে যা আপনি আপনার পাসওয়ার্ড রিসেট করতে ক্লিক করতে পারেন৷ এই লিঙ্কটির মেয়াদ {hour} ঘণ্টার মধ্যে শেষ হবে + send_email_btn: পাসওয়ার্ড রিসেট ইমেল পাঠান + reset_password_error: আপনার রিসেট অনুরোধ যাচাই করতে একটি সমস্যা হয়েছে - {reason} + no_token_found: ইউআরএলে বা সেশনে কোনো রিসেট পাসওয়ার্ড টোকেন পাওয়া যায়নি। + +user: + profile_updated: প্রোফাইল সফলভাবে আপডেট হয়েছে + registration_successful: রেজিস্ট্রেশন সফলভাবে সম্পন্ন হয়েছে + +'Reset your password': আপনার পাসওয়ার্ড পুনরায় সেট করুন +'Change Password': পাসওয়ার্ড পরিবর্তন করুন diff --git a/translations/messages+intl-icu.en.yaml b/translations/messages+intl-icu.en.yaml new file mode 100644 index 0000000..d72cc5f --- /dev/null +++ b/translations/messages+intl-icu.en.yaml @@ -0,0 +1,19 @@ +password: + change: Change Your Password + old: Old Password + current: Current Password + new: New Password + type_new: Type your new password + confirm: Confirm your new password + length: Password must be 6 digit long + changed_successfully: Password changed successfully + enter_email: Enter your email address and we we will send you a link to reset your password. + email_sent: An email has been sent that contains a link that you can click to reset your password. This link will expire in {hour} hour(s) + send_email_btn: Send password reset email + reset_password_error: There was a problem validating your reset request - {reason} + no_token_found: No reset password token found in the URL or in the session. + +user: + profile_updated: Profile successfully updated + registration_successful: Registration completed successfully + diff --git a/translations/messages.bn.yaml b/translations/messages.bn.yaml deleted file mode 100644 index 88c2c65..0000000 --- a/translations/messages.bn.yaml +++ /dev/null @@ -1,2 +0,0 @@ -password: - change: আপনার পাসওয়ার্ড পরিবর্তন করুন \ No newline at end of file diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml deleted file mode 100644 index f2c4364..0000000 --- a/translations/messages.en.yaml +++ /dev/null @@ -1,8 +0,0 @@ -password: - change: Change Your Password - old: Old Password - currrent: Current Password - new: New Password - type_new: Type your new password - confirm: Confirm your new password - length: Password must be 6 digit long