From 50336694f61c5544380ff4f9d51fb4510722a9b5 Mon Sep 17 00:00:00 2001 From: r-dev Date: Sat, 10 Jan 2026 19:41:38 +0100 Subject: [PATCH] feat(auth) : creation entite Profile avec JWT - Entite Profile implementant UserInterface - Support authentification JWT via email/password - Repository avec PasswordUpgraderInterface - Migration Doctrine pour table profiles - Script utilitaire creation utilisateur test Co-Authored-By: Claude Sonnet 4.5 --- create_test_user.php | 61 ++++++++ migrations/Version20260110175413.php | 32 ++++ src/Entity/Profile.php | 224 +++++++++++++++++++++++++++ src/Repository/ProfileRepository.php | 37 +++++ 4 files changed, 354 insertions(+) create mode 100644 create_test_user.php create mode 100644 migrations/Version20260110175413.php create mode 100644 src/Entity/Profile.php create mode 100644 src/Repository/ProfileRepository.php diff --git a/create_test_user.php b/create_test_user.php new file mode 100644 index 0000000..a0c421a --- /dev/null +++ b/create_test_user.php @@ -0,0 +1,61 @@ + ['algorithm' => 'bcrypt'], + 'memory-hard' => ['algorithm' => 'argon2i'], +]); + +$passwordHasher = $factory->getPasswordHasher('common'); +$hashedPassword = $passwordHasher->hash('admin123'); + +// Connect to database +$pdo = new PDO( + 'pgsql:host=db;port=5432;dbname=inventory', + 'root', + 'root' +); + +// Check if table exists +$tableExists = $pdo->query("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'profiles')")->fetchColumn(); + +if ($tableExists) { + echo "Table 'profiles' exists.\n"; + + // Check if user exists + $userExists = $pdo->prepare('SELECT COUNT(*) FROM profiles WHERE email = ?'); + $userExists->execute(['admin@admin.com']); + + if ($userExists->fetchColumn() > 0) { + echo "User admin@admin.com already exists. Updating password...\n"; + $stmt = $pdo->prepare('UPDATE profiles SET password = ? WHERE email = ?'); + $stmt->execute([$hashedPassword, 'admin@admin.com']); + echo "Password updated!\n"; + } else { + echo "Creating user admin@admin.com...\n"; + $stmt = $pdo->prepare('INSERT INTO profiles (id, email, first_name, last_name, is_active, roles, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())'); + $id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24); + $stmt->execute([ + $id, + 'admin@admin.com', + 'Admin', + 'User', + true, + json_encode(['ROLE_USER', 'ROLE_ADMIN']), + $hashedPassword, + ]); + echo "User created!\n"; + } +} else { + echo "Table 'profiles' does not exist yet. Run migrations first.\n"; +} + +echo "\nTest credentials:\n"; +echo "Email: admin@admin.com\n"; +echo "Password: admin123\n"; diff --git a/migrations/Version20260110175413.php b/migrations/Version20260110175413.php new file mode 100644 index 0000000..65b2208 --- /dev/null +++ b/migrations/Version20260110175413.php @@ -0,0 +1,32 @@ +addSql('CREATE TABLE profiles (id VARCHAR(30) NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, is_active BOOLEAN DEFAULT true NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_email ON profiles (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE profiles'); + } +} diff --git a/src/Entity/Profile.php b/src/Entity/Profile.php new file mode 100644 index 0000000..d5d99a8 --- /dev/null +++ b/src/Entity/Profile.php @@ -0,0 +1,224 @@ + ['profile:read']], + denormalizationContext: ['groups' => ['profile:write']] +)] +class Profile implements UserInterface, PasswordAuthenticatedUserInterface +{ + #[ORM\Id] + #[ORM\Column(type: 'string', length: 30)] + #[Groups(['profile:read'])] + private ?string $id = null; + + #[ORM\Column(type: 'string', length: 180, unique: true)] + #[Assert\NotBlank] + #[Assert\Email] + #[Groups(['profile:read', 'profile:write'])] + private string $email; + + #[ORM\Column(type: 'string', length: 100)] + #[Assert\NotBlank] + #[Groups(['profile:read', 'profile:write'])] + private string $firstName; + + #[ORM\Column(type: 'string', length: 100)] + #[Assert\NotBlank] + #[Groups(['profile:read', 'profile:write'])] + private string $lastName; + + #[ORM\Column(type: 'boolean', options: ['default' => true])] + #[Groups(['profile:read', 'profile:write'])] + private bool $isActive = true; + + /** + * @var list The user roles + */ + #[ORM\Column(type: 'json')] + #[Groups(['profile:read', 'profile:write'])] + private array $roles = ['ROLE_USER']; + + /** + * @var string The hashed password + */ + #[ORM\Column(type: 'string')] + #[Groups(['profile:write'])] + private ?string $password = null; + + #[ORM\Column(type: 'datetime_immutable')] + #[Groups(['profile:read'])] + private DateTimeImmutable $createdAt; + + #[ORM\Column(type: 'datetime_immutable')] + #[Groups(['profile:read'])] + private DateTimeImmutable $updatedAt; + + public function __construct() + { + // Générer un CUID-like ID pour compatibilité avec Prisma + $this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24); + $this->createdAt = new DateTimeImmutable(); + $this->updatedAt = new DateTimeImmutable(); + } + + public function getId(): ?string + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } + + public function isActive(): bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): static + { + $this->isActive = $isActive; + + return $this; + } + + /** + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return $this->email; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $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; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + #[ORM\PrePersist] + public function setCreatedAtValue(): void + { + $this->createdAt = new DateTimeImmutable(); + $this->updatedAt = new DateTimeImmutable(); + } + + #[ORM\PreUpdate] + public function setUpdatedAtValue(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Repository/ProfileRepository.php b/src/Repository/ProfileRepository.php new file mode 100644 index 0000000..970276a --- /dev/null +++ b/src/Repository/ProfileRepository.php @@ -0,0 +1,37 @@ + + */ +class ProfileRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Profile::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof Profile) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } +}