From baa293fd1b1417d32e1cac415d6e694867023b91 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Mon, 10 Jun 2024 13:46:04 +0500 Subject: [PATCH 1/7] login & register --- app/config/packages/framework.yaml | 2 + .../packages/lexik_jwt_authentication.yaml | 1 + app/config/packages/security.yaml | 26 +- app/config/routes.yaml | 3 + app/config/services.yaml | 46 +++- app/migrations/Version20240607055116.php | 42 ++++ app/src/Controller/AuthController.php | 22 ++ app/src/Entity/User.php | 237 ++++++++++++++++++ app/src/Entity/UserImage.php | 80 ++++++ app/src/Listeners/JwtListener.php | 92 +++++++ app/src/Repository/UserImageRepository.php | 43 ++++ app/src/Repository/UserRepository.php | 86 +++++++ app/src/Response/ApiResponse.php | 97 +++++++ app/src/Response/TokenResponse.php | 14 ++ .../Service/Action/ActionServiceInterface.php | 14 ++ app/src/Service/Action/BaseActionService.php | 34 +++ app/src/Service/Action/Classes/None.php | 22 ++ app/src/Service/Action/Classes/Register.php | 109 ++++++++ app/src/Service/Dto/BaseDto.php | 93 +++++++ .../Service/Dto/Classes/ChangePasswordDto.php | 28 +++ app/src/Service/Dto/Classes/NoneDto.php | 10 + app/src/Service/Dto/Classes/RegisterDto.php | 48 ++++ app/src/Service/Dto/DtoServiceInterface.php | 14 ++ .../Service/Response/BaseResponseService.php | 22 ++ .../Response/Classes/ProfileResponse.php | 14 ++ app/src/Service/Response/Classes/Response.php | 10 + .../Response/ResponseServiceInterface.php | 10 + app/src/Validators/PasswordValidator.php | 71 ++++++ 28 files changed, 1286 insertions(+), 4 deletions(-) create mode 100644 app/migrations/Version20240607055116.php create mode 100644 app/src/Controller/AuthController.php create mode 100644 app/src/Entity/User.php create mode 100644 app/src/Entity/UserImage.php create mode 100644 app/src/Listeners/JwtListener.php create mode 100644 app/src/Repository/UserImageRepository.php create mode 100644 app/src/Repository/UserRepository.php create mode 100644 app/src/Response/ApiResponse.php create mode 100644 app/src/Response/TokenResponse.php create mode 100644 app/src/Service/Action/ActionServiceInterface.php create mode 100644 app/src/Service/Action/BaseActionService.php create mode 100644 app/src/Service/Action/Classes/None.php create mode 100644 app/src/Service/Action/Classes/Register.php create mode 100644 app/src/Service/Dto/BaseDto.php create mode 100644 app/src/Service/Dto/Classes/ChangePasswordDto.php create mode 100644 app/src/Service/Dto/Classes/NoneDto.php create mode 100644 app/src/Service/Dto/Classes/RegisterDto.php create mode 100644 app/src/Service/Dto/DtoServiceInterface.php create mode 100644 app/src/Service/Response/BaseResponseService.php create mode 100644 app/src/Service/Response/Classes/ProfileResponse.php create mode 100644 app/src/Service/Response/Classes/Response.php create mode 100644 app/src/Service/Response/ResponseServiceInterface.php create mode 100644 app/src/Validators/PasswordValidator.php diff --git a/app/config/packages/framework.yaml b/app/config/packages/framework.yaml index 877eb25..5fc005d 100644 --- a/app/config/packages/framework.yaml +++ b/app/config/packages/framework.yaml @@ -8,6 +8,8 @@ framework: #esi: true #fragments: true + serializer: + name_converter: serializer.name_converter.camel_case_to_snake_case when@test: framework: diff --git a/app/config/packages/lexik_jwt_authentication.yaml b/app/config/packages/lexik_jwt_authentication.yaml index edfb69d..dd7a3aa 100644 --- a/app/config/packages/lexik_jwt_authentication.yaml +++ b/app/config/packages/lexik_jwt_authentication.yaml @@ -2,3 +2,4 @@ lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' + token_ttl: 3600 diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index 367af25..670446f 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -4,14 +4,33 @@ 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: email firewalls: + login: + pattern: ^/api/login + stateless: true + json_login: + check_path: /api/login + username_path: email + password_path: password + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + + api: + pattern: ^/api + stateless: true + jwt: ~ + dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,6 +41,9 @@ security: # 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: ^/api/login, roles: PUBLIC_ACCESS } + - { path: ^/api/register, roles: PUBLIC_ACCESS } + - { path: ^/api, roles: ROLE_CONFIRMED } # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } diff --git a/app/config/routes.yaml b/app/config/routes.yaml index 41ef814..dbc41f6 100644 --- a/app/config/routes.yaml +++ b/app/config/routes.yaml @@ -3,3 +3,6 @@ controllers: path: ../src/Controller/ namespace: App\Controller type: attribute + +api_login: + path: /api/login \ No newline at end of file diff --git a/app/config/services.yaml b/app/config/services.yaml index 2d6a76f..8c127d8 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -20,5 +20,47 @@ services: - '../src/Entity/' - '../src/Kernel.php' - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones + # Сервисы действий + App\Service\Action\ActionServiceInterface $registerService: '@App\Service\Action\Classes\Register' + + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' + + + # Сервисы Dto + App\Service\Dto\DtoServiceInterface $registerDto: '@App\Service\Dto\Classes\RegisterDto' + + App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' + + + # Сервисы ответа + App\Service\Response\ResponseServiceInterface $profileResponse: '@App\Service\Response\Classes\ProfileResponse' + + App\Service\Response\ResponseServiceInterface: '@App\Service\Response\Classes\Response' + + + + # События JWT авторизации + acme_api.event.authentication_success_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccessResponse } + + acme_api.event.authentication_failure_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_failure, method: onAuthenticationFailureResponse } + + acme_api.event.jwt_invalid_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_invalid, method: onJWTInvalid } + + acme_api.event.jwt_notfound_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_not_found, method: onJWTNotFound } + + acme_api.event.jwt_expired_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } \ No newline at end of file diff --git a/app/migrations/Version20240607055116.php b/app/migrations/Version20240607055116.php new file mode 100644 index 0000000..9baa894 --- /dev/null +++ b/app/migrations/Version20240607055116.php @@ -0,0 +1,42 @@ +addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE user_image_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, surname VARCHAR(255) NOT NULL, patronymic VARCHAR(255) NOT NULL, phone_number VARCHAR(255) DEFAULT NULL, confirm BOOLEAN NOT NULL, deleted BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + $this->addSql('CREATE TABLE user_image (id INT NOT NULL, related_user_id INT DEFAULT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_27FFFF0798771930 ON user_image (related_user_id)'); + $this->addSql('ALTER TABLE user_image ADD CONSTRAINT FK_27FFFF0798771930 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_id_seq" CASCADE'); + $this->addSql('DROP SEQUENCE user_image_id_seq CASCADE'); + $this->addSql('ALTER TABLE user_image DROP CONSTRAINT FK_27FFFF0798771930'); + $this->addSql('DROP TABLE "user"'); + $this->addSql('DROP TABLE user_image'); + } +} diff --git a/app/src/Controller/AuthController.php b/app/src/Controller/AuthController.php new file mode 100644 index 0000000..553613e --- /dev/null +++ b/app/src/Controller/AuthController.php @@ -0,0 +1,22 @@ +getResponse(); + } +} diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php new file mode 100644 index 0000000..47fb67c --- /dev/null +++ b/app/src/Entity/User.php @@ -0,0 +1,237 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + #[ORM\Column(length: 255)] + private ?string $name = null; + + #[ORM\Column(length: 255)] + private ?string $surname = null; + + #[ORM\Column(length: 255)] + private ?string $patronymic = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $phone_number = null; + + #[ORM\OneToOne(mappedBy: 'related_user', cascade: ['persist', 'remove'])] + private ?UserImage $image = null; + + #[ORM\Column] + private ?bool $confirm = null; + + #[ORM\Column] + private ?bool $deleted = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + if ($this->isDeleted()) { + $roles[] = 'ROLE_DELETED'; + } else if ($this->isConfirm()) { + $roles[] = 'ROLE_CONFIRMED'; + } else { + $roles[] = 'ROLE_NOT_CONFIRMED'; + } + + 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 getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getSurname(): ?string + { + return $this->surname; + } + + public function setSurname(string $surname): static + { + $this->surname = $surname; + + return $this; + } + + public function getPatronymic(): ?string + { + return $this->patronymic; + } + + public function setPatronymic(string $patronymic): static + { + $this->patronymic = $patronymic; + + return $this; + } + + public function getPhoneNumber(): ?string + { + return $this->phone_number; + } + + public function setPhoneNumber(?string $phone_number): static + { + $this->phone_number = $phone_number; + + return $this; + } + + public function getImage(): ?UserImage + { + return $this->image; + } + + public function setImage(?UserImage $image): static + { + // unset the owning side of the relation if necessary + if ($image === null && $this->image !== null) { + $this->image->setRelatedUser(null); + } + + // set the owning side of the relation if necessary + if ($image !== null && $image->getRelatedUser() !== $this) { + $image->setRelatedUser($this); + } + + $this->image = $image; + + return $this; + } + + public function isConfirm(): ?bool + { + return $this->confirm; + } + + public function setConfirm(bool $confirm): static + { + $this->confirm = $confirm; + + return $this; + } + + public function isDeleted(): ?bool + { + return $this->deleted; + } + + public function setDeleted(bool $deleted): static + { + $this->deleted = $deleted; + + return $this; + } + + public function getFullName(): string + { + return $this->getSurname() . ' ' . $this->getName() . ' ' . $this->getPatronymic() ?: ''; + } +} diff --git a/app/src/Entity/UserImage.php b/app/src/Entity/UserImage.php new file mode 100644 index 0000000..439f120 --- /dev/null +++ b/app/src/Entity/UserImage.php @@ -0,0 +1,80 @@ +id; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(string $path): static + { + $this->path = $path; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } +} diff --git a/app/src/Listeners/JwtListener.php b/app/src/Listeners/JwtListener.php new file mode 100644 index 0000000..625ee5f --- /dev/null +++ b/app/src/Listeners/JwtListener.php @@ -0,0 +1,92 @@ +getData(); + $user = $event->getUser(); + + if (!$user instanceof User) { + return; + } + + if ($user->isDeleted()) { + $response = new ApiResponse(); + $response->addError('Пользователь удален'); + } else { + $response = new TokenResponse(); + $response->setToken($data['token']); + $response->addMessage('Здравствуйте, ' . $user->getFullName()); + } + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + $event->setData($data); + } + + /** + * @param AuthenticationFailureEvent $event + */ + public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event): void + { + $response = new ApiResponse(); + $response->addError('Неверный email или пароль'); + $event->setResponse($response); + } + + /** + * @param JWTInvalidEvent $event + */ + public function onJWTInvalid(JWTInvalidEvent $event): void + { + $response = new ApiResponse(); + $response->addError('Неверный токен авторизации'); + $response->setStatusCode(Response::HTTP_FORBIDDEN); + + $event->setResponse($response); + } + + /** + * @param JWTNotFoundEvent $event + */ + public function onJWTNotFound(JWTNotFoundEvent $event): void + { + $response = new ApiResponse(); + $response->addError('Отсутствует токен'); + $response->setStatusCode(Response::HTTP_FORBIDDEN); + + $event->setResponse($response); + } + + /** + * @param JWTExpiredEvent $event + */ + public function onJWTExpired(JWTExpiredEvent $event): void + { + $response = new ApiResponse(); + $response->addError('Срок действия вашего токена истек, пожалуйста, обновите его'); + $response->setStatusCode(Response::HTTP_FORBIDDEN); + + $event->setResponse($response); + } +} \ No newline at end of file diff --git a/app/src/Repository/UserImageRepository.php b/app/src/Repository/UserImageRepository.php new file mode 100644 index 0000000..2c67991 --- /dev/null +++ b/app/src/Repository/UserImageRepository.php @@ -0,0 +1,43 @@ + + */ +class UserImageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserImage::class); + } + + // /** + // * @return UserImage[] Returns an array of UserImage 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): ?UserImage + // { + // 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 new file mode 100644 index 0000000..53ebb8c --- /dev/null +++ b/app/src/Repository/UserRepository.php @@ -0,0 +1,86 @@ + + */ +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(); + } + + /** + * Поиск пользователя по Email или номеру телефона + * + * @param string|null $sEmail + * @param string|null $sPhone + * @param int|null $iIgnoreId + * + * @return User|null + */ + public function findOneByUniq(?string $sEmail = null, ?string $sPhone = null, ?int $iIgnoreId = null): ?User + { + $oQuery = $this->createQueryBuilder('u'); + if (!empty($sEmail)) { + $oQuery->orWhere('u.email = :email')->setParameter('email', $sEmail); + } + if (!empty($sPhone)) { + $oQuery->orWhere('u.phone_number = :phone_number')->setParameter('phone_number', $sPhone); + } + if (!empty($iIgnoreId)) { + $oQuery->andWhere('u.id != :id')->setParameter('id', $iIgnoreId); + } + return $oQuery + ->getQuery() + ->getOneOrNullResult(); + } + + // /** + // * @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/app/src/Response/ApiResponse.php b/app/src/Response/ApiResponse.php new file mode 100644 index 0000000..87b5533 --- /dev/null +++ b/app/src/Response/ApiResponse.php @@ -0,0 +1,97 @@ +setResult(); + } + + /** + * Добавление ошибки + * + * @param string $message + * + * @return self + */ + public function addError(string $message): self + { + $this->status = false; + return $this->addMessage($message); + } + + /** + * Добавление ошибок + * + * @param array $errors + * + * @return $this + */ + public function addErrors(array $errors): self + { + $this->status = false; + foreach ($errors as $error) { + $this->addError($error); + } + + return $this; + } + + /** + * Добавление сообщения + * + * @param string $message + * + * @return self + */ + public function addMessage(string $message): self + { + $this->messages[] = $message; + return $this->setResult(); + } + + /** + * Запись контента ответа + * + * @param array|null $responseData + * + * @return void + */ + public function setResponseData(?array $responseData): void + { + $this->responseData = $responseData; + } + + /** + * Установка результата + * + * @return self + */ + protected function setResult(): self + { + $result = [ + 'status' => $this->status, + ]; + + if (!empty($this->responseData)) { + $result['data'] = $this->responseData; + } + if (!isset($result['data'])) { + $result['message'] = implode(', ', $this->messages); + } + + return $this->setData($result); + } +} \ No newline at end of file diff --git a/app/src/Response/TokenResponse.php b/app/src/Response/TokenResponse.php new file mode 100644 index 0000000..a941d6c --- /dev/null +++ b/app/src/Response/TokenResponse.php @@ -0,0 +1,14 @@ +setResponseData([ + 'token' => $token, + ]); + return $this; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/ActionServiceInterface.php b/app/src/Service/Action/ActionServiceInterface.php new file mode 100644 index 0000000..cec8a68 --- /dev/null +++ b/app/src/Service/Action/ActionServiceInterface.php @@ -0,0 +1,14 @@ +responseService = $baseResponseService; + } + + public function getResponse(): ApiResponse + { + if ($this->validate()) { + $this->runAction(); + } + + if ($this->responseService) { + return $this->responseService->getResponse(); + } + + $response = new ApiResponse(); + $response->addError('Ошибка получения ответа'); + return $response; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/None.php b/app/src/Service/Action/Classes/None.php new file mode 100644 index 0000000..d75a862 --- /dev/null +++ b/app/src/Service/Action/Classes/None.php @@ -0,0 +1,22 @@ +responseService) { + $this->responseService->getResponse()->addError('Действие не выбрано'); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/Register.php b/app/src/Service/Action/Classes/Register.php new file mode 100644 index 0000000..f6ec013 --- /dev/null +++ b/app/src/Service/Action/Classes/Register.php @@ -0,0 +1,109 @@ +createUser(); + if ($user !== null) { + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($user->getEmail(), $user->getPhoneNumber()); + + if ($userExists) { + $this->profileResponse->getResponse()->addError('Пользователь уже существует'); + } else { + try { + $user->setDeleted(false); + $user->setConfirm(false); + $hashedPassword = $this->passwordHasher->hashPassword( + $user, + $this->registerDto->getClass()->password ?: '' + ); + $user->setPassword($hashedPassword); + + $em = $this->doctrine->getManager(); + + $em->persist($user); + $em->flush(); + $this->profileResponse->getResponse()->addMessage('Пользователь зарегистрирован'); + } catch (\Exception $exception) { + $this->profileResponse->getResponse()->addError('Ошибка регистрации пользователя'); + } + + } + } + } + + /** + * Валидация + * + * @return bool + */ + public function validate(): bool + { + return $this->registerDto->validate($this->profileResponse); + } + + /** + * Создание пользователя из Dto + * + * @return User|null + */ + private function createUser(): ?User + { + $user = null; + + $data = $this->registerDto->toArray(); + + if ($data) { + $user = new User(); + + $reflectionClass = new ReflectionClass($user); + foreach ($reflectionClass->getProperties() as $property) { + $type = $property->getType(); + if (isset($data[$property->getName()])) { + $sValue = $data[$property->getName()] ?: null; + if ($sValue !== null && $type !== null && ($type->getName() !== 'array')) { + $property->setValue($user, $sValue); + } + } + } + } else { + $this->profileResponse->getResponse()->addError('Ошибка получения данных'); + } + + return $user; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/BaseDto.php b/app/src/Service/Dto/BaseDto.php new file mode 100644 index 0000000..7cbf7f5 --- /dev/null +++ b/app/src/Service/Dto/BaseDto.php @@ -0,0 +1,93 @@ +request = $requestStack->getCurrentRequest(); + } + } + + + /** + * Получение класса Dto + * + * @return DtoServiceInterface|null + */ + public function getClass(): ?DtoServiceInterface + { + if ($this->request) { + $normalizer = new ObjectNormalizer( + null, + new CamelCaseToSnakeCaseNameConverter(), + null, + new ReflectionExtractor() + ); + $serializer = new Serializer( + [$normalizer, new DateTimeNormalizer()], + [new JsonEncoder()] + ); + return $serializer->deserialize($this->request->getContent(), static::class, 'json'); + } + + return null; + } + + public function toArray(): ?array + { + try { + $oNormalizer = 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); + } catch (\Exception $oException) { + return null; + } + } + + /** + * Валидация Dto + * + * @param ResponseServiceInterface $response + * + * @return bool + */ + public function validate(ResponseServiceInterface $response): bool + { + $apiResponse = $response->getResponse(); + + $bValid = true; + $aErrors = $this->validator->validate($this->getClass()); + if (count($aErrors) > 0) { + foreach ($aErrors as $error) { + $apiResponse->addError($error->getMessage()); + } + $bValid = false; + } + return $bValid; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/Classes/ChangePasswordDto.php b/app/src/Service/Dto/Classes/ChangePasswordDto.php new file mode 100644 index 0000000..f15145b --- /dev/null +++ b/app/src/Service/Dto/Classes/ChangePasswordDto.php @@ -0,0 +1,28 @@ +response = new ApiResponse(); + } + + public function getResponse(): ApiResponse + { + return $this->response; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/ProfileResponse.php b/app/src/Service/Response/Classes/ProfileResponse.php new file mode 100644 index 0000000..384f3d0 --- /dev/null +++ b/app/src/Service/Response/Classes/ProfileResponse.php @@ -0,0 +1,14 @@ +getObject(); + if ($object) { + if ($object->password !== $object->repeatPassword) { + $context->buildViolation('Повтор пароля не совпадает') + ->addViolation(); + } + } + } + + /** + * Проверка на совпадение нового пароля и старого + * + * @param mixed $value + * @param ExecutionContextInterface $context + * @param mixed $payload + * + * @return void + */ + public static function validateNewPassword(mixed $value, ExecutionContextInterface $context, mixed $payload): void + { + $object = $context->getObject(); + if ($object) { + if ($object->password === $object->oldPassword) { + $context->buildViolation('Новый пароль не должен совпадать со старым') + ->addViolation(); + } + } + } + + /** + * Проверка пароля + * + * @param mixed $value + * @param ExecutionContextInterface $context + * @param mixed $payload + * + * @return void + */ + public static function validatePassword(mixed $value, ExecutionContextInterface $context, mixed $payload): void + { + $reg = '/(?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/'; + + $object = $context->getObject(); + + if ($object) { + if (!preg_match($reg, $object->password)) { + $context->buildViolation('Пароль должен содержать строчные и прописные латинские буквы, цифры, спецсимволы. Минимум 8 символов') + ->addViolation(); + } + } + } +} \ No newline at end of file -- GitLab From f572d8350f0cb8048ae3543fe1f29e9ca96bb9b5 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Wed, 19 Jun 2024 12:46:08 +0500 Subject: [PATCH 2/7] =?UTF-8?q?kafka=20(not=20custom)=20=D1=81onfirmation?= =?UTF-8?q?=20of=20registration=20profile=20delete=20&=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 9 + Makefile | 35 ++- app/.env.example | 8 + app/composer.json | 2 + app/composer.lock | 91 +++++- app/config/packages/messenger.yaml | 41 +++ app/config/packages/security.yaml | 7 + app/config/services.yaml | 32 ++ app/migrations/Version20240614064759.php | 37 +++ app/src/Controller/AuthController.php | 18 +- app/src/Controller/ProfileController.php | 44 +++ app/src/Entity/User.php | 39 +++ app/src/Entity/UserCode.php | 66 +++++ app/src/Entity/UserImage.php | 13 + app/src/Kafka/Transport/Connection.php | 274 ++++++++++++++++++ app/src/Kafka/Transport/KafkaFactory.php | 46 +++ app/src/Kafka/Transport/KafkaOption.php | 202 +++++++++++++ app/src/Kafka/Transport/KafkaReceiver.php | 64 ++++ app/src/Kafka/Transport/KafkaSender.php | 37 +++ app/src/Kafka/Transport/KafkaTransport.php | 58 ++++ .../Kafka/Transport/KafkaTransportFactory.php | 30 ++ app/src/Listeners/AccessDeniedListener.php | 34 +++ app/src/Listeners/CodeListener.php | 63 ++++ app/src/Messenger/Handler/MessageHandler.php | 51 ++++ app/src/Messenger/Message/SendMessage.php | 76 +++++ app/src/Repository/UserCodeRepository.php | 44 +++ app/src/Response/ApiResponse.php | 10 + .../Action/Classes/CheckRecoveryCode.php | 71 +++++ .../Action/Classes/CheckRegisterCode.php | 76 +++++ .../Service/Action/Classes/DeleteProfile.php | 56 ++++ app/src/Service/Action/Classes/GetProfile.php | 44 +++ .../Action/Classes/RecoveryProfile.php | 60 ++++ app/src/Service/Action/Classes/Register.php | 26 +- .../Action/Classes/SendRegisterCode.php | 60 ++++ app/src/Service/Dto/BaseDto.php | 40 ++- .../Service/Dto/Classes/RecoveryCodeDto.php | 31 ++ app/src/Service/Dto/Classes/RecoveryDto.php | 22 ++ .../Service/Dto/Classes/RegisterCodeDto.php | 18 ++ .../Service/Response/BaseResponseService.php | 4 +- .../Response/Classes/ProfileResponse.php | 14 - .../Classes/Code/RecoveryCodeSendService.php | 23 ++ .../Classes/Code/RegisterCodeSendService.php | 22 ++ .../Service/Send/Classes/CodeSendService.php | 127 ++++++++ app/src/Service/Send/SendService.php | 86 ++++++ app/src/Service/Send/SendServiceInterface.php | 8 + app/src/Validators/UserValidator.php | 28 ++ app/symfony.lock | 12 + compose.yaml | 16 +- docker/app/Dockerfile | 2 + 49 files changed, 2224 insertions(+), 53 deletions(-) create mode 100644 app/config/packages/messenger.yaml create mode 100644 app/migrations/Version20240614064759.php create mode 100644 app/src/Controller/ProfileController.php create mode 100644 app/src/Entity/UserCode.php create mode 100644 app/src/Kafka/Transport/Connection.php create mode 100644 app/src/Kafka/Transport/KafkaFactory.php create mode 100644 app/src/Kafka/Transport/KafkaOption.php create mode 100644 app/src/Kafka/Transport/KafkaReceiver.php create mode 100644 app/src/Kafka/Transport/KafkaSender.php create mode 100644 app/src/Kafka/Transport/KafkaTransport.php create mode 100644 app/src/Kafka/Transport/KafkaTransportFactory.php create mode 100644 app/src/Listeners/AccessDeniedListener.php create mode 100644 app/src/Listeners/CodeListener.php create mode 100644 app/src/Messenger/Handler/MessageHandler.php create mode 100644 app/src/Messenger/Message/SendMessage.php create mode 100644 app/src/Repository/UserCodeRepository.php create mode 100644 app/src/Service/Action/Classes/CheckRecoveryCode.php create mode 100644 app/src/Service/Action/Classes/CheckRegisterCode.php create mode 100644 app/src/Service/Action/Classes/DeleteProfile.php create mode 100644 app/src/Service/Action/Classes/GetProfile.php create mode 100644 app/src/Service/Action/Classes/RecoveryProfile.php create mode 100644 app/src/Service/Action/Classes/SendRegisterCode.php create mode 100644 app/src/Service/Dto/Classes/RecoveryCodeDto.php create mode 100644 app/src/Service/Dto/Classes/RecoveryDto.php create mode 100644 app/src/Service/Dto/Classes/RegisterCodeDto.php delete mode 100644 app/src/Service/Response/Classes/ProfileResponse.php create mode 100644 app/src/Service/Send/Classes/Code/RecoveryCodeSendService.php create mode 100644 app/src/Service/Send/Classes/Code/RegisterCodeSendService.php create mode 100644 app/src/Service/Send/Classes/CodeSendService.php create mode 100644 app/src/Service/Send/SendService.php create mode 100644 app/src/Service/Send/SendServiceInterface.php create mode 100644 app/src/Validators/UserValidator.php diff --git a/.env.example b/.env.example index 07265fe..6cbde90 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,12 @@ USER_ID=1000 # IDE XDEBUG_IDE_KEY=myproject + +# Redis +REDIS_PORT=6379 + +# Kafka/zookeeper +ZOOKEEPER_CLIENT_PORT=2181 +ZOOKEEPER_PORT=22181 +KAFKA_BROKER_ID=1 +KAFKA_PORT=29092 \ No newline at end of file diff --git a/Makefile b/Makefile index dcbbadc..725778c 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,15 @@ COMPOSE_PREFIX_CMD := DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 COMMAND ?= /bin/sh +DC=docker-compose +KAFKA_SERVERS=kafka:29092 +KAFKA_CONTAINER=kafka +EXEC_KAFKA=$(COMPOSE_PREFIX_CMD) $(DC) exec $(KAFKA_CONTAINER) + # -------------------------- .PHONY: deploy up build-up build down start stop logs images ps command \ - command-root shell-root shell restart rm help + command-root shell-root shell shell-kafka create-kafka-topic restart rm help deploy: ## Start using Prod Image in Prod Mode ${COMPOSE_PREFIX_CMD} docker compose -f compose.prod.yaml up --build -d @@ -52,6 +57,12 @@ shell-root: ## Enter container shell as root shell: ## Enter container shell @${COMPOSE_PREFIX_CMD} docker compose exec app /bin/sh +shell-kafka: ## Run bash shell in kafka container. + @${COMPOSE_PREFIX_CMD} docker compose exec kafka /bin/sh + +create-kafka-topic: ## Create kafka topic + $(MAKE) topic-create send_topic + restart: ## Restart container @${COMPOSE_PREFIX_CMD} docker compose restart @@ -64,4 +75,24 @@ clear: help: ## Show this help. @echo "\n\nMake Application Docker Images and Containers using Docker-Compose files" @echo "Make sure you are using \033[0;32mDocker Version >= 20.1\033[0m & \033[0;32mDocker-Compose >= 1.27\033[0m " - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m ENV= (default: dev)\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-12s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) \ No newline at end of file + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m ENV= (default: dev)\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-12s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + +.PHONY: topics topic topic-create producer-create consumer-groups consumer-group + +topics: ## Display list of topics + $(EXEC_KAFKA) kafka-topics --list --bootstrap-server $(KAFKA_SERVERS) + +topic: ## Describe existing topic + $(EXEC_KAFKA) kafka-topics --describe --bootstrap-server $(KAFKA_SERVERS) --topic $(filter-out $@,$(MAKECMDGOALS)) + +topic-create: ## Create new topic + $(EXEC_KAFKA) kafka-topics --create --bootstrap-server $(KAFKA_SERVERS) --topic $(filter-out $@,$(MAKECMDGOALS)) + +producer-create: ## Create a topic producer + $(EXEC_KAFKA) kafka-console-producer --bootstrap-server $(KAFKA_SERVERS) --topic $(filter-out $@,$(MAKECMDGOALS)) + +consumer-groups: ## Display list of consumer group + $(EXEC_KAFKA) kafka-consumer-groups --list --bootstrap-server $(KAFKA_SERVERS) + +consumer-group: ## Describe existing consumer group + $(EXEC_KAFKA) kafka-consumer-groups --describe --bootstrap-server $(KAFKA_SERVERS) --group $(filter-out $@,$(MAKECMDGOALS)) \ No newline at end of file diff --git a/app/.env.example b/app/.env.example index c9363a7..1b35983 100644 --- a/app/.env.example +++ b/app/.env.example @@ -13,3 +13,11 @@ JWT_PASSPHRASE= MAILER_ADDRESS= MAILER_DSN=smtp://user:pass@smtp.example.com:port ###< symfony/mailer ### + +CODE_TTL=300 +CONFIRM_TYPE=EMAIL + +###> symfony/messenger ### +MESSENGER_TRANSPORT_DSN=kafka:// +KAFKA_BROKERS=kafka:9092 +###< symfony/messenger ### \ No newline at end of file diff --git a/app/composer.json b/app/composer.json index 2677425..6ae3ba1 100644 --- a/app/composer.json +++ b/app/composer.json @@ -7,6 +7,7 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "ext-rdkafka": "*", "doctrine/dbal": "^3", "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", @@ -19,6 +20,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "7.0.*", "symfony/mailer": "7.0.*", + "symfony/messenger": "7.0.*", "symfony/mime": "7.0.*", "symfony/runtime": "7.0.*", "symfony/security-bundle": "7.0.*", diff --git a/app/composer.lock b/app/composer.lock index 0e49da8..f3c8f9a 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9ef02607d8522e77f2ce17f078546adb", + "content-hash": "b38b50076633f562508d6896c57850ce", "packages": [ { "name": "doctrine/cache", @@ -3381,6 +3381,92 @@ ], "time": "2024-05-31T14:55:39+00:00" }, + { + "name": "symfony/messenger", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "ed7bccfe31e7f0bdb5b101f48b6027622a7a48cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/ed7bccfe31e7f0bdb5b101f48b6027622a7a48cb", + "reference": "ed7bccfe31e7f0bdb5b101f48b6027622a7a48cb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:55:39+00:00" + }, { "name": "symfony/mime", "version": "v7.0.8", @@ -6351,7 +6437,8 @@ "platform": { "php": ">=8.2", "ext-ctype": "*", - "ext-iconv": "*" + "ext-iconv": "*", + "ext-rdkafka": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/app/config/packages/messenger.yaml b/app/config/packages/messenger.yaml new file mode 100644 index 0000000..6dc7da1 --- /dev/null +++ b/app/config/packages/messenger.yaml @@ -0,0 +1,41 @@ +framework: + messenger: + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. + # failure_transport: failed + + transports: + send_transport: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + metadata.broker.list: '%env(KAFKA_BROKERS)%' + security.protocol: 'plaintext' + group.id: 'my-group-id' + auto.offset.reset: 'earliest' + enable.partition.eof: 'true' + message.send.max.retries: 5 + producer_message_flags_block: false + + producer_topic: 'send_topic' + consumer_topics: + - 'send_topic' + + # https://symfony.com/doc/current/messenger.html#transport-configuration + # async: '%env(MESSENGER_TRANSPORT_DSN)%' + # failed: 'doctrine://default?queue_name=failed' + # sync: 'sync://' + + routing: + 'App\Messenger\Message\SendMessage': send_transport + # Route your messages to the transports + # 'App\Message\YourMessage': async + + serializer: + default_serializer: messenger.transport.symfony_serializer + +# when@test: +# framework: +# messenger: +# transports: +# # replace with your transport name here (e.g., my_transport: 'in-memory://') +# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test +# async: 'in-memory://' diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index 670446f..92236a8 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -31,6 +31,7 @@ security: main: lazy: true provider: app_user_provider + access_denied_handler: App\Listeners\AccessDeniedListener # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -42,7 +43,13 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } + - { path: ^/api/register, roles: PUBLIC_ACCESS } + - { path: ^/api/register/send, roles: ROLE_USER } + - { path: ^/api/register/check, roles: ROLE_USER } + + - { path: ^/api/profile/recovery, roles: PUBLIC_ACCESS } + - { path: ^/api/profile/recovery/check, roles: PUBLIC_ACCESS } - { 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 8c127d8..0a7f6ed 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -4,6 +4,9 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + confirm_type: '%env(CONFIRM_TYPE)%' + code_ttl: '%env(CODE_TTL)%' + from_email: '%env(MAILER_ADDRESS)%' services: # default configuration for services in *this* file @@ -23,12 +26,30 @@ services: # Сервисы действий App\Service\Action\ActionServiceInterface $registerService: '@App\Service\Action\Classes\Register' + App\Service\Action\ActionServiceInterface $profileService: '@App\Service\Action\Classes\GetProfile' + + App\Service\Action\ActionServiceInterface $deleteProfileService: '@App\Service\Action\Classes\DeleteProfile' + + App\Service\Action\ActionServiceInterface $recoveryProfileService: '@App\Service\Action\Classes\RecoveryProfile' + + App\Service\Action\ActionServiceInterface $checkRegisterService: '@App\Service\Action\Classes\CheckRegisterCode' + + App\Service\Action\ActionServiceInterface $checkRecoveryService: '@App\Service\Action\Classes\CheckRecoveryCode' + + App\Service\Action\ActionServiceInterface $sendRegisterService: '@App\Service\Action\Classes\SendRegisterCode' + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' # Сервисы Dto App\Service\Dto\DtoServiceInterface $registerDto: '@App\Service\Dto\Classes\RegisterDto' + App\Service\Dto\DtoServiceInterface $registerCodeDto: '@App\Service\Dto\Classes\RegisterCodeDto' + + App\Service\Dto\DtoServiceInterface $recoveryCodeDto: '@App\Service\Dto\Classes\RecoveryCodeDto' + + App\Service\Dto\DtoServiceInterface $recoveryDto: '@App\Service\Dto\Classes\RecoveryDto' + App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' @@ -38,6 +59,17 @@ services: App\Service\Response\ResponseServiceInterface: '@App\Service\Response\Classes\Response' + # Сервис отправки + App\Service\Send\SendService: + arguments: + $confirmType: '%confirm_type%' + $fromEmail: '%from_email%' + + App\Service\Send\SendServiceInterface $codeSendService: '@App\Service\Send\Classes\CodeSendService' + + App\Service\Send\SendServiceInterface $registerCodeSendService: '@App\Service\Send\Classes\Code\RegisterCodeSendService' + + App\Service\Send\SendServiceInterface $recoveryCodeSendService: '@App\Service\Send\Classes\Code\RecoveryCodeSendService' # События JWT авторизации acme_api.event.authentication_success_listener: diff --git a/app/migrations/Version20240614064759.php b/app/migrations/Version20240614064759.php new file mode 100644 index 0000000..1bf9346 --- /dev/null +++ b/app/migrations/Version20240614064759.php @@ -0,0 +1,37 @@ +addSql('CREATE SEQUENCE user_code_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE user_code (id INT NOT NULL, related_user_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_D947C5198771930 ON user_code (related_user_id)'); + $this->addSql('ALTER TABLE user_code ADD CONSTRAINT FK_D947C5198771930 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_code_id_seq CASCADE'); + $this->addSql('ALTER TABLE user_code DROP CONSTRAINT FK_D947C5198771930'); + $this->addSql('DROP TABLE user_code'); + } +} diff --git a/app/src/Controller/AuthController.php b/app/src/Controller/AuthController.php index 553613e..47b3178 100644 --- a/app/src/Controller/AuthController.php +++ b/app/src/Controller/AuthController.php @@ -2,11 +2,9 @@ namespace App\Controller; -use App\Response\ApiResponse; use App\Service\Action\ActionServiceInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; #[Route('/api', name: 'api_')] @@ -19,4 +17,20 @@ class AuthController extends AbstractController { return $registerService->getResponse(); } + + #[Route('/register/send', name: 'register_send', methods: ['GET'])] + public function sendRegisterCode( + ActionServiceInterface $sendRegisterService, + ): JsonResponse + { + return $sendRegisterService->getResponse(); + } + + #[Route('/register/check', name: 'register_check', methods: ['POST'])] + public function checkRegisterCode( + ActionServiceInterface $checkRegisterService + ): JsonResponse + { + return $checkRegisterService->getResponse(); + } } diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php new file mode 100644 index 0000000..51e5442 --- /dev/null +++ b/app/src/Controller/ProfileController.php @@ -0,0 +1,44 @@ +getResponse(); + } + + #[Route('/profile/delete', name: 'profile_delete', methods: ['GET'])] + public function deleteProfile( + ActionServiceInterface $deleteProfileService, + ): JsonResponse + { + return $deleteProfileService->getResponse(); + } + + #[Route('/profile/recovery', name: 'profile_recovery', methods: ['POST'])] + public function recoveryProfile( + ActionServiceInterface $recoveryProfileService, + ): JsonResponse + { + return $recoveryProfileService->getResponse(); + } + + #[Route('/profile/recovery/check', name: 'profile_recovery_check', methods: ['POST'])] + public function recoveryCodeProfile( + ActionServiceInterface $checkRecoveryService, + ): JsonResponse + { + return $checkRecoveryService->getResponse(); + } +} diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 47fb67c..1ec76ec 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -6,6 +6,8 @@ use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; 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\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserRepository::class)] @@ -54,11 +56,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column] private ?bool $deleted = null; + #[ORM\OneToOne(mappedBy: 'related_user', cascade: ['persist', 'remove'])] + private ?UserCode $register_code = null; + + #[Groups(['all'])] public function getId(): ?int { return $this->id; } + #[Groups(['all', 'profile', 'edit', 'card', 'detail'])] public function getEmail(): ?string { return $this->email; @@ -76,6 +83,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * * @see UserInterface */ + #[Ignore] public function getUserIdentifier(): string { return (string) $this->email; @@ -86,6 +94,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * * @return list */ + #[Groups(['all'])] public function getRoles(): array { $roles = $this->roles; @@ -115,6 +124,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * @see PasswordAuthenticatedUserInterface */ + #[Groups(['all'])] public function getPassword(): string { return $this->password; @@ -136,6 +146,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // $this->plainPassword = null; } + #[Groups(['all', 'profile', 'edit'])] public function getName(): ?string { return $this->name; @@ -148,6 +159,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile', 'edit'])] public function getSurname(): ?string { return $this->surname; @@ -160,6 +172,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile', 'edit'])] public function getPatronymic(): ?string { return $this->patronymic; @@ -172,6 +185,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile', 'edit'])] public function getPhoneNumber(): ?string { return $this->phone_number; @@ -184,6 +198,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile', 'edit'])] public function getImage(): ?UserImage { return $this->image; @@ -206,6 +221,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['all', 'profile'])] public function isConfirm(): ?bool { return $this->confirm; @@ -230,8 +246,31 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + #[Groups(['card', 'detail'])] public function getFullName(): string { return $this->getSurname() . ' ' . $this->getName() . ' ' . $this->getPatronymic() ?: ''; } + + public function getRegisterCode(): ?UserCode + { + return $this->register_code; + } + + public function setRegisterCode(?UserCode $register_code): static + { + // unset the owning side of the relation if necessary + if ($register_code === null && $this->register_code !== null) { + $this->register_code->setRelatedUser(null); + } + + // set the owning side of the relation if necessary + if ($register_code !== null && $register_code->getRelatedUser() !== $this) { + $register_code->setRelatedUser($this); + } + + $this->register_code = $register_code; + + return $this; + } } diff --git a/app/src/Entity/UserCode.php b/app/src/Entity/UserCode.php new file mode 100644 index 0000000..a338b61 --- /dev/null +++ b/app/src/Entity/UserCode.php @@ -0,0 +1,66 @@ +id; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): static + { + $this->code = $code; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } +} diff --git a/app/src/Entity/UserImage.php b/app/src/Entity/UserImage.php index 439f120..d58226d 100644 --- a/app/src/Entity/UserImage.php +++ b/app/src/Entity/UserImage.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Repository\UserImageRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: UserImageRepository::class)] class UserImage @@ -42,11 +43,21 @@ class UserImage return $this; } + #[Groups(['all', 'edit'])] public function getPath(): ?string { return $this->path; } + #[Groups(['all', 'profile', 'edit'])] + public function getPublicPath(): ?string + { + if ($this->path !== null) { + return str_replace('/app/public', '', $this->path); + } + return null; + } + public function setPath(string $path): static { $this->path = $path; @@ -54,6 +65,7 @@ class UserImage return $this; } + #[Groups(['all', 'profile', 'edit'])] public function getName(): ?string { return $this->name; @@ -66,6 +78,7 @@ class UserImage return $this; } + #[Groups(['all', 'edit'])] public function getType(): ?string { return $this->type; diff --git a/app/src/Kafka/Transport/Connection.php b/app/src/Kafka/Transport/Connection.php new file mode 100644 index 0000000..81a78e3 --- /dev/null +++ b/app/src/Kafka/Transport/Connection.php @@ -0,0 +1,274 @@ +> $kafkaConfig */ + public function __construct( + private readonly array $kafkaConfig, + private readonly string $transportName, + private readonly KafkaFactory $kafkaFactory = new KafkaFactory() + ) { + if (!\extension_loaded('rdkafka')) { + throw new LogicException(sprintf( + 'You cannot use the "%s" as the "rdkafka" extension is not installed.', __CLASS__ + )); + } + } + + public function setup(): void + { + if (!array_key_exists(self::BROKERS_LIST, $this->kafkaConfig)) { + throw new LogicException(sprintf( + 'The "%s" option is required for the Kafka Messenger transport "%s".', + self::BROKERS_LIST, + $this->transportName + )); + } + + if ( + !array_key_exists(self::CONSUMER_TOPICS_NAME, $this->kafkaConfig) && + !array_key_exists(self::PRODUCER_TOPIC_NAME, $this->kafkaConfig) + ) { + throw new LogicException(sprintf( + 'At least one of "%s" or "%s" options is required for the Kafka Messenger transport "%s".', + self::CONSUMER_TOPICS_NAME, + self::PRODUCER_TOPIC_NAME, + $this->transportName + )); + } + } + + /** @psalm-param array> $options */ + public static function builder(array $options = [], KafkaFactory $kafkaFactory = null): self + { + if (!array_key_exists(self::TRANSPORT_NAME, $options) || !is_string($options[self::TRANSPORT_NAME])) { + throw new RuntimeException('Transport name must be exist end type of string.'); + } + + self::optionsValidator($options, $options[self::TRANSPORT_NAME]); + + return new self($options, $options[self::TRANSPORT_NAME], $kafkaFactory ?? new KafkaFactory()); + } + + public function get(): \RdKafka\Message + { + if (!array_key_exists(self::GROUP_ID, $this->kafkaConfig)) { + throw new LogicException(sprintf( + 'The transport "%s" is not configured to consume messages because "%s" option is missing.', + $this->transportName, + self::GROUP_ID + )); + } + + $consumer = $this->kafkaFactory->createConsumer($this->kafkaConfig); + + try { + $consumer->subscribe($this->getTopics()); + + return $consumer->consume($this->getConsumerConsumeTimeout()); + } catch (\RdKafka\Exception $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + } + + /** @psalm-param array $headers */ + public function publish(string $body, array $headers = []): void + { + $producer = $this->kafkaFactory->createProducer($this->kafkaConfig); + + $topic = $producer->newTopic($this->getTopic()); + $topic->producev( + partition: $this->getPartitionId(), // todo: retrieve from stamp ? + msgflags: $this->getMessageFlags(), + payload: $body, + headers: $headers + ); + + $producer->poll($this->getProducerPollTimeout()); + $producer->flush($this->getProducerFlushTimeout()); + } + + /** @psalm-param array> $options */ + private static function optionsValidator(array $options, string $transportName): void + { + $invalidOptions = array_diff( + array_keys($options), + array_merge( + self::GLOBAL_OPTIONS, + array_keys( + array_merge(self::GLOBAL_OPTIONS, KafkaOption::consumer(), KafkaOption::producer()) + ) + ) + ); + + if (0 < \count($invalidOptions)) { + throw new LogicException(sprintf( + 'Invalid option(s) "%s" passed to the Kafka Messenger transport "%s".', + implode('", "', $invalidOptions), + $transportName + )); + } + } + + /** @psalm-return array */ + private function getTopics(): array + { + if (!array_key_exists(self::CONSUMER_TOPICS_NAME, $this->kafkaConfig)) { + throw new LogicException(sprintf( + 'The transport "%s" is not configured to consume messages because "%s" option is missing.', + $this->transportName, + self::CONSUMER_TOPICS_NAME + )); + } + + if (!is_array($this->kafkaConfig[self::CONSUMER_TOPICS_NAME])) { + throw new LogicException(sprintf( + 'The "%s" option type must be array, %s given in "%s" transport.', + self::CONSUMER_TOPICS_NAME, + gettype($this->kafkaConfig[self::CONSUMER_TOPICS_NAME]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::CONSUMER_TOPICS_NAME]; + } + + private function getConsumerConsumeTimeout(): int + { + if (!array_key_exists(self::CONSUMER_CONSUME_TIMEOUT_MS, $this->kafkaConfig)) { + return 10000; + } + + if (!is_int($this->kafkaConfig[self::CONSUMER_CONSUME_TIMEOUT_MS])) { + throw new LogicException(sprintf( + 'The "%s" option type must be integer, %s given in "%s" transport.', + self::CONSUMER_CONSUME_TIMEOUT_MS, + gettype($this->kafkaConfig[self::CONSUMER_CONSUME_TIMEOUT_MS]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::CONSUMER_CONSUME_TIMEOUT_MS]; + } + + private function getTopic(): string + { + if (!array_key_exists(self::PRODUCER_TOPIC_NAME, $this->kafkaConfig)) { + throw new LogicException(sprintf( + 'The transport "%s" is not configured to dispatch messages because "%s" option is missing.', + $this->transportName, + self::PRODUCER_TOPIC_NAME + )); + } + + if (!is_string($this->kafkaConfig[self::PRODUCER_TOPIC_NAME])) { + throw new LogicException(sprintf( + 'The "%s" option type must be string, %s given in "%s" transport.', + self::PRODUCER_TOPIC_NAME, + gettype($this->kafkaConfig[self::PRODUCER_TOPIC_NAME]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::PRODUCER_TOPIC_NAME]; + } + + private function getMessageFlags(): int + { + if (!array_key_exists(self::PRODUCER_MESSAGE_FLAGS_BLOCK, $this->kafkaConfig)) { + return 0; + } + + if (!is_bool($this->kafkaConfig[self::PRODUCER_MESSAGE_FLAGS_BLOCK])) { + throw new LogicException(sprintf( + 'The "%s" option type must be boolean, %s given in "%s" transport.', + self::PRODUCER_MESSAGE_FLAGS_BLOCK, + gettype($this->kafkaConfig[self::PRODUCER_MESSAGE_FLAGS_BLOCK]), + $this->transportName + )); + } + + return false === $this->kafkaConfig[self::PRODUCER_MESSAGE_FLAGS_BLOCK] ? 0 : RD_KAFKA_MSG_F_BLOCK; + } + + private function getPartitionId(): int + { + if (!array_key_exists(self::PRODUCER_PARTITION_ID_ASSIGNMENT, $this->kafkaConfig)) { + return RD_KAFKA_PARTITION_UA; + } + + if (!is_int($this->kafkaConfig[self::PRODUCER_PARTITION_ID_ASSIGNMENT])) { + throw new LogicException(sprintf( + 'The "%s" option type must be integer, %s given in "%s" transport.', + self::PRODUCER_PARTITION_ID_ASSIGNMENT, + gettype($this->kafkaConfig[self::PRODUCER_PARTITION_ID_ASSIGNMENT]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::PRODUCER_PARTITION_ID_ASSIGNMENT]; + } + + private function getProducerPollTimeout(): int + { + if (!array_key_exists(self::PRODUCER_POLL_TIMEOUT_MS, $this->kafkaConfig)) { + return 0; + } + + if (!is_int($this->kafkaConfig[self::PRODUCER_POLL_TIMEOUT_MS])) { + throw new LogicException(sprintf( + 'The "%s" option type must be integer, %s given in "%s" transport.', + self::PRODUCER_POLL_TIMEOUT_MS, + gettype($this->kafkaConfig[self::PRODUCER_POLL_TIMEOUT_MS]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::PRODUCER_POLL_TIMEOUT_MS]; + } + + private function getProducerFlushTimeout(): int + { + if (!array_key_exists(self::PRODUCER_FLUSH_TIMEOUT_MS, $this->kafkaConfig)) { + return 10000; + } + + if (!is_int($this->kafkaConfig[self::PRODUCER_FLUSH_TIMEOUT_MS])) { + throw new LogicException(sprintf( + 'The "%s" option type must be integer, %s given in "%s" transport.', + self::PRODUCER_FLUSH_TIMEOUT_MS, + gettype($this->kafkaConfig[self::PRODUCER_FLUSH_TIMEOUT_MS]), + $this->transportName + )); + } + + return $this->kafkaConfig[self::PRODUCER_FLUSH_TIMEOUT_MS]; + } +} diff --git a/app/src/Kafka/Transport/KafkaFactory.php b/app/src/Kafka/Transport/KafkaFactory.php new file mode 100644 index 0000000..ea4df30 --- /dev/null +++ b/app/src/Kafka/Transport/KafkaFactory.php @@ -0,0 +1,46 @@ +> $kafkaConfig */ + public function createConsumer(array $kafkaConfig): KafkaConsumer + { + $conf = new Conf(); + + foreach ($kafkaConfig as $key => $value) { + if (array_key_exists($key, array_merge(KafkaOption::global(), KafkaOption::consumer()))) { + if (!is_string($value)) { + // todo: warning + continue; + } + $conf->set($key, $value); + } + } + + return new KafkaConsumer($conf); + } + + /** @psalm-param array> $kafkaConfig */ + public function createProducer(array $kafkaConfig): Producer + { + $conf = new Conf(); + + foreach ($kafkaConfig as $key => $value) { + if (array_key_exists($key, array_merge(KafkaOption::global(), KafkaOption::producer()))) { + if (!is_string($value)) { + // todo: warning + continue; + } + $conf->set($key, $value); + } + } + + return new Producer($conf); + } +} diff --git a/app/src/Kafka/Transport/KafkaOption.php b/app/src/Kafka/Transport/KafkaOption.php new file mode 100644 index 0000000..a7bbdf6 --- /dev/null +++ b/app/src/Kafka/Transport/KafkaOption.php @@ -0,0 +1,202 @@ + */ + public static function consumer(): array + { + return array_merge( + self::global(), + [ + 'group.id' => 'C', + 'group.instance.id' => 'C', + 'partition.assignment.strategy' => 'C', + 'session.timeout.ms' => 'C', + 'heartbeat.interval.ms' => 'C', + 'group.protocol.type' => 'C', + 'coordinator.query.interval.ms' => 'C', + 'max.poll.interval.ms' => 'C', + 'enable.auto.commit' => 'C', + 'auto.commit.interval.ms' => 'C', + 'enable.auto.offset.store' => 'C', + 'queued.min.messages' => 'C', + 'queued.max.messages.kbytes' => 'C', + 'fetch.wait.max.ms' => 'C', + 'fetch.message.max.bytes' => 'C', + 'max.partition.fetch.bytes' => 'C', + 'fetch.max.bytes' => 'C', + 'fetch.min.bytes' => 'C', + 'fetch.error.backoff.ms' => 'C', + 'offset.store.method' => 'C', + 'isolation.level' => 'C', + 'consume_cb' => 'C', + 'rebalance_cb' => 'C', + 'offset_commit_cb' => 'C', + 'enable.partition.eof' => 'C', + 'check.crcs' => 'C', + 'auto.commit.enable' => 'C', + 'auto.offset.reset' => 'C', + 'offset.store.path' => 'C', + 'offset.store.sync.interval.ms' => 'C', + 'consume.callback.max.messages' => 'C', + ] + ); + } + + /** @psalm-return array */ + public static function producer(): array + { + return array_merge( + self::global(), + [ + 'transactional.id' => 'P', + 'transaction.timeout.ms' => 'P', + 'enable.idempotence' => 'P', + 'enable.gapless.guarantee' => 'P', + 'queue.buffering.max.messages' => 'P', + 'queue.buffering.max.kbytes' => 'P', + 'queue.buffering.max.ms' => 'P', + 'linger.ms' => 'P', + 'message.send.max.retries' => 'P', + 'retries' => 'P', + 'retry.backoff.ms' => 'P', + 'queue.buffering.backpressure.threshold' => 'P', + 'compression.codec' => 'P', + 'compression.type' => 'P', + 'batch.num.messages' => 'P', + 'batch.size' => 'P', + 'delivery.report.only.error' => 'P', + 'dr_cb' => 'P', + 'dr_msg_cb' => 'P', + 'sticky.partitioning.linger.ms' => 'P', + 'request.required.acks' => 'P', + 'acks' => 'P', + 'request.timeout.ms' => 'P', + 'message.timeout.ms' => 'P', + 'delivery.timeout.ms' => 'P', + 'queuing.strategy' => 'P', + 'produce.offset.report' => 'P', + 'partitioner' => 'P', + 'partitioner_cb' => 'P', + 'msg_order_cmp' => 'P', + 'compression.level' => 'P', + ] + ); + } + + /** @psalm-return array */ + public static function global(): array + { + return [ + 'builtin.features' => '*', + 'client.id' => '*', + 'metadata.broker.list' => '*', + 'bootstrap.servers' => '*', + 'message.max.bytes' => '*', + 'message.copy.max.bytes' => '*', + 'receive.message.max.bytes' => '*', + 'max.in.flight.requests.per.connection' => '*', + 'max.in.flight' => '*', + 'topic.metadata.refresh.interval.ms' => '*', + 'metadata.max.age.ms' => '*', + 'topic.metadata.refresh.fast.interval.ms' => '*', + 'topic.metadata.refresh.fast.cnt' => '*', + 'topic.metadata.refresh.sparse' => '*', + 'topic.metadata.propagation.max.ms' => '*', + 'topic.blacklist' => '*', + 'debug' => '*', + 'socket.timeout.ms' => '*', + 'socket.blocking.max.ms' => '*', + 'socket.send.buffer.bytes' => '*', + 'socket.receive.buffer.bytes' => '*', + 'socket.keepalive.enable' => '*', + 'socket.nagle.disable' => '*', + 'socket.max.fails' => '*', + 'broker.address.ttl' => '*', + 'broker.address.family' => '*', + 'socket.connection.setup.timeout.ms' => '*', + 'connections.max.idle.ms' => '*', + 'reconnect.backoff.jitter.ms' => '*', + 'reconnect.backoff.ms' => '*', + 'reconnect.backoff.max.ms' => '*', + 'statistics.interval.ms' => '*', + 'enabled_events' => '*', + 'error_cb' => '*', + 'throttle_cb' => '*', + 'stats_cb' => '*', + 'log_cb' => '*', + 'log_level' => '*', + 'log.queue' => '*', + 'log.thread.name' => '*', + 'enable.random.seed' => '*', + 'log.connection.close' => '*', + 'background_event_cb' => '*', + 'socket_cb' => '*', + 'connect_cb' => '*', + 'closesocket_cb' => '*', + 'open_cb' => '*', + 'resolve_cb' => '*', + 'opaque' => '*', + 'default_topic_conf' => '*', + 'internal.termination.signal' => '*', + 'api.version.request' => '*', + 'api.version.request.timeout.ms' => '*', + 'api.version.fallback.ms' => '*', + 'broker.version.fallback' => '*', + 'allow.auto.create.topics' => '*', + 'security.protocol' => '*', + 'ssl.cipher.suites' => '*', + 'ssl.curves.list' => '*', + 'ssl.sigalgs.list' => '*', + 'ssl.key.location' => '*', + 'ssl.key.password' => '*', + 'ssl.key.pem' => '*', + 'ssl_key' => '*', + 'ssl.certificate.location' => '*', + 'ssl.certificate.pem' => '*', + 'ssl_certificate' => '*', + 'ssl.ca.location' => '*', + 'ssl.ca.pem' => '*', + 'ssl_ca' => '*', + 'ssl.ca.certificate.stores' => '*', + 'ssl.crl.location' => '*', + 'ssl.keystore.location' => '*', + 'ssl.keystore.password' => '*', + 'ssl.providers' => '*', + 'ssl.engine.location' => '*', + 'ssl.engine.id' => '*', + 'ssl_engine_callback_data' => '*', + 'enable.ssl.certificate.verification' => '*', + 'ssl.endpoint.identification.algorithm' => '*', + 'ssl.certificate.verify_cb' => '*', + 'sasl.mechanisms' => '*', + 'sasl.mechanism' => '*', + 'sasl.kerberos.service.name' => '*', + 'sasl.kerberos.principal' => '*', + 'sasl.kerberos.kinit.cmd' => '*', + 'sasl.kerberos.keytab' => '*', + 'sasl.kerberos.min.time.before.relogin' => '*', + 'sasl.username' => '*', + 'sasl.password' => '*', + 'sasl.oauthbearer.config' => '*', + 'enable.sasl.oauthbearer.unsecure.jwt' => '*', + 'oauthbearer_token_refresh_cb' => '*', + 'sasl.oauthbearer.method' => '*', + 'sasl.oauthbearer.client.id' => '*', + 'sasl.oauthbearer.client.secret' => '*', + 'sasl.oauthbearer.scope' => '*', + 'sasl.oauthbearer.extensions' => '*', + 'sasl.oauthbearer.token.endpoint.url' => '*', + 'plugin.library.paths' => '*', + 'interceptors' => '*', + 'client.rack' => '*', + ]; + } +} diff --git a/app/src/Kafka/Transport/KafkaReceiver.php b/app/src/Kafka/Transport/KafkaReceiver.php new file mode 100644 index 0000000..e48e1fc --- /dev/null +++ b/app/src/Kafka/Transport/KafkaReceiver.php @@ -0,0 +1,64 @@ +connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + /** @psalm-return array */ + public function get(): iterable + { + yield from $this->getEnvelope(); + } + + /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + public function ack(Envelope $envelope): void + { + // no ack method for kafka transport + } + + /** @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + public function reject(Envelope $envelope): void + { + // no reject method for kafka transport + } + + /** @psalm-return array */ + private function getEnvelope(): iterable + { + try { + $kafkaMessage = $this->connection->get(); + } catch (\RdKafka\Exception $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + + if (RD_KAFKA_RESP_ERR_NO_ERROR !== $kafkaMessage->err) { + switch ($kafkaMessage->err) { + case RD_KAFKA_RESP_ERR__PARTITION_EOF: // No more messages + case RD_KAFKA_RESP_ERR__TIMED_OUT: // Attempt to connect again + return; + default: + throw new TransportException($kafkaMessage->errstr(), $kafkaMessage->err); + } + } + + yield $this->serializer->decode([ + 'body' => $kafkaMessage->payload, + 'headers' => $kafkaMessage->headers, + ]); + } +} diff --git a/app/src/Kafka/Transport/KafkaSender.php b/app/src/Kafka/Transport/KafkaSender.php new file mode 100644 index 0000000..f50994b --- /dev/null +++ b/app/src/Kafka/Transport/KafkaSender.php @@ -0,0 +1,37 @@ +connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + public function send(Envelope $envelope): Envelope + { + $encodedMessage = $this->serializer->encode($envelope); + + try { + $this->connection->publish( + $encodedMessage['body'], + $encodedMessage['headers'] ?? [] + ); + } catch (\RdKafka\Exception $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + return $envelope; + } +} diff --git a/app/src/Kafka/Transport/KafkaTransport.php b/app/src/Kafka/Transport/KafkaTransport.php new file mode 100644 index 0000000..65a2343 --- /dev/null +++ b/app/src/Kafka/Transport/KafkaTransport.php @@ -0,0 +1,58 @@ +connection = $connection; + $this->serializer = $serializer ?? new PhpSerializer(); + } + + public function setup(): void + { + $this->connection->setup(); + } + + public function get(): iterable + { + return $this->getReceiver()->get(); + } + + public function ack(Envelope $envelope): void + { + $this->getReceiver()->ack($envelope); + } + + public function reject(Envelope $envelope): void + { + $this->getReceiver()->reject($envelope); + } + + public function send(Envelope $envelope): Envelope + { + return $this->getSender()->send($envelope); + } + + private function getReceiver(): KafkaReceiver + { + return $this->receiver ??= new KafkaReceiver($this->connection, $this->serializer); + } + + private function getSender(): KafkaSender + { + return $this->sender ??= new KafkaSender($this->connection, $this->serializer); + } +} diff --git a/app/src/Kafka/Transport/KafkaTransportFactory.php b/app/src/Kafka/Transport/KafkaTransportFactory.php new file mode 100644 index 0000000..2c1a449 --- /dev/null +++ b/app/src/Kafka/Transport/KafkaTransportFactory.php @@ -0,0 +1,30 @@ +> $options + */ + public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface + { + return new KafkaTransport(Connection::builder($options), $serializer); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @psalm-param array> $options + */ + public function supports(string $dsn, array $options): bool + { + return str_starts_with($dsn, 'kafka://'); + } +} diff --git a/app/src/Listeners/AccessDeniedListener.php b/app/src/Listeners/AccessDeniedListener.php new file mode 100644 index 0000000..8299af1 --- /dev/null +++ b/app/src/Listeners/AccessDeniedListener.php @@ -0,0 +1,34 @@ + ['onAccessException', 2], + ]; + } + + public function onAccessException(ExceptionEvent $event): void + { + $response = new ApiResponse(); + $response->setStatusCode(Response::HTTP_FORBIDDEN); + $response->addError('Доступ запрещен'); + + $exception = $event->getThrowable(); + if (!$exception instanceof AccessDeniedException) { + return; + } + $event->setResponse($response); + } +} \ No newline at end of file diff --git a/app/src/Listeners/CodeListener.php b/app/src/Listeners/CodeListener.php new file mode 100644 index 0000000..5b143df --- /dev/null +++ b/app/src/Listeners/CodeListener.php @@ -0,0 +1,63 @@ +checkCode($code, $args->getObjectManager()); + } + + public function preUpdate(UserCode $code, PreUpdateEventArgs $args): void + { + $this->checkCode($code, $args->getObjectManager()); + } + + /** + * Проверка кода и генерация кода + * + * @param UserCode $code + * @param ObjectManager $om + * + * @return void + * + * @throws RandomException + */ + public function checkCode(UserCode $code, ObjectManager $om): void + { + $user = $code->getRelatedUser(); + if ($user === null) { + $om->remove($code); + $om->flush(); + } else { + $date = $code->getDate(); + $needNewCode = false; + if ($date === null) { + $needNewCode = true; + } else { + $currentDate = new \DateTime(); + if ($currentDate->getTimestamp() >= $date->getTimestamp()) { + $needNewCode = true; + } + } + + if ($needNewCode) { + $newDate = new \DateTime(); + $newDate->setTimestamp($newDate->getTimestamp() + $_ENV['CODE_TTL'] ?: 300); + $code->setDate($newDate); + $code->setCode(sprintf('%06d', random_int(0, 999999))); + } + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Handler/MessageHandler.php b/app/src/Messenger/Handler/MessageHandler.php new file mode 100644 index 0000000..097a247 --- /dev/null +++ b/app/src/Messenger/Handler/MessageHandler.php @@ -0,0 +1,51 @@ +getSendType()) { + case 'EMAIL': + $mail = new Email(); + $mail + ->subject($message->getSubject()) + ->from($message->getFrom()) + ->to($message->getTo()) + ->html($message->getBody()); + try { + $this->mailer->send($mail); + } catch (\Exception $exception) { + throw new \Exception('Ошибка отправки письма'); + } + break; + case 'SMS': + throw new \Exception('Отправка СМС недоступна'); + break; + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Message/SendMessage.php b/app/src/Messenger/Message/SendMessage.php new file mode 100644 index 0000000..1510b5b --- /dev/null +++ b/app/src/Messenger/Message/SendMessage.php @@ -0,0 +1,76 @@ +sendType; + } + + public function setSendType(?string $sendType): self + { + $this->sendType = $sendType; + + return $this; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getBody(): string + { + return $this->body; + } + + public function setBody(string $body): self + { + $this->body = $body; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } +} \ No newline at end of file diff --git a/app/src/Repository/UserCodeRepository.php b/app/src/Repository/UserCodeRepository.php new file mode 100644 index 0000000..9869854 --- /dev/null +++ b/app/src/Repository/UserCodeRepository.php @@ -0,0 +1,44 @@ + + */ +class UserCodeRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserCode::class); + } + + // /** + // * @return UserCode[] Returns an array of UserCode 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): ?UserCode + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Response/ApiResponse.php b/app/src/Response/ApiResponse.php index 87b5533..9e36975 100644 --- a/app/src/Response/ApiResponse.php +++ b/app/src/Response/ApiResponse.php @@ -72,6 +72,16 @@ class ApiResponse extends JsonResponse public function setResponseData(?array $responseData): void { $this->responseData = $responseData; + + $this->setResult(); + } + + /** + * @return bool + */ + public function isSuccess(): bool + { + return $this->status; } /** diff --git a/app/src/Service/Action/Classes/CheckRecoveryCode.php b/app/src/Service/Action/Classes/CheckRecoveryCode.php new file mode 100644 index 0000000..fa1c5ff --- /dev/null +++ b/app/src/Service/Action/Classes/CheckRecoveryCode.php @@ -0,0 +1,71 @@ +recoveryCodeDto->getClass(); + /** @var User $userExists */ + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($dto->email, $dto->phoneNumber); + + if ($userExists) { + $currentDate = new \DateTime(); + $code = $dto->code; + $registerCode = $userExists->getRegisterCode(); + if ($registerCode === null) { + $this->response->getResponse()->addError('Код подтверждения не отправлен'); + } else { + if ($registerCodeDate = $registerCode->getDate()) { + if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { + try { + $userExists->setDeleted(false); + $em = $this->doctrine->getManager(); + $em->persist($userExists); + $em->remove($registerCode); + $em->flush(); + $this->response->getResponse()->addMessage('Профиль восстановлен'); + } catch (\Exception $exception) { + $this->response->getResponse()->addError('Ошибка восстановления профиля'); + } + } else { + $this->response->getResponse()->addError('Код недействителен'); + } + } else { + $this->response->getResponse()->addError('Код недействителен'); + } + } + } else { + $this->response->getResponse()->addError('Пользователь не найден'); + } + } + + public function validate(): bool + { + return $this->recoveryCodeDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/CheckRegisterCode.php b/app/src/Service/Action/Classes/CheckRegisterCode.php new file mode 100644 index 0000000..ed6f147 --- /dev/null +++ b/app/src/Service/Action/Classes/CheckRegisterCode.php @@ -0,0 +1,76 @@ +user = $security->getUser(); + parent::__construct($response); + } + + /** + * Подтверждение регистрации по коду + * + * @return void + */ + public function runAction(): void + { + $currentDate = new \DateTime(); + $code = $this->registerCodeDto->getClass()->code; + $registerCode = $this->user->getRegisterCode(); + if ($registerCode === null) { + $this->response->getResponse()->addError('Код подтверждения не отправлен'); + } else { + if ($registerCodeDate = $registerCode->getDate()) { + if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { + try { + $this->user->setConfirm(true); + $em = $this->doctrine->getManager(); + $em->persist($this->user); + $em->remove($registerCode); + $em->flush(); + $this->response->getResponse()->addMessage('Регистрация подтверждена'); + } catch (\Exception $exception) { + $this->response->getResponse()->addError('Ошибка подтверждения регистрации'); + } + } else { + $this->response->getResponse()->addError('Код недействителен'); + } + } else { + $this->response->getResponse()->addError('Код недействителен'); + } + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->getResponse()->addError('Вы не авторизованы'); + return false; + } + return $this->registerCodeDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/DeleteProfile.php b/app/src/Service/Action/Classes/DeleteProfile.php new file mode 100644 index 0000000..01f042c --- /dev/null +++ b/app/src/Service/Action/Classes/DeleteProfile.php @@ -0,0 +1,56 @@ +user = $security->getUser(); + parent::__construct($response); + } + + /** + * Деактивация учетной записи + * + * @return void + */ + public function runAction(): void + { + try { + $this->user->setDeleted(true); + $em = $this->doctrine->getManager(); + $em->persist($this->user); + $em->flush(); + $this->response->getResponse()->addMessage('Профиль удален'); + } catch (\Exception $exception) { + $this->response->getResponse()->addError('Ошибка удаления профиля'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->getResponse()->addError('Вы не авторизованы'); + return false; + } + + if ($this->user->isDeleted()) { + $this->response->getResponse()->addError('Профиль уже удален'); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetProfile.php b/app/src/Service/Action/Classes/GetProfile.php new file mode 100644 index 0000000..0c39923 --- /dev/null +++ b/app/src/Service/Action/Classes/GetProfile.php @@ -0,0 +1,44 @@ +user = $security->getUser(); + parent::__construct($response); + } + + + /** + * Получение профиля пользователя + * + * @return void + * + * @throws \JsonException + */ + public function runAction(): void + { + $serializedUser = $this->serializer->serialize($this->user, 'json', ['groups' => ['profile']]); + $this->response->getResponse()->setResponseData(json_decode($serializedUser, true, 512, JSON_THROW_ON_ERROR)); + } + + public function validate(): bool + { + return $this->user->isConfirm() && !$this->user->isDeleted(); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/RecoveryProfile.php b/app/src/Service/Action/Classes/RecoveryProfile.php new file mode 100644 index 0000000..9cbf1c2 --- /dev/null +++ b/app/src/Service/Action/Classes/RecoveryProfile.php @@ -0,0 +1,60 @@ +recoveryDto->getClass(); + /** @var User $userExists */ + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($dto->email, $dto->phoneNumber); + + if ($userExists !== null) { + if (!$userExists->isDeleted()) { + $this->response->getResponse()->addError('Профиль не удален'); + } else { + $this->recoveryCodeSendService->setUser($userExists); + $this->recoveryCodeSendService->setResponse($this->response); + $this->recoveryCodeSendService->send(); + } + } else { + $this->response->getResponse()->addError('Пользователь не найден'); + } + } + + public function validate(): bool + { + return $this->recoveryDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/Register.php b/app/src/Service/Action/Classes/Register.php index f6ec013..88eebcd 100644 --- a/app/src/Service/Action/Classes/Register.php +++ b/app/src/Service/Action/Classes/Register.php @@ -7,6 +7,10 @@ use App\Service\Action\BaseActionService; use App\Service\Dto\Classes\RegisterDto; use App\Service\Dto\DtoServiceInterface; use App\Service\Response\ResponseServiceInterface; +use App\Service\Send\Classes\Code\RegisterCodeSendService; +use App\Service\Send\Classes\CodeSendService; +use App\Service\Send\SendService; +use App\Service\Send\SendServiceInterface; use Doctrine\Persistence\ManagerRegistry; use ReflectionClass; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; @@ -15,18 +19,20 @@ class Register extends BaseActionService { /** * @param RegisterDto $registerDto - * @param ResponseServiceInterface $profileResponse + * @param ResponseServiceInterface $response * @param UserPasswordHasherInterface $passwordHasher * @param ManagerRegistry $doctrine + * @param RegisterCodeSendService $registerCodeSendService */ public function __construct( private DtoServiceInterface $registerDto, - private ResponseServiceInterface $profileResponse, + private ResponseServiceInterface $response, private UserPasswordHasherInterface $passwordHasher, private ManagerRegistry $doctrine, + private SendServiceInterface $registerCodeSendService, ) { - parent::__construct($profileResponse); + parent::__construct($response); } /** @@ -42,7 +48,7 @@ class Register extends BaseActionService ->findOneByUniq($user->getEmail(), $user->getPhoneNumber()); if ($userExists) { - $this->profileResponse->getResponse()->addError('Пользователь уже существует'); + $this->response->getResponse()->addError('Пользователь уже существует'); } else { try { $user->setDeleted(false); @@ -57,9 +63,13 @@ class Register extends BaseActionService $em->persist($user); $em->flush(); - $this->profileResponse->getResponse()->addMessage('Пользователь зарегистрирован'); + $this->response->getResponse()->addMessage('Пользователь зарегистрирован'); + + $this->registerCodeSendService->setUser($user); + $this->registerCodeSendService->setResponse($this->response); + $this->registerCodeSendService->send(); } catch (\Exception $exception) { - $this->profileResponse->getResponse()->addError('Ошибка регистрации пользователя'); + $this->response->getResponse()->addError('Ошибка регистрации пользователя'); } } @@ -73,7 +83,7 @@ class Register extends BaseActionService */ public function validate(): bool { - return $this->registerDto->validate($this->profileResponse); + return $this->registerDto->validate($this->response); } /** @@ -101,7 +111,7 @@ class Register extends BaseActionService } } } else { - $this->profileResponse->getResponse()->addError('Ошибка получения данных'); + $this->response->getResponse()->addError('Ошибка получения данных'); } return $user; diff --git a/app/src/Service/Action/Classes/SendRegisterCode.php b/app/src/Service/Action/Classes/SendRegisterCode.php new file mode 100644 index 0000000..d6d5d4c --- /dev/null +++ b/app/src/Service/Action/Classes/SendRegisterCode.php @@ -0,0 +1,60 @@ +user = $security->getUser(); + parent::__construct($response); + } + + + /** + * Отправка кода подтверждения регистрации + * + * @return void + * + * @throws \JsonException + */ + public function runAction(): void + { + $this->registerCodeSendService->setUser($this->user); + $this->registerCodeSendService->setResponse($this->response); + $this->registerCodeSendService->send(); + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->getResponse()->addError('Вы не авторизованы'); + return false; + } + if ($this->user->isConfirm()) { + $this->response->getResponse()->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 7cbf7f5..600c2be 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -36,17 +36,21 @@ abstract class BaseDto implements DtoServiceInterface public function getClass(): ?DtoServiceInterface { if ($this->request) { - $normalizer = new ObjectNormalizer( - null, - new CamelCaseToSnakeCaseNameConverter(), - null, - new ReflectionExtractor() - ); - $serializer = new Serializer( - [$normalizer, new DateTimeNormalizer()], - [new JsonEncoder()] - ); - return $serializer->deserialize($this->request->getContent(), static::class, 'json'); + try { + $normalizer = new ObjectNormalizer( + null, + new CamelCaseToSnakeCaseNameConverter(), + null, + new ReflectionExtractor() + ); + $serializer = new Serializer( + [$normalizer, new DateTimeNormalizer()], + [new JsonEncoder()] + ); + return $serializer->deserialize($this->request->getContent(), static::class, 'json'); + } catch (\Exception $exception) { + return null; + } } return null; @@ -81,13 +85,19 @@ abstract class BaseDto implements DtoServiceInterface $apiResponse = $response->getResponse(); $bValid = true; - $aErrors = $this->validator->validate($this->getClass()); - if (count($aErrors) > 0) { - foreach ($aErrors as $error) { - $apiResponse->addError($error->getMessage()); + if ($classObj = $this->getClass()) { + $aErrors = $this->validator->validate($classObj); + if (count($aErrors) > 0) { + foreach ($aErrors as $error) { + $apiResponse->addError($error->getMessage()); + } + $bValid = false; } + } else { + $apiResponse->addError("Данные не получены"); $bValid = false; } + return $bValid; } } \ No newline at end of file diff --git a/app/src/Service/Dto/Classes/RecoveryCodeDto.php b/app/src/Service/Dto/Classes/RecoveryCodeDto.php new file mode 100644 index 0000000..2e9757e --- /dev/null +++ b/app/src/Service/Dto/Classes/RecoveryCodeDto.php @@ -0,0 +1,31 @@ +response = new ApiResponse(); } diff --git a/app/src/Service/Response/Classes/ProfileResponse.php b/app/src/Service/Response/Classes/ProfileResponse.php deleted file mode 100644 index 384f3d0..0000000 --- a/app/src/Service/Response/Classes/ProfileResponse.php +++ /dev/null @@ -1,14 +0,0 @@ -Уважаемый {surname} {name} {patronymic} +
Ваш код для восстановления: {code}
+
Время действия кода: {time}
+ HTML; + } + +} \ No newline at end of file diff --git a/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php b/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php new file mode 100644 index 0000000..9371af7 --- /dev/null +++ b/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php @@ -0,0 +1,22 @@ +Уважаемый {surname} {name} {patronymic} +
Ваш код для подтверждения: {code}
+
Время действия кода: {time}
+ HTML; + } +} \ No newline at end of file diff --git a/app/src/Service/Send/Classes/CodeSendService.php b/app/src/Service/Send/Classes/CodeSendService.php new file mode 100644 index 0000000..b35d49f --- /dev/null +++ b/app/src/Service/Send/Classes/CodeSendService.php @@ -0,0 +1,127 @@ +response = $response; + } + + public function setUser(?User $user): void + { + $this->user = $user; + } + + public function getSubject(): string + { + return ''; + } + + public function getBody(): string + { + return '{code}'; + } + + public function send(): void + { + if ($this->user === null) { + $this->response->getResponse()->addError('Письмо не отправлено, пользователь не получен'); + return; + } + $serializedUser = $this->serializer->serialize($this->user, 'json', ['groups' => ['profile']]); + $values = json_decode($serializedUser, true, 512, JSON_THROW_ON_ERROR) ?: []; + + $codeObj = $this->user->getRegisterCode(); + $code = null; + $time = null; + if ($codeObj === null) { + $codeObj = new UserCode(); + $codeObj->setRelatedUser($this->user); + } + + try { + $om = $this->doctrine->getManager(); + $om->persist($codeObj); + $om->flush(); + $code = $codeObj->getCode(); + $date = $codeObj->getDate(); + $time = $date?->diff(new \DateTime()); + } catch (\Exception $exception) { + $this->response->getResponse()->addError('Ошибка генерации кода'); + } + + if ($code) { + $values['code'] = $code; + $timeStr = 'нет'; + if ($time) { + $timeStr = $time->format('%H:%I:%S'); + } + $values['time'] = $timeStr; + $this->sendService->setTo($this->user->getEmail()); + $this->sendService->setSubject($this->formatSubject($values)); + $this->sendService->setBody($this->formatBody($values)); + $this->sendService->send(); + $this->response->getResponse()->addMessage('Письмо с кодом отправлено'); + } else { + $this->response->getResponse()->addError('Ошибка генерации кода'); + } + } + + /** + * Подстановка значений в письмо + * + * @param array $values + * + * @return string + */ + private function formatBody(array $values): string + { + $body = $this->getBody(); + + foreach ($values as $name => $value) { + $body = str_replace('{' . $name . '}', $value, $body); + } + + return $body; + } + + /** + * Подстановка значений в тему письма + * + * @param array $values + * + * @return string + */ + private function formatSubject(array $values): string + { + $subject = $this->getSubject(); + + foreach ($values as $name => $value) { + $subject = str_replace('{' . $name . '}', $value, $subject); + } + + return $subject; + } +} \ No newline at end of file diff --git a/app/src/Service/Send/SendService.php b/app/src/Service/Send/SendService.php new file mode 100644 index 0000000..eb1d5e3 --- /dev/null +++ b/app/src/Service/Send/SendService.php @@ -0,0 +1,86 @@ +from; + } + + public function setFrom(?string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): ?string + { + return $this->to; + } + + public function setTo(?string $to): self + { + $this->to = $to; + + return $this; + } + + public function getSubject(): ?string + { + return $this->subject; + } + + public function setSubject(?string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getBody(): ?string + { + return $this->body; + } + + public function setBody(?string $body): self + { + $this->body = $body; + + return $this; + } + + public function send(): void + { + try { + $this->bus->dispatch(new SendMessage( + $this->from ?: $this->fromEmail, + $this->to, + $this->subject, + $this->body, + $this->confirmType + )); + } catch (Throwable $e) { + dd($e); + } + } +} \ No newline at end of file diff --git a/app/src/Service/Send/SendServiceInterface.php b/app/src/Service/Send/SendServiceInterface.php new file mode 100644 index 0000000..a1b6ad8 --- /dev/null +++ b/app/src/Service/Send/SendServiceInterface.php @@ -0,0 +1,8 @@ +getObject(); + if ($oObject) { + if ($oObject->email !== null && $oObject->phoneNumber !== null) { + $context->buildViolation('Передайте либо Email либо номер телефона') + ->addViolation(); + } + } + } +} \ No newline at end of file diff --git a/app/symfony.lock b/app/symfony.lock index ff98b32..9ef8d81 100644 --- a/app/symfony.lock +++ b/app/symfony.lock @@ -102,6 +102,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/messenger": { + "version": "7.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, "symfony/routing": { "version": "7.0", "recipe": { diff --git a/compose.yaml b/compose.yaml index 0b9b0e8..a537a6c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -74,7 +74,7 @@ services: image: redis:6.2-alpine restart: unless-stopped ports: - - '6379:6379' + - ${REDIS_PORT}:${REDIS_PORT} command: redis-server --save 20 1 --loglevel warning volumes: - redis:/data @@ -82,23 +82,25 @@ services: zookeeper: container_name: ${CONTAINER_NAME}-zookeeper image: confluentinc/cp-zookeeper:latest + restart: unless-stopped environment: - ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_CLIENT_PORT: ${ZOOKEEPER_CLIENT_PORT} ZOOKEEPER_TICK_TIME: 2000 ports: - - 22181:2181 + - ${ZOOKEEPER_PORT}:${ZOOKEEPER_CLIENT_PORT} kafka: container_name: ${CONTAINER_NAME}-kafka image: confluentinc/cp-kafka:latest + restart: unless-stopped depends_on: - zookeeper ports: - - 29092:29092 + - ${KAFKA_PORT}:${KAFKA_PORT} environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_BROKER_ID: ${KAFKA_BROKER_ID} + KAFKA_ZOOKEEPER_CONNECT: zookeeper:${ZOOKEEPER_CLIENT_PORT} + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:${KAFKA_PORT} KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index bd634f2..985283e 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -43,6 +43,7 @@ RUN apk add --no-cache --virtual .build-deps \ libzip-dev \ icu-dev \ postgresql-dev \ + librdkafka-dev \ # PHP Extensions --------------------------------- \ && curl -sSLf \ -o /usr/local/bin/install-php-extensions \ @@ -57,6 +58,7 @@ RUN apk add --no-cache --virtual .build-deps \ pdo_pgsql \ # Pecl Extensions --------------------------------- \ && pecl install apcu && docker-php-ext-enable apcu \ + && pecl install rdkafka && docker-php-ext-enable rdkafka \ # --------------------------------------------------\ # Install Xdebug at this step to make editing dev image cache-friendly, we delete xdebug from production image later \ && pecl install xdebug-${XDEBUG_VERSION} \ -- GitLab From c61ef65d4d6c63570334405115b837f8f428d8ed Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Thu, 20 Jun 2024 09:53:27 +0500 Subject: [PATCH 3/7] swagger --- app/composer.json | 7 +- app/composer.lock | 984 +++++++++++++++++- app/config/bundles.php | 3 + app/config/packages/nelmio_api_doc.yaml | 21 + app/config/packages/security.yaml | 1 + app/config/packages/twig.yaml | 6 + app/config/routes/nelmio_api_doc.yaml | 12 + app/src/Controller/AuthController.php | 19 + app/src/Controller/ProfileController.php | 41 + app/src/Listeners/AccessDeniedListener.php | 5 +- app/src/Listeners/JwtListener.php | 26 +- app/src/Response/ApiResponse.php | 107 -- app/src/Response/TokenResponse.php | 14 - .../Service/Action/ActionServiceInterface.php | 4 +- app/src/Service/Action/BaseActionService.php | 10 +- .../Action/Classes/CheckRecoveryCode.php | 12 +- .../Action/Classes/CheckRegisterCode.php | 12 +- .../Service/Action/Classes/DeleteProfile.php | 8 +- app/src/Service/Action/Classes/GetProfile.php | 20 +- .../Action/Classes/RecoveryProfile.php | 4 +- app/src/Service/Action/Classes/Register.php | 8 +- .../Action/Classes/SendRegisterCode.php | 4 +- app/src/Service/Dto/BaseDto.php | 2 + .../Service/Response/BaseResponseService.php | 20 - .../Response/Classes/ProfileResponse.php | 27 + app/src/Service/Response/Classes/Response.php | 145 ++- .../Response/Classes/TokenResponse.php | 19 + .../Response/ResponseServiceInterface.php | 10 +- .../Service/Send/Classes/CodeSendService.php | 8 +- app/symfony.lock | 29 + app/templates/base.html.twig | 16 + 31 files changed, 1387 insertions(+), 217 deletions(-) create mode 100644 app/config/packages/nelmio_api_doc.yaml create mode 100644 app/config/packages/twig.yaml create mode 100644 app/config/routes/nelmio_api_doc.yaml delete mode 100644 app/src/Response/ApiResponse.php delete mode 100644 app/src/Response/TokenResponse.php delete mode 100644 app/src/Service/Response/BaseResponseService.php create mode 100644 app/src/Service/Response/Classes/ProfileResponse.php create mode 100644 app/src/Service/Response/Classes/TokenResponse.php create mode 100644 app/templates/base.html.twig diff --git a/app/composer.json b/app/composer.json index 6ae3ba1..804133d 100644 --- a/app/composer.json +++ b/app/composer.json @@ -13,6 +13,8 @@ "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", "lexik/jwt-authentication-bundle": "^3.0", + "nelmio/api-doc-bundle": "^4.27", + "symfony/asset": "7.0.*", "symfony/cache": "7.0.*", "symfony/console": "7.0.*", "symfony/dotenv": "7.0.*", @@ -25,8 +27,11 @@ "symfony/runtime": "7.0.*", "symfony/security-bundle": "7.0.*", "symfony/serializer": "7.0.*", + "symfony/twig-bundle": "7.0.*", "symfony/validator": "7.0.*", - "symfony/yaml": "7.0.*" + "symfony/yaml": "7.0.*", + "twig/extra-bundle": "^2.12|^3.0", + "twig/twig": "^2.12|^3.0" }, "require-dev": { "roave/security-advisories": "dev-latest", diff --git a/app/composer.lock b/app/composer.lock index f3c8f9a..df47980 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b38b50076633f562508d6896c57850ce", + "content-hash": "a47e42ecd6c25174dcfe545065022162", "packages": [ { "name": "doctrine/cache", @@ -1548,6 +1548,344 @@ ], "time": "2024-05-05T17:49:24+00:00" }, + { + "name": "nelmio/api-doc-bundle", + "version": "v4.27.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioApiDocBundle.git", + "reference": "221a1febaf861435b51c80cffd1a78efb4168345" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/221a1febaf861435b51c80cffd1a78efb4168345", + "reference": "221a1febaf861435b51c80cffd1a78efb4168345", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0", + "phpdocumentor/type-resolver": "^1.8.2", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/config": "^5.4 || ^6.4 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4.24 || ^6.4 || ^7.0", + "symfony/http-foundation": "^5.4 || ^6.4 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/property-info": "^5.4.10 || ^6.4 || ^7.0", + "symfony/routing": "^5.4 || ^6.4 || ^7.0", + "zircote/swagger-php": "^4.6.1" + }, + "conflict": { + "zircote/swagger-php": "4.8.7" + }, + "require-dev": { + "api-platform/core": "^2.7.0 || ^3", + "composer/package-versions-deprecated": "1.11.99.1", + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.52", + "friendsofsymfony/rest-bundle": "^2.8 || ^3.0", + "jms/serializer": "^1.14 || ^3.0", + "jms/serializer-bundle": "^2.3 || ^3.0 || ^4.0 || ^5.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^9.6 || ^10.5", + "symfony/asset": "^5.4 || ^6.4 || ^7.0", + "symfony/browser-kit": "^5.4 || ^6.4 || ^7.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.0", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.4 || ^7.0", + "symfony/form": "^5.4 || ^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4", + "symfony/property-access": "^5.4 || ^6.4 || ^7.0", + "symfony/security-csrf": "^5.4 || ^6.4 || ^7.0", + "symfony/serializer": "^5.4 || ^6.4 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0", + "symfony/templating": "^5.4 || ^6.4 || ^7.0", + "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.0", + "symfony/validator": "^5.4 || ^6.4 || ^7.0", + "willdurand/hateoas-bundle": "^1.0 || ^2.0" + }, + "suggest": { + "api-platform/core": "For using an API oriented framework.", + "doctrine/annotations": "For using doctrine annotations", + "friendsofsymfony/rest-bundle": "For using the parameters annotations.", + "jms/serializer-bundle": "For describing your models.", + "symfony/asset": "For using the Swagger UI.", + "symfony/cache": "For using a PSR-6 compatible cache implementation with the API doc generator.", + "symfony/form": "For describing your form type models.", + "symfony/monolog-bundle": "For using a PSR-3 compatible logger implementation with the API PHP describer.", + "symfony/security-csrf": "For using csrf protection tokens in forms.", + "symfony/serializer": "For describing your models.", + "symfony/twig-bundle": "For using the Swagger UI.", + "symfony/validator": "For describing the validation constraints in your models.", + "willdurand/hateoas-bundle": "For extracting HATEOAS metadata." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\ApiDocBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" + } + ], + "description": "Generates documentation for your REST API from annotations and attributes", + "keywords": [ + "api", + "doc", + "documentation", + "rest" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.27.0" + }, + "time": "2024-06-12T23:47:19+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.4.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" + }, + "time": "2024-05-21T05:55:05+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "153ae662783729388a584b4361f2545e4d841e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + }, + "time": "2024-02-23T11:10:43+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.29.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + }, + "time": "2024-05-31T08:52:43+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -1798,6 +2136,75 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "symfony/asset", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "0f106714bb8d857560edd2ada7f387d2f437c830" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/0f106714bb8d857560edd2ada7f387d2f437c830", + "reference": "0f106714bb8d857560edd2ada7f387d2f437c830", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:55:39+00:00" + }, { "name": "symfony/cache", "version": "v7.0.8", @@ -3552,28 +3959,95 @@ "time": "2024-06-02T15:49:03+00:00" }, { - "name": "symfony/password-hasher", + "name": "symfony/options-resolver", "version": "v7.0.8", "source": { "type": "git", - "url": "https://github.com/symfony/password-hasher.git", - "reference": "25c66dba8ca72c9636b16e9a4b33d18554969a3f" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "19eecfc6f1b0e4b093db7f4a71eedc91843e711a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/25c66dba8ca72c9636b16e9a4b33d18554969a3f", - "reference": "25c66dba8ca72c9636b16e9a4b33d18554969a3f", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/19eecfc6f1b0e4b093db7f4a71eedc91843e711a", + "reference": "19eecfc6f1b0e4b093db7f4a71eedc91843e711a", "shasum": "" }, "require": { - "php": ">=8.2" - }, - "conflict": { - "symfony/security-core": "<6.4" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, - "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0" + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:55:39+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "25c66dba8ca72c9636b16e9a4b33d18554969a3f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/25c66dba8ca72c9636b16e9a4b33d18554969a3f", + "reference": "25c66dba8ca72c9636b16e9a4b33d18554969a3f", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -5096,6 +5570,198 @@ ], "time": "2024-04-18T09:32:20+00:00" }, + { + "name": "symfony/twig-bridge", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "c8e05d7545962198df715d705c132de0674dc5b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/c8e05d7545962198df715d705c132de0674dc5b2", + "reference": "c8e05d7545962198df715d705c132de0674dc5b2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.0.4" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<6.4", + "symfony/form": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^6.4|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/html-sanitizer": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/security-http": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:55:39+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "a90e474bc260e59bed98a556db63673e6420a0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/a90e474bc260e59bed98a556db63673e6420a0be", + "reference": "a90e474bc260e59bed98a556db63673e6420a0be", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0", + "twig/twig": "^3.0.4" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/web-link": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v7.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:55:39+00:00" + }, { "name": "symfony/validator", "version": "v7.0.8", @@ -5420,6 +6086,298 @@ } ], "time": "2024-04-28T11:44:19+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/cdc6e23aeb7f4953c1039568c3439aab60c56454", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "twig/twig": "^3.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-05-11T07:35:57+00:00" + }, + { + "name": "twig/twig", + "version": "v3.10.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php80": "^1.22" + }, + "require-dev": { + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.10.3" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-05-16T10:04:27+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "4.10.0", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": ">=2.2", + "symfony/yaml": ">=3.3" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^1.7 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", + "phpstan/phpstan": "^1.6", + "phpunit/phpunit": ">=8", + "vimeo/psalm": "^4.23" + }, + "suggest": { + "doctrine/annotations": "^1.7 || ^2.0" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/4.10.0" + }, + "time": "2024-06-06T22:42:02+00:00" } ], "packages-dev": [ diff --git a/app/config/bundles.php b/app/config/bundles.php index afcea0b..386a29b 100644 --- a/app/config/bundles.php +++ b/app/config/bundles.php @@ -7,4 +7,7 @@ return [ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], + Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], ]; diff --git a/app/config/packages/nelmio_api_doc.yaml b/app/config/packages/nelmio_api_doc.yaml new file mode 100644 index 0000000..0e7eb67 --- /dev/null +++ b/app/config/packages/nelmio_api_doc.yaml @@ -0,0 +1,21 @@ +nelmio_api_doc: + use_validation_groups: true + documentation: + servers: + - url: http://127.0.0.1:8080 + description: Localhost + info: + title: Квесты + description: Тестовое задание + version: 1.0.0 + components: + securitySchemes: + Bearer: + type: http + scheme: bearer + bearerFormat: JWT + security: + - Bearer: [ ] + areas: # to filter documented areas + path_patterns: + - ^/api(?!/doc$) # Accepts routes under /api except /api/doc diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index 92236a8..e1644bc 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -43,6 +43,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } + - { path: ^/api/doc, roles: PUBLIC_ACCESS } - { path: ^/api/register, roles: PUBLIC_ACCESS } - { path: ^/api/register/send, roles: ROLE_USER } diff --git a/app/config/packages/twig.yaml b/app/config/packages/twig.yaml new file mode 100644 index 0000000..3f795d9 --- /dev/null +++ b/app/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/app/config/routes/nelmio_api_doc.yaml b/app/config/routes/nelmio_api_doc.yaml new file mode 100644 index 0000000..e160911 --- /dev/null +++ b/app/config/routes/nelmio_api_doc.yaml @@ -0,0 +1,12 @@ +# Expose your documentation as JSON swagger compliant +app.swagger: + path: /api/doc.json + methods: GET + defaults: { _controller: nelmio_api_doc.controller.swagger } + +## Requires the Asset component and the Twig bundle +## $ composer require twig asset +app.swagger_ui: + path: /api/doc + methods: GET + defaults: { _controller: nelmio_api_doc.controller.swagger_ui } \ No newline at end of file diff --git a/app/src/Controller/AuthController.php b/app/src/Controller/AuthController.php index 47b3178..72e3782 100644 --- a/app/src/Controller/AuthController.php +++ b/app/src/Controller/AuthController.php @@ -3,14 +3,30 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; +use App\Service\Dto\Classes\RegisterCodeDto; +use App\Service\Dto\Classes\RegisterDto; +use App\Service\Response\Classes\Response; +use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; +use OpenApi\Attributes as OA; #[Route('/api', name: 'api_')] +#[OA\Tag(name: 'Авторизация')] +#[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) +)] class AuthController extends AbstractController { #[Route('/register', name: 'register', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: RegisterDto::class)) + )] public function register( ActionServiceInterface $registerService ): JsonResponse @@ -27,6 +43,9 @@ class AuthController extends AbstractController } #[Route('/register/check', name: 'register_check', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: RegisterCodeDto::class)) + )] public function checkRegisterCode( ActionServiceInterface $checkRegisterService ): JsonResponse diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index 51e5442..75f10cf 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -3,14 +3,28 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; +use App\Service\Dto\Classes\RecoveryCodeDto; +use App\Service\Dto\Classes\RecoveryDto; +use App\Service\Response\Classes\ProfileResponse; +use App\Service\Response\Classes\Response; +use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; +use OpenApi\Attributes as OA; #[Route('/api', name: 'api_')] +#[OA\Tag(name: 'Профиль')] class ProfileController extends AbstractController { #[Route('/profile', name: 'profile', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: ProfileResponse::class, groups: ["message", "data", "profile"]) + ) + )] public function profile( ActionServiceInterface $profileService ): JsonResponse @@ -19,6 +33,13 @@ class ProfileController extends AbstractController } #[Route('/profile/delete', name: 'profile_delete', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] public function deleteProfile( ActionServiceInterface $deleteProfileService, ): JsonResponse @@ -27,6 +48,16 @@ class ProfileController extends AbstractController } #[Route('/profile/recovery', name: 'profile_recovery', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: RecoveryDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] public function recoveryProfile( ActionServiceInterface $recoveryProfileService, ): JsonResponse @@ -35,6 +66,16 @@ class ProfileController extends AbstractController } #[Route('/profile/recovery/check', name: 'profile_recovery_check', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: RecoveryCodeDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] public function recoveryCodeProfile( ActionServiceInterface $checkRecoveryService, ): JsonResponse diff --git a/app/src/Listeners/AccessDeniedListener.php b/app/src/Listeners/AccessDeniedListener.php index 8299af1..7a7aa3f 100644 --- a/app/src/Listeners/AccessDeniedListener.php +++ b/app/src/Listeners/AccessDeniedListener.php @@ -2,7 +2,6 @@ namespace App\Listeners; -use App\Response\ApiResponse; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; @@ -21,7 +20,7 @@ class AccessDeniedListener implements EventSubscriberInterface public function onAccessException(ExceptionEvent $event): void { - $response = new ApiResponse(); + $response = new \App\Service\Response\Classes\Response(); $response->setStatusCode(Response::HTTP_FORBIDDEN); $response->addError('Доступ запрещен'); @@ -29,6 +28,6 @@ class AccessDeniedListener implements EventSubscriberInterface if (!$exception instanceof AccessDeniedException) { return; } - $event->setResponse($response); + $event->setResponse($response->getResponse()); } } \ No newline at end of file diff --git a/app/src/Listeners/JwtListener.php b/app/src/Listeners/JwtListener.php index 625ee5f..07e56ad 100644 --- a/app/src/Listeners/JwtListener.php +++ b/app/src/Listeners/JwtListener.php @@ -3,8 +3,7 @@ namespace App\Listeners; use App\Entity\User; -use App\Response\ApiResponse; -use App\Response\TokenResponse; +use App\Service\Response\Classes\TokenResponse; use JsonException; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; @@ -31,16 +30,15 @@ class JwtListener return; } + $response = new TokenResponse(); + if ($user->isDeleted()) { - $response = new ApiResponse(); $response->addError('Пользователь удален'); } else { - $response = new TokenResponse(); $response->setToken($data['token']); - $response->addMessage('Здравствуйте, ' . $user->getFullName()); } - $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + $data = json_decode($response->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR); $event->setData($data); } @@ -49,9 +47,9 @@ class JwtListener */ public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event): void { - $response = new ApiResponse(); + $response = new \App\Service\Response\Classes\Response(); $response->addError('Неверный email или пароль'); - $event->setResponse($response); + $event->setResponse($response->getResponse()); } /** @@ -59,11 +57,11 @@ class JwtListener */ public function onJWTInvalid(JWTInvalidEvent $event): void { - $response = new ApiResponse(); + $response = new \App\Service\Response\Classes\Response(); $response->addError('Неверный токен авторизации'); $response->setStatusCode(Response::HTTP_FORBIDDEN); - $event->setResponse($response); + $event->setResponse($response->getResponse()); } /** @@ -71,11 +69,11 @@ class JwtListener */ public function onJWTNotFound(JWTNotFoundEvent $event): void { - $response = new ApiResponse(); + $response = new \App\Service\Response\Classes\Response(); $response->addError('Отсутствует токен'); $response->setStatusCode(Response::HTTP_FORBIDDEN); - $event->setResponse($response); + $event->setResponse($response->getResponse()); } /** @@ -83,10 +81,10 @@ class JwtListener */ public function onJWTExpired(JWTExpiredEvent $event): void { - $response = new ApiResponse(); + $response = new \App\Service\Response\Classes\Response(); $response->addError('Срок действия вашего токена истек, пожалуйста, обновите его'); $response->setStatusCode(Response::HTTP_FORBIDDEN); - $event->setResponse($response); + $event->setResponse($response->getResponse()); } } \ No newline at end of file diff --git a/app/src/Response/ApiResponse.php b/app/src/Response/ApiResponse.php deleted file mode 100644 index 9e36975..0000000 --- a/app/src/Response/ApiResponse.php +++ /dev/null @@ -1,107 +0,0 @@ -setResult(); - } - - /** - * Добавление ошибки - * - * @param string $message - * - * @return self - */ - public function addError(string $message): self - { - $this->status = false; - return $this->addMessage($message); - } - - /** - * Добавление ошибок - * - * @param array $errors - * - * @return $this - */ - public function addErrors(array $errors): self - { - $this->status = false; - foreach ($errors as $error) { - $this->addError($error); - } - - return $this; - } - - /** - * Добавление сообщения - * - * @param string $message - * - * @return self - */ - public function addMessage(string $message): self - { - $this->messages[] = $message; - return $this->setResult(); - } - - /** - * Запись контента ответа - * - * @param array|null $responseData - * - * @return void - */ - public function setResponseData(?array $responseData): void - { - $this->responseData = $responseData; - - $this->setResult(); - } - - /** - * @return bool - */ - public function isSuccess(): bool - { - return $this->status; - } - - /** - * Установка результата - * - * @return self - */ - protected function setResult(): self - { - $result = [ - 'status' => $this->status, - ]; - - if (!empty($this->responseData)) { - $result['data'] = $this->responseData; - } - if (!isset($result['data'])) { - $result['message'] = implode(', ', $this->messages); - } - - return $this->setData($result); - } -} \ No newline at end of file diff --git a/app/src/Response/TokenResponse.php b/app/src/Response/TokenResponse.php deleted file mode 100644 index a941d6c..0000000 --- a/app/src/Response/TokenResponse.php +++ /dev/null @@ -1,14 +0,0 @@ -setResponseData([ - 'token' => $token, - ]); - return $this; - } -} \ No newline at end of file diff --git a/app/src/Service/Action/ActionServiceInterface.php b/app/src/Service/Action/ActionServiceInterface.php index cec8a68..0f16176 100644 --- a/app/src/Service/Action/ActionServiceInterface.php +++ b/app/src/Service/Action/ActionServiceInterface.php @@ -2,11 +2,11 @@ namespace App\Service\Action; -use App\Response\ApiResponse; +use Symfony\Component\HttpFoundation\JsonResponse; interface ActionServiceInterface { - public function getResponse(): ApiResponse; + public function getResponse(): JsonResponse; public function runAction(): void; diff --git a/app/src/Service/Action/BaseActionService.php b/app/src/Service/Action/BaseActionService.php index a2007b7..97235c5 100644 --- a/app/src/Service/Action/BaseActionService.php +++ b/app/src/Service/Action/BaseActionService.php @@ -2,9 +2,9 @@ namespace App\Service\Action; -use App\Response\ApiResponse; -use App\Service\Dto\DtoServiceInterface; +use App\Service\Response\Classes\Response; use App\Service\Response\ResponseServiceInterface; +use Symfony\Component\HttpFoundation\JsonResponse; abstract class BaseActionService implements ActionServiceInterface { @@ -17,7 +17,7 @@ abstract class BaseActionService implements ActionServiceInterface $this->responseService = $baseResponseService; } - public function getResponse(): ApiResponse + public function getResponse(): JsonResponse { if ($this->validate()) { $this->runAction(); @@ -27,8 +27,8 @@ abstract class BaseActionService implements ActionServiceInterface return $this->responseService->getResponse(); } - $response = new ApiResponse(); + $response = new Response(); $response->addError('Ошибка получения ответа'); - return $response; + return $response->getResponse(); } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/CheckRecoveryCode.php b/app/src/Service/Action/Classes/CheckRecoveryCode.php index fa1c5ff..4a1d3a0 100644 --- a/app/src/Service/Action/Classes/CheckRecoveryCode.php +++ b/app/src/Service/Action/Classes/CheckRecoveryCode.php @@ -38,7 +38,7 @@ class CheckRecoveryCode extends BaseActionService $code = $dto->code; $registerCode = $userExists->getRegisterCode(); if ($registerCode === null) { - $this->response->getResponse()->addError('Код подтверждения не отправлен'); + $this->response->addError('Код подтверждения не отправлен'); } else { if ($registerCodeDate = $registerCode->getDate()) { if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { @@ -48,19 +48,19 @@ class CheckRecoveryCode extends BaseActionService $em->persist($userExists); $em->remove($registerCode); $em->flush(); - $this->response->getResponse()->addMessage('Профиль восстановлен'); + $this->response->addMessage('Профиль восстановлен'); } catch (\Exception $exception) { - $this->response->getResponse()->addError('Ошибка восстановления профиля'); + $this->response->addError('Ошибка восстановления профиля'); } } else { - $this->response->getResponse()->addError('Код недействителен'); + $this->response->addError('Код недействителен'); } } else { - $this->response->getResponse()->addError('Код недействителен'); + $this->response->addError('Код недействителен'); } } } else { - $this->response->getResponse()->addError('Пользователь не найден'); + $this->response->addError('Пользователь не найден'); } } diff --git a/app/src/Service/Action/Classes/CheckRegisterCode.php b/app/src/Service/Action/Classes/CheckRegisterCode.php index ed6f147..6411a73 100644 --- a/app/src/Service/Action/Classes/CheckRegisterCode.php +++ b/app/src/Service/Action/Classes/CheckRegisterCode.php @@ -42,7 +42,7 @@ class CheckRegisterCode extends BaseActionService $code = $this->registerCodeDto->getClass()->code; $registerCode = $this->user->getRegisterCode(); if ($registerCode === null) { - $this->response->getResponse()->addError('Код подтверждения не отправлен'); + $this->response->addError('Код подтверждения не отправлен'); } else { if ($registerCodeDate = $registerCode->getDate()) { if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { @@ -52,15 +52,15 @@ class CheckRegisterCode extends BaseActionService $em->persist($this->user); $em->remove($registerCode); $em->flush(); - $this->response->getResponse()->addMessage('Регистрация подтверждена'); + $this->response->addMessage('Регистрация подтверждена'); } catch (\Exception $exception) { - $this->response->getResponse()->addError('Ошибка подтверждения регистрации'); + $this->response->addError('Ошибка подтверждения регистрации'); } } else { - $this->response->getResponse()->addError('Код недействителен'); + $this->response->addError('Код недействителен'); } } else { - $this->response->getResponse()->addError('Код недействителен'); + $this->response->addError('Код недействителен'); } } } @@ -68,7 +68,7 @@ class CheckRegisterCode extends BaseActionService public function validate(): bool { if ($this->user === null) { - $this->response->getResponse()->addError('Вы не авторизованы'); + $this->response->addError('Вы не авторизованы'); return false; } return $this->registerCodeDto->validate($this->response); diff --git a/app/src/Service/Action/Classes/DeleteProfile.php b/app/src/Service/Action/Classes/DeleteProfile.php index 01f042c..9408091 100644 --- a/app/src/Service/Action/Classes/DeleteProfile.php +++ b/app/src/Service/Action/Classes/DeleteProfile.php @@ -34,21 +34,21 @@ class DeleteProfile extends BaseActionService $em = $this->doctrine->getManager(); $em->persist($this->user); $em->flush(); - $this->response->getResponse()->addMessage('Профиль удален'); + $this->response->addMessage('Профиль удален'); } catch (\Exception $exception) { - $this->response->getResponse()->addError('Ошибка удаления профиля'); + $this->response->addError('Ошибка удаления профиля'); } } public function validate(): bool { if ($this->user === null) { - $this->response->getResponse()->addError('Вы не авторизованы'); + $this->response->addError('Вы не авторизованы'); return false; } if ($this->user->isDeleted()) { - $this->response->getResponse()->addError('Профиль уже удален'); + $this->response->addError('Профиль уже удален'); return false; } return true; diff --git a/app/src/Service/Action/Classes/GetProfile.php b/app/src/Service/Action/Classes/GetProfile.php index 0c39923..73f5526 100644 --- a/app/src/Service/Action/Classes/GetProfile.php +++ b/app/src/Service/Action/Classes/GetProfile.php @@ -4,6 +4,7 @@ namespace App\Service\Action\Classes; use App\Entity\User; use App\Service\Action\BaseActionService; +use App\Service\Response\Classes\ProfileResponse; use App\Service\Response\ResponseServiceInterface; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Http\Attribute\CurrentUser; @@ -13,14 +14,18 @@ class GetProfile extends BaseActionService { private ?User $user; + /** + * @param ProfileResponse $profileResponse + * @param Security $security + * @param SerializerInterface $serializer + */ public function __construct( - private ResponseServiceInterface $response, - Security $security, - private SerializerInterface $serializer + private ResponseServiceInterface $profileResponse, + Security $security ) { $this->user = $security->getUser(); - parent::__construct($response); + parent::__construct($profileResponse); } @@ -33,12 +38,15 @@ class GetProfile extends BaseActionService */ public function runAction(): void { - $serializedUser = $this->serializer->serialize($this->user, 'json', ['groups' => ['profile']]); - $this->response->getResponse()->setResponseData(json_decode($serializedUser, true, 512, JSON_THROW_ON_ERROR)); + $this->profileResponse->setData($this->user); } public function validate(): bool { + if ($this->user === null) { + $this->profileResponse->addError('Вы не авторизованы'); + return false; + } return $this->user->isConfirm() && !$this->user->isDeleted(); } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/RecoveryProfile.php b/app/src/Service/Action/Classes/RecoveryProfile.php index 9cbf1c2..d6e7e68 100644 --- a/app/src/Service/Action/Classes/RecoveryProfile.php +++ b/app/src/Service/Action/Classes/RecoveryProfile.php @@ -42,14 +42,14 @@ class RecoveryProfile extends BaseActionService if ($userExists !== null) { if (!$userExists->isDeleted()) { - $this->response->getResponse()->addError('Профиль не удален'); + $this->response->addError('Профиль не удален'); } else { $this->recoveryCodeSendService->setUser($userExists); $this->recoveryCodeSendService->setResponse($this->response); $this->recoveryCodeSendService->send(); } } else { - $this->response->getResponse()->addError('Пользователь не найден'); + $this->response->addError('Пользователь не найден'); } } diff --git a/app/src/Service/Action/Classes/Register.php b/app/src/Service/Action/Classes/Register.php index 88eebcd..4dd29cd 100644 --- a/app/src/Service/Action/Classes/Register.php +++ b/app/src/Service/Action/Classes/Register.php @@ -48,7 +48,7 @@ class Register extends BaseActionService ->findOneByUniq($user->getEmail(), $user->getPhoneNumber()); if ($userExists) { - $this->response->getResponse()->addError('Пользователь уже существует'); + $this->response->addError('Пользователь уже существует'); } else { try { $user->setDeleted(false); @@ -63,13 +63,13 @@ class Register extends BaseActionService $em->persist($user); $em->flush(); - $this->response->getResponse()->addMessage('Пользователь зарегистрирован'); + $this->response->addMessage('Пользователь зарегистрирован'); $this->registerCodeSendService->setUser($user); $this->registerCodeSendService->setResponse($this->response); $this->registerCodeSendService->send(); } catch (\Exception $exception) { - $this->response->getResponse()->addError('Ошибка регистрации пользователя'); + $this->response->addError('Ошибка регистрации пользователя'); } } @@ -111,7 +111,7 @@ class Register extends BaseActionService } } } else { - $this->response->getResponse()->addError('Ошибка получения данных'); + $this->response->addError('Ошибка получения данных'); } return $user; diff --git a/app/src/Service/Action/Classes/SendRegisterCode.php b/app/src/Service/Action/Classes/SendRegisterCode.php index d6d5d4c..27666d3 100644 --- a/app/src/Service/Action/Classes/SendRegisterCode.php +++ b/app/src/Service/Action/Classes/SendRegisterCode.php @@ -47,11 +47,11 @@ class SendRegisterCode extends BaseActionService public function validate(): bool { if ($this->user === null) { - $this->response->getResponse()->addError('Вы не авторизованы'); + $this->response->addError('Вы не авторизованы'); return false; } if ($this->user->isConfirm()) { - $this->response->getResponse()->addError('Учетная запись уже подтверждена'); + $this->response->addError('Учетная запись уже подтверждена'); return false; } diff --git a/app/src/Service/Dto/BaseDto.php b/app/src/Service/Dto/BaseDto.php index 600c2be..47a3786 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -6,6 +6,7 @@ use App\Service\Response\ResponseServiceInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -33,6 +34,7 @@ abstract class BaseDto implements DtoServiceInterface * * @return DtoServiceInterface|null */ + #[Ignore] public function getClass(): ?DtoServiceInterface { if ($this->request) { diff --git a/app/src/Service/Response/BaseResponseService.php b/app/src/Service/Response/BaseResponseService.php deleted file mode 100644 index 6c97ee8..0000000 --- a/app/src/Service/Response/BaseResponseService.php +++ /dev/null @@ -1,20 +0,0 @@ -response = new ApiResponse(); - } - - public function getResponse(): ApiResponse - { - return $this->response; - } -} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/ProfileResponse.php b/app/src/Service/Response/Classes/ProfileResponse.php new file mode 100644 index 0000000..91c773c --- /dev/null +++ b/app/src/Service/Response/Classes/ProfileResponse.php @@ -0,0 +1,27 @@ +data = $user; + + return $this; + } + + public function getGroups(): array + { + return ['profile']; + } +} \ 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 390215f..cf0ffde 100644 --- a/app/src/Service/Response/Classes/Response.php +++ b/app/src/Service/Response/Classes/Response.php @@ -2,9 +2,150 @@ namespace App\Service\Response\Classes; -use App\Service\Response\BaseResponseService; +use App\Service\Response\ResponseServiceInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +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\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; -class Response extends BaseResponseService +class Response implements ResponseServiceInterface { + /** + * @var bool + */ + #[Groups(["message", "data"])] + public bool $status = true; + /** + * @var string + */ + #[Groups(["message"])] + public string $message = ''; + + #[Ignore] + public int $statusCode = 200; + + #[Ignore] + private array $messages = []; + + #[Ignore] + private array $errors = []; + + #[Ignore] + private JsonResponse $response; + + public function __construct() + { + $this->response = new JsonResponse(); + } + + /** + * Группы сериализации + * + * @return array + */ + #[Ignore] + protected function getGroups(): array + { + return []; + } + + /** + * Добавление ошибки + * + * @param string $message + * + * @return $this + * + * @throws \JsonException + */ + #[Ignore] + public function addError(string $message): self + { + $this->errors[] = $message; + + return $this; + } + + /** + * Добавление сообщения + * + * @param string $message + * + * @return $this + * + * @throws \JsonException + */ + #[Ignore] + public function addMessage(string $message): self + { + $this->messages[] = $message; + + return $this; + } + + #[Ignore] + public function isSuccess(): bool + { + if (!empty($this->errors)) { + $this->status = false; + } else { + $this->status = true; + } + + return $this->status; + } + + #[Ignore] + public function getResponse(): JsonResponse + { + $this->refresh(); + return $this->response; + } + + #[Ignore] + public function setStatusCode(int $code): self + { + $this->statusCode = $code; + return $this; + } + + #[Ignore] + protected function refresh(): self + { + $groups = ['message']; + if (!empty($this->errors)) { + $this->status = false; + } else { + $this->status = true; + } + + $this->message = implode(', ', array_merge($this->messages, $this->errors)); + + if (isset($this->data) && !empty($this->data)) { + $groups = ['data']; + $groups = array_merge($groups, $this->getGroups()); + } + + $normalizer = new ObjectNormalizer( + new ClassMetadataFactory(new AttributeLoader()), + new CamelCaseToSnakeCaseNameConverter(), + null, + new ReflectionExtractor() + ); + $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $dataStr = $serializer->serialize($this, 'json', ['groups' => $groups]); + $dataArray = json_decode($dataStr, true, 512, JSON_THROW_ON_ERROR); + + $this->response->setData($dataArray); + $this->response->setStatusCode($this->statusCode); + + return $this; + } } \ No newline at end of file diff --git a/app/src/Service/Response/Classes/TokenResponse.php b/app/src/Service/Response/Classes/TokenResponse.php new file mode 100644 index 0000000..8ce5a95 --- /dev/null +++ b/app/src/Service/Response/Classes/TokenResponse.php @@ -0,0 +1,19 @@ +data = ['token' => $token]; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/ResponseServiceInterface.php b/app/src/Service/Response/ResponseServiceInterface.php index d291c5a..e20fdd5 100644 --- a/app/src/Service/Response/ResponseServiceInterface.php +++ b/app/src/Service/Response/ResponseServiceInterface.php @@ -2,9 +2,15 @@ namespace App\Service\Response; -use App\Response\ApiResponse; +use Symfony\Component\HttpFoundation\JsonResponse; interface ResponseServiceInterface { - public function getResponse(): ApiResponse; + public function getResponse(): JsonResponse; + + public function addError(string $message): self; + + public function addMessage(string $message): self; + + public function isSuccess(): bool; } \ No newline at end of file diff --git a/app/src/Service/Send/Classes/CodeSendService.php b/app/src/Service/Send/Classes/CodeSendService.php index b35d49f..d8b5e4f 100644 --- a/app/src/Service/Send/Classes/CodeSendService.php +++ b/app/src/Service/Send/Classes/CodeSendService.php @@ -47,7 +47,7 @@ class CodeSendService implements SendServiceInterface public function send(): void { if ($this->user === null) { - $this->response->getResponse()->addError('Письмо не отправлено, пользователь не получен'); + $this->response->addError('Письмо не отправлено, пользователь не получен'); return; } $serializedUser = $this->serializer->serialize($this->user, 'json', ['groups' => ['profile']]); @@ -69,7 +69,7 @@ class CodeSendService implements SendServiceInterface $date = $codeObj->getDate(); $time = $date?->diff(new \DateTime()); } catch (\Exception $exception) { - $this->response->getResponse()->addError('Ошибка генерации кода'); + $this->response->addError('Ошибка генерации кода'); } if ($code) { @@ -83,9 +83,9 @@ class CodeSendService implements SendServiceInterface $this->sendService->setSubject($this->formatSubject($values)); $this->sendService->setBody($this->formatBody($values)); $this->sendService->send(); - $this->response->getResponse()->addMessage('Письмо с кодом отправлено'); + $this->response->addMessage('Письмо с кодом отправлено'); } else { - $this->response->getResponse()->addError('Ошибка генерации кода'); + $this->response->addError('Ошибка генерации кода'); } } diff --git a/app/symfony.lock b/app/symfony.lock index 9ef8d81..6babe26 100644 --- a/app/symfony.lock +++ b/app/symfony.lock @@ -38,6 +38,19 @@ "config/packages/lexik_jwt_authentication.yaml" ] }, + "nelmio/api-doc-bundle": { + "version": "4.27", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.0", + "ref": "c8e0c38e1a280ab9e37587a8fa32b251d5bc1c94" + }, + "files": [ + "config/packages/nelmio_api_doc.yaml", + "config/routes/nelmio_api_doc.yaml" + ] + }, "symfony/console": { "version": "7.0", "recipe": { @@ -140,6 +153,19 @@ "config/routes/security.yaml" ] }, + "symfony/twig-bundle": { + "version": "7.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, "symfony/validator": { "version": "7.0", "recipe": { @@ -151,5 +177,8 @@ "files": [ "config/packages/validator.yaml" ] + }, + "twig/extra-bundle": { + "version": "v3.10.0" } } diff --git a/app/templates/base.html.twig b/app/templates/base.html.twig new file mode 100644 index 0000000..1069c14 --- /dev/null +++ b/app/templates/base.html.twig @@ -0,0 +1,16 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {% endblock %} + + {% block javascripts %} + {% endblock %} + + + {% block body %}{% endblock %} + + -- GitLab From 92fe172c9b3bbc48bfd75b57ddcbc411241f9ba8 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Thu, 20 Jun 2024 10:58:39 +0500 Subject: [PATCH 4/7] password reset --- app/config/packages/security.yaml | 4 + app/config/services.yaml | 12 +++ app/src/Controller/AuthController.php | 30 ++++++++ .../Service/Action/Classes/ResetPassword.php | 63 ++++++++++++++++ .../Action/Classes/ResetPasswordCode.php | 73 +++++++++++++++++++ .../Action/Classes/SendRegisterCode.php | 1 - .../Action/Classes/SendResetPasswordCode.php | 46 ++++++++++++ app/src/Service/Dto/BaseDto.php | 6 +- .../Dto/Classes/ResetPasswordCodeDto.php | 44 +++++++++++ .../Classes/Code/PasswordCodeSendService.php | 23 ++++++ 10 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 app/src/Service/Action/Classes/ResetPassword.php create mode 100644 app/src/Service/Action/Classes/ResetPasswordCode.php create mode 100644 app/src/Service/Action/Classes/SendResetPasswordCode.php create mode 100644 app/src/Service/Dto/Classes/ResetPasswordCodeDto.php create mode 100644 app/src/Service/Send/Classes/Code/PasswordCodeSendService.php diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index e1644bc..198b9ae 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -49,6 +49,10 @@ security: - { path: ^/api/register/send, roles: ROLE_USER } - { path: ^/api/register/check, roles: ROLE_USER } + - { path: ^/api/password/reset, roles: ROLE_USER } + - { path: ^/api/password/send, roles: PUBLIC_ACCESS } + - { path: ^/api/password/reset/check, roles: PUBLIC_ACCESS } + - { path: ^/api/profile/recovery, roles: PUBLIC_ACCESS } - { path: ^/api/profile/recovery/check, roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_CONFIRMED } diff --git a/app/config/services.yaml b/app/config/services.yaml index 0a7f6ed..4fe458b 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -38,6 +38,12 @@ services: App\Service\Action\ActionServiceInterface $sendRegisterService: '@App\Service\Action\Classes\SendRegisterCode' + App\Service\Action\ActionServiceInterface $sendPasswordCodeService: '@App\Service\Action\Classes\SendResetPasswordCode' + + App\Service\Action\ActionServiceInterface $resetPasswordCodeService: '@App\Service\Action\Classes\ResetPasswordCode' + + App\Service\Action\ActionServiceInterface $resetPasswordService: '@App\Service\Action\Classes\ResetPassword' + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' @@ -48,6 +54,10 @@ services: App\Service\Dto\DtoServiceInterface $recoveryCodeDto: '@App\Service\Dto\Classes\RecoveryCodeDto' + App\Service\Dto\DtoServiceInterface $passwordResetDto: '@App\Service\Dto\Classes\ResetPasswordCodeDto' + + App\Service\Dto\DtoServiceInterface $passwordDto: '@App\Service\Dto\Classes\ChangePasswordDto' + App\Service\Dto\DtoServiceInterface $recoveryDto: '@App\Service\Dto\Classes\RecoveryDto' App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' @@ -71,6 +81,8 @@ services: App\Service\Send\SendServiceInterface $recoveryCodeSendService: '@App\Service\Send\Classes\Code\RecoveryCodeSendService' + App\Service\Send\SendServiceInterface $passwordCodeSendService: '@App\Service\Send\Classes\Code\PasswordCodeSendService' + # События JWT авторизации acme_api.event.authentication_success_listener: class: App\Listeners\JwtListener diff --git a/app/src/Controller/AuthController.php b/app/src/Controller/AuthController.php index 72e3782..9135e2a 100644 --- a/app/src/Controller/AuthController.php +++ b/app/src/Controller/AuthController.php @@ -3,8 +3,11 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; +use App\Service\Dto\Classes\ChangePasswordDto; +use App\Service\Dto\Classes\RecoveryDto; use App\Service\Dto\Classes\RegisterCodeDto; use App\Service\Dto\Classes\RegisterDto; +use App\Service\Dto\Classes\ResetPasswordCodeDto; use App\Service\Response\Classes\Response; use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -52,4 +55,31 @@ class AuthController extends AbstractController { return $checkRegisterService->getResponse(); } + + #[Route('/password/reset', name: 'password_reset', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: ChangePasswordDto::class)) + )] + public function resetPassword(ActionServiceInterface $resetPasswordService): JsonResponse + { + return $resetPasswordService->getResponse(); + } + + #[Route('/password/send', name: 'password_send', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: RecoveryDto::class)) + )] + public function sendResetPassword(ActionServiceInterface $sendPasswordCodeService): JsonResponse + { + return $sendPasswordCodeService->getResponse(); + } + + #[Route('/password/reset/check', name: 'password_reset_check', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: ResetPasswordCodeDto::class)) + )] + public function resetCheckPassword(ActionServiceInterface $resetPasswordCodeService): JsonResponse + { + return $resetPasswordCodeService->getResponse(); + } } diff --git a/app/src/Service/Action/Classes/ResetPassword.php b/app/src/Service/Action/Classes/ResetPassword.php new file mode 100644 index 0000000..ec611ab --- /dev/null +++ b/app/src/Service/Action/Classes/ResetPassword.php @@ -0,0 +1,63 @@ +user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + /** @var ChangePasswordDto $dto */ + $dto = $this->passwordDto->getClass(); + + if ($this->passwordHasher->isPasswordValid($this->user, $dto->oldPassword)) { + $hashedPassword = $this->passwordHasher->hashPassword( + $this->user, + $dto->password ?: '' + ); + $this->user->setPassword($hashedPassword); + + try { + $em = $this->doctrine->getManager(); + $em->persist($this->user); + $em->flush(); + $this->response->addMessage('Пароль изменен'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка изменения пароля'); + } + } else { + $this->response->addError('Текущий пароль неверен'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->addError('Вы не авторизованы'); + return false; + } + return $this->passwordDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/ResetPasswordCode.php b/app/src/Service/Action/Classes/ResetPasswordCode.php new file mode 100644 index 0000000..bc5ecd4 --- /dev/null +++ b/app/src/Service/Action/Classes/ResetPasswordCode.php @@ -0,0 +1,73 @@ +passwordResetDto->getClass(); + /** @var User $userExists */ + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($dto->email, $dto->phoneNumber); + + if ($userExists !== null) { + $currentDate = new \DateTime(); + $code = $dto->code; + $registerCode = $userExists->getRegisterCode(); + if ($registerCode === null) { + $this->response->addError('Код подтверждения не отправлен'); + } else { + if ($registerCodeDate = $registerCode->getDate()) { + if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { + try { + $hashedPassword = $this->passwordHasher->hashPassword( + $userExists, + $dto->password ?: '' + ); + $userExists->setPassword($hashedPassword); + $em = $this->doctrine->getManager(); + $em->persist($userExists); + $em->remove($registerCode); + $em->flush(); + $this->response->addMessage('Пароль изменен'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка изменения пароля'); + } + } else { + $this->response->addError('Код недействителен'); + } + } else { + $this->response->addError('Код недействителен'); + } + } + } else { + $this->response->addError('Пользователь не найден'); + } + } + + public function validate(): bool + { + return $this->passwordResetDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SendRegisterCode.php b/app/src/Service/Action/Classes/SendRegisterCode.php index 27666d3..8796d95 100644 --- a/app/src/Service/Action/Classes/SendRegisterCode.php +++ b/app/src/Service/Action/Classes/SendRegisterCode.php @@ -6,7 +6,6 @@ use App\Entity\User; use App\Service\Action\BaseActionService; use App\Service\Response\ResponseServiceInterface; use App\Service\Send\Classes\Code\RegisterCodeSendService; -use App\Service\Send\Classes\CodeSendService; use App\Service\Send\SendServiceInterface; use Symfony\Bundle\SecurityBundle\Security; diff --git a/app/src/Service/Action/Classes/SendResetPasswordCode.php b/app/src/Service/Action/Classes/SendResetPasswordCode.php new file mode 100644 index 0000000..7426d9b --- /dev/null +++ b/app/src/Service/Action/Classes/SendResetPasswordCode.php @@ -0,0 +1,46 @@ +recoveryDto->getClass(); + /** @var User $userExists */ + $userExists = $this->doctrine->getRepository(User::class) + ->findOneByUniq($dto->email, $dto->phoneNumber); + + if ($userExists !== null) { + $this->passwordCodeSendService->setUser($userExists); + $this->passwordCodeSendService->setResponse($this->response); + $this->passwordCodeSendService->send(); + } else { + $this->response->addError('Пользователь не найден'); + } + } + + public function validate(): bool + { + return $this->recoveryDto->validate($this->response); + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/BaseDto.php b/app/src/Service/Dto/BaseDto.php index 47a3786..0a149d4 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -84,19 +84,17 @@ abstract class BaseDto implements DtoServiceInterface */ public function validate(ResponseServiceInterface $response): bool { - $apiResponse = $response->getResponse(); - $bValid = true; if ($classObj = $this->getClass()) { $aErrors = $this->validator->validate($classObj); if (count($aErrors) > 0) { foreach ($aErrors as $error) { - $apiResponse->addError($error->getMessage()); + $response->addError($error->getMessage()); } $bValid = false; } } else { - $apiResponse->addError("Данные не получены"); + $response->addError("Данные не получены"); $bValid = false; } diff --git a/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php b/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php new file mode 100644 index 0000000..302631a --- /dev/null +++ b/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php @@ -0,0 +1,44 @@ +Уважаемый {surname} {name} {patronymic} +
Ваш код для восстановления пароля: {code}
+
Время действия кода: {time}
+ HTML; + } + +} \ No newline at end of file -- GitLab From cc616af1eb70ce07131ffa9f331aa89c4d2cc389 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Fri, 21 Jun 2024 10:28:22 +0500 Subject: [PATCH 5/7] 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 @@ +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 @@ +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 + */ + #[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 + */ + #[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 @@ +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 @@ + '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 @@ + + */ +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 @@ +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 @@ +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 @@ +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 From 53020f827a70cf9d27030b6d912bfac6e3121d09 Mon Sep 17 00:00:00 2001 From: Ilya Vasilenko Date: Fri, 21 Jun 2024 13:31:59 +0500 Subject: [PATCH 6/7] load & delete user image --- .gitignore | 1 + app/config/services.yaml | 12 ++ app/src/Controller/ProfileController.php | 34 +++++ app/src/Entity/User.php | 4 +- app/src/Entity/UserHistory.php | 13 ++ app/src/Entity/UserImage.php | 2 +- app/src/Listeners/UserImageListener.php | 77 ++++++++++ app/src/Listeners/UserListener.php | 82 ++++++---- .../Service/Action/Classes/DeleteImage.php | 61 ++++++++ app/src/Service/Action/Classes/SaveImage.php | 144 ++++++++++++++++++ app/src/Service/Dto/Classes/ImageDto.php | 23 +++ 11 files changed, 417 insertions(+), 36 deletions(-) create mode 100644 app/src/Listeners/UserImageListener.php create mode 100644 app/src/Service/Action/Classes/DeleteImage.php create mode 100644 app/src/Service/Action/Classes/SaveImage.php create mode 100644 app/src/Service/Dto/Classes/ImageDto.php diff --git a/.gitignore b/.gitignore index d38a7f5..647b7ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.vscode/ .env +/app/public/uploads/ diff --git a/app/config/services.yaml b/app/config/services.yaml index cc9ab17..43d8e48 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -7,6 +7,8 @@ parameters: confirm_type: '%env(CONFIRM_TYPE)%' code_ttl: '%env(CODE_TTL)%' from_email: '%env(MAILER_ADDRESS)%' + # Директория сохранения файлов + images_directory: '%kernel.project_dir%/public/uploads/user_images' services: # default configuration for services in *this* file @@ -48,6 +50,14 @@ services: App\Service\Action\ActionServiceInterface $resetEmailService: '@App\Service\Action\Classes\ResetEmail' + App\Service\Action\ActionServiceInterface $deleteImageService: '@App\Service\Action\Classes\DeleteImage' + + App\Service\Action\ActionServiceInterface $saveImageService: '@App\Service\Action\Classes\SaveImage' + + App\Service\Action\Classes\SaveImage: + arguments: + $targetDirectory: '%images_directory%' + App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' @@ -66,6 +76,8 @@ services: App\Service\Dto\DtoServiceInterface $recoveryDto: '@App\Service\Dto\Classes\RecoveryDto' + App\Service\Dto\DtoServiceInterface $imageDto: '@App\Service\Dto\Classes\ImageDto' + App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index 348eac7..dda47fc 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Service\Action\ActionServiceInterface; use App\Service\Dto\Classes\ChangeProfileDto; +use App\Service\Dto\Classes\ImageDto; use App\Service\Dto\Classes\RecoveryCodeDto; use App\Service\Dto\Classes\RecoveryDto; use App\Service\Response\Classes\ProfileResponse; @@ -116,4 +117,37 @@ class ProfileController extends AbstractController { return $resetEmailService->getResponse(); } + + #[Route('/profile/image', name: 'profile_image', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: ImageDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function saveImage( + ActionServiceInterface $saveImageService, + ): JsonResponse + { + return $saveImageService->getResponse(); + } + + #[Route('/profile/image/delete', name: 'profile_image_delete', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function deleteImage( + ActionServiceInterface $deleteImageService, + ): JsonResponse + { + return $deleteImageService->getResponse(); + } } diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 96c4f82..2082aa3 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -363,7 +363,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface new ReflectionExtractor() ); $serializer = new Serializer( - [$normalizer, new DateTimeNormalizer()], + [new DateTimeNormalizer(), $normalizer], [new JsonEncoder()] ); return $serializer->deserialize( @@ -388,7 +388,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface null, new ReflectionExtractor() ); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); + $serializer = new Serializer([new DateTimeNormalizer(), $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 index cc4d288..887b4b9 100644 --- a/app/src/Entity/UserHistory.php +++ b/app/src/Entity/UserHistory.php @@ -129,6 +129,19 @@ class UserHistory $type = $this->getType(); switch ($field = $this->getField()) { + case 'image': + switch ($type) { + case self::TYPE_CREATE: + $text = 'Изображение загружено'; + break; + case self::TYPE_UPDATE: + $text = 'Изображение обновлено'; + break; + case self::TYPE_DELETE: + $text = 'Изображение удалено'; + break; + } + break; case 'confirm': switch ($type) { case self::TYPE_CREATE: diff --git a/app/src/Entity/UserImage.php b/app/src/Entity/UserImage.php index d58226d..0716a49 100644 --- a/app/src/Entity/UserImage.php +++ b/app/src/Entity/UserImage.php @@ -14,7 +14,7 @@ class UserImage #[ORM\Column] private ?int $id = null; - #[ORM\OneToOne(inversedBy: 'image', cascade: ['persist', 'remove'])] + #[ORM\OneToOne(inversedBy: 'image', cascade: ['persist'])] private ?User $related_user = null; #[ORM\Column(length: 255)] diff --git a/app/src/Listeners/UserImageListener.php b/app/src/Listeners/UserImageListener.php new file mode 100644 index 0000000..0eafa48 --- /dev/null +++ b/app/src/Listeners/UserImageListener.php @@ -0,0 +1,77 @@ +getObjectManager(); + if (!$this->checkFile($file)) { + $om->remove($file); + $om->flush(); + } + } + + public function preUpdate(UserImage $file, PreUpdateEventArgs $args): void + { + $om = $args->getObjectManager(); + if (!$this->checkFile($file)) { + $om->remove($file); + $om->flush(); + } + } + + public function postRemove(UserImage $file, PostRemoveEventArgs $args): void + { + $om = $args->getObjectManager(); + $this->removeFile($file); + } + + /** + * Проверка наличия файла + * + * @param UserImage $file + * + * @return bool + */ + private function checkFile(UserImage $file): bool + { + if ($path = $file->getPath()) { + $filesystem = new Filesystem(); + if ($filesystem->exists([$path])) { + return true; + } + } + + return false; + } + + /** + * Удаление файла + * + * @param UserImage $file + * + * @return void + */ + private function removeFile(UserImage $file): void + { + if ($path = $file->getPath()) { + $filesystem = new Filesystem(); + if ($filesystem->exists([$path])) { + $filesystem->remove([$path]); + } + } + } +} \ No newline at end of file diff --git a/app/src/Listeners/UserListener.php b/app/src/Listeners/UserListener.php index ac6c084..7af6d63 100644 --- a/app/src/Listeners/UserListener.php +++ b/app/src/Listeners/UserListener.php @@ -49,48 +49,64 @@ class UserListener } $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; + if ($image = $user->getImage()) { + $imageOriginalValues = $uow->getOriginalEntityData($user->getImage()); + if ($imageOriginalValues) { + if ($image->getPath() !== $imageOriginalValues['path']) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_UPDATE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($image->getName()); + $newUserHistory->setOldValue($imageOriginalValues['name']); + $user->addUserHistory($newUserHistory); } - $value = $property->getValue($checkUser); - $oldValue = $property->getValue($userExists); - if ($value !== $oldValue) { - if (empty($value) && empty($oldValue)) { + } + } + + if ($user->getId()) { + if ($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; + $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($type); - $newUserHistory->setField($name); - $newUserHistory->setValue($value); - $newUserHistory->setOldValue($oldValue); + $newUserHistory->setType($user->isDeleted() ? UserHistory::TYPE_DELETE : UserHistory::TYPE_RECOVERY); + $newUserHistory->setField('user'); $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()); + 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); } - $newUserHistory->setField('confirm'); - $user->addUserHistory($newUserHistory); } } else { $newUserHistory = new UserHistory(); diff --git a/app/src/Service/Action/Classes/DeleteImage.php b/app/src/Service/Action/Classes/DeleteImage.php new file mode 100644 index 0000000..6431f4f --- /dev/null +++ b/app/src/Service/Action/Classes/DeleteImage.php @@ -0,0 +1,61 @@ +user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + $image = $this->user->getImage(); + if ($image) { + $em = $this->doctrine->getManager(); + + try { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_DELETE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($image->getName()); + $this->user->addUserHistory($newUserHistory); + $em->remove($image); + $em->flush(); + $this->response->addMessage('Изображение удалено'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка удаления изображения'); + } + } else { + $this->response->addError('Нет изображения'); + } + } + + 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/Action/Classes/SaveImage.php b/app/src/Service/Action/Classes/SaveImage.php new file mode 100644 index 0000000..4e437de --- /dev/null +++ b/app/src/Service/Action/Classes/SaveImage.php @@ -0,0 +1,144 @@ +user = $security->getUser(); + parent::__construct($response); + } + + public function runAction(): void + { + $file = $this->saveFile(); + if ($file) { + $em = $this->doctrine->getManager(); + + $oldImage = $this->user->getImage(); + if ($oldImage) { + $oldImage->setName($file->getName()); + $oldImage->setType($file->getType()); + $oldImage->setPath($file->getPath()); + $file = $oldImage; + } else { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_CREATE); + $newUserHistory->setField('image'); + $newUserHistory->setValue($file->getName()); + $this->user->addUserHistory($newUserHistory); + $this->user->setImage($file); + } + try { + $em->persist($file); + $em->flush(); + $this->response->addMessage('Изображение сохранено'); + } catch (\Exception $exception) { + $this->response->addError('Ошибка сохранения файла пользователя'); + } + } else { + $this->response->addError('Ошибка сохранения файла'); + } + } + + public function validate(): bool + { + if ($this->user === null) { + $this->response->addError('Вы не авторизованы'); + return false; + } + if ($this->user->isDeleted()) { + $this->response->addError('Профиль удален'); + return false; + } + return $this->imageDto->validate($this->response); + } + + public function saveFile(): ?UserImage + { + /** @var ImageDto $dto */ + $dto = $this->imageDto->getClass(); + + $matches = []; + if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,([a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$)/i', $dto->data ?: '', $matches)) { + return null; + } + $extension = $matches[1]; + $content = $matches[4]; + if ($extension && $content) { + $mimeTypes = new MimeTypes(); + $types = $mimeTypes->getExtensions($extension); + if (empty($types)) { + $this->response->addError('Неизвестное расширения файла'); + return null; + } + if (empty(array_intersect($types, self::IMAGE_EXTENSIONS))) { + $this->response->addError('Файл расширения "'. reset($types) .'" недоступен для загрузки. Доступные расширения: ' . implode(', ', self::IMAGE_EXTENSIONS) . '.'); + return null; + } + + $filename = pathinfo($dto->name, PATHINFO_FILENAME); + + $filename = $filename . '.' . reset($types); + + $decoded = base64_decode($content); + if (!$decoded) { + $this->response->addError('Ошибка декодирования файла'); + return null; + } + + $tmpPath = sys_get_temp_dir() . '/file_upload' . uniqid(); + + if (file_put_contents($tmpPath, base64_decode($content))) { + $uploadFile = new UploadedFile($tmpPath, $filename, $extension, null, true); + + $originalFilename = pathinfo($uploadFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $this->slugger->slug($originalFilename); + $fileName = $safeFilename . '-' . uniqid() . '.' . $uploadFile->guessExtension(); + $filedir = $this->targetDirectory; + try { + $file = $uploadFile->move($filedir, $fileName); + $dmFile = new UserImage(); + $dmFile->setName($uploadFile->getClientOriginalName()); + $dmFile->setPath($file->getRealPath()); + $dmFile->setType($extension); + return $dmFile; + } catch (FileException $e) { + return null; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/Classes/ImageDto.php b/app/src/Service/Dto/Classes/ImageDto.php new file mode 100644 index 0000000..5f47f0d --- /dev/null +++ b/app/src/Service/Dto/Classes/ImageDto.php @@ -0,0 +1,23 @@ + Date: Mon, 24 Jun 2024 13:38:39 +0500 Subject: [PATCH 7/7] new autowire --- README.md | 15 ++++ app/config/packages/security.yaml | 2 +- app/config/services.yaml | 67 +---------------- app/src/Controller/AuthController.php | 37 +++++++--- app/src/Controller/ProfileController.php | 41 ++++++---- app/src/Entity/User.php | 22 +++--- app/src/Entity/UserHistory.php | 3 + app/src/Listeners/KernelExceptionListener.php | 25 +++++++ app/src/Listeners/UserListener.php | 6 ++ .../Service/Action/ActionServiceInterface.php | 14 ++++ app/src/Service/Action/BaseActionService.php | 70 +++++++++++++++++- .../Service/Action/Classes/ChangeProfile.php | 67 ++++++++++------- .../Action/Classes/CheckRecoveryCode.php | 36 ++++----- .../Action/Classes/CheckRegisterCode.php | 65 ++++++++-------- .../Service/Action/Classes/DeleteImage.php | 48 +++++------- .../Service/Action/Classes/DeleteProfile.php | 45 ++++------- app/src/Service/Action/Classes/GetProfile.php | 54 ++++++-------- app/src/Service/Action/Classes/None.php | 9 ++- .../Action/Classes/RecoveryProfile.php | 48 ++++++------ app/src/Service/Action/Classes/Register.php | 74 +++++++++---------- app/src/Service/Action/Classes/ResetEmail.php | 55 +++++--------- .../Service/Action/Classes/ResetPassword.php | 55 ++++++++------ .../Action/Classes/ResetPasswordCode.php | 37 ++++++---- app/src/Service/Action/Classes/SaveImage.php | 73 +++++++++--------- .../Action/Classes/SendRegisterCode.php | 60 +++++++-------- .../Action/Classes/SendResetPasswordCode.php | 41 ++++++---- .../Service/Action/UserBaseActionService.php | 42 +++++++++++ app/src/Service/Dto/BaseDto.php | 3 + .../Service/Dto/Classes/ChangePasswordDto.php | 2 + .../Service/Dto/Classes/ChangeProfileDto.php | 2 + app/src/Service/Dto/Classes/ImageDto.php | 2 + app/src/Service/Dto/Classes/NoneDto.php | 2 + .../Service/Dto/Classes/RecoveryCodeDto.php | 2 + app/src/Service/Dto/Classes/RecoveryDto.php | 2 + .../Service/Dto/Classes/RegisterCodeDto.php | 2 + app/src/Service/Dto/Classes/RegisterDto.php | 2 + .../Dto/Classes/ResetPasswordCodeDto.php | 2 + .../Response/Classes/ProfileResponse.php | 2 + app/src/Service/Response/Classes/Response.php | 5 ++ .../Response/Classes/TokenResponse.php | 2 + .../Response/ResponseServiceInterface.php | 2 + .../Classes/Code/PasswordCodeSendService.php | 2 + .../Classes/Code/RecoveryCodeSendService.php | 2 + .../Classes/Code/RegisterCodeSendService.php | 2 + .../Service/Send/Classes/CodeSendService.php | 2 + app/src/Service/Send/SendService.php | 1 - 46 files changed, 659 insertions(+), 493 deletions(-) create mode 100644 app/src/Listeners/KernelExceptionListener.php create mode 100644 app/src/Service/Action/UserBaseActionService.php diff --git a/README.md b/README.md index a99a2a1..362f296 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,21 @@ ## Инструкция +## Kafka +
+Инструкция + +1. Создание топика + 1. Запустить команду - `make create-kafka-topic` +2. Запуск воркера + 1. Перейти в командную строку php - `make shell` + 2. Запустить команду - `bin/console messenger:consume send_transport` +3. Остановка воркера + 1. Перейти в командную строку php - `make shell` + 2. Запустить команду - `bin/console messenger:stop-workers` + +
+ ## Настройка Xdebug (PHPStorm)
Инструкция diff --git a/app/config/packages/security.yaml b/app/config/packages/security.yaml index 37c76cf..ffde54b 100644 --- a/app/config/packages/security.yaml +++ b/app/config/packages/security.yaml @@ -49,9 +49,9 @@ security: - { path: ^/api/register/send, roles: ROLE_USER } - { path: ^/api/register/check, roles: ROLE_USER } + - { path: ^/api/password/reset/check, roles: PUBLIC_ACCESS } - { path: ^/api/password/reset, roles: ROLE_USER } - { path: ^/api/password/send, roles: PUBLIC_ACCESS } - - { path: ^/api/password/reset/check, roles: PUBLIC_ACCESS } - { path: ^/api/profile/recovery, roles: PUBLIC_ACCESS } - { path: ^/api/profile/recovery/check, roles: PUBLIC_ACCESS } diff --git a/app/config/services.yaml b/app/config/services.yaml index 43d8e48..02884eb 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -25,67 +25,10 @@ services: - '../src/Entity/' - '../src/Kernel.php' - # Сервисы действий - App\Service\Action\ActionServiceInterface $registerService: '@App\Service\Action\Classes\Register' - - App\Service\Action\ActionServiceInterface $profileService: '@App\Service\Action\Classes\GetProfile' - - App\Service\Action\ActionServiceInterface $deleteProfileService: '@App\Service\Action\Classes\DeleteProfile' - - App\Service\Action\ActionServiceInterface $recoveryProfileService: '@App\Service\Action\Classes\RecoveryProfile' - - App\Service\Action\ActionServiceInterface $checkRegisterService: '@App\Service\Action\Classes\CheckRegisterCode' - - App\Service\Action\ActionServiceInterface $checkRecoveryService: '@App\Service\Action\Classes\CheckRecoveryCode' - - App\Service\Action\ActionServiceInterface $sendRegisterService: '@App\Service\Action\Classes\SendRegisterCode' - - App\Service\Action\ActionServiceInterface $sendPasswordCodeService: '@App\Service\Action\Classes\SendResetPasswordCode' - - App\Service\Action\ActionServiceInterface $resetPasswordCodeService: '@App\Service\Action\Classes\ResetPasswordCode' - - 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 $deleteImageService: '@App\Service\Action\Classes\DeleteImage' - - App\Service\Action\ActionServiceInterface $saveImageService: '@App\Service\Action\Classes\SaveImage' - App\Service\Action\Classes\SaveImage: arguments: $targetDirectory: '%images_directory%' - App\Service\Action\ActionServiceInterface: '@App\Service\Action\Classes\None' - - - # Сервисы Dto - App\Service\Dto\DtoServiceInterface $registerDto: '@App\Service\Dto\Classes\RegisterDto' - - App\Service\Dto\DtoServiceInterface $registerCodeDto: '@App\Service\Dto\Classes\RegisterCodeDto' - - App\Service\Dto\DtoServiceInterface $recoveryCodeDto: '@App\Service\Dto\Classes\RecoveryCodeDto' - - App\Service\Dto\DtoServiceInterface $passwordResetDto: '@App\Service\Dto\Classes\ResetPasswordCodeDto' - - 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 $imageDto: '@App\Service\Dto\Classes\ImageDto' - - App\Service\Dto\DtoServiceInterface: '@App\Service\Dto\Classes\NoneDto' - - - # Сервисы ответа - App\Service\Response\ResponseServiceInterface $profileResponse: '@App\Service\Response\Classes\ProfileResponse' - - App\Service\Response\ResponseServiceInterface: '@App\Service\Response\Classes\Response' - # Сервис отправки App\Service\Send\SendService: @@ -93,13 +36,9 @@ services: $confirmType: '%confirm_type%' $fromEmail: '%from_email%' - App\Service\Send\SendServiceInterface $codeSendService: '@App\Service\Send\Classes\CodeSendService' - - App\Service\Send\SendServiceInterface $registerCodeSendService: '@App\Service\Send\Classes\Code\RegisterCodeSendService' - - App\Service\Send\SendServiceInterface $recoveryCodeSendService: '@App\Service\Send\Classes\Code\RecoveryCodeSendService' - - App\Service\Send\SendServiceInterface $passwordCodeSendService: '@App\Service\Send\Classes\Code\PasswordCodeSendService' + App\Listeners\KernelExceptionListener: + tags: + - { name: kernel.event_listener, event: kernel.exception } # События JWT авторизации acme_api.event.authentication_success_listener: diff --git a/app/src/Controller/AuthController.php b/app/src/Controller/AuthController.php index 9135e2a..cc257a8 100644 --- a/app/src/Controller/AuthController.php +++ b/app/src/Controller/AuthController.php @@ -11,6 +11,7 @@ use App\Service\Dto\Classes\ResetPasswordCodeDto; use App\Service\Response\Classes\Response; use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; use OpenApi\Attributes as OA; @@ -31,18 +32,20 @@ class AuthController extends AbstractController content: new OA\JsonContent(ref: new Model(type: RegisterDto::class)) )] public function register( - ActionServiceInterface $registerService + #[Autowire(service: 'action.register')] + ActionServiceInterface $actionService ): JsonResponse { - return $registerService->getResponse(); + return $actionService->getResponse(); } #[Route('/register/send', name: 'register_send', methods: ['GET'])] public function sendRegisterCode( - ActionServiceInterface $sendRegisterService, + #[Autowire(service: 'action.register.send')] + ActionServiceInterface $actionService, ): JsonResponse { - return $sendRegisterService->getResponse(); + return $actionService->getResponse(); } #[Route('/register/check', name: 'register_check', methods: ['POST'])] @@ -50,36 +53,46 @@ class AuthController extends AbstractController content: new OA\JsonContent(ref: new Model(type: RegisterCodeDto::class)) )] public function checkRegisterCode( - ActionServiceInterface $checkRegisterService + #[Autowire(service: 'action.register.code')] + ActionServiceInterface $actionService ): JsonResponse { - return $checkRegisterService->getResponse(); + return $actionService->getResponse(); } #[Route('/password/reset', name: 'password_reset', methods: ['POST'])] #[OA\RequestBody( content: new OA\JsonContent(ref: new Model(type: ChangePasswordDto::class)) )] - public function resetPassword(ActionServiceInterface $resetPasswordService): JsonResponse + public function resetPassword( + #[Autowire(service: 'action.reset.password.change')] + ActionServiceInterface $actionService + ): JsonResponse { - return $resetPasswordService->getResponse(); + return $actionService->getResponse(); } #[Route('/password/send', name: 'password_send', methods: ['POST'])] #[OA\RequestBody( content: new OA\JsonContent(ref: new Model(type: RecoveryDto::class)) )] - public function sendResetPassword(ActionServiceInterface $sendPasswordCodeService): JsonResponse + public function sendResetPassword( + #[Autowire(service: 'action.reset.password.send')] + ActionServiceInterface $actionService + ): JsonResponse { - return $sendPasswordCodeService->getResponse(); + return $actionService->getResponse(); } #[Route('/password/reset/check', name: 'password_reset_check', methods: ['POST'])] #[OA\RequestBody( content: new OA\JsonContent(ref: new Model(type: ResetPasswordCodeDto::class)) )] - public function resetCheckPassword(ActionServiceInterface $resetPasswordCodeService): JsonResponse + public function resetCheckPassword( + #[Autowire(service: 'action.reset.password.code')] + ActionServiceInterface $actionService + ): JsonResponse { - return $resetPasswordCodeService->getResponse(); + return $actionService->getResponse(); } } diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index dda47fc..2f94488 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -11,6 +11,7 @@ use App\Service\Response\Classes\ProfileResponse; use App\Service\Response\Classes\Response; use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; use OpenApi\Attributes as OA; @@ -28,10 +29,11 @@ class ProfileController extends AbstractController ) )] public function profile( - ActionServiceInterface $profileService + #[Autowire(service: 'action.profile')] + ActionServiceInterface $actionService ): JsonResponse { - return $profileService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/delete', name: 'profile_delete', methods: ['GET'])] @@ -43,10 +45,11 @@ class ProfileController extends AbstractController ) )] public function deleteProfile( - ActionServiceInterface $deleteProfileService, + #[Autowire(service: 'action.profile.delete')] + ActionServiceInterface $actionService, ): JsonResponse { - return $deleteProfileService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/recovery', name: 'profile_recovery', methods: ['POST'])] @@ -61,10 +64,11 @@ class ProfileController extends AbstractController ) )] public function recoveryProfile( - ActionServiceInterface $recoveryProfileService, + #[Autowire(service: 'action.recovery.send')] + ActionServiceInterface $actionService, ): JsonResponse { - return $recoveryProfileService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/recovery/check', name: 'profile_recovery_check', methods: ['POST'])] @@ -79,10 +83,11 @@ class ProfileController extends AbstractController ) )] public function recoveryCodeProfile( - ActionServiceInterface $checkRecoveryService, + #[Autowire(service: 'action.recovery.code')] + ActionServiceInterface $actionService, ): JsonResponse { - return $checkRecoveryService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/change', name: 'profile_change', methods: ['POST'])] @@ -97,10 +102,11 @@ class ProfileController extends AbstractController ) )] public function changeProfile( - ActionServiceInterface $profileChangeService, + #[Autowire(service: 'action.profile.change')] + ActionServiceInterface $actionService, ): JsonResponse { - return $profileChangeService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/reset/email', name: 'profile_reset_email', methods: ['GET'])] @@ -112,10 +118,11 @@ class ProfileController extends AbstractController ) )] public function resetLastConfirmEmail( - ActionServiceInterface $resetEmailService, + #[Autowire(service: 'action.reset.email')] + ActionServiceInterface $actionService, ): JsonResponse { - return $resetEmailService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/image', name: 'profile_image', methods: ['POST'])] @@ -130,10 +137,11 @@ class ProfileController extends AbstractController ) )] public function saveImage( - ActionServiceInterface $saveImageService, + #[Autowire(service: 'action.profile.image.save')] + ActionServiceInterface $actionService, ): JsonResponse { - return $saveImageService->getResponse(); + return $actionService->getResponse(); } #[Route('/profile/image/delete', name: 'profile_image_delete', methods: ['GET'])] @@ -145,9 +153,10 @@ class ProfileController extends AbstractController ) )] public function deleteImage( - ActionServiceInterface $deleteImageService, + #[Autowire(service: 'action.profile.image.delete')] + ActionServiceInterface $actionService, ): JsonResponse { - return $deleteImageService->getResponse(); + return $actionService->getResponse(); } } diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 2082aa3..348b043 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -87,7 +87,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this->id; } - #[Groups(['all', 'profile', 'edit', 'card', 'detail'])] + #[Groups(['all', 'profile', 'edit', 'card', 'detail', 'listen'])] public function getEmail(): ?string { return $this->email; @@ -146,7 +146,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface /** * @see PasswordAuthenticatedUserInterface */ - #[Groups(['all'])] + #[Groups(['all', 'listen'])] public function getPassword(): string { return $this->password; @@ -168,7 +168,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface // $this->plainPassword = null; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function getName(): ?string { return $this->name; @@ -181,7 +181,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function getSurname(): ?string { return $this->surname; @@ -194,7 +194,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function getPatronymic(): ?string { return $this->patronymic; @@ -207,7 +207,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function getPhoneNumber(): ?string { return $this->phone_number; @@ -220,7 +220,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function getImage(): ?UserImage { return $this->image; @@ -243,7 +243,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function isConfirm(): ?bool { return $this->confirm; @@ -256,7 +256,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - #[Groups(['all', 'profile', 'edit'])] + #[Groups(['all', 'profile', 'edit', 'listen'])] public function isDeleted(): ?bool { return $this->deleted; @@ -353,7 +353,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface * @return self */ #[Ignore()] - public static function createByArray(array $data, array $groups = ['edit']): ?self + public static function createByArray(array $data, array $groups = ['listen']): ?self { try { $normalizer = new ObjectNormalizer( @@ -380,7 +380,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface } #[Ignore()] - public function newCopy(array $groups = ['edit']): ?self + public function newCopy(array $groups = ['listen']): ?self { $normalizer = new ObjectNormalizer( new ClassMetadataFactory(new AttributeLoader()), diff --git a/app/src/Entity/UserHistory.php b/app/src/Entity/UserHistory.php index 887b4b9..3cf93de 100644 --- a/app/src/Entity/UserHistory.php +++ b/app/src/Entity/UserHistory.php @@ -129,6 +129,9 @@ class UserHistory $type = $this->getType(); switch ($field = $this->getField()) { + case 'password': + $text = 'Пароль изменен'; + break; case 'image': switch ($type) { case self::TYPE_CREATE: diff --git a/app/src/Listeners/KernelExceptionListener.php b/app/src/Listeners/KernelExceptionListener.php new file mode 100644 index 0000000..a9ef3cc --- /dev/null +++ b/app/src/Listeners/KernelExceptionListener.php @@ -0,0 +1,25 @@ + 'onKernelException', + ]; + } + + public function onKernelException(ExceptionEvent $event) + { + $response = new \App\Service\Response\Classes\Response(); + $response->setStatusCode(Response::HTTP_FORBIDDEN); + $response->addError($event->getThrowable()->getMessage()); + $event->setResponse($response->getResponse()); + } +} \ No newline at end of file diff --git a/app/src/Listeners/UserListener.php b/app/src/Listeners/UserListener.php index 7af6d63..9c2ae42 100644 --- a/app/src/Listeners/UserListener.php +++ b/app/src/Listeners/UserListener.php @@ -107,6 +107,12 @@ class UserListener $newUserHistory->setField('confirm'); $user->addUserHistory($newUserHistory); } + if ($userExists->getPassword() !== $user->getPassword()) { + $newUserHistory = new UserHistory(); + $newUserHistory->setType(UserHistory::TYPE_UPDATE); + $newUserHistory->setField('password'); + $user->addUserHistory($newUserHistory); + } } } else { $newUserHistory = new UserHistory(); diff --git a/app/src/Service/Action/ActionServiceInterface.php b/app/src/Service/Action/ActionServiceInterface.php index 0f16176..f3e7391 100644 --- a/app/src/Service/Action/ActionServiceInterface.php +++ b/app/src/Service/Action/ActionServiceInterface.php @@ -2,6 +2,10 @@ namespace App\Service\Action; +use App\Service\Dto\DtoServiceInterface; +use App\Service\Response\ResponseServiceInterface; +use App\Service\Send\SendServiceInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\JsonResponse; interface ActionServiceInterface @@ -11,4 +15,14 @@ interface ActionServiceInterface public function runAction(): void; public function validate(): bool; + + public function customValidate(): bool; + + public function initResponse(ResponseServiceInterface $responseService): void; + + public function initDto(DtoServiceInterface $dtoService): void; + + public function initDoctrine(ManagerRegistry $doctrine): void; + + public function initSend(SendServiceInterface $sendService): void; } \ No newline at end of file diff --git a/app/src/Service/Action/BaseActionService.php b/app/src/Service/Action/BaseActionService.php index 97235c5..df831f0 100644 --- a/app/src/Service/Action/BaseActionService.php +++ b/app/src/Service/Action/BaseActionService.php @@ -2,21 +2,54 @@ namespace App\Service\Action; +use App\Service\Dto\DtoServiceInterface; use App\Service\Response\Classes\Response; use App\Service\Response\ResponseServiceInterface; +use App\Service\Send\SendServiceInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Contracts\Service\Attribute\Required; abstract class BaseActionService implements ActionServiceInterface { protected ?ResponseServiceInterface $responseService; - public function __construct( - ResponseServiceInterface $baseResponseService, - ) + protected ?DtoServiceInterface $dtoService; + + protected ?ManagerRegistry $doctrine; + + protected ?SendServiceInterface $sendService; + + #[Required] + public function initResponse( + ResponseServiceInterface $responseService + ): void + { + $this->responseService = $responseService; + } + + #[Required] + public function initDto( + DtoServiceInterface $dtoService + ): void { - $this->responseService = $baseResponseService; + $this->dtoService = $dtoService; } + #[Required] + public function initDoctrine(ManagerRegistry $doctrine): void + { + $this->doctrine = $doctrine; + } + + #[Required] + public function initSend(SendServiceInterface $sendService): void + { + $this->sendService = $sendService; + } + + abstract public function needDto(): bool; + public function getResponse(): JsonResponse { if ($this->validate()) { @@ -31,4 +64,33 @@ abstract class BaseActionService implements ActionServiceInterface $response->addError('Ошибка получения ответа'); return $response->getResponse(); } + + protected function getDto(): ?DtoServiceInterface + { + if ($this->dtoService) { + return $this->dtoService->getClass(); + } + + return null; + } + + public function validate(): bool + { + $valid = true; + + if ($this->needDto() && $this->dtoService) { + $valid = $this->dtoService->validate($this->responseService); + } + + if ($valid) { + $valid = $this->customValidate(); + } + + return $valid; + } + + public function customValidate(): bool + { + return true; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/ChangeProfile.php b/app/src/Service/Action/Classes/ChangeProfile.php index 62d1ef0..9c155d8 100644 --- a/app/src/Service/Action/Classes/ChangeProfile.php +++ b/app/src/Service/Action/Classes/ChangeProfile.php @@ -3,32 +3,37 @@ namespace App\Service\Action\Classes; use App\Entity\User; -use App\Service\Action\BaseActionService; +use App\Service\Action\UserBaseActionService; 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; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; -class ChangeProfile extends BaseActionService +#[AsAlias(id: 'action.profile.change', public: true)] +class ChangeProfile extends UserBaseActionService { - private ?User $user; - - public function __construct( - private ResponseServiceInterface $profileResponse, - private DtoServiceInterface $profileDto, - private ManagerRegistry $doctrine, - Security $security - ) + #[Required] public function initResponse( + #[Autowire(service: 'response.profile')] + ResponseServiceInterface $responseService + ): void { - $this->user = $security->getUser(); - parent::__construct($profileResponse); + parent::initResponse($responseService); + } + + #[Required] public function initDto( + #[Autowire(service: 'dto.profile.change')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); } public function runAction(): void { /** @var ChangeProfileDto $dto */ - $dto = $this->profileDto->getClass(); + $dto = $this->getDto(); /** @var ?User $userExists */ $userExists = $this->doctrine->getRepository(User::class) @@ -36,11 +41,11 @@ class ChangeProfile extends BaseActionService if ($userExists !== null) { if ($userExists->getEmail() === $dto->email) { - $this->profileResponse->addError('Email занят другим пользователем'); + $this->responseService->addError('Email занят другим пользователем'); } elseif ($userExists->getPhoneNumber() === $dto->phoneNumber) { - $this->profileResponse->addError('Номер телефона занят другим пользователем'); + $this->responseService->addError('Номер телефона занят другим пользователем'); } else { - $this->profileResponse->addError('Email или номер телефона занят другим пользователем'); + $this->responseService->addError('Email или номер телефона занят другим пользователем'); } } else { $changed = false; @@ -65,7 +70,7 @@ class ChangeProfile extends BaseActionService $changed = true; } - if ($dto->email !== null && $dto->email !== $this->user->getEmail()) { + if ($dto->email !== null && $dto->email !== $this->user->getEmail()) { $this->user->setEmail($dto->email); $changed = true; } @@ -75,22 +80,28 @@ class ChangeProfile extends BaseActionService $em = $this->doctrine->getManager(); $em->persist($this->user); $em->flush(); - $this->profileResponse->setData($this->user); + $this->responseService->setData($this->user); } catch (\Exception $e) { - $this->profileResponse->addError('Ошибка сохранения профиля'); + $this->responseService->addError('Ошибка сохранения профиля'); } } else { - $this->profileResponse->setData($this->user); + $this->responseService->setData($this->user); } } } - public function validate(): bool + public function checkDelete(): bool { - if ($this->user === null) { - $this->profileResponse->addError('Вы не авторизованы'); - return false; - } - return $this->profileDto->validate($this->profileResponse); + return true; + } + + public function checkConfirm(): bool + { + return false; + } + + public function needDto(): bool + { + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/CheckRecoveryCode.php b/app/src/Service/Action/Classes/CheckRecoveryCode.php index 4a1d3a0..afdfff9 100644 --- a/app/src/Service/Action/Classes/CheckRecoveryCode.php +++ b/app/src/Service/Action/Classes/CheckRecoveryCode.php @@ -6,20 +6,22 @@ use App\Entity\User; use App\Service\Action\BaseActionService; use App\Service\Dto\Classes\RecoveryCodeDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; +#[AsAlias(id: 'action.recovery.code', public: true)] class CheckRecoveryCode extends BaseActionService { - public function __construct( - private DtoServiceInterface $recoveryCodeDto, - private ResponseServiceInterface $response, - private ManagerRegistry $doctrine, - ) + #[Required] public function initDto( + #[Autowire(service: 'dto.recovery.code')] + DtoServiceInterface $dtoService + ): void { - parent::__construct($response); + parent::initDto($dtoService); } + /** * Восстановление учетной записи по коду * @@ -28,7 +30,7 @@ class CheckRecoveryCode extends BaseActionService public function runAction(): void { /** @var RecoveryCodeDto $dto */ - $dto = $this->recoveryCodeDto->getClass(); + $dto = $this->getDto(); /** @var User $userExists */ $userExists = $this->doctrine->getRepository(User::class) ->findOneByUniq($dto->email, $dto->phoneNumber); @@ -38,7 +40,7 @@ class CheckRecoveryCode extends BaseActionService $code = $dto->code; $registerCode = $userExists->getRegisterCode(); if ($registerCode === null) { - $this->response->addError('Код подтверждения не отправлен'); + $this->responseService->addError('Код подтверждения не отправлен'); } else { if ($registerCodeDate = $registerCode->getDate()) { if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { @@ -48,24 +50,24 @@ class CheckRecoveryCode extends BaseActionService $em->persist($userExists); $em->remove($registerCode); $em->flush(); - $this->response->addMessage('Профиль восстановлен'); + $this->responseService->addMessage('Профиль восстановлен'); } catch (\Exception $exception) { - $this->response->addError('Ошибка восстановления профиля'); + $this->responseService->addError('Ошибка восстановления профиля'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } } else { - $this->response->addError('Пользователь не найден'); + $this->responseService->addError('Пользователь не найден'); } } - public function validate(): bool + public function needDto(): bool { - return $this->recoveryCodeDto->validate($this->response); + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/CheckRegisterCode.php b/app/src/Service/Action/Classes/CheckRegisterCode.php index 6411a73..3a26d7f 100644 --- a/app/src/Service/Action/Classes/CheckRegisterCode.php +++ b/app/src/Service/Action/Classes/CheckRegisterCode.php @@ -2,35 +2,24 @@ namespace App\Service\Action\Classes; -use App\Entity\User; -use App\Service\Action\BaseActionService; -use App\Service\Dto\Classes\RegisterCodeDto; +use App\Service\Action\UserBaseActionService; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; -class CheckRegisterCode extends BaseActionService +#[AsAlias(id: 'action.register.code', public: true)] +class CheckRegisterCode extends UserBaseActionService { - private ?User $user; - - /** - * @param RegisterCodeDto $registerCodeDto - * @param ResponseServiceInterface $response - * @param ManagerRegistry $doctrine - * @param Security $security - */ - public function __construct( - private DtoServiceInterface $registerCodeDto, - private ResponseServiceInterface $response, - private ManagerRegistry $doctrine, - Security $security, - ) + #[Required] public function initDto( + #[Autowire(service: 'dto.register.code')] + DtoServiceInterface $dtoService + ): void { - $this->user = $security->getUser(); - parent::__construct($response); + parent::initDto($dtoService); } + /** * Подтверждение регистрации по коду * @@ -39,10 +28,10 @@ class CheckRegisterCode extends BaseActionService public function runAction(): void { $currentDate = new \DateTime(); - $code = $this->registerCodeDto->getClass()->code; + $code = $this->getDto()->code; $registerCode = $this->user->getRegisterCode(); if ($registerCode === null) { - $this->response->addError('Код подтверждения не отправлен'); + $this->responseService->addError('Код подтверждения не отправлен'); } else { if ($registerCodeDate = $registerCode->getDate()) { if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { @@ -52,25 +41,31 @@ class CheckRegisterCode extends BaseActionService $em->persist($this->user); $em->remove($registerCode); $em->flush(); - $this->response->addMessage('Регистрация подтверждена'); + $this->responseService->addMessage('Регистрация подтверждена'); } catch (\Exception $exception) { - $this->response->addError('Ошибка подтверждения регистрации'); + $this->responseService->addError('Ошибка подтверждения регистрации'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } } - public function validate(): bool + public function needDto(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } - return $this->registerCodeDto->validate($this->response); + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/DeleteImage.php b/app/src/Service/Action/Classes/DeleteImage.php index 6431f4f..ff32f3d 100644 --- a/app/src/Service/Action/Classes/DeleteImage.php +++ b/app/src/Service/Action/Classes/DeleteImage.php @@ -2,27 +2,13 @@ namespace App\Service\Action\Classes; -use App\Entity\User; use App\Entity\UserHistory; -use App\Service\Action\BaseActionService; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bundle\SecurityBundle\Security; +use App\Service\Action\UserBaseActionService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; -class DeleteImage extends BaseActionService +#[AsAlias(id: 'action.profile.image.delete', public: true)] +class DeleteImage extends UserBaseActionService { - private ?User $user; - - public function __construct( - private ResponseServiceInterface $response, - private ManagerRegistry $doctrine, - Security $security - ) - { - $this->user = $security->getUser(); - parent::__construct($response); - } - public function runAction(): void { $image = $this->user->getImage(); @@ -37,25 +23,27 @@ class DeleteImage extends BaseActionService $this->user->addUserHistory($newUserHistory); $em->remove($image); $em->flush(); - $this->response->addMessage('Изображение удалено'); + $this->responseService->addMessage('Изображение удалено'); } catch (\Exception $exception) { - $this->response->addError('Ошибка удаления изображения'); + $this->responseService->addError('Ошибка удаления изображения'); } } else { - $this->response->addError('Нет изображения'); + $this->responseService->addError('Нет изображения'); } } - public function validate(): bool + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } - if ($this->user->isDeleted()) { - $this->response->addError('Профиль удален'); - return false; - } return true; } + + public function checkConfirm(): bool + { + return false; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/DeleteProfile.php b/app/src/Service/Action/Classes/DeleteProfile.php index 9408091..d50a894 100644 --- a/app/src/Service/Action/Classes/DeleteProfile.php +++ b/app/src/Service/Action/Classes/DeleteProfile.php @@ -2,26 +2,12 @@ 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; +use App\Service\Action\UserBaseActionService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; -class DeleteProfile extends BaseActionService +#[AsAlias(id: 'action.profile.delete', public: true)] +class DeleteProfile extends UserBaseActionService { - private ?User $user; - - public function __construct( - private ResponseServiceInterface $response, - Security $security, - private ManagerRegistry $doctrine, - ) - { - $this->user = $security->getUser(); - parent::__construct($response); - } - /** * Деактивация учетной записи * @@ -34,23 +20,24 @@ class DeleteProfile extends BaseActionService $em = $this->doctrine->getManager(); $em->persist($this->user); $em->flush(); - $this->response->addMessage('Профиль удален'); + $this->responseService->addMessage('Профиль удален'); } catch (\Exception $exception) { - $this->response->addError('Ошибка удаления профиля'); + $this->responseService->addError('Ошибка удаления профиля'); } } - public function validate(): bool + public function needDto(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } + return false; + } - if ($this->user->isDeleted()) { - $this->response->addError('Профиль уже удален'); - return false; - } + public function checkDelete(): bool + { return true; } + + public function checkConfirm(): bool + { + return false; + } } \ 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 8a98a3a..73a47bf 100644 --- a/app/src/Service/Action/Classes/GetProfile.php +++ b/app/src/Service/Action/Classes/GetProfile.php @@ -2,32 +2,24 @@ namespace App\Service\Action\Classes; -use App\Entity\User; -use App\Service\Action\BaseActionService; -use App\Service\Response\Classes\ProfileResponse; +use App\Service\Action\UserBaseActionService; use App\Service\Response\ResponseServiceInterface; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\Security\Http\Attribute\CurrentUser; -use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; -class GetProfile extends BaseActionService +#[AsAlias(id: 'action.profile', public: true)] +class GetProfile extends UserBaseActionService { - private ?User $user; - - /** - * @param ProfileResponse $profileResponse - * @param Security $security - */ - public function __construct( - private ResponseServiceInterface $profileResponse, - Security $security - ) + #[Required] + public function initResponse( + #[Autowire(service: 'response.profile')] + ResponseServiceInterface $responseService + ): void { - $this->user = $security->getUser(); - parent::__construct($profileResponse); + parent::initResponse($responseService); } - /** * Получение профиля пользователя * @@ -37,19 +29,21 @@ class GetProfile extends BaseActionService */ public function runAction(): void { - $this->profileResponse->setData($this->user); + $this->responseService->setData($this->user); } - public function validate(): bool + public function checkDelete(): bool { - if ($this->user === null) { - $this->profileResponse->addError('Вы не авторизованы'); - return false; - } - if ($this->user->isDeleted()) { - $this->profileResponse->addError('Профиль удален'); - return false; - } return true; } + + public function checkConfirm(): bool + { + return false; + } + + public function needDto(): bool + { + return false; + } } \ 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 f891845..4dea602 100644 --- a/app/src/Service/Action/Classes/None.php +++ b/app/src/Service/Action/Classes/None.php @@ -3,7 +3,9 @@ namespace App\Service\Action\Classes; use App\Service\Action\BaseActionService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +#[AsAlias] class None extends BaseActionService { @@ -12,11 +14,16 @@ class None extends BaseActionService } - public function validate(): bool + public function customValidate(): bool { if ($this->responseService) { $this->responseService->addError('Действие не выбрано'); } return false; } + + public function needDto(): bool + { + return false; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/RecoveryProfile.php b/app/src/Service/Action/Classes/RecoveryProfile.php index d6e7e68..7cad306 100644 --- a/app/src/Service/Action/Classes/RecoveryProfile.php +++ b/app/src/Service/Action/Classes/RecoveryProfile.php @@ -6,27 +6,31 @@ use App\Entity\User; use App\Service\Action\BaseActionService; use App\Service\Dto\Classes\RecoveryDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; use App\Service\Send\SendServiceInterface; -use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; +#[AsAlias(id: 'action.recovery.send', public: true)] class RecoveryProfile extends BaseActionService { - /** - * @param DtoServiceInterface $recoveryDto - * @param ResponseServiceInterface $response - * @param ManagerRegistry $doctrine - */ - public function __construct( - private DtoServiceInterface $recoveryDto, - private ResponseServiceInterface $response, - private ManagerRegistry $doctrine, - private SendServiceInterface $recoveryCodeSendService - ) + #[Required] public function initDto( + #[Autowire(service: 'dto.recovery.send')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); + } + + #[Required] public function initSend( + #[Autowire(service: 'send.code.recovery')] + SendServiceInterface $sendService + ): void { - parent::__construct($response); + parent::initSend($sendService); } + /** * Отправка кода восстановления учетной записи * @@ -35,26 +39,26 @@ class RecoveryProfile extends BaseActionService public function runAction(): void { /** @var RecoveryDto $dto */ - $dto = $this->recoveryDto->getClass(); + $dto = $this->getDto(); /** @var User $userExists */ $userExists = $this->doctrine->getRepository(User::class) ->findOneByUniq($dto->email, $dto->phoneNumber); if ($userExists !== null) { if (!$userExists->isDeleted()) { - $this->response->addError('Профиль не удален'); + $this->responseService->addError('Профиль не удален'); } else { - $this->recoveryCodeSendService->setUser($userExists); - $this->recoveryCodeSendService->setResponse($this->response); - $this->recoveryCodeSendService->send(); + $this->sendService->setUser($userExists); + $this->sendService->setResponse($this->responseService); + $this->sendService->send(); } } else { - $this->response->addError('Пользователь не найден'); + $this->responseService->addError('Пользователь не найден'); } } - public function validate(): bool + public function needDto(): bool { - return $this->recoveryDto->validate($this->response); + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/Register.php b/app/src/Service/Action/Classes/Register.php index 1229109..42117d6 100644 --- a/app/src/Service/Action/Classes/Register.php +++ b/app/src/Service/Action/Classes/Register.php @@ -4,37 +4,43 @@ namespace App\Service\Action\Classes; use App\Entity\User; use App\Service\Action\BaseActionService; -use App\Service\Dto\Classes\RegisterDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use App\Service\Send\Classes\Code\RegisterCodeSendService; -use App\Service\Send\Classes\CodeSendService; -use App\Service\Send\SendService; use App\Service\Send\SendServiceInterface; -use Doctrine\Persistence\ManagerRegistry; use ReflectionClass; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Contracts\Service\Attribute\Required; +#[AsAlias(id: 'action.register', public: true)] class Register extends BaseActionService { /** - * @param RegisterDto $registerDto - * @param ResponseServiceInterface $response * @param UserPasswordHasherInterface $passwordHasher - * @param ManagerRegistry $doctrine - * @param RegisterCodeSendService $registerCodeSendService */ public function __construct( - private DtoServiceInterface $registerDto, - private ResponseServiceInterface $response, private UserPasswordHasherInterface $passwordHasher, - private ManagerRegistry $doctrine, - private SendServiceInterface $registerCodeSendService, ) { - parent::__construct($response); } + #[Required] public function initDto( + #[Autowire(service: 'dto.register')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); + } + + #[Required] public function initSend( + #[Autowire(service: 'send.code.register')] + SendServiceInterface $sendService + ): void + { + parent::initSend($sendService); + } + + /** * Регистрация * @@ -42,20 +48,21 @@ class Register extends BaseActionService */ public function runAction(): void { + $dto = $this->getDto(); $user = $this->createUser(); - if ($user !== null) { + if ($user !== null && $dto) { $userExists = $this->doctrine->getRepository(User::class) ->findOneByUniq($user->getEmail(), $user->getPhoneNumber()); if ($userExists) { - $this->response->addError('Пользователь уже существует'); + $this->responseService->addError('Пользователь уже существует'); } else { try { $user->setDeleted(false); $user->setConfirm(false); $hashedPassword = $this->passwordHasher->hashPassword( $user, - $this->registerDto->getClass()->password ?: '' + $dto->password ?: '' ); $user->setPassword($hashedPassword); @@ -63,30 +70,18 @@ class Register extends BaseActionService $em->persist($user); $em->flush(); - $this->response->addMessage('Пользователь зарегистрирован'); + $this->responseService->addMessage('Пользователь зарегистрирован'); - $this->registerCodeSendService->setUser($user); - $this->registerCodeSendService->setResponse($this->response); - $this->registerCodeSendService->send(); + $this->sendService->setUser($user); + $this->sendService->setResponse($this->responseService); + $this->sendService->send(); } catch (\Exception $exception) { - dd($exception); - $this->response->addError('Ошибка регистрации пользователя'); + $this->responseService->addError('Ошибка регистрации пользователя'); } - } } } - /** - * Валидация - * - * @return bool - */ - public function validate(): bool - { - return $this->registerDto->validate($this->response); - } - /** * Создание пользователя из Dto * @@ -96,7 +91,7 @@ class Register extends BaseActionService { $user = null; - $data = $this->registerDto->toArray(); + $data = $this->dtoService->toArray(); if ($data) { $user = new User(); @@ -112,9 +107,14 @@ class Register extends BaseActionService } } } else { - $this->response->addError('Ошибка получения данных'); + $this->responseService->addError('Ошибка получения данных'); } return $user; } + + public function needDto(): bool + { + return true; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/ResetEmail.php b/app/src/Service/Action/Classes/ResetEmail.php index 07f2709..cd5e06c 100644 --- a/app/src/Service/Action/Classes/ResetEmail.php +++ b/app/src/Service/Action/Classes/ResetEmail.php @@ -2,62 +2,45 @@ 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; +use App\Service\Action\UserBaseActionService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; -class ResetEmail extends BaseActionService +#[AsAlias(id: 'action.reset.email', public: true)] +class ResetEmail extends UserBaseActionService { - 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 уже установлен'); + $this->responseService->addMessage('Подтвержденный email уже установлен'); } else { try { $this->user->setEmail($email); $em = $this->doctrine->getManager(); $em->persist($this->user); $em->flush(); - $this->response->addMessage('Установлен email: ' . $email); + $this->responseService->addMessage('Установлен email: ' . $email); } catch (\Exception $e) { - $this->response->addError('Ошибка сохранения email'); + $this->responseService->addError('Ошибка сохранения email'); } } } else { - $this->response->addError('Нет последнего подтвержденного email'); + $this->responseService->addError('Нет последнего подтвержденного email'); } } - public function validate(): bool + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } - if ($this->user->isDeleted()) { - $this->response->addError('Профиль удален'); - return false; - } return true; } + + public function checkConfirm(): bool + { + return false; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/ResetPassword.php b/app/src/Service/Action/Classes/ResetPassword.php index ec611ab..21d0e7b 100644 --- a/app/src/Service/Action/Classes/ResetPassword.php +++ b/app/src/Service/Action/Classes/ResetPassword.php @@ -2,35 +2,36 @@ namespace App\Service\Action\Classes; -use App\Entity\User; -use App\Service\Action\BaseActionService; +use App\Service\Action\UserBaseActionService; use App\Service\Dto\Classes\ChangePasswordDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Contracts\Service\Attribute\Required; -class ResetPassword extends BaseActionService +#[AsAlias(id: 'action.reset.password.change', public: true)] +class ResetPassword extends UserBaseActionService { - private ?User $user; - public function __construct( - private ResponseServiceInterface $response, private UserPasswordHasherInterface $passwordHasher, - private DtoServiceInterface $passwordDto, - private ManagerRegistry $doctrine, - Security $security ) { - $this->user = $security->getUser(); - parent::__construct($response); } + #[Required] public function initDto( + #[Autowire(service: 'dto.password.change')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); + } + + public function runAction(): void { /** @var ChangePasswordDto $dto */ - $dto = $this->passwordDto->getClass(); + $dto = $this->getDto(); if ($this->passwordHasher->isPasswordValid($this->user, $dto->oldPassword)) { $hashedPassword = $this->passwordHasher->hashPassword( @@ -43,21 +44,27 @@ class ResetPassword extends BaseActionService $em = $this->doctrine->getManager(); $em->persist($this->user); $em->flush(); - $this->response->addMessage('Пароль изменен'); + $this->responseService->addMessage('Пароль изменен'); } catch (\Exception $exception) { - $this->response->addError('Ошибка изменения пароля'); + $this->responseService->addError('Ошибка изменения пароля'); } } else { - $this->response->addError('Текущий пароль неверен'); + $this->responseService->addError('Текущий пароль неверен'); } } - public function validate(): bool + public function needDto(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } - return $this->passwordDto->validate($this->response); + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/ResetPasswordCode.php b/app/src/Service/Action/Classes/ResetPasswordCode.php index bc5ecd4..8368ef2 100644 --- a/app/src/Service/Action/Classes/ResetPasswordCode.php +++ b/app/src/Service/Action/Classes/ResetPasswordCode.php @@ -6,27 +6,34 @@ use App\Entity\User; use App\Service\Action\BaseActionService; use App\Service\Dto\Classes\ResetPasswordCodeDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Contracts\Service\Attribute\Required; +#[AsAlias(id: 'action.reset.password.code', public: true)] class ResetPasswordCode extends BaseActionService { public function __construct( - private ResponseServiceInterface $response, private UserPasswordHasherInterface $passwordHasher, - private DtoServiceInterface $passwordResetDto, - private ManagerRegistry $doctrine ) { - parent::__construct($response); } + #[Required] public function initDto( + #[Autowire(service: 'dto.password.code')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); + } + + public function runAction(): void { /** @var ResetPasswordCodeDto $dto */ - $dto = $this->passwordResetDto->getClass(); + $dto = $this->getDto(); /** @var User $userExists */ $userExists = $this->doctrine->getRepository(User::class) ->findOneByUniq($dto->email, $dto->phoneNumber); @@ -36,7 +43,7 @@ class ResetPasswordCode extends BaseActionService $code = $dto->code; $registerCode = $userExists->getRegisterCode(); if ($registerCode === null) { - $this->response->addError('Код подтверждения не отправлен'); + $this->responseService->addError('Код подтверждения не отправлен'); } else { if ($registerCodeDate = $registerCode->getDate()) { if ($registerCode->getCode() === $code && $currentDate->getTimestamp() < $registerCodeDate->getTimestamp()) { @@ -50,24 +57,24 @@ class ResetPasswordCode extends BaseActionService $em->persist($userExists); $em->remove($registerCode); $em->flush(); - $this->response->addMessage('Пароль изменен'); + $this->responseService->addMessage('Пароль изменен'); } catch (\Exception $exception) { - $this->response->addError('Ошибка изменения пароля'); + $this->responseService->addError('Ошибка изменения пароля'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } else { - $this->response->addError('Код недействителен'); + $this->responseService->addError('Код недействителен'); } } } else { - $this->response->addError('Пользователь не найден'); + $this->responseService->addError('Пользователь не найден'); } } - public function validate(): bool + public function needDto(): bool { - return $this->passwordResetDto->validate($this->response); + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SaveImage.php b/app/src/Service/Action/Classes/SaveImage.php index 4e437de..36fb0f9 100644 --- a/app/src/Service/Action/Classes/SaveImage.php +++ b/app/src/Service/Action/Classes/SaveImage.php @@ -2,21 +2,21 @@ namespace App\Service\Action\Classes; -use App\Entity\User; use App\Entity\UserHistory; use App\Entity\UserImage; -use App\Service\Action\BaseActionService; +use App\Service\Action\UserBaseActionService; use App\Service\Dto\Classes\ImageDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\String\Slugger\SluggerInterface; +use Symfony\Contracts\Service\Attribute\Required; -class SaveImage extends BaseActionService +#[AsAlias(id: 'action.profile.image.save', public: true)] +class SaveImage extends UserBaseActionService { // Доступные расширения файлов public const IMAGE_EXTENSIONS = [ @@ -24,21 +24,22 @@ class SaveImage extends BaseActionService 'png' ]; - private ?User $user; - public function __construct( private string $targetDirectory, - private DtoServiceInterface $imageDto, - private ResponseServiceInterface $response, - private SluggerInterface $slugger, - private ManagerRegistry $doctrine, - Security $security + private SluggerInterface $slugger ) { - $this->user = $security->getUser(); - parent::__construct($response); } + #[Required] public function initDto( + #[Autowire(service: 'dto.image')] + DtoServiceInterface $dtoService + ): void + { + parent::initDto($dtoService); + } + + public function runAction(): void { $file = $this->saveFile(); @@ -62,32 +63,19 @@ class SaveImage extends BaseActionService try { $em->persist($file); $em->flush(); - $this->response->addMessage('Изображение сохранено'); + $this->responseService->addMessage('Изображение сохранено'); } catch (\Exception $exception) { - $this->response->addError('Ошибка сохранения файла пользователя'); + $this->responseService->addError('Ошибка сохранения файла пользователя'); } } else { - $this->response->addError('Ошибка сохранения файла'); - } - } - - public function validate(): bool - { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } - if ($this->user->isDeleted()) { - $this->response->addError('Профиль удален'); - return false; + $this->responseService->addError('Ошибка сохранения файла'); } - return $this->imageDto->validate($this->response); } public function saveFile(): ?UserImage { /** @var ImageDto $dto */ - $dto = $this->imageDto->getClass(); + $dto = $this->getDto(); $matches = []; if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,([a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$)/i', $dto->data ?: '', $matches)) { @@ -99,11 +87,11 @@ class SaveImage extends BaseActionService $mimeTypes = new MimeTypes(); $types = $mimeTypes->getExtensions($extension); if (empty($types)) { - $this->response->addError('Неизвестное расширения файла'); + $this->responseService->addError('Неизвестное расширения файла'); return null; } if (empty(array_intersect($types, self::IMAGE_EXTENSIONS))) { - $this->response->addError('Файл расширения "'. reset($types) .'" недоступен для загрузки. Доступные расширения: ' . implode(', ', self::IMAGE_EXTENSIONS) . '.'); + $this->responseService->addError('Файл расширения "'. reset($types) .'" недоступен для загрузки. Доступные расширения: ' . implode(', ', self::IMAGE_EXTENSIONS) . '.'); return null; } @@ -113,7 +101,7 @@ class SaveImage extends BaseActionService $decoded = base64_decode($content); if (!$decoded) { - $this->response->addError('Ошибка декодирования файла'); + $this->responseService->addError('Ошибка декодирования файла'); return null; } @@ -141,4 +129,19 @@ class SaveImage extends BaseActionService return null; } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SendRegisterCode.php b/app/src/Service/Action/Classes/SendRegisterCode.php index 8796d95..135e2f9 100644 --- a/app/src/Service/Action/Classes/SendRegisterCode.php +++ b/app/src/Service/Action/Classes/SendRegisterCode.php @@ -2,30 +2,21 @@ namespace App\Service\Action\Classes; -use App\Entity\User; -use App\Service\Action\BaseActionService; -use App\Service\Response\ResponseServiceInterface; -use App\Service\Send\Classes\Code\RegisterCodeSendService; +use App\Service\Action\UserBaseActionService; use App\Service\Send\SendServiceInterface; -use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; -class SendRegisterCode extends BaseActionService +#[AsAlias(id: 'action.register.send', public: true)] +class SendRegisterCode extends UserBaseActionService { - private ?User $user; - - /** - * @param ResponseServiceInterface $response - * @param RegisterCodeSendService $registerCodeSendService - * @param Security $security - */ - public function __construct( - private ResponseServiceInterface $response, - private SendServiceInterface $registerCodeSendService, - Security $security - ) + #[Required] public function initSend( + #[Autowire(service: 'send.code.register')] + SendServiceInterface $sendService + ): void { - $this->user = $security->getUser(); - parent::__construct($response); + parent::initSend($sendService); } @@ -38,22 +29,33 @@ class SendRegisterCode extends BaseActionService */ public function runAction(): void { - $this->registerCodeSendService->setUser($this->user); - $this->registerCodeSendService->setResponse($this->response); - $this->registerCodeSendService->send(); + $this->sendService->setUser($this->user); + $this->sendService->setResponse($this->responseService); + $this->sendService->send(); } - public function validate(): bool + public function customValidate(): bool { - if ($this->user === null) { - $this->response->addError('Вы не авторизованы'); - return false; - } if ($this->user->isConfirm()) { - $this->response->addError('Учетная запись уже подтверждена'); + $this->responseService->addError('Учетная запись уже подтверждена'); return false; } return true; } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } } \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SendResetPasswordCode.php b/app/src/Service/Action/Classes/SendResetPasswordCode.php index 7426d9b..6984819 100644 --- a/app/src/Service/Action/Classes/SendResetPasswordCode.php +++ b/app/src/Service/Action/Classes/SendResetPasswordCode.php @@ -6,41 +6,50 @@ use App\Entity\User; use App\Service\Action\BaseActionService; use App\Service\Dto\Classes\RecoveryDto; use App\Service\Dto\DtoServiceInterface; -use App\Service\Response\ResponseServiceInterface; use App\Service\Send\SendServiceInterface; -use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Service\Attribute\Required; +#[AsAlias(id: 'action.reset.password.send', public: true)] class SendResetPasswordCode extends BaseActionService { - public function __construct( - private ResponseServiceInterface $response, - private DtoServiceInterface $recoveryDto, - private ManagerRegistry $doctrine, - private SendServiceInterface $passwordCodeSendService - ) + #[Required] public function initDto( + #[Autowire(service: 'dto.recovery.send')] + DtoServiceInterface $dtoService + ): void { - parent::__construct($response); + parent::initDto($dtoService); } + #[Required] public function initSend( + #[Autowire(service: 'send.code.password')] + SendServiceInterface $sendService + ): void + { + parent::initSend($sendService); + } + + public function runAction(): void { /** @var RecoveryDto $dto */ - $dto = $this->recoveryDto->getClass(); + $dto = $this->getDto(); /** @var User $userExists */ $userExists = $this->doctrine->getRepository(User::class) ->findOneByUniq($dto->email, $dto->phoneNumber); if ($userExists !== null) { - $this->passwordCodeSendService->setUser($userExists); - $this->passwordCodeSendService->setResponse($this->response); - $this->passwordCodeSendService->send(); + $this->sendService->setUser($userExists); + $this->sendService->setResponse($this->responseService); + $this->sendService->send(); } else { - $this->response->addError('Пользователь не найден'); + $this->responseService->addError('Пользователь не найден'); } } - public function validate(): bool + public function needDto(): bool { - return $this->recoveryDto->validate($this->response); + return true; } } \ No newline at end of file diff --git a/app/src/Service/Action/UserBaseActionService.php b/app/src/Service/Action/UserBaseActionService.php new file mode 100644 index 0000000..c6b2019 --- /dev/null +++ b/app/src/Service/Action/UserBaseActionService.php @@ -0,0 +1,42 @@ +user = $security->getUser(); + } + + abstract public function checkDelete(): bool; + + abstract public function checkConfirm(): bool; + + public function customValidate(): bool + { + if ($this->user === null) { + $this->responseService->addError('Вы не авторизованы'); + return false; + } + + if ($this->checkDelete() && $this->user->isDeleted()) { + $this->responseService->addError('Профиль удален'); + return false; + } + + if ($this->checkConfirm() && !$this->user->isConfirm()) { + $this->responseService->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 b3abff0..363af97 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -5,6 +5,7 @@ namespace App\Service\Dto; use App\Service\Response\ResponseServiceInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -90,11 +91,13 @@ abstract class BaseDto implements DtoServiceInterface if (count($aErrors) > 0) { foreach ($aErrors as $error) { $response->addError($error->getMessage()); + $response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY); } $bValid = false; } } else { $response->addError("Данные не получены"); + $response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY); $bValid = false; } diff --git a/app/src/Service/Dto/Classes/ChangePasswordDto.php b/app/src/Service/Dto/Classes/ChangePasswordDto.php index f15145b..952993f 100644 --- a/app/src/Service/Dto/Classes/ChangePasswordDto.php +++ b/app/src/Service/Dto/Classes/ChangePasswordDto.php @@ -4,11 +4,13 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; use App\Validators\PasswordValidator; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Callback([PasswordValidator::class, 'validateRepeatPassword'])] #[Assert\Callback([PasswordValidator::class, 'validateNewPassword'])] #[Assert\Callback([PasswordValidator::class, 'validatePassword'])] +#[AsAlias(id: 'dto.password.change', public: true)] class ChangePasswordDto extends BaseDto { #[Assert\NotBlank( diff --git a/app/src/Service/Dto/Classes/ChangeProfileDto.php b/app/src/Service/Dto/Classes/ChangeProfileDto.php index 129af96..9ab7166 100644 --- a/app/src/Service/Dto/Classes/ChangeProfileDto.php +++ b/app/src/Service/Dto/Classes/ChangeProfileDto.php @@ -3,8 +3,10 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; +#[AsAlias(id: 'dto.profile.change', public: true)] class ChangeProfileDto extends BaseDto { #[Assert\Email( diff --git a/app/src/Service/Dto/Classes/ImageDto.php b/app/src/Service/Dto/Classes/ImageDto.php index 5f47f0d..51ade14 100644 --- a/app/src/Service/Dto/Classes/ImageDto.php +++ b/app/src/Service/Dto/Classes/ImageDto.php @@ -3,8 +3,10 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; +#[AsAlias(id: 'dto.image', public: true)] class ImageDto extends BaseDto { #[Assert\NotBlank( diff --git a/app/src/Service/Dto/Classes/NoneDto.php b/app/src/Service/Dto/Classes/NoneDto.php index bb31bf3..37981d8 100644 --- a/app/src/Service/Dto/Classes/NoneDto.php +++ b/app/src/Service/Dto/Classes/NoneDto.php @@ -3,7 +3,9 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +#[AsAlias] class NoneDto extends BaseDto { diff --git a/app/src/Service/Dto/Classes/RecoveryCodeDto.php b/app/src/Service/Dto/Classes/RecoveryCodeDto.php index 2e9757e..20f5917 100644 --- a/app/src/Service/Dto/Classes/RecoveryCodeDto.php +++ b/app/src/Service/Dto/Classes/RecoveryCodeDto.php @@ -4,9 +4,11 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; use App\Validators\UserValidator; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Callback([UserValidator::class, 'validateEmailOrPhone'])] +#[AsAlias(id: 'dto.recovery.code', public: true)] class RecoveryCodeDto extends BaseDto { #[Assert\Email( diff --git a/app/src/Service/Dto/Classes/RecoveryDto.php b/app/src/Service/Dto/Classes/RecoveryDto.php index 3f99521..3558c6e 100644 --- a/app/src/Service/Dto/Classes/RecoveryDto.php +++ b/app/src/Service/Dto/Classes/RecoveryDto.php @@ -4,9 +4,11 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; use App\Validators\UserValidator; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Callback([UserValidator::class, 'validateEmailOrPhone'])] +#[AsAlias(id: 'dto.recovery.send', public: true)] class RecoveryDto extends BaseDto { #[Assert\Email( diff --git a/app/src/Service/Dto/Classes/RegisterCodeDto.php b/app/src/Service/Dto/Classes/RegisterCodeDto.php index 2989049..ff5859c 100644 --- a/app/src/Service/Dto/Classes/RegisterCodeDto.php +++ b/app/src/Service/Dto/Classes/RegisterCodeDto.php @@ -3,8 +3,10 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; +#[AsAlias(id: 'dto.register.code', public: true)] class RegisterCodeDto extends BaseDto { #[Assert\NotBlank( diff --git a/app/src/Service/Dto/Classes/RegisterDto.php b/app/src/Service/Dto/Classes/RegisterDto.php index 74247d5..1659ba0 100644 --- a/app/src/Service/Dto/Classes/RegisterDto.php +++ b/app/src/Service/Dto/Classes/RegisterDto.php @@ -4,10 +4,12 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; use App\Validators\PasswordValidator; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Callback([PasswordValidator::class, 'validateRepeatPassword'])] #[Assert\Callback([PasswordValidator::class, 'validatePassword'])] +#[AsAlias(id: 'dto.register', public: true)] class RegisterDto extends BaseDto { #[Assert\NotBlank( diff --git a/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php b/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php index 302631a..25c9268 100644 --- a/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php +++ b/app/src/Service/Dto/Classes/ResetPasswordCodeDto.php @@ -5,11 +5,13 @@ namespace App\Service\Dto\Classes; use App\Service\Dto\BaseDto; use App\Validators\PasswordValidator; use App\Validators\UserValidator; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Validator\Constraints as Assert; #[Assert\Callback([UserValidator::class, 'validateEmailOrPhone'])] #[Assert\Callback([PasswordValidator::class, 'validateRepeatPassword'])] #[Assert\Callback([PasswordValidator::class, 'validatePassword'])] +#[AsAlias(id: 'dto.password.code', public: true)] class ResetPasswordCodeDto extends BaseDto { #[Assert\Email( diff --git a/app/src/Service/Response/Classes/ProfileResponse.php b/app/src/Service/Response/Classes/ProfileResponse.php index 91c773c..5d63c7b 100644 --- a/app/src/Service/Response/Classes/ProfileResponse.php +++ b/app/src/Service/Response/Classes/ProfileResponse.php @@ -3,8 +3,10 @@ namespace App\Service\Response\Classes; use App\Entity\User; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Serializer\Annotation\Groups; +#[AsAlias(id: 'response.profile', public: true)] class ProfileResponse extends Response { /** diff --git a/app/src/Service/Response/Classes/Response.php b/app/src/Service/Response/Classes/Response.php index 5e10801..b0f32f1 100644 --- a/app/src/Service/Response/Classes/Response.php +++ b/app/src/Service/Response/Classes/Response.php @@ -3,6 +3,7 @@ namespace App\Service\Response\Classes; use App\Service\Response\ResponseServiceInterface; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Annotation\Groups; @@ -15,6 +16,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +#[AsAlias] class Response implements ResponseServiceInterface { /** @@ -123,6 +125,9 @@ class Response implements ResponseServiceInterface $groups = ['message']; if (!empty($this->errors)) { $this->status = false; + if ($this->statusCode === 200) { + $this->statusCode = \Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST; + } } else { $this->status = true; } diff --git a/app/src/Service/Response/Classes/TokenResponse.php b/app/src/Service/Response/Classes/TokenResponse.php index 8ce5a95..c97b562 100644 --- a/app/src/Service/Response/Classes/TokenResponse.php +++ b/app/src/Service/Response/Classes/TokenResponse.php @@ -2,8 +2,10 @@ namespace App\Service\Response\Classes; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Serializer\Annotation\Groups; +#[AsAlias(id: 'response.token', public: true)] class TokenResponse extends Response { /** diff --git a/app/src/Service/Response/ResponseServiceInterface.php b/app/src/Service/Response/ResponseServiceInterface.php index e20fdd5..2f0b6e5 100644 --- a/app/src/Service/Response/ResponseServiceInterface.php +++ b/app/src/Service/Response/ResponseServiceInterface.php @@ -13,4 +13,6 @@ interface ResponseServiceInterface public function addMessage(string $message): self; public function isSuccess(): bool; + + public function setStatusCode(int $code): self; } \ No newline at end of file diff --git a/app/src/Service/Send/Classes/Code/PasswordCodeSendService.php b/app/src/Service/Send/Classes/Code/PasswordCodeSendService.php index a571254..041b335 100644 --- a/app/src/Service/Send/Classes/Code/PasswordCodeSendService.php +++ b/app/src/Service/Send/Classes/Code/PasswordCodeSendService.php @@ -3,7 +3,9 @@ namespace App\Service\Send\Classes\Code; use App\Service\Send\Classes\CodeSendService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +#[AsAlias(id: 'send.code.password', public: true)] class PasswordCodeSendService extends CodeSendService { public function getSubject(): string diff --git a/app/src/Service/Send/Classes/Code/RecoveryCodeSendService.php b/app/src/Service/Send/Classes/Code/RecoveryCodeSendService.php index 0e9c5e6..306e169 100644 --- a/app/src/Service/Send/Classes/Code/RecoveryCodeSendService.php +++ b/app/src/Service/Send/Classes/Code/RecoveryCodeSendService.php @@ -3,7 +3,9 @@ namespace App\Service\Send\Classes\Code; use App\Service\Send\Classes\CodeSendService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +#[AsAlias(id: 'send.code.recovery', public: true)] class RecoveryCodeSendService extends CodeSendService { public function getSubject(): string diff --git a/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php b/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php index 9371af7..4a3dbb9 100644 --- a/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php +++ b/app/src/Service/Send/Classes/Code/RegisterCodeSendService.php @@ -3,7 +3,9 @@ namespace App\Service\Send\Classes\Code; use App\Service\Send\Classes\CodeSendService; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; +#[AsAlias(id: 'send.code.register', public: true)] class RegisterCodeSendService extends CodeSendService { public function getSubject(): string diff --git a/app/src/Service/Send/Classes/CodeSendService.php b/app/src/Service/Send/Classes/CodeSendService.php index eabbcb8..1aafc51 100644 --- a/app/src/Service/Send/Classes/CodeSendService.php +++ b/app/src/Service/Send/Classes/CodeSendService.php @@ -8,8 +8,10 @@ use App\Service\Response\ResponseServiceInterface; use App\Service\Send\SendService; use App\Service\Send\SendServiceInterface; use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\Serializer\SerializerInterface; +#[AsAlias] class CodeSendService implements SendServiceInterface { private ?User $user = null; diff --git a/app/src/Service/Send/SendService.php b/app/src/Service/Send/SendService.php index eb1d5e3..4870235 100644 --- a/app/src/Service/Send/SendService.php +++ b/app/src/Service/Send/SendService.php @@ -80,7 +80,6 @@ class SendService $this->confirmType )); } catch (Throwable $e) { - dd($e); } } } \ No newline at end of file -- GitLab