diff --git a/project/assets/controllers/admin-users_controller.js b/project/assets/controllers/admin-users_controller.js new file mode 100644 index 0000000..3b4e201 --- /dev/null +++ b/project/assets/controllers/admin-users_controller.js @@ -0,0 +1,27 @@ +import { Controller } from '@hotwired/stimulus'; +import $ from 'jquery'; + +export default class extends Controller { + mainObj + deleteButtons + confirmModalObj + confirmModalUsernameObj + connect() { + this.mainObj = $(".page.admin.users"); + this.deleteButtons = this.mainObj.find(".confirmRemoveUser"); + this.confirmModalObj = this.mainObj.find("#removeConfirmModal"); + this.confirmModalUsernameObj = this.confirmModalObj.find(".insertUsername"); + this.bindOnClick() + } + + bindOnClick() { + this.deleteButtons.on("click", (e) => { + const obj = $(e.currentTarget), + username = $(obj).attr("data-username"), + formLink = $(obj).attr("data-formlink"); + + this.confirmModalUsernameObj.html(username); + this.confirmModalObj.find("form").attr("action", formLink); + }); + } +} \ No newline at end of file diff --git a/project/config/packages/security.yaml b/project/config/packages/security.yaml index 1d0cde8..7ea2a62 100644 --- a/project/config/packages/security.yaml +++ b/project/config/packages/security.yaml @@ -37,8 +37,8 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/, roles: ROLE_USER } - # - { path: ^/admin, roles: ROLE_ADMIN } when@test: security: diff --git a/project/migrations/Version20240729204423.php b/project/migrations/Version20240729204423.php new file mode 100644 index 0000000..07072c3 --- /dev/null +++ b/project/migrations/Version20240729204423.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user ADD activation_hash VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user DROP activation_hash'); + } +} diff --git a/project/migrations/Version20240730092145.php b/project/migrations/Version20240730092145.php new file mode 100644 index 0000000..c0ed455 --- /dev/null +++ b/project/migrations/Version20240730092145.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user ADD date_activation_hash DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user DROP date_activation_hash'); + } +} diff --git a/project/src/Controller/Admin/BaseAdminController.php b/project/src/Controller/Admin/BaseAdminController.php new file mode 100644 index 0000000..bcf5d3f --- /dev/null +++ b/project/src/Controller/Admin/BaseAdminController.php @@ -0,0 +1,15 @@ +render('admin/index.html.twig'); + } +} \ No newline at end of file diff --git a/project/src/Controller/Admin/UsersController.php b/project/src/Controller/Admin/UsersController.php new file mode 100644 index 0000000..7dafd81 --- /dev/null +++ b/project/src/Controller/Admin/UsersController.php @@ -0,0 +1,119 @@ +getRepository(User::class)->findAll(); + + return $this->render('admin/users/list.html.twig', [ + 'users' => $allUsers, + ]); + } + + #[Route('/admin/users/edit/{id}', name: 'app_admin_user_edit')] + public function editPermission(EntityManagerInterface $entityManager, User $user, Request $request): Response + { + if ($request->getMethod() === 'POST' && + $this->isCsrfTokenValid('admin-user-edit-permission', $request->get('token'))) { + foreach (UserPermission::cases() as $case) { + if ($case->value === "ROLE_ADMIN") { + continue; + } + $isPermission = false; + if ($request->get($case->value)) { + $isPermission = true; + } + + if ($isPermission) { + $user->addRoles($case->value); + } else { + $user->removeRoles($case->value); + } + } + $entityManager->persist($user); + $entityManager->flush(); + } + + return $this->render('admin/users/edit.html.twig', [ + 'user' => $user, + 'permissionAll' => UserPermission::casesGroup() + ]); + } + + #[Route('/admin/users/add', name: 'app_admin_user_add')] + public function addUser(Request $request, EntityManagerInterface $entityManager, UserPasswordHasherInterface $hasher) : Response + { + $user = new User(); + $form = $this->createFormBuilder($user) + ->add('username',TextType::class, ['label'=>'Nazwa użytkownika']) + ->add('submit',SubmitType::class) + ->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var User $newUser */ + $newUser = $form->getData(); + $hash = $hasher->hashPassword($newUser, HashGenerator::generateHash(32)); + $newUser->setPassword($hash); + $entityManager->persist($newUser); + $entityManager->flush(); + + return $this->redirectToRoute('app_admin_user_add_success',['id'=>$newUser->getId()]); + } + + return $this->render('admin/users/add.html.twig',[ + 'form' => $form->createView(), + 'plainPassword' => null + ]); + } + + #[Route('/admin/users/add/{id}', name: 'app_admin_user_add_success')] + public function addUserSuccess(User $user, EntityManagerInterface $entityManager) : Response + { + $newUser = false; + if (!$user->getActivationHash() || $user->getDateActivationHash() < new \DateTime()) { + $user->setActivationHash(HashGenerator::generateHash(64)); + $user->setDateActivationHash(new \DateTime("+ ".self::_ACTIVATION_HASH_EXPIRE." hours")); + $entityManager->persist($user); + $entityManager->flush(); + $newUser = true; + } + + return $this->render('admin/users/userAddSuccess.html.twig', [ + 'user' => $user, + 'activationLink' => $this->generateUrl('app_user_activate', ['id'=>$user->getId(),'hash'=>$user->getActivationHash()], UrlGeneratorInterface::ABSOLUTE_URL), + 'newUser' => $newUser + ]); + } + + #[Route('/admin/users/delete/{id}', name: 'app_admin_user_remove', methods: ['POST'])] + public function deleteUser(User $user, EntityManagerInterface $entityManager, Request $request) : Response + { + if ($this->isCsrfTokenValid('admin-remove-user',$request->get('token'))){ + $entityManager->remove($user); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_admin_users'); + } +} \ No newline at end of file diff --git a/project/src/Controller/User/SecurityController.php b/project/src/Controller/User/SecurityController.php index 9f1d742..3665570 100644 --- a/project/src/Controller/User/SecurityController.php +++ b/project/src/Controller/User/SecurityController.php @@ -2,12 +2,18 @@ namespace App\Controller\User; +use App\Entity\User; +use App\Helper\HashGenerator; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\Entity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use function Symfony\Component\String\u; class SecurityController extends AbstractController { @@ -35,4 +41,27 @@ class SecurityController extends AbstractController { throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); } + + #[Route(path: '/user/activate/{id}/{hash}', name: 'app_user_activate')] + public function activate(User $user, string $hash, EntityManagerInterface $entityManager, UserPasswordHasherInterface $hasher) : Response + { + $dateNow = new \DateTime(); + if ($user->getActivationHash() !== $hash || $user->getDateActivationHash() < $dateNow) { + return $this->render('security/passwordGenerate.html.twig', [ + 'user' => null, + ]); + } + $newPassword = HashGenerator::generateHash(12); + $user->setPassword($hasher->hashPassword($user, $newPassword)); + $user->setActivationHash(null); + $user->setDateActivationHash(null); + $entityManager->persist($user); + $entityManager->flush(); + + return $this->render('security/passwordGenerate.html.twig', [ + 'user' => $user, + 'newPassword' => $newPassword, + ]); +// return new Response("Twój login: `{$user->getUsername()}`. Twoje hasło: `{$newPassword}`"); + } } diff --git a/project/src/Entity/User.php b/project/src/Entity/User.php index 48bedd2..e981e72 100644 --- a/project/src/Entity/User.php +++ b/project/src/Entity/User.php @@ -2,7 +2,9 @@ namespace App\Entity; +use App\Enum\UserPermission; use App\Repository\UserRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -31,6 +33,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?string $password = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $activationHash = null; + + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTimeInterface $dateActivationHash = null; + public function getId(): ?int { return $this->id; @@ -82,6 +90,33 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + public function addRoles(string $role): void + { + if ($role === 'ROLE_USER') { + return; + } + $roles = $this->getRoles(); + $roles[] = $role; + + $this->roles = $roles; + } + + public function removeRoles(string $roleRemove): void + { + if ($roleRemove === 'ROLE_USER') { + return; + } + $roles = $this->getRoles(); + foreach ($roles as $key => $role) { + if ($role === $roleRemove) { + unset($roles[$key]); + break; + } + } + $roles = array_values($roles); + $this->setRoles($roles); + } + /** * @see PasswordAuthenticatedUserInterface */ @@ -105,4 +140,42 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } + + public function getEnumRoles(): array + { + $tmp=[]; + foreach ($this->getRoles() as $role) { + $tmp[]=UserPermission::from($role); + } + return $tmp; + } + + public function isGranted(UserPermission $permission): bool + { + return in_array($permission, $this->getEnumRoles(), true); + } + + public function getActivationHash(): ?string + { + return $this->activationHash; + } + + public function setActivationHash(?string $activationHash): static + { + $this->activationHash = $activationHash; + + return $this; + } + + public function getDateActivationHash(): ?\DateTimeInterface + { + return $this->dateActivationHash; + } + + public function setDateActivationHash(?\DateTimeInterface $dateActivationHash): static + { + $this->dateActivationHash = $dateActivationHash; + + return $this; + } } diff --git a/project/src/Enum/UserPermission.php b/project/src/Enum/UserPermission.php new file mode 100644 index 0000000..11d70cf --- /dev/null +++ b/project/src/Enum/UserPermission.php @@ -0,0 +1,47 @@ + 'Admin', + self::USER => 'Zalogowany', + self::ALL => 'Pełny dostęp', + self::CLIENT_ADD => 'Dodawanie klienta', + self::CLIENT_VIEW => 'Wyświetlanie klienta', + self::CLIENT_DELETE => 'Usuwanie klientów', + self::REPORT_VIEW => 'Wyświetlanie sprawozdań', + self::REPORT_EDIT => 'Edycja sprawozdań', + }; + } + + public static function casesGroup() : array + { + return [ + 'case' => UserPermission::ADMIN, + 'children' => [ + [ + 'case' => UserPermission::ALL, + 'children' => [ + UserPermission::CLIENT_ADD, + UserPermission::CLIENT_VIEW, + UserPermission::CLIENT_DELETE, + UserPermission::REPORT_VIEW, + UserPermission::REPORT_EDIT, + ] + ] + ] + ]; + } +} \ No newline at end of file diff --git a/project/src/Helper/HashGenerator.php b/project/src/Helper/HashGenerator.php new file mode 100644 index 0000000..0a21acb --- /dev/null +++ b/project/src/Helper/HashGenerator.php @@ -0,0 +1,11 @@ +security->isGranted('ROLE_ACCESS_ALL'); } -} -//HqmN6Egol5o_K5nY \ No newline at end of file +} \ No newline at end of file diff --git a/project/templates/admin/index.html.twig b/project/templates/admin/index.html.twig new file mode 100644 index 0000000..7129de3 --- /dev/null +++ b/project/templates/admin/index.html.twig @@ -0,0 +1,18 @@ +{% extends 'base.html.twig' %} +{% block title %}Admin{% endblock %} +{% block body %} +
| # | +Nazwa | +Uprawnienia | +Reset hasła | +Usuń | +|
|---|---|---|---|---|---|
| {{ loop.index }} | +{{ user.username }} | ++ {% for role in user.getEnumRoles %} + {{ role.takeLang }}, + {% endfor %} + | ++ Edytuj Uprawnienia + | ++ Reset hasła + | ++ + | +
+ Nazwa nowego użytkownika: {{ user.username }} +
++ Link do wygenerowania hasła: + +
+ +