feat: Ajoute authentification via entité Account

Body:
Implémente l'authentification avec l'entité Account et formulaire de login.
Passe la locale par défaut en français.
Ignorer le dossier build.
This commit is contained in:
Serreau Jovann
2025-07-16 16:01:26 +02:00
parent 08dc61fdc3
commit 66b1a42171
8 changed files with 294 additions and 32 deletions

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ phpstan.neon
.coverage
coverage/
.phpunit.cache
/public/build

View File

@@ -1,39 +1,45 @@
# config/packages/security.yaml
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
# Appelle votre provider d'utilisateurs.
# Ici, nous configurons un provider d'entité pour notre classe Account,
# en spécifiant 'username' comme propriété d'identification.
app_account_provider:
entity:
class: App\Entity\Account
property: username # Utilise le champ 'username' de votre entité Account pour l'authentification
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
provider: app_account_provider # Utilise le provider que nous avons défini ci-dessus
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
form_login:
login_path: app_login # La route vers votre formulaire de connexion (GET)
check_path: app_login # L'URL où le formulaire POST sera soumis
enable_csrf: true # Active la protection CSRF
csrf_token_id: authenticate # ID du jeton CSRF (doit correspondre à celui dans votre Twig)
logout:
path: app_logout # La route pour la déconnexion
target: app_login # Redirection après déconnexion réussie
# Configuration des algorithmes de hachage des mots de passe.
# Symfony choisira automatiquement le meilleur algorithme par défaut si non spécifié,
# mais vous pouvez le configurer explicitement.
password_hashers:
App\Entity\Account: 'auto' # 'auto' sélectionne le meilleur algorithme disponible (recommandé)
# Ou pour spécifier bcrypt explicitement :
# App\Entity\Account:
# algorithm: bcrypt
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# 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 }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
- { path: ^/artemis, roles: ROLE_ARTEMIS }
- { path: ^/, roles: PUBLIC_ACCESS } # Toutes les autres pages nécessitent une authentification complète

View File

@@ -1,5 +1,5 @@
framework:
default_locale: en
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
providers:

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250716131626 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "account" (id SERIAL NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, uuid UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_USERNAME ON "account" (username)');
$this->addSql('CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
$this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;');
$this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;');
$this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE "account"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -12,7 +12,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class HomeController extends AbstractController
{
#[Route(path: '/',name: 'app_login',methods: 'GET')]
#[Route(path: '/',name: 'app_login',methods: ['GET', 'POST'])]
public function index(AuthenticationUtils $authenticationUtils): Response
{
return$this->render('admin/login.twig',[

146
src/Entity/Account.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
namespace App\Entity;
use App\Repository\AccountRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: AccountRepository::class)]
#[ORM\Table(name: '`account`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])]
#[UniqueEntity(fields: ['email'], message: 'Cette adresse e-mail est déjà utilisée.')]
#[UniqueEntity(fields: ['uuid'], message: 'Cet identifiant unique (UUID) est déjà utilisé.')]
class Account implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180)]
private ?string $username = null;
/**
* @var list<string> The user roles
*/
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
private ?string $password = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
#[ORM\Column(type: Types::GUID)]
private ?string $uuid = null;
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* @param list<string> $roles
*/
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
/**
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0".self::class."\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// @deprecated, to be removed when upgrading to Symfony 8
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getUuid(): ?string
{
return $this->uuid;
}
public function setUuid(string $uuid): static
{
$this->uuid = $uuid;
return $this;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Repository;
use App\Entity\Account;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<Account>
*/
class AccountRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Account::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof Account) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
}
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
// /**
// * @return Account[] Returns an array of Account objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Account
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -42,11 +42,11 @@
<div class="rounded-md shadow-sm -space-y-px">
<div>
<label for="username" class="sr-only">Adresse email</label>
<label for="username" class="sr-only">Utilisateur</label>
{# Changement ici : champs de saisie pour le thème sombre #}
<input id="username" name="_username" type="email" autocomplete="email" required
<input id="username" name="_username" type="text" autocomplete="text" required
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-600 placeholder-gray-400 text-gray-100 bg-gray-700 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Adresse email" value="{{ last_username }}">
placeholder="Utilisateur" value="{{ last_username }}">
</div>
<div>
<label for="password" class="sr-only">Mot de passe</label>