2 Commits
Author SHA1 Message Date
Daniel-Garmig 84d9c81afd User management - Login and user password hash. 2024-04-26 21:37:39 +02:00
Daniel-Garmig 30773d7af4 Admin Panel - User management 2024-04-26 19:52:35 +02:00
9 changed files with 275 additions and 2 deletions
+51
View File
@@ -0,0 +1,51 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/app" : "/src"
}
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}
]
}
+9 -1
View File
@@ -16,6 +16,14 @@ security:
main: main:
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # https://symfony.com/doc/current/security.html#the-firewall
@@ -26,7 +34,7 @@ security:
# Easy way to control access for large sections of your site # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } # - { path: ^/profile, roles: ROLE_USER }
when@test: when@test:
+2
View File
@@ -3,3 +3,5 @@
; The `client_host` below may optionally be replaced with `discover_client_host=yes` ; The `client_host` below may optionally be replaced with `discover_client_host=yes`
; Add `start_with_request=yes` to start debug session on each request ; Add `start_with_request=yes` to start debug session on each request
xdebug.client_host = host.docker.internal xdebug.client_host = host.docker.internal
xdebug.start_with_request=yes
xdebug.mode=debug
@@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use App\Entity\RoomCategory; use App\Entity\RoomCategory;
use App\Entity\User;
class SomniaDashboardController extends AbstractDashboardController class SomniaDashboardController extends AbstractDashboardController
{ {
@@ -30,5 +31,8 @@ class SomniaDashboardController extends AbstractDashboardController
yield MenuItem::Section('Room Management'); yield MenuItem::Section('Room Management');
yield MenuItem::linkToCrud('Category', 'fas fa-list', RoomCategory::class); yield MenuItem::linkToCrud('Category', 'fas fa-list', RoomCategory::class);
yield MenuItem::Section('App Management');
yield MenuItem::linkToCrud('Users', 'fas fa-user', User::class);
} }
} }
@@ -0,0 +1,90 @@
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserCrudController extends AbstractCrudController
{
public function __construct(
public UserPasswordHasherInterface $userPasswordHasher
) {}
public static function getEntityFqcn(): string
{
return User::class;
}
public function configureFields(string $pageName): iterable
{
yield TextField::new ('username');
yield TextField::new ('username')->onlyWhenUpdating()->setDisabled(true);
$availableRoles = ['User' => 'ROLE_USER', 'Admin' => 'ROLE_ADMIN', 'Test' => 'ROLE_TEST'];
yield ChoiceField::new ('roles')
->setChoices($availableRoles)
->allowMultipleChoices(true);
yield FormField::addPanel('Set password')->setIcon('fa fa-key');
$fieldPassword = Field::new ('plainPassword', 'New password')
->hideOnIndex()
->setRequired(false)
->setFormType(RepeatedType::class)
->setFormTypeOptions([
'type' => PasswordType::class,
'first_options' => ['label' => 'New password'],
'second_options' => ['label' => 'Repeat password'],
'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.',
]);
if($pageName == Crud::PAGE_NEW) {
$fieldPassword->setRequired(true);
}
yield $fieldPassword;
}
/**
* @param User $entityInstance
*/
public function persistEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
//Hash password on creation.
$plaintextPassword = $entityInstance->getPlainPassword();
$hashedPassword = $this->userPasswordHasher->hashPassword($entityInstance, $plaintextPassword);
$entityInstance->setPassword($hashedPassword);
parent::updateEntity($entityManager, $entityInstance);
}
/**
* @param User $entityInstance
*/
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
{
$plaintextPassword = $entityInstance->getPlainPassword();
if($plaintextPassword && $plaintextPassword != "") {
//New password set. Hash password.
$hashedPassword = $this->userPasswordHasher->hashPassword($entityInstance, $plaintextPassword);
$entityInstance->setPassword($hashedPassword);
}
parent::updateEntity($entityManager, $entityInstance);
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuild
class RoomCategoryController extends AbstractController class RoomCategoryController extends AbstractController
{ {
#[Route('/room/category', name: 'app_room_category')] #[Route('/room/category', name: 'app_room_AllCategories')]
public function getAll(RoomCategoryRepository $catRepo, SerializerInterface $serializer): Response public function getAll(RoomCategoryRepository $catRepo, SerializerInterface $serializer): Response
{ {
$catList = $catRepo->findAll(); $catList = $catRepo->findAll();
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User;
use App\Repository\UserRepository;
class SecurityController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
#[Route(path: '/addAdmin', name: 'app_security_addAdmin')]
public function addAdmin(UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $entityManager): Response
{
$user = new User();
$user->setUsername("admin");
$user->setRoles(["ROLE_USER", "ROLE_ADMIN"]);
$plaintextPassword = "admin";
// hash the password (based on the security.yaml config for the $user class)
$hashedPassword = $passwordHasher->hashPassword(
$user,
$plaintextPassword
);
$user->setPassword($hashedPassword);
$entityManager->persist($user);
$entityManager->flush();
return new Response("Created user - Admin");
}
}
+18
View File
@@ -32,6 +32,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column] #[ORM\Column]
private ?string $password = null; private ?string $password = null;
/**
* @var string Store unhashed user password on user creation/edit.
*/
private ?string $plainPassword = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@@ -98,6 +103,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getPlainPassword(): string
{
return $this->plainPassword;
}
public function setPlainPassword(string $plainPassword): static
{
$this->plainPassword = $plainPassword;
return $this;
}
/** /**
* @see UserInterface * @see UserInterface
*/ */
+42
View File
@@ -0,0 +1,42 @@
{% extends 'base.html.twig' %}
{% block title %}Log in!{% endblock %}
{% block body %}
<form method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{% if app.user %}
<div class="mb-3">
You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Logout</a>
</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="username">Username</label>
<input type="text" value="{{ last_username }}" name="_username" id="username" class="form-control" autocomplete="username" required autofocus>
<label for="password">Password</label>
<input type="password" name="_password" id="password" class="form-control" autocomplete="current-password" required>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
{#
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
See https://symfony.com/doc/current/security/remember_me.html
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
#}
<button class="btn btn-lg btn-primary" type="submit">
Sign in
</button>
</form>
{% endblock %}