From 62e0bf5f11b6c6a0c8095dbae7dadb64d70f2bb0 Mon Sep 17 00:00:00 2001 From: matthieu Date: Wed, 20 May 2026 00:06:01 +0200 Subject: [PATCH] feat(mail) : MailSettings ApiResource singleton (GET/PATCH /api/mail/configuration) - ApiResource MailSettings expose les operations Get + Patch sur /api/mail/configuration - Provider + Processor relient le DTO a l'entite MailConfiguration (singleton) - password en write-only (jamais retourne) + hasPassword en lecture - chiffrement password via TokenEncryptor (sodium) - securite ROLE_ADMIN sur les deux operations Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ApiResource/MailSettings.php | 69 ++++++++++ src/State/Mail/MailSettingsProcessor.php | 83 ++++++++++++ src/State/Mail/MailSettingsProvider.php | 39 ++++++ .../Mail/MailSettingsControllerTest.php | 121 ++++++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 src/ApiResource/MailSettings.php create mode 100644 src/State/Mail/MailSettingsProcessor.php create mode 100644 src/State/Mail/MailSettingsProvider.php create mode 100644 tests/Functional/Controller/Mail/MailSettingsControllerTest.php diff --git a/src/ApiResource/MailSettings.php b/src/ApiResource/MailSettings.php new file mode 100644 index 0000000..615b9d1 --- /dev/null +++ b/src/ApiResource/MailSettings.php @@ -0,0 +1,69 @@ + ['mail_settings:read']], + provider: MailSettingsProvider::class, + security: "is_granted('ROLE_ADMIN')", + ), + new Patch( + uriTemplate: '/mail/configuration', + denormalizationContext: ['groups' => ['mail_settings:write']], + normalizationContext: ['groups' => ['mail_settings:read']], + provider: MailSettingsProvider::class, + processor: MailSettingsProcessor::class, + security: "is_granted('ROLE_ADMIN')", + ), + ], +)] +final class MailSettings +{ + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $protocol = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $imapHost = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?int $imapPort = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $imapEncryption = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $smtpHost = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?int $smtpPort = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $smtpEncryption = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $username = null; + + #[Groups(['mail_settings:write'])] + public ?string $password = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public ?string $sentFolderPath = null; + + #[Groups(['mail_settings:read', 'mail_settings:write'])] + public bool $enabled = false; + + #[Groups(['mail_settings:read'])] + public bool $hasPassword = false; +} diff --git a/src/State/Mail/MailSettingsProcessor.php b/src/State/Mail/MailSettingsProcessor.php new file mode 100644 index 0000000..9b7a7df --- /dev/null +++ b/src/State/Mail/MailSettingsProcessor.php @@ -0,0 +1,83 @@ +configRepository->findSingleton(); + if (null === $config) { + $config = new MailConfiguration(); + } + + if (null !== $data->protocol) { + $config->setProtocol($data->protocol); + } + if (null !== $data->imapHost) { + $config->setImapHost($data->imapHost); + } + if (null !== $data->imapPort) { + $config->setImapPort($data->imapPort); + } + if (null !== $data->imapEncryption) { + $config->setImapEncryption($data->imapEncryption); + } + if (null !== $data->smtpHost) { + $config->setSmtpHost($data->smtpHost); + } + if (null !== $data->smtpPort) { + $config->setSmtpPort($data->smtpPort); + } + if (null !== $data->smtpEncryption) { + $config->setSmtpEncryption($data->smtpEncryption); + } + if (null !== $data->username) { + $config->setUsername($data->username); + } + if (null !== $data->sentFolderPath) { + $config->setSentFolderPath($data->sentFolderPath); + } + $config->setEnabled($data->enabled); + + if (null !== $data->password && '' !== $data->password) { + $config->setEncryptedPassword($this->tokenEncryptor->encrypt($data->password)); + } + + $this->em->persist($config); + $this->em->flush(); + + $result = new MailSettings(); + $result->protocol = $config->getProtocol(); + $result->imapHost = $config->getImapHost(); + $result->imapPort = $config->getImapPort(); + $result->imapEncryption = $config->getImapEncryption(); + $result->smtpHost = $config->getSmtpHost(); + $result->smtpPort = $config->getSmtpPort(); + $result->smtpEncryption = $config->getSmtpEncryption(); + $result->username = $config->getUsername(); + $result->sentFolderPath = $config->getSentFolderPath(); + $result->enabled = $config->isEnabled(); + $result->hasPassword = $config->hasPassword(); + + return $result; + } +} diff --git a/src/State/Mail/MailSettingsProvider.php b/src/State/Mail/MailSettingsProvider.php new file mode 100644 index 0000000..9a60292 --- /dev/null +++ b/src/State/Mail/MailSettingsProvider.php @@ -0,0 +1,39 @@ +configRepository->findSingleton(); + $dto = new MailSettings(); + + if (null !== $config) { + $dto->protocol = $config->getProtocol(); + $dto->imapHost = $config->getImapHost(); + $dto->imapPort = $config->getImapPort(); + $dto->imapEncryption = $config->getImapEncryption(); + $dto->smtpHost = $config->getSmtpHost(); + $dto->smtpPort = $config->getSmtpPort(); + $dto->smtpEncryption = $config->getSmtpEncryption(); + $dto->username = $config->getUsername(); + $dto->sentFolderPath = $config->getSentFolderPath(); + $dto->enabled = $config->isEnabled(); + $dto->hasPassword = $config->hasPassword(); + } + + return $dto; + } +} diff --git a/tests/Functional/Controller/Mail/MailSettingsControllerTest.php b/tests/Functional/Controller/Mail/MailSettingsControllerTest.php new file mode 100644 index 0000000..63c1d0c --- /dev/null +++ b/tests/Functional/Controller/Mail/MailSettingsControllerTest.php @@ -0,0 +1,121 @@ +request('GET', '/api/mail/configuration'); + + self::assertResponseStatusCodeSame(401); + } + + public function testGetConfigurationReturns403ForRoleUser(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $user = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); + $client->loginUser($user); + $client->request('GET', '/api/mail/configuration'); + + self::assertResponseStatusCodeSame(403); + } + + public function testGetConfigurationReturns200ForAdmin(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $admin = $em->getRepository(User::class)->findOneBy(['username' => 'admin']); + $client->loginUser($admin); + $client->request('GET', '/api/mail/configuration'); + + self::assertResponseIsSuccessful(); + $data = json_decode($client->getResponse()->getContent(), true); + + self::assertArrayNotHasKey('password', $data); + self::assertArrayNotHasKey('encryptedPassword', $data); + self::assertArrayHasKey('hasPassword', $data); + self::assertArrayHasKey('imapHost', $data); + self::assertArrayHasKey('enabled', $data); + } + + public function testPatchConfigurationReturns403ForRoleUser(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $user = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); + $client->loginUser($user); + $client->request( + 'PATCH', + '/api/mail/configuration', + [], + [], + ['CONTENT_TYPE' => 'application/merge-patch+json'], + json_encode(['enabled' => false]) + ); + + self::assertResponseStatusCodeSame(403); + } + + public function testPatchConfigurationUpdatesFieldsForAdmin(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $admin = $em->getRepository(User::class)->findOneBy(['username' => 'admin']); + $client->loginUser($admin); + $client->request( + 'PATCH', + '/api/mail/configuration', + [], + [], + ['CONTENT_TYPE' => 'application/merge-patch+json'], + json_encode(['imapHost' => 'imap.example.com', 'enabled' => false]) + ); + + self::assertResponseIsSuccessful(); + $data = json_decode($client->getResponse()->getContent(), true); + self::assertSame('imap.example.com', $data['imapHost']); + self::assertArrayNotHasKey('password', $data); + } + + public function testPatchConfigurationWithPasswordEncryptsIt(): void + { + $client = static::createClient(); + $container = static::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $admin = $em->getRepository(User::class)->findOneBy(['username' => 'admin']); + $client->loginUser($admin); + $client->request( + 'PATCH', + '/api/mail/configuration', + [], + [], + ['CONTENT_TYPE' => 'application/merge-patch+json'], + json_encode(['password' => 'secret123']) + ); + + self::assertResponseIsSuccessful(); + $data = json_decode($client->getResponse()->getContent(), true); + self::assertTrue($data['hasPassword']); + self::assertArrayNotHasKey('password', $data); + } +}