User management - Login and user password hash.

This commit is contained in:
2024-04-26 21:37:39 +02:00
parent 30773d7af4
commit 84d9c81afd
7 changed files with 230 additions and 8 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
+50 -7
View File
@@ -2,18 +2,25 @@
namespace App\Controller\Admin; namespace App\Controller\Admin;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User; use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field; use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use Doctrine\ORM\EntityManagerInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; 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\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserCrudController extends AbstractCrudController class UserCrudController extends AbstractCrudController
{ {
public function __construct(
public UserPasswordHasherInterface $userPasswordHasher
) {}
public static function getEntityFqcn(): string public static function getEntityFqcn(): string
{ {
return User::class; return User::class;
@@ -29,9 +36,12 @@ class UserCrudController extends AbstractCrudController
->setChoices($availableRoles) ->setChoices($availableRoles)
->allowMultipleChoices(true); ->allowMultipleChoices(true);
yield FormField::addPanel('Change password')->setIcon('fa fa-key');
yield Field::new ('password', 'New password')->onlyWhenCreating()->setRequired(true) yield FormField::addPanel('Set password')->setIcon('fa fa-key');
$fieldPassword = Field::new ('plainPassword', 'New password')
->hideOnIndex()
->setRequired(false)
->setFormType(RepeatedType::class) ->setFormType(RepeatedType::class)
->setFormTypeOptions([ ->setFormTypeOptions([
'type' => PasswordType::class, 'type' => PasswordType::class,
@@ -40,8 +50,41 @@ class UserCrudController extends AbstractCrudController
'error_bubbling' => true, 'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.', 'invalid_message' => 'The password fields do not match.',
]); ]);
if($pageName == Crud::PAGE_NEW) {
$fieldPassword->setRequired(true);
}
//TODO: Allow password change on Update. 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);
} }
} }
+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 %}