diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..0417909 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,26 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + + logout: + path: app_security_logout + + json_login: + check_path: app_security_login + username_path: username + password_path: password # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..3c760b0 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,38 @@ + $this->iriConverter->getIriFromResource($user) + ]); + } + + #[Route('/logout', name: 'app_security_logout')] + public function logoutAction(): void + { + // This Exception should never be reached unless + // the firewall in security.firewalls.main.logout was not hit + throw new \Exception(); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..3f5064f --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,186 @@ + ['user:read']], + denormalizationContext: ['groups' => ['user:write']], +)] +#[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] +#[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] +#[ORM\Entity(repositoryClass: UserRepository::class)] +class User implements UserInterface, PasswordAuthenticatedUserInterface +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + /** + * Human readable, non white space, unique string. + */ + #[Assert\NotBlank()] + #[Assert\Length(min: 4, max: 30)] + #[Assert\Regex('/^[a-z0-9_-]+$/')] + #[Groups(['user:read', 'user:write'])] + #[ORM\Column(length: 30, unique: true)] + private ?string $username = null; + + /** + * @var list The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The user password + */ + #[ORM\Column] + private ?string $password = null; + + /** + * Plain-text, will be hashed by the platform. + */ + #[Assert\NotBlank()] + #[Assert\Length(min: 12)] + #[Groups(['user:write'])] + #[SerializedName('password')] + private ?string $plainPassword = null; + + #[Assert\NotBlank()] + #[Assert\Email()] + #[Groups(['user:read', 'user:write'])] + #[ORM\Column(length: 255)] + private ?string $email = null; + + #[Groups(['user:read'])] + #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\JoinColumn(nullable: false)] + private ?Account $account = null; + + public function __construct() + { + $this->account = new Account; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = strtolower($username); + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @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; + } + + public function getPlainPassword(): string + { + return $this->plainPassword; + } + + public function setPlainPassword(string $plainPassword): static + { + $this->plainPassword = $plainPassword; + + 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 getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getAccount(): ?Account + { + return $this->account; + } + + public function setAccount(Account $account): static + { + $this->account = $account; + + return $this; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..50a7511 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,67 @@ + + * + * @implements PasswordUpgraderInterface + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/State/UserStateProcessor.php b/src/State/UserStateProcessor.php new file mode 100644 index 0000000..7dbef5e --- /dev/null +++ b/src/State/UserStateProcessor.php @@ -0,0 +1,30 @@ +getPlainPassword()) { + $data->setPassword( + $this->userPasswordHasher->hashPassword($data, $data->getPlainPassword()) + ); + } + + return $this->innerProcessor->process($data, $operation, $uriVariables, $context); + } +}