From cc616af1eb70ce07131ffa9f331aa89c4d2cc389 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko <i.vasilenko@iqdev.digital> Date: Fri, 21 Jun 2024 10:28:22 +0500 Subject: [PATCH] password reset user history profile change --- app/config/packages/security.yaml | 4 + app/config/services.yaml | 6 + app/migrations/Version20240620091453.php | 37 ++++ app/migrations/Version20240620121602.php | 32 +++ app/src/Controller/ProfileController.php | 34 ++++ app/src/Entity/User.php | 126 +++++++++++- app/src/Entity/UserHistory.php | 182 ++++++++++++++++++ app/src/Listeners/UserListener.php | 139 +++++++++++++ app/src/Repository/UserHistoryRepository.php | 43 +++++ app/src/Repository/UserRepository.php | 7 + .../Service/Action/Classes/ChangeProfile.php | 96 +++++++++ app/src/Service/Action/Classes/GetProfile.php | 7 +- app/src/Service/Action/Classes/None.php | 2 +- app/src/Service/Action/Classes/Register.php | 1 + app/src/Service/Action/Classes/ResetEmail.php | 63 ++++++ app/src/Service/Dto/BaseDto.php | 8 +- .../Service/Dto/Classes/ChangeProfileDto.php | 26 +++ app/src/Service/Response/Classes/Response.php | 3 +- .../Service/Send/Classes/CodeSendService.php | 31 ++- 19 files changed, 829 insertions(+), 18 deletions(-) create mode 100644 app/migrations/Version20240620091453.php create mode 100644 app/migrations/Version20240620121602.php create mode 100644 app/src/Entity/UserHistory.php create mode 100644 app/src/Listeners/UserListener.php create mode 100644 app/src/Repository/UserHistoryRepository.php create mode 100644 app/src/Service/Action/Classes/ChangeProfile.php create mode 100644 app/src/Service/Action/Classes/ResetEmail.php create mode 100644 app/src/Service/Dto/Classes/ChangeProfileDto.php diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index 198b9ae..37c76cf 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -55,6 +55,10 @@ security: - { path: ^/api/profile/recovery, roles: PUBLIC_ACCESS } - { path: ^/api/profile/recovery/check, roles: PUBLIC_ACCESS } + - { path: ^/api/profile/reset/email, roles: ROLE_USER } + - { path: ^/api/profile/reset/field, roles: ROLE_USER } + - { path: ^/api/profile/change, roles: ROLE_USER } + - { path: ^/api/profile, roles: ROLE_USER } - { path: ^/api, roles: ROLE_CONFIRMED } # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } diff --git a/app/config/services.yaml b/app/config/services.yaml index 4fe458b..cc9ab17 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -44,6 +44,10 @@ services: App\Service\Action\ActionServiceInterface $resetPasswordService: '@App\Service\Action\Classes\ResetPassword' + App\Service\Action\ActionServiceInterface $profileChangeService: '@App\Service\Action\Classes\ChangeProfile' + + App\Service\Action\ActionServiceInterface $resetEmailService: '@App\Service\Action\Classes\ResetEmail' + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' @@ -58,6 +62,8 @@ services: App\Service\Dto\DtoServiceInterface $passwordDto: '@App\Service\Dto\Classes\ChangePasswordDto' + App\Service\Dto\DtoServiceInterface $profileDto: '@App\Service\Dto\Classes\ChangeProfileDto' + App\Service\Dto\DtoServiceInterface $recoveryDto: '@App\Service\Dto\Classes\RecoveryDto' App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' diff --git a/app/migrations/Version20240620091453.php b/app/migrations/Version20240620091453.php new file mode 100644 index 0000000..0cbe709 --- /dev/null +++ b/app/migrations/Version20240620091453.php @@ -0,0 +1,37 @@ +<?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 Version20240620091453 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 SEQUENCE user_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE user_history (id INT NOT NULL, related_user_id INT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, field VARCHAR(255) DEFAULT NULL, type VARCHAR(255) NOT NULL, value VARCHAR(255) DEFAULT NULL, old_value VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7FB76E4198771930 ON user_history (related_user_id)'); + $this->addSql('ALTER TABLE user_history ADD CONSTRAINT FK_7FB76E4198771930 FOREIGN KEY (related_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 SEQUENCE user_history_id_seq CASCADE'); + $this->addSql('ALTER TABLE user_history DROP CONSTRAINT FK_7FB76E4198771930'); + $this->addSql('DROP TABLE user_history'); + } +} diff --git a/app/migrations/Version20240620121602.php b/app/migrations/Version20240620121602.php new file mode 100644 index 0000000..56d05ee --- /dev/null +++ b/app/migrations/Version20240620121602.php @@ -0,0 +1,32 @@ +<?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 Version20240620121602 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('ALTER TABLE "user" ALTER patronymic DROP NOT NULL'); + } + + 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('ALTER TABLE "user" ALTER patronymic SET NOT NULL'); + } +} diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index 75f10cf..348eac7 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -3,6 +3,7 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; +use App\Service\Dto\Classes\ChangeProfileDto; use App\Service\Dto\Classes\RecoveryCodeDto; use App\Service\Dto\Classes\RecoveryDto; use App\Service\Response\Classes\ProfileResponse; @@ -82,4 +83,37 @@ class ProfileController extends AbstractController { return $checkRecoveryService->getResponse(); } + + #[Route('/profile/change', name: 'profile_change', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: ChangeProfileDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: ProfileResponse::class, groups: ["message", "data", "profile"]) + ) + )] + public function changeProfile( + ActionServiceInterface $profileChangeService, + ): JsonResponse + { + return $profileChangeService->getResponse(); + } + + #[Route('/profile/reset/email', name: 'profile_reset_email', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function resetLastConfirmEmail( + ActionServiceInterface $resetEmailService, + ): JsonResponse + { + return $resetEmailService->getResponse(); + } } diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 1ec76ec..96c4f82 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -3,11 +3,22 @@ namespace App\Entity; use App\Repository\UserRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use ReflectionClass; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Ignore; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserRepository::class)] @@ -41,7 +52,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(length: 255)] private ?string $surname = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $patronymic = null; #[ORM\Column(length: 255, nullable: true)] @@ -59,6 +70,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToOne(mappedBy: 'related_user', cascade: ['persist', 'remove'])] private ?UserCode $register_code = null; + /** + * @var Collection<int, UserHistory> + */ + #[ORM\OneToMany(targetEntity: UserHistory::class, mappedBy: 'related_user', cascade: ['persist', 'remove'],fetch: 'EAGER')] + private Collection $userHistories; + + public function __construct() + { + $this->userHistories = new ArrayCollection(); + } + #[Groups(['all'])] public function getId(): ?int { @@ -178,7 +200,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this->patronymic; } - public function setPatronymic(string $patronymic): static + public function setPatronymic(?string $patronymic): static { $this->patronymic = $patronymic; @@ -221,7 +243,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile'])] + #[Groups(['all', 'profile', 'edit'])] public function isConfirm(): ?bool { return $this->confirm; @@ -234,6 +256,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile', 'edit'])] public function isDeleted(): ?bool { return $this->deleted; @@ -273,4 +296,101 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection<int, UserHistory> + */ + #[Groups(['all', 'profile'])] + public function getUserHistories(): Collection + { + return $this->userHistories; + } + + public function addUserHistory(UserHistory $userHistory): static + { + if (!$this->userHistories->contains($userHistory)) { + $this->userHistories->add($userHistory); + $userHistory->setRelatedUser($this); + } + + return $this; + } + + public function removeUserHistory(UserHistory $userHistory): static + { + if ($this->userHistories->removeElement($userHistory)) { + // set the owning side to null (unless already changed) + if ($userHistory->getRelatedUser() === $this) { + $userHistory->setRelatedUser(null); + } + } + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getLastConfirmEmail(): ?string + { + $lastDate = null; + $lastEmail = null; + foreach ($this->getUserHistories() as $userHistory) { + if ($userHistory->getField() === 'confirm' && $userHistory->getDate() && $userHistory->getType() === UserHistory::TYPE_CREATE) { + if ($lastDate === null || $lastDate->getTimestamp() < $userHistory->getDate()->getTimestamp()) { + $lastDate = $userHistory->getDate(); + $lastEmail = $userHistory->getValue(); + } + } + } + + return $lastEmail; + } + + /** + * Создание Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¿Ð¾ маÑÑиву + * + * @param array $data + * + * @return self + */ + #[Ignore()] + public static function createByArray(array $data, array $groups = ['edit']): ?self + { + try { + $normalizer = new ObjectNormalizer( + new ClassMetadataFactory(new AttributeLoader()), + new CamelCaseToSnakeCaseNameConverter(), + null, + new ReflectionExtractor() + ); + $serializer = new Serializer( + [$normalizer, new DateTimeNormalizer()], + [new JsonEncoder()] + ); + return $serializer->deserialize( + json_encode($data, JSON_THROW_ON_ERROR), + __CLASS__, + 'json', + [ + 'groups' => $groups, + ] + ); + } catch (\Exception $exception) { + return null; + } + } + + #[Ignore()] + public function newCopy(array $groups = ['edit']): ?self + { + $normalizer = new ObjectNormalizer( + new ClassMetadataFactory(new AttributeLoader()), + new CamelCaseToSnakeCaseNameConverter(), + null, + new ReflectionExtractor() + ); + $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $data = $serializer->serialize($this, 'json', ['groups' => $groups]); + $array = json_decode($data, true, 512, JSON_THROW_ON_ERROR); + return self::createByArray($array, $groups); + } } diff --git a/app/src/Entity/UserHistory.php b/app/src/Entity/UserHistory.php new file mode 100644 index 0000000..cc4d288 --- /dev/null +++ b/app/src/Entity/UserHistory.php @@ -0,0 +1,182 @@ +<?php + +namespace App\Entity; + +use App\Listeners\UserListener; +use App\Repository\UserHistoryRepository; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ORM\Entity(repositoryClass: UserHistoryRepository::class)] +class UserHistory +{ + public const TYPE_CREATE = 'create'; + public const TYPE_UPDATE = 'update'; + public const TYPE_DELETE = 'delete'; + public const TYPE_RECOVERY = 'recovery'; + + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'userHistories')] + #[ORM\JoinColumn(nullable: false)] + private ?User $related_user = null; + + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTimeInterface $date = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $field = null; + + #[ORM\Column(length: 255)] + private ?string $type = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $value = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $old_value = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getField(): ?string + { + return $this->field; + } + + public function setField(?string $field): static + { + $this->field = $field; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(?string $value): static + { + $this->value = $value; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getOldValue(): ?string + { + return $this->old_value; + } + + public function setOldValue(?string $old_value): static + { + $this->old_value = $old_value; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getText(): ?string + { + $text = ''; + $type = $this->getType(); + + switch ($field = $this->getField()) { + case 'confirm': + switch ($type) { + case self::TYPE_CREATE: + $text = 'Почта подтверждена'; + break; + case self::TYPE_DELETE: + $text = 'Подтверждение почты Ñброшено'; + break; + } + break; + case 'user': + switch ($type) { + case self::TYPE_CREATE: + $text = 'Пользователь зарегиÑтрирован'; + break; + case self::TYPE_UPDATE: + $text = 'Пользователь обновлен'; + break; + case self::TYPE_RECOVERY: + $text = 'Пользователь воÑÑтановлен'; + break; + case self::TYPE_DELETE: + $text = 'Пользователь удален'; + break; + } + break; + default: + if (isset(UserListener::HISTORY_FIELDS[$field])) { + switch ($type) { + case self::TYPE_CREATE: + $text = 'Поле "' . UserListener::HISTORY_FIELDS[$field] . '" заполнено'; + break; + case self::TYPE_UPDATE: + $text = 'Поле "' . UserListener::HISTORY_FIELDS[$field] . '" обновлено'; + break; + case self::TYPE_DELETE: + $text = 'Поле "' . UserListener::HISTORY_FIELDS[$field] . '" удалено '; + break; + } + } + break; + } + + $time = ''; + if ($date = $this->getDate()) { + $time = $date->format('d.m.Y H:i:s') . ' - '; + } + + return $time . $text; + } +} diff --git a/app/src/Listeners/UserListener.php b/app/src/Listeners/UserListener.php new file mode 100644 index 0000000..ac6c084 --- /dev/null +++ b/app/src/Listeners/UserListener.php @@ -0,0 +1,139 @@ +<?php + +namespace App\Listeners; + +use App\Entity\User; +use App\Entity\UserHistory; +use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener; +use Doctrine\ORM\Event\PreFlushEventArgs; +use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\Events; +use Doctrine\ORM\UnitOfWork; +use Doctrine\Persistence\ObjectManager; +use ReflectionClass; + +#[AsEntityListener(event: Events::preFlush, method: 'prePersist', entity: User::class)] +#[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: User::class)] +#[AsEntityListener(event: Events::preFlush, method: 'prePersistHistory', entity: UserHistory::class)] +#[AsEntityListener(event: Events::preUpdate, method: 'preUpdateHistory', entity: UserHistory::class)] +class UserListener +{ + public const HISTORY_FIELDS = [ + 'email' => 'Email', + 'name' => 'ИмÑ', + 'surname' => 'ФамилиÑ', + 'patronymic' => 'ОтчеÑтво', + 'phone_number' => 'Ðомер телефона' + ]; + + public function prePersist(User $user, PreFlushEventArgs $args): void + { + $this->checkEmail($user, $args->getObjectManager()); + $this->saveHistory($user, $args->getObjectManager()); + } + + public function preUpdate(User $user, PreUpdateEventArgs $args): void + { + $this->checkEmail($user, $args->getObjectManager()); + $this->saveHistory($user, $args->getObjectManager()); + } + + public function saveHistory(User $user, ObjectManager $om): void + { + /** @var UnitOfWork $uow */ + $uow = $om->getUnitOfWork(); + $originalValues = $uow->getOriginalEntityData($user); + $userExists = null; + if ($originalValues) { + $userExists = User::createByArray($originalValues); + } + $checkUser = $user->newCopy(); + + if ($user->getId() && $userExists) { + $reflectionClass = new ReflectionClass($user); + foreach ($reflectionClass->getProperties() as $property) { + $name = $property->getName(); + if (!isset(self::HISTORY_FIELDS[$name])) { + continue; + } + $value = $property->getValue($checkUser); + $oldValue = $property->getValue($userExists); + if ($value !== $oldValue) { + if (empty($value) && empty($oldValue)) { + continue; + } + + $type = UserHistory::TYPE_UPDATE; + if (empty($value)) { + $type = UserHistory::TYPE_DELETE; + } elseif (empty($oldValue)) { + $type = UserHistory::TYPE_CREATE; + } + $newUserHistory = new UserHistory(); + $newUserHistory->setType($type); + $newUserHistory->setField($name); + $newUserHistory->setValue($value); + $newUserHistory->setOldValue($oldValue); + $user->addUserHistory($newUserHistory); + } + } + if ($userExists->isDeleted() !== $user->isDeleted()) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType($user->isDeleted() ? UserHistory::TYPE_DELETE : UserHistory::TYPE_RECOVERY); + $newUserHistory->setField('user'); + $user->addUserHistory($newUserHistory); + } + if ($userExists->isConfirm() !== $user->isConfirm()) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType($user->isConfirm() ? UserHistory::TYPE_CREATE : UserHistory::TYPE_DELETE); + if ($user->isConfirm()) { + $newUserHistory->setValue($user->getEmail()); + } + $newUserHistory->setField('confirm'); + $user->addUserHistory($newUserHistory); + } + } else { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_CREATE); + $newUserHistory->setField('user'); + $user->addUserHistory($newUserHistory); + } + } + + public function checkEmail(User $user, ObjectManager $om): void + { + if ($user->getId()) { + /** @var UnitOfWork $uow */ + $uow = $om->getUnitOfWork(); + $originalValues = $uow->getOriginalEntityData($user); + $userExists = null; + if ($originalValues) { + $userExists = User::createByArray($originalValues); + } + if ($userExists) { + if ($userExists->getEmail() !== $user->getEmail()) { + $user->setConfirm($user->getLastConfirmEmail() === $user->getEmail()); + } + } + } else { + $user->setConfirm(false); + } + } + + public function prePersistHistory(UserHistory $userHistory, PreFlushEventArgs $args): void + { + $this->setHistoryDate($userHistory); + } + + public function preUpdateHistory(UserHistory $userHistory, PreUpdateEventArgs $args): void + { + $this->setHistoryDate($userHistory); + } + + public function setHistoryDate(UserHistory $userHistory): void + { + if (!$userHistory->getDate()) { + $userHistory->setDate(new \DateTime()); + } + } +} \ No newline at end of file diff --git a/app/src/Repository/UserHistoryRepository.php b/app/src/Repository/UserHistoryRepository.php new file mode 100644 index 0000000..276bcaa --- /dev/null +++ b/app/src/Repository/UserHistoryRepository.php @@ -0,0 +1,43 @@ +<?php + +namespace App\Repository; + +use App\Entity\UserHistory; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<UserHistory> + */ +class UserHistoryRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserHistory::class); + } + +// /** +// * @return UserHistory[] Returns an array of UserHistory 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): ?UserHistory +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/app/src/Repository/UserRepository.php b/app/src/Repository/UserRepository.php index 53ebb8c..7046472 100644 --- a/app/src/Repository/UserRepository.php +++ b/app/src/Repository/UserRepository.php @@ -59,6 +59,13 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getOneOrNullResult(); } + public function getById(int $id): ?User + { + $oQuery = $this->createQueryBuilder('u'); + $oQuery->andWhere('u.id = :id')->setParameter('id', $id); + return $oQuery->getQuery()->getOneOrNullResult(); + } + // /** // * @return User[] Returns an array of User objects // */ diff --git a/app/src/Service/Action/Classes/ChangeProfile.php b/app/src/Service/Action/Classes/ChangeProfile.php new file mode 100644 index 0000000..62d1ef0 --- /dev/null +++ b/app/src/Service/Action/Classes/ChangeProfile.php @@ -0,0 +1,96 @@ +<?php + +namespace App\Service\Action\Classes; + +use App\Entity\User; +use App\Service\Action\BaseActionService; +use App\Service\Dto\Classes\ChangeProfileDto; +use App\Service\Dto\DtoServiceInterface; +use App\Service\Response\ResponseServiceInterface; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bundle\SecurityBundle\Security; + +class ChangeProfile extends BaseActionService +{ + private ?User $user; + + public function __construct( + private ResponseServiceInterface $profileResponse, + private DtoServiceInterface $profileDto, + private ManagerRegistry $doctrine, + Security $security + ) + { + $this->user = $security->getUser(); + parent::__construct($profileResponse); + } + + public function runAction(): void + { + /** @var ChangeProfileDto $dto */ + $dto = $this->profileDto->getClass(); + + /** @var ?User $userExists */ + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($dto->email, $dto->phoneNumber, $this->user->getId()); + + if ($userExists !== null) { + if ($userExists->getEmail() === $dto->email) { + $this->profileResponse->addError('Email занÑÑ‚ другим пользователем'); + } elseif ($userExists->getPhoneNumber() === $dto->phoneNumber) { + $this->profileResponse->addError('Ðомер телефона занÑÑ‚ другим пользователем'); + } else { + $this->profileResponse->addError('Email или номер телефона занÑÑ‚ другим пользователем'); + } + } else { + $changed = false; + + if ($dto->name !== null && $dto->name !== $this->user->getName()) { + $this->user->setName($dto->name); + $changed = true; + } + + if ($dto->surname !== null && $dto->surname !== $this->user->getSurname()) { + $this->user->setSurname($dto->surname); + $changed = true; + } + + if ($dto->patronymic !== null && $dto->patronymic !== $this->user->getPatronymic()) { + $this->user->setPatronymic($dto->patronymic); + $changed = true; + } + + if ($dto->phoneNumber !== null && $dto->phoneNumber !== $this->user->getPhoneNumber()) { + $this->user->setPhoneNumber($dto->phoneNumber); + $changed = true; + } + + if ($dto->email !== null && $dto->email !== $this->user->getEmail()) { + $this->user->setEmail($dto->email); + $changed = true; + } + + if ($changed) { + try { + $em = $this->doctrine->getManager(); + $em->persist($this->user); + $em->flush(); + $this->profileResponse->setData($this->user); + } catch (\Exception $e) { + $this->profileResponse->addError('Ошибка ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ'); + } + } else { + $this->profileResponse->setData($this->user); + } + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->profileResponse->addError('Ð’Ñ‹ не авторизованы'); + return false; + } + return $this->profileDto->validate($this->profileResponse); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetProfile.php b/app/src/Service/Action/Classes/GetProfile.php index 73f5526..8a98a3a 100644 --- a/app/src/Service/Action/Classes/GetProfile.php +++ b/app/src/Service/Action/Classes/GetProfile.php @@ -17,7 +17,6 @@ class GetProfile extends BaseActionService /** * @param ProfileResponse $profileResponse * @param Security $security - * @param SerializerInterface $serializer */ public function __construct( private ResponseServiceInterface $profileResponse, @@ -47,6 +46,10 @@ class GetProfile extends BaseActionService $this->profileResponse->addError('Ð’Ñ‹ не авторизованы'); return false; } - return $this->user->isConfirm() && !$this->user->isDeleted(); + if ($this->user->isDeleted()) { + $this->profileResponse->addError('Профиль удален'); + return false; + } + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/None.php b/app/src/Service/Action/Classes/None.php index d75a862..f891845 100644 --- a/app/src/Service/Action/Classes/None.php +++ b/app/src/Service/Action/Classes/None.php @@ -15,7 +15,7 @@ class None extends BaseActionService public function validate(): bool { if ($this->responseService) { - $this->responseService->getResponse()->addError('ДейÑтвие не выбрано'); + $this->responseService->addError('ДейÑтвие не выбрано'); } return false; } diff --git a/app/src/Service/Action/Classes/Register.php b/app/src/Service/Action/Classes/Register.php index 4dd29cd..1229109 100644 --- a/app/src/Service/Action/Classes/Register.php +++ b/app/src/Service/Action/Classes/Register.php @@ -69,6 +69,7 @@ class Register extends BaseActionService $this->registerCodeSendService->setResponse($this->response); $this->registerCodeSendService->send(); } catch (\Exception $exception) { + dd($exception); $this->response->addError('Ошибка региÑтрации пользователÑ'); } diff --git a/app/src/Service/Action/Classes/ResetEmail.php b/app/src/Service/Action/Classes/ResetEmail.php new file mode 100644 index 0000000..07f2709 --- /dev/null +++ b/app/src/Service/Action/Classes/ResetEmail.php @@ -0,0 +1,63 @@ +<?php + +namespace App\Service\Action\Classes; + +use App\Entity\User; +use App\Service\Action\BaseActionService; +use App\Service\Response\ResponseServiceInterface; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bundle\SecurityBundle\Security; + +class ResetEmail extends BaseActionService +{ + private ?User $user; + + /** + * @param ResponseServiceInterface $response + * @param ManagerRegistry $doctrine + * @param Security $security + */ + public function __construct( + private ResponseServiceInterface $response, + private ManagerRegistry $doctrine, + Security $security + ) + { + $this->user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + if ($email = $this->user->getLastConfirmEmail()) { + if ($email === $this->user->getEmail()) { + $this->response->addMessage('Подтвержденный email уже уÑтановлен'); + } else { + try { + $this->user->setEmail($email); + $em = $this->doctrine->getManager(); + $em->persist($this->user); + $em->flush(); + $this->response->addMessage('УÑтановлен email: ' . $email); + } catch (\Exception $e) { + $this->response->addError('Ошибка ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ email'); + } + } + } else { + $this->response->addError('Ðет поÑледнего подтвержденного email'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->addError('Ð’Ñ‹ не авторизованы'); + return false; + } + if ($this->user->isDeleted()) { + $this->response->addError('Профиль удален'); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/BaseDto.php b/app/src/Service/Dto/BaseDto.php index 0a149d4..b3abff0 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -61,15 +61,15 @@ abstract class BaseDto implements DtoServiceInterface public function toArray(): ?array { try { - $oNormalizer = new ObjectNormalizer( + $normalizer = new ObjectNormalizer( null, new CamelCaseToSnakeCaseNameConverter(), null, new ReflectionExtractor() ); - $oSerializer = new Serializer([$oNormalizer], [new JsonEncoder()]); - $sData = $oSerializer->serialize($this->getClass(), 'json'); - return json_decode($sData, true, 512, JSON_THROW_ON_ERROR); + $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $data = $serializer->serialize($this->getClass(), 'json'); + return json_decode($data, true, 512, JSON_THROW_ON_ERROR); } catch (\Exception $oException) { return null; } diff --git a/app/src/Service/Dto/Classes/ChangeProfileDto.php b/app/src/Service/Dto/Classes/ChangeProfileDto.php new file mode 100644 index 0000000..129af96 --- /dev/null +++ b/app/src/Service/Dto/Classes/ChangeProfileDto.php @@ -0,0 +1,26 @@ +<?php + +namespace App\Service\Dto\Classes; + +use App\Service\Dto\BaseDto; +use Symfony\Component\Validator\Constraints as Assert; + +class ChangeProfileDto extends BaseDto +{ + #[Assert\Email( + message: 'Email "{{ value }}" неверный.', + )] + public ?string $email = null; + + public ?string $name = null; + + public ?string $surname = null; + + public ?string $patronymic = null; + + #[Assert\Regex( + pattern: '/^((8|\+7)[\- ]?)?(\(?\d{3}\)?[\- ]?)?[\d\- ]{7,10}$/i', + message: 'Ðеверный формат телефона' + )] + public ?string $phoneNumber = null; +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/Response.php b/app/src/Service/Response/Classes/Response.php index cf0ffde..5e10801 100644 --- a/app/src/Service/Response/Classes/Response.php +++ b/app/src/Service/Response/Classes/Response.php @@ -11,6 +11,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -139,7 +140,7 @@ class Response implements ResponseServiceInterface null, new ReflectionExtractor() ); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $serializer = new Serializer([new DateTimeNormalizer(), $normalizer], [new JsonEncoder()]); $dataStr = $serializer->serialize($this, 'json', ['groups' => $groups]); $dataArray = json_decode($dataStr, true, 512, JSON_THROW_ON_ERROR); diff --git a/app/src/Service/Send/Classes/CodeSendService.php b/app/src/Service/Send/Classes/CodeSendService.php index d8b5e4f..eabbcb8 100644 --- a/app/src/Service/Send/Classes/CodeSendService.php +++ b/app/src/Service/Send/Classes/CodeSendService.php @@ -100,11 +100,7 @@ class CodeSendService implements SendServiceInterface { $body = $this->getBody(); - foreach ($values as $name => $value) { - $body = str_replace('{' . $name . '}', $value, $body); - } - - return $body; + return self::textReplace($body, $values); } /** @@ -118,10 +114,31 @@ class CodeSendService implements SendServiceInterface { $subject = $this->getSubject(); + return self::textReplace($subject, $values); + } + + /** + * Замена переменных текÑта + * + * @param string $text + * @param array $values + * + * @return string + */ + private static function textReplace(string $text, array $values): string + { foreach ($values as $name => $value) { - $subject = str_replace('{' . $name . '}', $value, $subject); + if (is_string($value)) { + $text = str_replace('{' . $name . '}', $value, $text); + } + } + + $match = []; + preg_match('/{\w+}/', $text, $match); + foreach ($match as $value) { + $text = str_replace($value, '', $text); } - return $subject; + return $text; } } \ No newline at end of file -- GitLab