$allMessages
33 | */
34 | return $allMessages;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Request/Pagination/PaginationRequest.php:
--------------------------------------------------------------------------------
1 | $offset
14 | * @param positive-int $limit
15 | */
16 | public function __construct(public int $offset = 0, public int $limit = 10) {}
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Request/ValidateAcceptHeader.php:
--------------------------------------------------------------------------------
1 | getRequest();
20 | $acceptableContentTypes = $request->getAcceptableContentTypes();
21 |
22 | if (\in_array('application/json', $acceptableContentTypes, true)) {
23 | return;
24 | }
25 |
26 | if (
27 | $acceptableContentTypes === []
28 | || \in_array('*/*', $acceptableContentTypes, true)
29 | || \in_array('application/*', $acceptableContentTypes, true)
30 | ) {
31 | $request->headers->set('Accept', 'application/json');
32 |
33 | return;
34 | }
35 |
36 | throw new ApiBadRequestException(['Укажите заголовок Accept: application/json']);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Response/ApiListObjectResponse.php:
--------------------------------------------------------------------------------
1 | $data
14 | * @param object|null $meta Дополнительная мета-информация в ответе (фильтры, ссылки и т.п.)
15 | */
16 | public function __construct(
17 | public iterable $data,
18 | public PaginationResponse $pagination,
19 | public ?object $meta = null,
20 | public ResponseStatus $status = ResponseStatus::Success,
21 | ) {}
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Response/ApiObjectResponse.php:
--------------------------------------------------------------------------------
1 | status = ResponseStatus::Success;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/ValueObject/Email.php:
--------------------------------------------------------------------------------
1 | value = $value;
31 | }
32 |
33 | /**
34 | * @param object $other
35 | */
36 | public function equalTo(mixed $other): bool
37 | {
38 | return $other::class === self::class && $this->value === $other->value;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/di.php:
--------------------------------------------------------------------------------
1 | services()->defaults()->autowire()->autoconfigure();
11 |
12 | $services
13 | ->load('App\\', '../*')
14 | ->exclude([
15 | './{di.php}',
16 | '../**/{di.php}',
17 | '../**/{config.php}',
18 | ]);
19 | };
20 |
--------------------------------------------------------------------------------
/backend/src/Logger/Http/RequestIdLoggerProcessor.php:
--------------------------------------------------------------------------------
1 | requestStack->getCurrentRequest();
27 |
28 | if ($request !== null && $request->headers->has(RequestIdListener::TRACE_ID_HEADER)) {
29 | $record->extra['traceId'] = $request->headers->get(RequestIdListener::TRACE_ID_HEADER);
30 | }
31 |
32 | return $record;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/Mailer/Notification/EmailConfirmation/ConfirmEmailMessage.php:
--------------------------------------------------------------------------------
1 | to($message->email->value)
23 | ->subject('Подтверждение email')
24 | ->htmlTemplate('@mails/emails/confirm.html.twig')
25 | ->context([
26 | 'confirmToken' => $message->confirmToken,
27 | ]);
28 |
29 | $email->getHeaders()->addTextHeader('confirmToken', (string) $message->confirmToken);
30 |
31 | $this->mailer->send($email);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/Mailer/Notification/PasswordRecovery/RecoveryPasswordMessage.php:
--------------------------------------------------------------------------------
1 | to($message->email->value)
25 | ->subject($subject)
26 | ->htmlTemplate('@mails/emails/recoverPassword.html.twig')
27 | ->context([
28 | 'recoverToken' => $message->token,
29 | ]);
30 |
31 | $email->getHeaders()->addTextHeader('recoverToken', (string) $message->token);
32 |
33 | $this->mailer->send($email);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/Mailer/Notification/UncompletedTasks/SendUncompletedTasksToUser.php:
--------------------------------------------------------------------------------
1 | to($message->email->value)
23 | ->subject('Невыполненные задачи')
24 | ->htmlTemplate('@mails/emails/uncompleted-tasks.html.twig')
25 | ->context([
26 | 'tasks' => $message->tasks,
27 | ]);
28 |
29 | $this->mailer->send($email);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/Mailer/Notification/UncompletedTasks/TaskData.php:
--------------------------------------------------------------------------------
1 | messenger()->routing(Message::class);
15 |
16 | if ($containerConfigurator->env() === 'dev') {
17 | $messengerRouting->senders(['async']);
18 | }
19 |
20 | if ($containerConfigurator->env() === 'test') {
21 | $messengerRouting->senders(['sync']);
22 | }
23 |
24 | if ($containerConfigurator->env() === 'prod') {
25 | $messengerRouting->senders(['async']);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/backend/src/Mailer/templates/emails/confirm.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@mails/layout.html.twig' %}
2 | {% block body %}
3 | Токен подтверждения email {{ confirmToken }}
4 | {% endblock %}
5 |
--------------------------------------------------------------------------------
/backend/src/Mailer/templates/emails/recoverPassword.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@mails/layout.html.twig' %}
2 | {% block body %}
3 | Токен восстановления пароля {{ recoverToken }}
4 | {% endblock %}
5 |
--------------------------------------------------------------------------------
/backend/src/Mailer/templates/emails/uncompleted-tasks.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@mails/layout.html.twig' %}
2 | {% block body %}
3 | У вас есть невыполненные задачи:
4 |
5 | {% for task in tasks %}
6 | -
7 | Задача {{ task.taskName }} от {{ task.createdAt|date('Y-m-d H:i:s') }}
8 |
9 | {% endfor %}
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/backend/src/Mailer/templates/layout.html.twig:
--------------------------------------------------------------------------------
1 | {% block header %}
2 |
3 | symfony-starter-kit
4 |
5 | {% endblock %}
6 | {% block body %}
7 | {% endblock %}
8 | {% block footer %}
9 | https://www.15web.ru
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/backend/src/Ping/Http/Site/PingAction.php:
--------------------------------------------------------------------------------
1 | connection->fetchOne("select 'pong'");
28 |
29 | return new ApiObjectResponse(new Pong($result));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/Ping/Http/Site/Pong.php:
--------------------------------------------------------------------------------
1 | seoRepository->findByTypeIdentity(
21 | type: $command->type,
22 | identity: $command->identity,
23 | );
24 |
25 | if ($seo === null) {
26 | $seo = new Seo(
27 | type: $command->type,
28 | identity: $command->identity,
29 | title: $command->title,
30 | );
31 |
32 | $this->seoRepository->add($seo);
33 | }
34 |
35 | $seo->change(
36 | title: $command->title,
37 | description: $command->description,
38 | keywords: $command->keywords,
39 | );
40 |
41 | ($this->flush)();
42 |
43 | return $seo;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/backend/src/Seo/Command/SaveSeoCommand.php:
--------------------------------------------------------------------------------
1 | entityManager->persist($entity);
19 | }
20 |
21 | public function findByTypeIdentity(SeoResourceType $type, string $identity): ?Seo
22 | {
23 | return $this->entityManager->getRepository(Seo::class)->findOneBy([
24 | 'type' => $type->value,
25 | 'identity' => $identity,
26 | ]);
27 | }
28 |
29 | public function findOneByTypeAndIdentity(string $type, string $identity): ?Seo
30 | {
31 | return $this->entityManager->getRepository(Seo::class)->findOneBy(['type' => $type, 'identity' => $identity]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/Seo/Domain/SeoResourceType.php:
--------------------------------------------------------------------------------
1 | seoRepository->findOneByTypeAndIdentity(
32 | type: $type,
33 | identity: $identity,
34 | );
35 |
36 | return new ApiObjectResponse(
37 | data: $this->buildResponseData($seo),
38 | );
39 | }
40 |
41 | private function buildResponseData(?Seo $seo): SeoData
42 | {
43 | return new SeoData(
44 | title: $seo?->getTitle(),
45 | description: $seo?->getDescription(),
46 | keywords: $seo?->getKeywords(),
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/backend/src/Seo/Http/Site/SeoData.php:
--------------------------------------------------------------------------------
1 | settingsRepository->findByType($command->type);
24 |
25 | if ($setting === null) {
26 | throw new SettingNotFoundException();
27 | }
28 |
29 | $setting->change($command->value);
30 |
31 | return $setting;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/Setting/Command/SaveSettingCommand.php:
--------------------------------------------------------------------------------
1 | entityManager->persist($entity);
19 | }
20 |
21 | public function findByType(SettingType $type): ?Setting
22 | {
23 | return $this->entityManager
24 | ->getRepository(Setting::class)
25 | ->findOneBy(['type' => $type->value]);
26 | }
27 |
28 | /**
29 | * @return list
30 | */
31 | public function getAll(): array
32 | {
33 | /** @var list $settings */
34 | $settings = $this->entityManager
35 | ->getRepository(Setting::class)
36 | ->createQueryBuilder('s')
37 | ->orderBy('s.createdAt', 'DESC')
38 | ->getQuery()
39 | ->getResult();
40 |
41 | return $settings;
42 | }
43 |
44 | /**
45 | * @return list
46 | */
47 | public function getAllPublic(): array
48 | {
49 | /** @var list $settings */
50 | $settings = $this->entityManager
51 | ->getRepository(Setting::class)
52 | ->createQueryBuilder('s')
53 | ->where('s.isPublic = true')
54 | ->getQuery()->getResult();
55 |
56 | return $settings;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/backend/src/Setting/Http/Admin/SettingListData.php:
--------------------------------------------------------------------------------
1 | settingsRepository->getAllPublic();
27 |
28 | return new ApiListObjectResponse(
29 | data: $this->buildResponseData($settings),
30 | pagination: new PaginationResponse(total: \count($settings)),
31 | );
32 | }
33 |
34 | /**
35 | * @param list $settings
36 | *
37 | * @return iterable
38 | */
39 | private function buildResponseData(array $settings): iterable
40 | {
41 | foreach ($settings as $setting) {
42 | yield new SettingListData(
43 | type: $setting->getType()->value,
44 | value: $setting->getValue(),
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/backend/src/Setting/Http/Site/SettingListData.php:
--------------------------------------------------------------------------------
1 | commentBody),
30 | );
31 |
32 | $task->addComment($comment);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/Comment/Add/AddCommentOnTaskCommand.php:
--------------------------------------------------------------------------------
1 | markAsDone();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/CreateTask/CreateTask.php:
--------------------------------------------------------------------------------
1 | taskName), $userId);
26 |
27 | $this->taskRepository->add($task);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/CreateTask/CreateTaskCommand.php:
--------------------------------------------------------------------------------
1 | taskRepository->remove($task);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/UpdateTaskName/UpdateTaskName.php:
--------------------------------------------------------------------------------
1 | changeTaskName(new TaskName($command->taskName));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/UpdateTaskName/UpdateTaskNameCommand.php:
--------------------------------------------------------------------------------
1 | id = $commentId->getValue();
37 | $this->body = $taskCommentBody;
38 | $this->task = $task;
39 | $this->createdAt = new DateTimeImmutable();
40 | $this->updatedAt = null;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/Task/Domain/TaskCommentBody.php:
--------------------------------------------------------------------------------
1 | value = $value;
24 | }
25 |
26 | /**
27 | * @param object $other
28 | */
29 | public function equalTo(mixed $other): bool
30 | {
31 | return $other::class === self::class && $this->value === $other->value;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/Task/Domain/TaskCommentId.php:
--------------------------------------------------------------------------------
1 | value = new UuidV7();
20 | }
21 |
22 | /**
23 | * @param object $other
24 | */
25 | public function equalTo(mixed $other): bool
26 | {
27 | return $other::class === self::class && $this->value->equals($other->value);
28 | }
29 |
30 | public function getValue(): Uuid
31 | {
32 | return $this->value;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/Task/Domain/TaskId.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/Task/Domain/TaskName.php:
--------------------------------------------------------------------------------
1 | value = $value;
26 | }
27 |
28 | /**
29 | * @param object $other
30 | */
31 | public function equalTo(mixed $other): bool
32 | {
33 | return $other::class === self::class && $this->value === $other->value;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/Task/Domain/TaskRepository.php:
--------------------------------------------------------------------------------
1 | entityManager->getRepository(Task::class)->findOneBy([
19 | 'id' => $taskId->value,
20 | ]);
21 | }
22 |
23 | public function add(Task $task): void
24 | {
25 | $this->entityManager->persist($task);
26 | }
27 |
28 | public function remove(Task $task): void
29 | {
30 | $this->entityManager->remove($task);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/Task/Http/Site/CreateTask/TaskData.php:
--------------------------------------------------------------------------------
1 | entityManager->createQuery($dql);
31 | $dqlQuery->setParameter('taskId', $findAllQuery->taskId);
32 | $dqlQuery->setParameter('userId', $findAllQuery->userId);
33 |
34 | /** @var CommentData[] $commentData */
35 | $commentData = $dqlQuery->getResult();
36 |
37 | return $commentData;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Task/FindAllByUserId/CountAllTasksByUserId.php:
--------------------------------------------------------------------------------
1 | connection->createQueryBuilder()
22 | ->select('COUNT(t.id)')
23 | ->from('task', 't');
24 |
25 | $this->filter->applyFilter($queryBuilder, $query);
26 |
27 | /** @var int $result */
28 | $result = $queryBuilder->executeQuery()->fetchOne();
29 |
30 | return $result;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Task/FindAllByUserId/Filter.php:
--------------------------------------------------------------------------------
1 | andWhere('t.user_id = :user_id')
18 | ->setParameter('user_id', $query->userId);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Task/FindAllByUserId/FindAllTasksByUserIdQuery.php:
--------------------------------------------------------------------------------
1 | entityManager->createQuery($dql);
26 | $dqlQuery->setParameter('id', $query->taskId);
27 | $dqlQuery->setParameter('userId', $query->userId);
28 |
29 | /** @var ?TaskData $result */
30 | $result = $dqlQuery->getOneOrNullResult();
31 |
32 | if ($result === null) {
33 | throw new TaskNotFoundException();
34 | }
35 |
36 | return $result;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Task/FindById/FindTaskByIdQuery.php:
--------------------------------------------------------------------------------
1 | entityManager->createQuery($dql);
30 | $dqlQuery->setParameter('userId', $query->userId);
31 |
32 | /** @var TaskData[] $taskData */
33 | $taskData = $dqlQuery->getResult();
34 |
35 | return $taskData;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Task/FindUncompletedTasksByUserId/FindUncompletedTasksByUserIdQuery.php:
--------------------------------------------------------------------------------
1 | userRepository->findById($command->userId);
29 |
30 | if ($user === null) {
31 | throw new UserNotFoundException();
32 | }
33 |
34 | $user->applyPassword(
35 | new UserPassword(
36 | cleanPassword: $command->newPassword,
37 | hashCost: $this->hashCost,
38 | ),
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Command/ChangePasswordCommand.php:
--------------------------------------------------------------------------------
1 | $hashCost
20 | */
21 | public function __construct(
22 | #[Autowire('%app.hash_cost%')]
23 | private int $hashCost,
24 | private UserRepository $userRepository,
25 | ) {}
26 |
27 | /**
28 | * @throws UserNotFoundException
29 | */
30 | public function __invoke(
31 | RecoveryToken $recoveryToken,
32 | RecoverPasswordCommand $recoverPasswordCommand,
33 | ): void {
34 | $user = $this->userRepository->findById($recoveryToken->getUserId());
35 |
36 | if ($user === null) {
37 | throw new UserNotFoundException();
38 | }
39 |
40 | $user->applyPassword(
41 | new UserPassword(
42 | cleanPassword: $recoverPasswordCommand->password,
43 | hashCost: $this->hashCost,
44 | ),
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Command/RecoverPasswordCommand.php:
--------------------------------------------------------------------------------
1 | id = $id;
37 | $this->userId = $userId->value;
38 | $this->token = $token;
39 | }
40 |
41 | public function getUserId(): UserId
42 | {
43 | return new UserId($this->userId);
44 | }
45 |
46 | public function getToken(): Uuid
47 | {
48 | return $this->token;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Domain/RecoveryTokenRepository.php:
--------------------------------------------------------------------------------
1 | entityManager->getRepository(RecoveryToken::class)
20 | ->findOneBy(['token' => $token]);
21 | }
22 |
23 | public function add(RecoveryToken $recoveryToken): void
24 | {
25 | $this->entityManager->persist($recoveryToken);
26 | }
27 |
28 | public function remove(RecoveryToken $recoveryToken): void
29 | {
30 | $this->entityManager->remove($recoveryToken);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Http/ChangePasswordRequest.php:
--------------------------------------------------------------------------------
1 | newPassword, UserPassword::MIN_LENGTH, 'newPassword: длина не может быть ментьше %2$s симоволов, указано %s');
30 | Assert::eq($this->newPassword, $this->newPasswordConfirmation, 'newPasswordConfirmation: пароль и его повтор не совпадают');
31 | Assert::notEq($this->newPassword, $this->currentPassword, 'newPassword: новый пароль не может совпадать с текущим');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Command/SaveProfile/CreateProfile.php:
--------------------------------------------------------------------------------
1 | phone),
28 | name: $command->name,
29 | );
30 |
31 | $this->profileRepository->add($profile);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Command/SaveProfile/SaveProfile.php:
--------------------------------------------------------------------------------
1 | profileRepository->findByUserId($userId);
26 |
27 | if ($profile !== null) {
28 | ($this->updateProfile)(
29 | command: $command,
30 | profile: $profile,
31 | );
32 |
33 | return;
34 | }
35 |
36 | ($this->createProfile)(
37 | command: $command,
38 | userId: $userId,
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Command/SaveProfile/SaveProfileCommand.php:
--------------------------------------------------------------------------------
1 | changeName($command->name);
20 | $profile->changePhone(new Phone($command->phone));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Domain/Profile.php:
--------------------------------------------------------------------------------
1 | id = $profileId->value;
40 | $this->userId = $userId->value;
41 | $this->phone = $phone;
42 | $this->name = $name;
43 | }
44 |
45 | public function changePhone(Phone $phone): void
46 | {
47 | $this->phone = $phone;
48 | }
49 |
50 | public function changeName(string $name): void
51 | {
52 | $this->name = $name;
53 | }
54 |
55 | public function getProfileId(): ProfileId
56 | {
57 | return new ProfileId($this->id);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Domain/ProfileId.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Domain/ProfileRepository.php:
--------------------------------------------------------------------------------
1 | entityManager->getRepository(Profile::class)
20 | ->findOneBy(['userId' => $userId->value]);
21 | }
22 |
23 | public function add(Profile $task): void
24 | {
25 | $this->entityManager->persist($task);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Query/FindByUserId/FindProfileByUserId.php:
--------------------------------------------------------------------------------
1 | entityManager->createQuery($dql);
26 | $dqlQuery->setParameter(
27 | key: 'userId',
28 | value: $query->userId,
29 | );
30 |
31 | /** @var ?ProfileData $result */
32 | $result = $dqlQuery->getOneOrNullResult();
33 |
34 | return $result ?? new ProfileData();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Query/FindByUserId/FindProfileByUserIdQuery.php:
--------------------------------------------------------------------------------
1 | |string>
24 | */
25 | #[Override]
26 | public static function getSubscribedEvents(): array
27 | {
28 | return [
29 | KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 1000],
30 | ];
31 | }
32 |
33 | public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
34 | {
35 | /** @var IsGranted|null $attribute */
36 | $attribute = $event->getAttributes()[IsGranted::class][0] ?? null;
37 |
38 | if ($attribute === null) {
39 | return;
40 | }
41 |
42 | $request = $event->getRequest();
43 |
44 | ($this->checkRoleGranted)($request, $attribute->userRole);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/backend/src/User/Security/Http/UserIdArgumentValueResolver.php:
--------------------------------------------------------------------------------
1 |
27 | *
28 | * @throws ApiUnauthorizedException
29 | */
30 | #[Override]
31 | public function resolve(Request $request, ArgumentMetadata $argument): iterable
32 | {
33 | if ($argument->getType() !== UserId::class) {
34 | return [];
35 | }
36 |
37 | try {
38 | $userToken = $this->tokenManager->getToken($request);
39 | } catch (TokenException) {
40 | throw new ApiUnauthorizedException(['Необходимо пройти аутентификацию']);
41 | }
42 |
43 | return [$userToken->getUserId()];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/backend/src/User/Security/Service/TokenException.php:
--------------------------------------------------------------------------------
1 | tokenId,
25 | userId: $userId,
26 | hash: $token->hash(),
27 | );
28 |
29 | $this->userTokenRepository->add($userToken);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/User/SignIn/Command/DeleteToken.php:
--------------------------------------------------------------------------------
1 | userTokenRepository->findById($userTokenId);
21 |
22 | if ($userToken === null) {
23 | throw new TokenException('Токен не найден');
24 | }
25 |
26 | $this->userTokenRepository->remove($userToken);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/User/SignIn/Command/SignInCommand.php:
--------------------------------------------------------------------------------
1 | findUser)(
34 | new FindUserQuery(confirmToken: $confirmToken)
35 | );
36 |
37 | if ($userData === null) {
38 | throw new UserNotFoundException();
39 | }
40 |
41 | $user = $this->userRepository->findById(new UserId($userData->userId));
42 |
43 | if ($user === null) {
44 | throw new UserNotFoundException();
45 | }
46 |
47 | $user->confirm();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/backend/src/User/SignUp/Command/SignUpCommand.php:
--------------------------------------------------------------------------------
1 | value = $value;
25 | }
26 |
27 | /**
28 | * @param object $other
29 | */
30 | public function equalTo(mixed $other): bool
31 | {
32 | return $other::class === self::class && $this->value->equals($other->value);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/Exception/EmailAlreadyIsConfirmedException.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserPassword.php:
--------------------------------------------------------------------------------
1 | value = $this->hash();
35 | }
36 |
37 | /**
38 | * @return non-empty-string
39 | */
40 | public function hash(): string
41 | {
42 | /** @var non-empty-string $hash */
43 | $hash = password_hash(
44 | password: $this->cleanPassword,
45 | algo: self::HASH_ALGO,
46 | options: [
47 | 'cost' => $this->hashCost,
48 | ],
49 | );
50 |
51 | return $hash;
52 | }
53 |
54 | public function verify(string $hash): bool
55 | {
56 | return password_verify(
57 | password: $this->cleanPassword,
58 | hash: $hash,
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserRepository.php:
--------------------------------------------------------------------------------
1 | entityManager
19 | ->getRepository(User::class)
20 | ->find($userId->value);
21 | }
22 |
23 | public function add(User $user): void
24 | {
25 | $this->entityManager->persist($user);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserRole.php:
--------------------------------------------------------------------------------
1 | id = $id->value;
38 | $this->userId = $userId->value;
39 | $this->hash = $hash;
40 |
41 | $this->createdAt = new DateTimeImmutable();
42 | }
43 |
44 | public function getId(): UserTokenId
45 | {
46 | return new UserTokenId($this->id);
47 | }
48 |
49 | public function getUserId(): UserId
50 | {
51 | return new UserId($this->userId);
52 | }
53 |
54 | /**
55 | * @return non-empty-string
56 | */
57 | public function getHash(): string
58 | {
59 | /**
60 | * @var non-empty-string $hash
61 | */
62 | $hash = $this->hash;
63 |
64 | return $hash;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserTokenId.php:
--------------------------------------------------------------------------------
1 | entityManager
19 | ->getRepository(UserToken::class)
20 | ->find($userTokenId->value);
21 | }
22 |
23 | public function remove(UserToken $userToken): void
24 | {
25 | $this->entityManager->remove($userToken);
26 | }
27 |
28 | public function add(UserToken $userToken): void
29 | {
30 | $this->entityManager->persist($userToken);
31 | }
32 |
33 | public function removeAllByUserId(UserId $userId): void
34 | {
35 | $this->entityManager->createQueryBuilder()
36 | ->delete(UserToken::class, 't')
37 | ->where('t.userId = :userId')
38 | ->setParameter('userId', $userId->value)
39 | ->getQuery()
40 | ->execute();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/backend/src/User/User/Query/FindAllUsers.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | public function __invoke(): array
20 | {
21 | $dql = <<<'DQL'
22 | SELECT
23 | NEW App\User\User\Query\UserListData(u.id, u.userEmail.value)
24 | FROM App\User\User\Domain\User as u
25 | DQL;
26 |
27 | /**
28 | * @var array $allUsers
29 | */
30 | $allUsers = $this->entityManager->createQuery($dql)->getResult();
31 |
32 | return $allUsers;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/User/User/Query/FindUserQuery.php:
--------------------------------------------------------------------------------
1 | email = new Email($email);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/User/config.php:
--------------------------------------------------------------------------------
1 | env()) {
12 | 'test' => 4,
13 | 'dev' => 8,
14 | default => 12,
15 | };
16 |
17 | $container->parameters()
18 | ->set('app.hash_cost', $hashCost);
19 |
20 | $framework
21 | ->rateLimiter()
22 | ->limiter('sign_in')
23 | ->policy('fixed_window')
24 | ->limit(3)
25 | ->interval('1 minute');
26 |
27 | $framework
28 | ->rateLimiter()
29 | ->limiter('change_password')
30 | ->policy('fixed_window')
31 | ->limit(3)
32 | ->interval('1 minute');
33 | };
34 |
--------------------------------------------------------------------------------
/docker/.env.dist:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME='symfony-starter-kit-local'
2 | COMPOSE_FILE='docker-compose.local.yml'
3 |
4 | USER_ID=1000
5 |
6 | NGINX_PORT=8088
7 | PGSQL_PORT=8432
8 |
--------------------------------------------------------------------------------
/docker/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.3.20-fpm AS base
2 |
3 | RUN apt-get update; \
4 | apt-get install -y --no-install-recommends unzip git;
5 |
6 | COPY php.ini /usr/local/etc/php/php.ini
7 |
8 | COPY --from=composer:2.8.8 /usr/bin/composer /usr/bin/composer
9 |
10 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
11 | RUN install-php-extensions pdo_pgsql intl sysvsem pcov bcmath
12 |
13 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
14 |
15 | ARG USER_ID
16 | RUN groupadd --gid "$USER_ID" dev \
17 | && useradd --uid "$USER_ID" --gid dev --shell /bin/bash --create-home dev
18 |
19 | COPY www.conf /usr/local/etc/php-fpm.d/www.conf
20 |
21 | RUN su dev -c 'mkdir -p /home/dev/.composer/ /home/dev/app/'
22 |
23 | USER dev
24 |
25 | WORKDIR /app/
26 |
27 | FROM base AS messenger
28 |
29 | USER root
30 |
31 | RUN apt-get update; \
32 | apt-get install -y --no-install-recommends supervisor;
33 |
34 | # https://symfony.com/doc/current/messenger.html#graceful-shutdown
35 | RUN install-php-extensions pcntl
36 |
37 | COPY messenger/messenger-worker.conf /etc/supervisor/conf.d/messenger-worker.conf
38 | COPY messenger/scheduler.conf /etc/supervisor/conf.d/scheduler.conf
39 | COPY messenger/supervisord.conf /etc/supervisor/supervisord.conf
40 |
41 | CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
42 |
--------------------------------------------------------------------------------
/docker/backend/messenger/messenger-worker.conf:
--------------------------------------------------------------------------------
1 | [program:messenger-consume]
2 | command=php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=100
3 | user=dev
4 | numprocs=2
5 | autostart=true
6 | autorestart=true
7 | startretries=35
8 | process_name=%(program_name)s_%(process_num)02d
9 | stdout_logfile_maxbytes = 0
10 | stderr_logfile_maxbytes = 0
11 | stderr_logfile = /dev/stdout
12 | stdout_logfile = /dev/stdout
13 |
--------------------------------------------------------------------------------
/docker/backend/messenger/scheduler.conf:
--------------------------------------------------------------------------------
1 | [program:scheduler]
2 | command=php /app/bin/console messenger:consume scheduler_default -vv
3 | user=dev
4 | numprocs=1
5 | autostart=true
6 | autorestart=true
7 | startretries=35
8 | process_name=%(program_name)s_%(process_num)02d
9 | stdout_logfile_maxbytes = 0
10 | stderr_logfile_maxbytes = 0
11 | stderr_logfile = /dev/stdout
12 | stdout_logfile = /dev/stdout
13 |
--------------------------------------------------------------------------------
/docker/backend/messenger/supervisord.conf:
--------------------------------------------------------------------------------
1 | ; supervisor config file
2 |
3 | [unix_http_server]
4 | file=/var/run/supervisor.sock ; (the path to the socket file)
5 | chmod=0700 ; sockef file mode (default 0700)
6 |
7 | [supervisord]
8 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
9 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
10 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP)
11 | nodaemon=true
12 | user=root
13 |
14 | ; the below section must remain in the config file for RPC
15 | ; (supervisorctl/web interface) to work, additional interfaces may be
16 | ; added by defining them in separate rpcinterface: sections
17 | [rpcinterface:supervisor]
18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
19 |
20 | [supervisorctl]
21 | serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket
22 |
23 | ; The [include] section can just contain the "files" setting. This
24 | ; setting can list multiple files (separated by whitespace or
25 | ; newlines). It can also contain wildcards. The filenames are
26 | ; interpreted as relative to this file. Included files *cannot*
27 | ; include files themselves.
28 |
29 | [include]
30 | files = /etc/supervisor/conf.d/*.conf
31 |
--------------------------------------------------------------------------------
/docker/backend/php.ini:
--------------------------------------------------------------------------------
1 | memory_limit=512M
2 |
--------------------------------------------------------------------------------
/docker/backend/www.conf:
--------------------------------------------------------------------------------
1 | [www]
2 |
3 | user = dev
4 | group = dev
5 |
6 | listen = 127.0.0.1:9000
7 |
8 | pm = static
9 |
10 | pm.max_children = 3
11 |
--------------------------------------------------------------------------------
/docker/bin/check_commit_message.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | COMMIT_MSG=$(git log -1 --pretty=%B)
4 | COMMIT_STRUCTURE=$(echo "$COMMIT_MSG" | cat -A)
5 | LINE_COUNT=$(echo "$COMMIT_MSG" | wc -l)
6 | LAST_LINE=$(echo "$COMMIT_MSG" | tail -n 1)
7 | REGEX="^((Merge[ a-z-]* branch.*)|(Revert*)|((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?!?: .*))"
8 | MERGE_REGEX="^Merge pull request .*"
9 |
10 | if [[ $(echo "$COMMIT_MSG" | head -n 1) =~ $MERGE_REGEX ]]; then
11 | echo "Обнаружен merge-коммит"
12 | exit 0
13 | fi
14 |
15 | if ! [[ $COMMIT_MSG =~ $REGEX ]]; then
16 | echo "ОШИБКА: Commit не соответствует стандарту Conventional Commits"
17 | echo "Допустимые типы: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test"
18 | exit 1
19 | fi
20 |
21 | echo "Сообщение коммита корректно"
22 | exit 0
23 |
--------------------------------------------------------------------------------
/docker/bin/replace_env.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -Eeuo pipefail
4 | #set -x # uncomment for debug
5 |
6 | os-depend-sed-in-place() {
7 | if [ "$(uname -s)" == 'Darwin' ]; then
8 | sed -i '' "$@"
9 | else
10 | sed --in-place "$@"
11 | fi
12 | }
13 |
14 | ENV_FILE_PATH="${1}"
15 | ENV_NAME="${2}"
16 | NEW_VALUE="${3}"
17 |
18 | if [ "$(grep ${ENV_NAME} ${ENV_FILE_PATH})" != '' ]; then
19 | os-depend-sed-in-place -E "s|^.*${ENV_NAME}=.*$|${ENV_NAME}='${NEW_VALUE}'|" ${ENV_FILE_PATH}
20 | else
21 | echo "${ENV_NAME}='${NEW_VALUE}'" >>${ENV_FILE_PATH}
22 | fi
23 |
--------------------------------------------------------------------------------
/docker/bin/setup_envs.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -Eeuo pipefail
4 |
5 | replace_env() {
6 | ./docker/bin/replace_env.bash "$@"
7 | }
8 |
9 | COMPOSE_ENV_PATH="$(realpath ./.env)"
10 | COMPOSE_DIST_ENV_PATH="$(realpath ./docker/.env.dist)"
11 |
12 | # У docker compose странный баг: когда ./.env загружен, compose пытается загрузить ./docker/.env,
13 | # вероятно из-за того, что там находится docker-compose.local.yml,
14 | # это приводит к непредсказуемому поведению и ошибкам
15 | rm -f ./docker/.env
16 |
17 | [ ! -f "${COMPOSE_ENV_PATH}" ] && cp "${COMPOSE_DIST_ENV_PATH}" "${COMPOSE_ENV_PATH}"
18 |
19 | replace_env "${COMPOSE_ENV_PATH}" 'COMPOSE_PROJECT_NAME' 'symfony-starter-kit-local'
20 | replace_env "${COMPOSE_ENV_PATH}" 'COMPOSE_FILE' './docker/docker-compose.local.yml'
21 | replace_env "${COMPOSE_ENV_PATH}" 'USER_ID' "$(id -u)"
22 |
23 | [ ! -f 'docker/pgsql/.env' ] && cp 'docker/pgsql/.env.dist' 'docker/pgsql/.env'
24 |
25 | echo 'Envs set up!';
26 |
--------------------------------------------------------------------------------
/docker/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.27.4
2 |
3 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
4 |
5 | COPY default.conf /etc/nginx/conf.d/default.conf
6 |
7 | WORKDIR /app/
8 |
--------------------------------------------------------------------------------
/docker/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | location /docs {
3 | proxy_pass http://docs:8080;
4 | }
5 |
6 | location = /mailhog {
7 | absolute_redirect off;
8 | rewrite /mailhog /mailhog/ permanent;
9 | }
10 |
11 | location ~ ^/mailhog {
12 | chunked_transfer_encoding on;
13 | proxy_set_header X-NginX-Proxy true;
14 | proxy_set_header Upgrade $http_upgrade;
15 | proxy_set_header Connection "upgrade";
16 | proxy_http_version 1.1;
17 | proxy_redirect off;
18 | proxy_buffering off;
19 | rewrite ^/mailhog(/.*)$ $1 break;
20 | proxy_set_header Host $host;
21 | proxy_pass http://mailhog:8025;
22 | }
23 |
24 | location / {
25 | fastcgi_pass backend:9000;
26 | include fastcgi_params;
27 |
28 | fastcgi_buffer_size 128k;
29 | fastcgi_buffers 4 256k;
30 | fastcgi_busy_buffers_size 256k;
31 |
32 | fastcgi_param SCRIPT_FILENAME /app/public/index.php;
33 | fastcgi_param DOCUMENT_ROOT /app/public/;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/docker/pgsql/.env.dist:
--------------------------------------------------------------------------------
1 | PGDATA=/var/lib/postgresql/data/pgdata
2 | PGUSER=postgres
3 | POSTGRES_PASSWORD=db_password
4 | POSTGRES_DB=db_name
5 |
--------------------------------------------------------------------------------
/docker/pgsql/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:17.4
2 |
3 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
4 |
5 | COPY pgsql.conf /var/lib/postgresql/data/postgresql.conf
6 |
--------------------------------------------------------------------------------
/docker/pgsql/pgsql.conf:
--------------------------------------------------------------------------------
1 | # -----------------------------
2 | # PostgreSQL configuration file
3 | # -----------------------------
4 | #
5 | # This file consists of lines of the form:
6 | #
7 | # name = value
8 | #
9 | # (The "=" is optional.) Whitespace may be used. Comments are introduced with
10 | # "#" anywhere on a line. The complete list of parameter names and allowed
11 | # values can be found in the PostgreSQL documentation.
12 | #
13 | # The commented-out settings shown in this file represent the default values.
14 | # Re-commenting a setting is NOT sufficient to revert it to the default value;
15 | # you need to reload the server.
16 | #
17 | # This file is read on server startup and when the server receives a SIGHUP
18 | # signal. If you edit the file on a running system, you have to SIGHUP the
19 | # server for the changes to take effect, or use "pg_ctl reload". Some
20 | # parameters, which are marked below, require a server shutdown and restart to
21 | # take effect.
22 | #
23 | # Any parameter can also be given as a command-line option to the server, e.g.,
24 | # "postgres -c log_connections=on". Some parameters can be changed at run time
25 | # with the "SET" SQL command.
26 | #
27 | # Memory units: kB = kilobytes Time units: ms = milliseconds
28 | # MB = megabytes s = seconds
29 | # GB = gigabytes min = minutes
30 | # h = hours
31 | # d = days
32 |
33 | timezone = 'UTC'
34 |
--------------------------------------------------------------------------------