diff --git a/makefile b/makefile index fdabd61..ee0059a 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -.PHONY: setup start stop reset logs clean test lint help update +.PHONY: setup start stop reset logs clean test lint help update db-user db-order rabbit-listen ## Update dependencies in all services update: @@ -29,6 +29,11 @@ setup: start: @echo "🐳 Starting all services..." docker-compose up -d --build + @docker-compose exec -d notification-service php bin/console messenger:consume async + +## run listener +rabbit-listen: + @docker-compose exec notification-service php bin/console messenger:consume async ## Stop all services stop: @@ -91,6 +96,11 @@ composer-update: @docker compose exec $(service) composer update --no-interaction --prefer-dist --optimize-autoloader @echo "✅ Dependencies updated for $(service)." +db-user: + @docker compose exec user-db psql -U app -d user_service +db-order: + @docker compose exec order-db psql -U app -d order_service + ## Show this help help: diff --git a/services/notification-service/composer.json b/services/notification-service/composer.json index e23c6a9..8f03020 100644 --- a/services/notification-service/composer.json +++ b/services/notification-service/composer.json @@ -10,12 +10,14 @@ "ext-ctype": "*", "ext-iconv": "*", "microservices/shared-contracts": "1.*", + "monolog/monolog": "*", "symfony/amqp-messenger": "7.3.*", "symfony/console": "7.3.*", "symfony/dotenv": "7.3.*", "symfony/flex": "^2.8.2", "symfony/framework-bundle": "7.3.*", "symfony/messenger": "7.3.*", + "symfony/monolog-bundle": "^3.10", "symfony/runtime": "7.3.*", "symfony/yaml": "7.3.*" }, diff --git a/services/notification-service/config/bundles.php b/services/notification-service/config/bundles.php index 49d3fb6..5b11b41 100644 --- a/services/notification-service/config/bundles.php +++ b/services/notification-service/config/bundles.php @@ -2,4 +2,5 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/services/notification-service/config/packages/monolog.yaml b/services/notification-service/config/packages/monolog.yaml new file mode 100644 index 0000000..93cca9d --- /dev/null +++ b/services/notification-service/config/packages/monolog.yaml @@ -0,0 +1,28 @@ +monolog: + channels: + - messenger + + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!messenger"] + + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + + console: + type: console + process_psr_3_messages: false + channels: ["!messenger", "!event"] + + # Логи Messenger в отдельный файл + messenger: + type: stream + path: "%kernel.logs_dir%/messenger.%kernel.environment%.log" + level: debug + channels: [messenger] diff --git a/services/notification-service/src/Controller/HealthController.php b/services/notification-service/src/Controller/HealthController.php index 72774f0..69fd2a1 100644 --- a/services/notification-service/src/Controller/HealthController.php +++ b/services/notification-service/src/Controller/HealthController.php @@ -12,8 +12,8 @@ class HealthController extends AbstractController public function health(): JsonResponse { return $this->json([ - 'status' => 'error', - 'message' => 'test 500 status code' - ], 500); + 'status' => 'success', + 'message' => 'is running' + ], 200); } } diff --git a/services/notification-service/src/Controller/LuckyController.php b/services/notification-service/src/Controller/LuckyController.php deleted file mode 100644 index 65ccdce..0000000 --- a/services/notification-service/src/Controller/LuckyController.php +++ /dev/null @@ -1,24 +0,0 @@ -Lucky number: '.$number.'' - ); - } -} diff --git a/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php b/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php index 4a58a79..4e8d7b8 100644 --- a/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php +++ b/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php @@ -48,9 +48,6 @@ private function sendWelcomeEmail(string $email, string $firstName, string $last // Имитация отправки email $this->logger->info("📧 Sending welcome email to: {$email}"); $this->logger->info("👋 Dear {$firstName} {$lastName}, welcome to our platform!"); - - // Заглушка для реальной отправки email - // sleep(1); } private function sendPushNotification(string $userId, string $firstName): void @@ -58,8 +55,5 @@ private function sendPushNotification(string $userId, string $firstName): void // Имитация отправки push-уведомления $this->logger->info("📱 Sending push notification to user: {$userId}"); $this->logger->info("🔔 Welcome {$firstName}! Your account has been created successfully."); - - // Заглушка для реальной отправки push - // sleep(1); } } diff --git a/services/notification-service/symfony.lock b/services/notification-service/symfony.lock index 640e94c..b1dcc47 100644 --- a/services/notification-service/symfony.lock +++ b/services/notification-service/symfony.lock @@ -83,6 +83,18 @@ "config/packages/messenger.yaml" ] }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/routing": { "version": "7.3", "recipe": { diff --git a/services/user-service/composer.json b/services/user-service/composer.json index 86a891a..2960673 100644 --- a/services/user-service/composer.json +++ b/services/user-service/composer.json @@ -9,19 +9,32 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "api-platform/doctrine-orm": "^4.2", + "api-platform/symfony": "^4.2", "doctrine/dbal": "^3", "doctrine/doctrine-bundle": "^2.18", "doctrine/doctrine-migrations-bundle": "^3.5", "doctrine/orm": "^3.5", "microservices/shared-contracts": "1.*", + "nelmio/cors-bundle": "^2.5", + "php-amqplib/php-amqplib": "^2.0", + "phpdocumentor/reflection-docblock": "^5.6", + "phpstan/phpdoc-parser": "^2.3", "symfony/amqp-messenger": "7.3.*", + "symfony/asset": "7.3.*", "symfony/console": "7.3.*", "symfony/dotenv": "7.3.*", + "symfony/expression-language": "7.3.*", "symfony/flex": "^2.8.2", "symfony/framework-bundle": "7.3.*", "symfony/messenger": "7.3.*", + "symfony/property-access": "7.3.*", + "symfony/property-info": "7.3.*", "symfony/runtime": "7.3.*", "symfony/security-bundle": "7.3.*", + "symfony/serializer": "7.3.*", + "symfony/twig-bundle": "7.3.*", + "symfony/validator": "7.3.*", "symfony/yaml": "7.3.*" }, "require-dev": { diff --git a/services/user-service/config/bundles.php b/services/user-service/config/bundles.php index 690585a..bae9815 100644 --- a/services/user-service/config/bundles.php +++ b/services/user-service/config/bundles.php @@ -5,4 +5,7 @@ Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], ]; diff --git a/services/user-service/config/packages/api_platform.yaml b/services/user-service/config/packages/api_platform.yaml new file mode 100644 index 0000000..02f295a --- /dev/null +++ b/services/user-service/config/packages/api_platform.yaml @@ -0,0 +1,7 @@ +api_platform: + title: Hello API Platform + version: 1.0.0 + defaults: + stateless: true + cache_headers: + vary: ['Content-Type', 'Authorization', 'Origin'] diff --git a/services/user-service/config/packages/nelmio_cors.yaml b/services/user-service/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..c766508 --- /dev/null +++ b/services/user-service/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/': null diff --git a/services/user-service/config/packages/twig.yaml b/services/user-service/config/packages/twig.yaml new file mode 100644 index 0000000..3f795d9 --- /dev/null +++ b/services/user-service/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/services/user-service/config/packages/validator.yaml b/services/user-service/config/packages/validator.yaml new file mode 100644 index 0000000..dd47a6a --- /dev/null +++ b/services/user-service/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/services/user-service/config/routes/api_platform.yaml b/services/user-service/config/routes/api_platform.yaml new file mode 100644 index 0000000..38f11cb --- /dev/null +++ b/services/user-service/config/routes/api_platform.yaml @@ -0,0 +1,4 @@ +api_platform: + resource: . + type: api_platform + prefix: /api diff --git a/services/user-service/config/services.yaml b/services/user-service/config/services.yaml index 6bbad87..23d2e86 100644 --- a/services/user-service/config/services.yaml +++ b/services/user-service/config/services.yaml @@ -18,3 +18,8 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + App\State\UserPasswordHasher: + bind: + $processor: '@api_platform.doctrine.orm.state.persist_processor' + tags: [ 'api_platform.state_processor' ] diff --git a/services/user-service/src/ApiResource/.gitignore b/services/user-service/src/ApiResource/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/services/user-service/src/Controller/BaseController.php b/services/user-service/src/Controller/BaseController.php deleted file mode 100644 index 7b58cae..0000000 --- a/services/user-service/src/Controller/BaseController.php +++ /dev/null @@ -1,26 +0,0 @@ -json(['message' => 'Users API is working!']); - } -} diff --git a/services/user-service/src/Controller/UserController.php b/services/user-service/src/Controller/UserController.php deleted file mode 100644 index 3722ed6..0000000 --- a/services/user-service/src/Controller/UserController.php +++ /dev/null @@ -1,112 +0,0 @@ -getContent(), true); - - // Валидация обязательных полей - if (!isset($data['email'], $data['firstName'], $data['lastName'], $data['password'])) { - return $this->json([ - 'error' => 'Missing required fields: email, firstName, lastName, password' - ], 400); - } - - // Проверяем существует ли пользователь - $existingUser = $this->userRepository->findByEmail($data['email']); - if ($existingUser) { - return $this->json([ - 'error' => 'User with this email already exists' - ], 409); - } - - // Создаем DTO из контракта - $createRequest = CreateUserRequest::fromArray($data); - - // Создаем пользователя - $user = new User(); - $user->setEmail($createRequest->email); - $user->setFirstName($createRequest->firstName); - $user->setLastName($createRequest->lastName); - - $hashedPassword = $this->passwordHasher->hashPassword($user, $createRequest->password); - $user->setPassword($hashedPassword); - - // Сохраняем в БД - $this->userRepository->save($user, true); - - // Создаем и отправляем событие - $event = new UserRegisteredEvent( - eventId: uniqid('user_reg_', true), - userId: (string)$user->getId(), - email: $user->getEmail(), - firstName: $user->getFirstName(), - lastName: $user->getLastName(), - registeredAt: $user->getCreatedAt() - ); - - // Отправляем событие в RabbitMQ - $this->messageBus->dispatch($event); - - // Создаем ответ по контракту - $userResponse = new UserResponse( - id: (string)$user->getId(), - email: $user->getEmail(), - firstName: $user->getFirstName(), - lastName: $user->getLastName(), - status: $user->getStatus(), - createdAt: $user->getCreatedAt(), - updatedAt: $user->getUpdatedAt() - ); - - return $this->json($userResponse, 201); - } - - #[Route('/get/{id}', name: 'user_get', methods: ['GET'])] - public function get(int $id): JsonResponse - { - /** @var User|null $user */ - $user = $this->userRepository->find($id); - - if (is_null($user)) { - return $this->json([ - 'error' => 'User not found' - ], 404); - } - - $userResponse = new UserResponse( - id: (string)$user->getId(), - email: $user->getEmail(), - firstName: $user->getFirstName(), - lastName: $user->getLastName(), - status: $user->getStatus(), - createdAt: $user->getCreatedAt(), - updatedAt: $user->getUpdatedAt() - ); - - return $this->json($userResponse); - } -} diff --git a/services/user-service/src/Entity/User.php b/services/user-service/src/Entity/User.php index 2ad7745..41b5e0e 100644 --- a/services/user-service/src/Entity/User.php +++ b/services/user-service/src/Entity/User.php @@ -1,90 +1,214 @@ ['Default', 'user:create']], processor: UserPasswordHasher::class), + new Get(), + new Put(processor: UserPasswordHasher::class), + new Patch(processor: UserPasswordHasher::class), + new Delete(), + ], + normalizationContext: ['groups' => ['user:read']], + denormalizationContext: ['groups' => ['user:create', 'user:update']], +)] #[ORM\Entity(repositoryClass: UserRepository::class)] -#[ORM\Table(name: 'users')] -class User implements PasswordAuthenticatedUserInterface +#[ORM\Table(name: '`users`')] +#[UniqueEntity('email')] +class User implements UserInterface, PasswordAuthenticatedUserInterface { + #[Groups(['user:read'])] #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'IDENTITY')] #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue] private ?int $id = null; - #[ORM\Column(type: 'string', length: 180, unique: true)] + #[Assert\NotBlank] + #[Assert\Email] + #[Groups(['user:read', 'user:create', 'user:update'])] + #[ORM\Column(length: 180, unique: true)] private ?string $email = null; - #[ORM\Column(type: 'string')] + #[ORM\Column] private ?string $password = null; - #[ORM\Column(type: 'string', length: 100)] + #[Assert\NotBlank(groups: ['user:create'])] + #[Groups(['user:create', 'user:update'])] + private ?string $plainPassword = null; + + #[ORM\Column(length: 255)] + #[Assert\NotBlank] + #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $firstName = null; - #[ORM\Column(type: 'string', length: 100)] + #[ORM\Column(length: 255)] + #[Assert\NotBlank] + #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $lastName = null; - #[ORM\Column(type: 'string', length: 20)] + #[ORM\Column(length: 50)] + #[Groups(['user:read'])] private string $status = 'active'; - #[ORM\Column(type: 'datetime_immutable')] + #[ORM\Column] + #[Groups(['user:read'])] private \DateTimeImmutable $createdAt; - #[ORM\Column(type: 'datetime_immutable')] + #[ORM\Column] + #[Groups(['user:read'])] private \DateTimeImmutable $updatedAt; + #[ORM\Column(type: 'json')] + private array $roles = []; + + public function __construct() { $this->createdAt = new \DateTimeImmutable(); $this->updatedAt = new \DateTimeImmutable(); } - // Геттеры - public function getId(): ?int { return $this->id; } - public function getEmail(): ?string { return $this->email; } - public function getFirstName(): ?string { return $this->firstName; } - public function getLastName(): ?string { return $this->lastName; } - public function getPassword(): ?string { return $this->password; } - public function getStatus(): string { return $this->status; } - public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } - public function getUpdatedAt(): \DateTimeImmutable { return $this->updatedAt; } + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } - // Сеттеры - public function setEmail(string $email): self { + public function setEmail(string $email): self + { $this->email = $email; - $this->updatedAt = new \DateTimeImmutable(); + return $this; } - public function setFirstName(string $firstName): self { - $this->firstName = $firstName; - $this->updatedAt = new \DateTimeImmutable(); + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + return $this; } - public function setLastName(string $lastName): self { - $this->lastName = $lastName; - $this->updatedAt = new \DateTimeImmutable(); + public function getPlainPassword(): ?string + { + return $this->plainPassword; + } + + public function setPlainPassword(?string $plainPassword): self + { + $this->plainPassword = $plainPassword; + return $this; } - public function setPassword(string $password): self { - $this->password = $password; - $this->updatedAt = new \DateTimeImmutable(); + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + return $this; } - public function setStatus(string $status): self { + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + * + * Required until Symfony 8.0, where eraseCredentials() will be removed from the interface. + * No-op since plainPassword is cleared manually in the password processor. + */ + public function eraseCredentials(): void + { + // Intentionally left blank + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(?string $lastName): void + { + $this->lastName = $lastName; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(?string $firstName): void + { + $this->firstName = $firstName; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): void + { $this->status = $status; - $this->updatedAt = new \DateTimeImmutable(); - return $this; } - public function setUpdatedAt(\DateTimeImmutable $updatedAt): self { - $this->updatedAt = $updatedAt; - return $this; + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): \DateTimeImmutable + { + return $this->updatedAt; } } diff --git a/services/user-service/src/Repository/UserRepository.php b/services/user-service/src/Repository/UserRepository.php index 8efd23a..7af7fe7 100644 --- a/services/user-service/src/Repository/UserRepository.php +++ b/services/user-service/src/Repository/UserRepository.php @@ -1,45 +1,59 @@ + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class UserRepository extends ServiceEntityRepository +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, User::class); } - public function save(User $user, bool $flush = true): void + public function save(User $entity, bool $flush = false): void { - $this->getEntityManager()->persist($user); + $this->getEntityManager()->persist($entity); + if ($flush) { $this->getEntityManager()->flush(); } } - - /** - * @param string $email - * @return User|null - */ - public function findByEmail(string $email): User|null + public function remove(User $entity, bool $flush = false): void { - /** @var User|null $user */ - $user = $this->findOneBy(['email' => $email]); - return $user; - } + $this->getEntityManager()->remove($entity); - public function remove(User $user, bool $flush = true): void - { - $this->getEntityManager()->remove($user); if ($flush) { $this->getEntityManager()->flush(); } } + + /** + * 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.', \get_class($user))); + } + + $user->setPassword($newHashedPassword); + + $this->save($user, true); + } } diff --git a/services/user-service/src/State/UserPasswordHasher.php b/services/user-service/src/State/UserPasswordHasher.php new file mode 100644 index 0000000..0c49b63 --- /dev/null +++ b/services/user-service/src/State/UserPasswordHasher.php @@ -0,0 +1,100 @@ +logger->info('❌ Not a User instance, passing through'); + return $this->processor->process($data, $operation, $uriVariables, $context); + } + + $this->logger->info('🔄 UserPasswordHasher: Processing user creation', [ + 'email' => $data->getEmail(), + 'operation' => $operation->getName() + ]); + + // Хешируем пароль если он предоставлен + $plainPassword = $data->getPlainPassword(); + + if ($plainPassword) { + $this->logger->info('🔐 Hashing password for user', ['email' => $data->getEmail()]); + $hashedPassword = $this->passwordHasher->hashPassword($data, $plainPassword); + $data->setPassword($hashedPassword); + $data->eraseCredentials(); + } + + $this->logger->info('📦 Calling standard processor to save user'); + + try { + // Обрабатываем через стандартный процессор + $result = $this->processor->process($data, $operation, $uriVariables, $context); + $this->logger->info('✅ Standard processor completed successfully'); + + } catch (\Exception $e) { + $this->logger->error('❌ Error in standard processor', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + + // Отправляем событие только после успешного создания пользователя + if ($operation->getName() === 'post' || $operation instanceof \ApiPlatform\Metadata\Post) { + $this->logger->info('🎉 User created successfully, sending event to RabbitMQ', [ + 'userId' => $data->getId(), + 'email' => $data->getEmail(), + 'user_exists' => $data->getId() !== null + ]); + + // Проверяем что у пользователя есть ID (сохранен в БД) + if ($data->getId() === null) { + $this->logger->error('❌ User has no ID, cannot send event'); + return $result; + } + + $event = new UserRegisteredEvent( + eventId: uniqid('user_reg_', true), + userId: (string)$data->getId(), + email: $data->getEmail(), + firstName: $data->getFirstName(), + lastName: $data->getLastName(), + registeredAt: $data->getCreatedAt() + ); + + $this->messageBus->dispatch($event); + + $this->logger->info('✅ Event sent to RabbitMQ', [ + 'eventId' => $event->eventId, + 'userId' => $event->userId + ]); + } else { + $this->logger->info('📭 Not a POST operation, skipping event sending', [ + 'operation' => $operation->getName() + ]); + } + + return $result; + } +} diff --git a/services/user-service/symfony.lock b/services/user-service/symfony.lock index 5bb58de..f8217ab 100644 --- a/services/user-service/symfony.lock +++ b/services/user-service/symfony.lock @@ -1,4 +1,18 @@ { + "api-platform/symfony": { + "version": "4.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.0", + "ref": "e9952e9f393c2d048f10a78f272cd35e807d972b" + }, + "files": [ + "config/packages/api_platform.yaml", + "config/routes/api_platform.yaml", + "src/ApiResource/.gitignore" + ] + }, "doctrine/deprecations": { "version": "1.1", "recipe": { @@ -35,6 +49,18 @@ "migrations/.gitignore" ] }, + "nelmio/cors-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, "phpstan/phpstan": { "version": "1.12", "recipe": { @@ -152,5 +178,39 @@ "config/packages/security.yaml", "config/routes/security.yaml" ] + }, + "symfony/twig-bundle": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/uid": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5" + } + }, + "symfony/validator": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.0", + "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd" + }, + "files": [ + "config/packages/validator.yaml" + ] } } diff --git a/services/user-service/templates/base.html.twig b/services/user-service/templates/base.html.twig new file mode 100644 index 0000000..1069c14 --- /dev/null +++ b/services/user-service/templates/base.html.twig @@ -0,0 +1,16 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {% endblock %} + + {% block javascripts %} + {% endblock %} + + + {% block body %}{% endblock %} + +