From 9924b5575e0b6539b5e5ed161274d783b9065d6d Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 20 Nov 2025 11:34:43 +0100 Subject: [PATCH 01/38] Add rate limiting + test --- .../src/Api/Console/RateLimit/RateLimit.php | 98 ++++++++++ .../Console/RateLimit/RateLimitListener.php | 154 +++++++++++++++ .../App/RateLimit/RateLimiterProvider.php | 38 ++++ backend/tests/Api/Console/RateLimitTest.php | 179 ++++++++++++++++++ 4 files changed, 469 insertions(+) create mode 100644 backend/src/Api/Console/RateLimit/RateLimit.php create mode 100644 backend/src/Api/Console/RateLimit/RateLimitListener.php create mode 100644 backend/src/Service/App/RateLimit/RateLimiterProvider.php create mode 100644 backend/tests/Api/Console/RateLimitTest.php diff --git a/backend/src/Api/Console/RateLimit/RateLimit.php b/backend/src/Api/Console/RateLimit/RateLimit.php new file mode 100644 index 00000000..c5ce071e --- /dev/null +++ b/backend/src/Api/Console/RateLimit/RateLimit.php @@ -0,0 +1,98 @@ +isDev = $this->env === 'dev'; + } + + /** + * Rate limit for a user session. + * 60 per minute + * @return RateLimitConfig + */ + public function session(): array + { + return [ + 'id' => 'console_api_session', + 'policy' => 'fixed_window', + 'limit' => $this->isDev ? 1000 : 60, + 'interval' => '1 minute', + ]; + } + + /** + * Rate limit for an API key. + * 100 per minute + * @return RateLimitConfig + */ + public function apiKey(): array + { + return [ + 'id' => 'console_api_api_key', + 'policy' => 'fixed_window', + 'limit' => $this->isDev ? 1000 : 100, + 'interval' => '1 minute', + ]; + } + + /** + * Rate limit for the /sends endpoint. + * 10 per second + * @return RateLimitConfig + */ + public function subsciber(): array + { + return [ + 'id' => 'console_api_sends', + 'policy' => 'fixed_window', + 'limit' => 10, + 'interval' => '1 second', + ]; + } + + /** + * Rate limit for the POST /subscribers endpoint. + * 1 subscribe per email per minute + * @return RateLimitConfig + */ + public function subscriberPerMinute(): array + { + return [ + 'id' => 'console_api_subscriber_per_minute', + 'policy' => 'fixed_window', + 'limit' => 1, + 'interval' => '1 minute', + ]; + } + + /** + * Rate limit for the POST /subscribers endpoint. + * 6 subscribes per email per hour + * @return RateLimitConfig + */ + public function subscriberPerHour(): array + { + return [ + 'id' => 'console_api_subscriber_per_hour', + 'policy' => 'fixed_window', + 'limit' => 6, + 'interval' => '1 hour', + ]; + } + +} diff --git a/backend/src/Api/Console/RateLimit/RateLimitListener.php b/backend/src/Api/Console/RateLimit/RateLimitListener.php new file mode 100644 index 00000000..af6693d3 --- /dev/null +++ b/backend/src/Api/Console/RateLimit/RateLimitListener.php @@ -0,0 +1,154 @@ +getPathInfo(), '/api/console'); + } + + private function isPublicApiRequest(Request $request): bool + { + return str_starts_with($request->getPathInfo(), '/api/public'); + } + + private function isSubscribePostRequest(Request $request): bool + { + return $request->getMethod() === 'POST' + && $request->getPathInfo() === '/api/public/form/subscribe'; + } + + private function getRateLimiter(Request $request): LimiterInterface + { + // check if this is a session request (user logged in) + if (AuthorizationListener::hasUser($request)) { + $user = AuthorizationListener::getUser($request); + return $this->rateLimiterProvider->rateLimiter($this->rateLimit->session(), "user:" . $user->id); + } + + $apiKey = AuthorizationListener::getApiKey($request); + + return $this->rateLimiterProvider->rateLimiter($this->rateLimit->apiKey(), 'api_key:' . $apiKey->getId()); + } + + public function onController(ControllerEvent $controllerEvent): void + { + if ($controllerEvent->isMainRequest() === false) { + return; + } + + $request = $controllerEvent->getRequest(); + + // Apply subscribe-specific rate limiting for public form submissions + if ($this->isSubscribePostRequest($request)) { + $this->applySubscribeRateLimit($request); + return; // Skip general rate limiting for this endpoint + } + + if (!$this->isConsoleApiRequest($request)) { + return; + } + + $limiter = $this->getRateLimiter($request); + $limit = $limiter->consume(); + + $resetIn = max($limit->getRetryAfter()->getTimestamp() - time(), 0); + $request->attributes->set(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY, [ + 'X-RateLimit-Limit' => $limit->getLimit(), + 'X-RateLimit-Remaining' => $limit->getRemainingTokens(), + 'X-RateLimit-Reset' => $resetIn, + ]); + + if ($limit->isAccepted() === false) { + throw new TooManyRequestsHttpException( + message: 'Rate limit exceeded. Please try again later in ' . $resetIn . ' seconds.', + ); + } + } + + private function applySubscribeRateLimit(Request $request): void + { + $content = $request->getContent(); + $data = json_decode($content, true); + + if (!isset($data['email']) || !is_string($data['email'])) { + return; // Skip rate limiting if email is not provided or invalid + } + + $email = $data['email']; + + $perMinuteLimiter = $this->rateLimiterProvider->rateLimiter( + $this->rateLimit->subscriberPerMinute(), + 'subscriber_email:' . $email + ); + $perMinuteLimit = $perMinuteLimiter->consume(); + + if ($perMinuteLimit->isAccepted() === false) { + $resetIn = max($perMinuteLimit->getRetryAfter()->getTimestamp() - time(), 0); + throw new TooManyRequestsHttpException( + message: 'You have recently requested a subscription confirm link. Please try again in ' . $resetIn . ' seconds.', + ); + } + + $perHourLimiter = $this->rateLimiterProvider->rateLimiter( + $this->rateLimit->subscriberPerHour(), + 'subscriber_email:' . $email + ); + $perHourLimit = $perHourLimiter->consume(); + + if ($perHourLimit->isAccepted() === false) { + $resetIn = max($perHourLimit->getRetryAfter()->getTimestamp() - time(), 0); + throw new TooManyRequestsHttpException( + message: 'You have recently requested a subscription confirm link. Please try again in ' . $resetIn . ' seconds.', + ); + } + } + + public function onResponse(ResponseEvent $responseEvent): void + { + if ($responseEvent->isMainRequest() === false) { + return; + } + + $request = $responseEvent->getRequest(); + if (!$this->isConsoleApiRequest($request)) { + return; + } + + $response = $responseEvent->getResponse(); + + if ($request->attributes->has(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY)) { + /** @var array $rateLimitHeaders */ + $rateLimitHeaders = $request->attributes->get(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY); + foreach ($rateLimitHeaders as $header => $value) { + $response->headers->set($header, (string)$value); + } + } + } + +} diff --git a/backend/src/Service/App/RateLimit/RateLimiterProvider.php b/backend/src/Service/App/RateLimit/RateLimiterProvider.php new file mode 100644 index 00000000..a87110b8 --- /dev/null +++ b/backend/src/Service/App/RateLimit/RateLimiterProvider.php @@ -0,0 +1,38 @@ + $config + */ + public function factory(array $config): RateLimiterFactory + { + $storage = new CacheStorage($this->cacheItemPool); + return new RateLimiterFactory($config, $storage, $this->lockFactory); + } + + /** + * @param array $config + */ + public function rateLimiter(array $config, string $key): LimiterInterface + { + $rateLimiter = $this->factory($config); + return $rateLimiter->create($key); + } + +} diff --git a/backend/tests/Api/Console/RateLimitTest.php b/backend/tests/Api/Console/RateLimitTest.php new file mode 100644 index 00000000..c1e20af1 --- /dev/null +++ b/backend/tests/Api/Console/RateLimitTest.php @@ -0,0 +1,179 @@ + $newsletter, + ]); + + $response = $this->consoleApi( + $newsletter, + 'POST', + '/subscribers', + [ + 'email' => 'thibault@hyvor.com', + 'list_ids' => [$list->getId()], + ] + ); + + $this->assertResponseStatusCodeSame(200); + $this->assertResponseHeaderSame('X-RateLimit-Limit', '100'); + $this->assertResponseHeaderSame('X-RateLimit-Remaining', '99'); + $this->assertResponseHeaderSame('X-RateLimit-Reset', '0'); + } + + public function test_429_on_rate_limited(): void + { + $newsletter = NewsletterFactory::createOne(['user_id' => 1]); + $list = NewsletterListFactory::createOne([ + 'newsletter' => $newsletter, + ]); + + $rateLimit = new RateLimit(); + /** @var RateLimiterProvider $rateLimiterProvider */ + $rateLimiterProvider = $this->getContainer()->get(RateLimiterProvider::class); + + $limiter = $rateLimiterProvider->rateLimiter($rateLimit->session(), "user:1"); + $limiter->consume(60); + $limiter->consume(10); + + $response = $this->consoleApi( + $newsletter, + 'POST', + '/subscribers', + [ + 'email' => 'thibault@hyvor.com', + 'list_ids' => [$list->getId()], + ], + useSession: true + ); + + $this->assertResponseStatusCodeSame(429); + + $this->assertResponseHeaderSame('X-RateLimit-Limit', '60'); + $this->assertResponseHeaderSame('X-RateLimit-Remaining', '0'); + $this->assertResponseHeaderSame('X-RateLimit-Reset', '60'); + } + + public function test_subscribe_endpoint_applies_per_minute_email_rate_limit(): void + { + $this->mockRelayClient(); + $newsletter = NewsletterFactory::createOne(); + SendingProfileFactory::createOne(['newsletter' => $newsletter]); + $list = NewsletterListFactory::createOne(['newsletter' => $newsletter]); + + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => 'test@example.com', + 'list_ids' => [$list->getId()] + ] + ); + + $this->assertResponseStatusCodeSame(200); + + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => 'test@example.com', + 'list_ids' => [$list->getId()] + ] + ); + + $this->assertResponseStatusCodeSame(429); + $json = $this->getJson(); + $this->assertStringContainsString('You have recently requested a subscription confirm link', $json['message']); + $this->assertStringContainsString('seconds', $json['message']); + } + + public function test_subscribe_endpoint_different_emails_not_rate_limited(): void + { + $this->mockRelayClient(); + $newsletter = NewsletterFactory::createOne(); + SendingProfileFactory::createOne(['newsletter' => $newsletter]); + $list = NewsletterListFactory::createOne(['newsletter' => $newsletter]); + + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => 'test1@example.com', + 'list_ids' => [$list->getId()] + ] + ); + + $this->assertResponseStatusCodeSame(200); + + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => 'test2@example.com', + 'list_ids' => [$list->getId()] + ] + ); + + $this->assertResponseStatusCodeSame(200); + } + + public function test_subscribe_endpoint_hourly_rate_limit(): void + { + $this->mockRelayClient(); + $newsletter = NewsletterFactory::createOne(); + SendingProfileFactory::createOne(['newsletter' => $newsletter]); + $list = NewsletterListFactory::createOne(['newsletter' => $newsletter]); + + $rateLimit = new RateLimit(); + /** @var RateLimiterProvider $rateLimiterProvider */ + $rateLimiterProvider = $this->getContainer()->get(RateLimiterProvider::class); + + $email = 'test@example.com'; + + // Consume the hourly limit (6 requests) + $perHourLimiter = $rateLimiterProvider->rateLimiter( + $rateLimit->subscriberPerHour(), + 'subscriber_email:' . $email + ); + $perHourLimiter->consume(6); + + + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => $email, + 'list_ids' => [$list->getId()] + ] + ); + + $this->assertResponseStatusCodeSame(429); + $json = $this->getJson(); + $this->assertStringContainsString('You have recently requested a subscription confirm link', $json['message']); + $this->assertStringContainsString('seconds', $json['message']); + } +} From 25a3932fb785b95646c6219152bed99575f259dd Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 20 Nov 2025 11:52:43 +0100 Subject: [PATCH 02/38] Fix phpstan --- backend/src/Api/Console/RateLimit/RateLimitListener.php | 9 ++++----- backend/tests/Api/Console/RateLimitTest.php | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/Api/Console/RateLimit/RateLimitListener.php b/backend/src/Api/Console/RateLimit/RateLimitListener.php index af6693d3..6c74db59 100644 --- a/backend/src/Api/Console/RateLimit/RateLimitListener.php +++ b/backend/src/Api/Console/RateLimit/RateLimitListener.php @@ -32,11 +32,6 @@ private function isConsoleApiRequest(Request $request): bool return str_starts_with($request->getPathInfo(), '/api/console'); } - private function isPublicApiRequest(Request $request): bool - { - return str_starts_with($request->getPathInfo(), '/api/public'); - } - private function isSubscribePostRequest(Request $request): bool { return $request->getMethod() === 'POST' @@ -96,6 +91,10 @@ private function applySubscribeRateLimit(Request $request): void $content = $request->getContent(); $data = json_decode($content, true); + if (!is_array($data)) { + return; + } + if (!isset($data['email']) || !is_string($data['email'])) { return; // Skip rate limiting if email is not provided or invalid } diff --git a/backend/tests/Api/Console/RateLimitTest.php b/backend/tests/Api/Console/RateLimitTest.php index c1e20af1..1c2a3e84 100644 --- a/backend/tests/Api/Console/RateLimitTest.php +++ b/backend/tests/Api/Console/RateLimitTest.php @@ -104,6 +104,7 @@ public function test_subscribe_endpoint_applies_per_minute_email_rate_limit(): v $this->assertResponseStatusCodeSame(429); $json = $this->getJson(); + $this->assertIsString($json['message']); $this->assertStringContainsString('You have recently requested a subscription confirm link', $json['message']); $this->assertStringContainsString('seconds', $json['message']); } @@ -173,6 +174,7 @@ public function test_subscribe_endpoint_hourly_rate_limit(): void $this->assertResponseStatusCodeSame(429); $json = $this->getJson(); + $this->assertIsString($json['message']); $this->assertStringContainsString('You have recently requested a subscription confirm link', $json['message']); $this->assertStringContainsString('seconds', $json['message']); } From 5bd9368ae8dfa0407119cf4e3c2e56c01ebcf2e2 Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 21 Nov 2025 11:55:13 +0100 Subject: [PATCH 03/38] Add abstraction --- .../src/Api/Console/RateLimit/RateLimit.php | 25 ++------- .../Console/RateLimit/RateLimitListener.php | 51 +++++++++++-------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/backend/src/Api/Console/RateLimit/RateLimit.php b/backend/src/Api/Console/RateLimit/RateLimit.php index c5ce071e..b19b897c 100644 --- a/backend/src/Api/Console/RateLimit/RateLimit.php +++ b/backend/src/Api/Console/RateLimit/RateLimit.php @@ -50,32 +50,17 @@ public function apiKey(): array ]; } - /** - * Rate limit for the /sends endpoint. - * 10 per second - * @return RateLimitConfig - */ - public function subsciber(): array - { - return [ - 'id' => 'console_api_sends', - 'policy' => 'fixed_window', - 'limit' => 10, - 'interval' => '1 second', - ]; - } - /** * Rate limit for the POST /subscribers endpoint. * 1 subscribe per email per minute * @return RateLimitConfig */ - public function subscriberPerMinute(): array + public function subscriberPerEmailPerMinute(): array { return [ - 'id' => 'console_api_subscriber_per_minute', + 'id' => 'public_api_subscriber_per_minute', 'policy' => 'fixed_window', - 'limit' => 1, + 'limit' => 2, 'interval' => '1 minute', ]; } @@ -85,10 +70,10 @@ public function subscriberPerMinute(): array * 6 subscribes per email per hour * @return RateLimitConfig */ - public function subscriberPerHour(): array + public function subscriberPerEmailPerHour(): array { return [ - 'id' => 'console_api_subscriber_per_hour', + 'id' => 'public_api_subscriber_per_hour', 'policy' => 'fixed_window', 'limit' => 6, 'interval' => '1 hour', diff --git a/backend/src/Api/Console/RateLimit/RateLimitListener.php b/backend/src/Api/Console/RateLimit/RateLimitListener.php index 6c74db59..0958f5b7 100644 --- a/backend/src/Api/Console/RateLimit/RateLimitListener.php +++ b/backend/src/Api/Console/RateLimit/RateLimitListener.php @@ -51,6 +51,26 @@ private function getRateLimiter(Request $request): LimiterInterface return $this->rateLimiterProvider->rateLimiter($this->rateLimit->apiKey(), 'api_key:' . $apiKey->getId()); } + + /** + * @param array{id: string, policy: string, limit: int, interval: string} $rateLimitConfig + * @param string $identifier + * @param string $errorMessage Message with %d placeholder for resetIn seconds + * @return void + */ + private function checkRateLimit(array $rateLimitConfig, string $identifier, string $errorMessage): void + { + $limiter = $this->rateLimiterProvider->rateLimiter($rateLimitConfig, $identifier); + $limit = $limiter->consume(); + + if ($limit->isAccepted() === false) { + $resetIn = max($limit->getRetryAfter()->getTimestamp() - time(), 0); + throw new TooManyRequestsHttpException( + message: sprintf($errorMessage, $resetIn), + ); + } + } + public function onController(ControllerEvent $controllerEvent): void { if ($controllerEvent->isMainRequest() === false) { @@ -100,32 +120,19 @@ private function applySubscribeRateLimit(Request $request): void } $email = $data['email']; + $identifier = 'subscriber_email:' . $email; - $perMinuteLimiter = $this->rateLimiterProvider->rateLimiter( - $this->rateLimit->subscriberPerMinute(), - 'subscriber_email:' . $email + $this->checkRateLimit( + $this->rateLimit->subscriberPerEmailPerMinute(), + $identifier, + 'You have recently requested a subscription confirm link. Please try again in %d seconds.' ); - $perMinuteLimit = $perMinuteLimiter->consume(); - - if ($perMinuteLimit->isAccepted() === false) { - $resetIn = max($perMinuteLimit->getRetryAfter()->getTimestamp() - time(), 0); - throw new TooManyRequestsHttpException( - message: 'You have recently requested a subscription confirm link. Please try again in ' . $resetIn . ' seconds.', - ); - } - $perHourLimiter = $this->rateLimiterProvider->rateLimiter( - $this->rateLimit->subscriberPerHour(), - 'subscriber_email:' . $email + $this->checkRateLimit( + $this->rateLimit->subscriberPerEmailPerHour(), + $identifier, + 'You have recently requested a subscription confirm link. Please try again in %d seconds.' ); - $perHourLimit = $perHourLimiter->consume(); - - if ($perHourLimit->isAccepted() === false) { - $resetIn = max($perHourLimit->getRetryAfter()->getTimestamp() - time(), 0); - throw new TooManyRequestsHttpException( - message: 'You have recently requested a subscription confirm link. Please try again in ' . $resetIn . ' seconds.', - ); - } } public function onResponse(ResponseEvent $responseEvent): void From 7eff330b9c371f34c78dc873903c71a798770ade Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 21 Nov 2025 11:56:29 +0100 Subject: [PATCH 04/38] Fix tests --- backend/tests/Api/Console/RateLimitTest.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/tests/Api/Console/RateLimitTest.php b/backend/tests/Api/Console/RateLimitTest.php index 1c2a3e84..5dd98062 100644 --- a/backend/tests/Api/Console/RateLimitTest.php +++ b/backend/tests/Api/Console/RateLimitTest.php @@ -90,6 +90,16 @@ public function test_subscribe_endpoint_applies_per_minute_email_rate_limit(): v ] ); + $response = $this->publicApi( + 'POST', + '/form/subscribe', + [ + 'newsletter_subdomain' => $newsletter->getSubdomain(), + 'email' => 'test@example.com', + 'list_ids' => [$list->getId()] + ] + ); + $this->assertResponseStatusCodeSame(200); $response = $this->publicApi( @@ -156,7 +166,7 @@ public function test_subscribe_endpoint_hourly_rate_limit(): void // Consume the hourly limit (6 requests) $perHourLimiter = $rateLimiterProvider->rateLimiter( - $rateLimit->subscriberPerHour(), + $rateLimit->subscriberPerEmailPerHour(), 'subscriber_email:' . $email ); $perHourLimiter->consume(6); From c2fc1109d08b5c6e648b33dae705399657938e3d Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 21 Nov 2025 11:57:22 +0100 Subject: [PATCH 05/38] move up the rate limit --- backend/src/Api/{Console => }/RateLimit/RateLimit.php | 2 +- backend/src/Api/{Console => }/RateLimit/RateLimitListener.php | 2 +- backend/tests/Api/Console/RateLimitTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename backend/src/Api/{Console => }/RateLimit/RateLimit.php (98%) rename backend/src/Api/{Console => }/RateLimit/RateLimitListener.php (99%) diff --git a/backend/src/Api/Console/RateLimit/RateLimit.php b/backend/src/Api/RateLimit/RateLimit.php similarity index 98% rename from backend/src/Api/Console/RateLimit/RateLimit.php rename to backend/src/Api/RateLimit/RateLimit.php index b19b897c..aad67bff 100644 --- a/backend/src/Api/Console/RateLimit/RateLimit.php +++ b/backend/src/Api/RateLimit/RateLimit.php @@ -1,6 +1,6 @@ Date: Fri, 21 Nov 2025 12:04:44 +0100 Subject: [PATCH 06/38] Move wehbook relay to machine API --- backend/config/routes/routes.php | 5 ++ .../Relay/RelayWebhookController.php | 2 +- .../Integration/Relay/RelayWebhookTest.php | 6 +- backend/tests/Api/Public/RateLimitTest.php | 77 +++++++++++++++++++ backend/tests/Case/WebTestCase.php | 28 +++++++ 5 files changed, 114 insertions(+), 4 deletions(-) rename backend/src/Api/{Public => Machine}/Controller/Integration/Relay/RelayWebhookController.php (98%) rename backend/tests/Api/{Public => Machine}/Integration/Relay/RelayWebhookTest.php (99%) create mode 100644 backend/tests/Api/Public/RateLimitTest.php diff --git a/backend/config/routes/routes.php b/backend/config/routes/routes.php index 7ec3556d..0e2daf1e 100644 --- a/backend/config/routes/routes.php +++ b/backend/config/routes/routes.php @@ -19,6 +19,11 @@ ->prefix('/api/sudo') ->namePrefix('api_sudo_'); + // machine API + $routes->import('../../src/Api/Machine/Controller', 'attribute') + ->prefix('/api/machine') + ->namePrefix('api_machine_'); + $routes->import('../../src/Api/Local', 'attribute') ->prefix('/api/local') ->condition('env("APP_ENV") in ["dev", "test"]') diff --git a/backend/src/Api/Public/Controller/Integration/Relay/RelayWebhookController.php b/backend/src/Api/Machine/Controller/Integration/Relay/RelayWebhookController.php similarity index 98% rename from backend/src/Api/Public/Controller/Integration/Relay/RelayWebhookController.php rename to backend/src/Api/Machine/Controller/Integration/Relay/RelayWebhookController.php index 52b279ec..a20e5101 100644 --- a/backend/src/Api/Public/Controller/Integration/Relay/RelayWebhookController.php +++ b/backend/src/Api/Machine/Controller/Integration/Relay/RelayWebhookController.php @@ -1,6 +1,6 @@ publicApi( + return $this->machineApi( 'POST', '/integration/relay/webhook', $data @@ -324,4 +324,4 @@ public function test_suppression_created(): void $this->assertSame(SubscriberStatus::UNSUBSCRIBED, $subscriber3->getStatus()); $this->assertSame(SubscriberStatus::SUBSCRIBED, $subscriber4->getStatus()); } -} \ No newline at end of file +} diff --git a/backend/tests/Api/Public/RateLimitTest.php b/backend/tests/Api/Public/RateLimitTest.php new file mode 100644 index 00000000..73fd0b66 --- /dev/null +++ b/backend/tests/Api/Public/RateLimitTest.php @@ -0,0 +1,77 @@ +getSubdomain(); + + $response = $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ]); + + $this->assertResponseStatusCodeSame(200); + $this->assertResponseHeaderSame('RateLimit-Limit', '30'); + $this->assertResponseHeaderSame('RateLimit-Remaining', '29'); + $this->assertTrue($this->client->getResponse()->headers->has('RateLimit-Reset')); + } + + public function test_429_on_rate_limited(): void + { + $newsletter = NewsletterFactory::createOne(); + $subdomain = $newsletter->getSubdomain(); + + // Make 30 requests to exhaust the rate limit + for ($i = 0; $i < 30; $i++) { + $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ]); + } + + // 31st request should be rate limited + $response = $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ]); + + $this->assertResponseStatusCodeSame(429); + $this->assertResponseHeaderSame('RateLimit-Limit', '30'); + $this->assertResponseHeaderSame('RateLimit-Remaining', '0'); + } + + public function test_rate_limit_per_ip(): void + { + $newsletter = NewsletterFactory::createOne(); + $subdomain = $newsletter->getSubdomain(); + + // Make 30 requests from first IP + for ($i = 0; $i < 30; $i++) { + $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ], clientIp: '192.168.1.1'); + } + + // 31st request from first IP should be rate limited + $response = $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ], clientIp: '192.168.1.1'); + + $this->assertResponseStatusCodeSame(429); + + // Request from different IP should succeed + $response = $this->publicApi('POST', '/form/init', [ + 'newsletter_subdomain' => $subdomain, + ], clientIp: '192.168.1.2'); + + $this->assertResponseStatusCodeSame(200); + $this->assertResponseHeaderSame('RateLimit-Remaining', '29'); + } +} diff --git a/backend/tests/Case/WebTestCase.php b/backend/tests/Case/WebTestCase.php index f1ecf983..5b619c9c 100644 --- a/backend/tests/Case/WebTestCase.php +++ b/backend/tests/Case/WebTestCase.php @@ -193,6 +193,34 @@ public function publicApi( return $this->client->getResponse(); } + /** + * @param array $data + * @param array $headers + */ + public function machineApi( + string $method, + string $uri, + array $data = [], + array $headers = [], + ): Response + { + $server = [ + 'CONTENT_TYPE' => 'application/json', + ]; + + foreach ($headers as $key => $value) { + $server['HTTP_' . strtoupper(str_replace('-', '_', $key))] = $value; + } + + $this->client->request( + $method, + '/api/machine' . $uri, + server: $server, + content: (string)json_encode($data) + ); + return $this->client->getResponse(); + } + /** * @param array $data * @param array $server From d515bb15f2ae5fa6b0770f75612a0a8d0b0b961e Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 21 Nov 2025 12:09:03 +0100 Subject: [PATCH 07/38] Add machine api rate limit test --- .../Integration/Relay/RelayWebhookTest.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/tests/Api/Machine/Integration/Relay/RelayWebhookTest.php b/backend/tests/Api/Machine/Integration/Relay/RelayWebhookTest.php index 4b5190e6..667c1d09 100644 --- a/backend/tests/Api/Machine/Integration/Relay/RelayWebhookTest.php +++ b/backend/tests/Api/Machine/Integration/Relay/RelayWebhookTest.php @@ -324,4 +324,36 @@ public function test_suppression_created(): void $this->assertSame(SubscriberStatus::UNSUBSCRIBED, $subscriber3->getStatus()); $this->assertSame(SubscriberStatus::SUBSCRIBED, $subscriber4->getStatus()); } + + public function test_machine_api_has_no_rate_limits(): void + { + $send = SendFactory::createOne([ + 'status' => SendStatus::PENDING, + 'delivered_at' => null + ]); + + $data = [ + "event" => "send.recipient.accepted", + "payload" => [ + "send" => [ + "headers" => [ + "X-Newsletter-Send-ID" => $send->getId() + ] + ], + "attempt" => [ + "created_at" => 1758221942 + ] + ] + ]; + + // Make 40 requests (more than the public API limit of 30) + for ($i = 0; $i < 40; $i++) { + $response = $this->callWebhook($data); + $this->assertSame(200, $response->getStatusCode()); + + $this->assertFalse($response->headers->has('RateLimit-Limit')); + $this->assertFalse($response->headers->has('RateLimit-Remaining')); + $this->assertFalse($response->headers->has('RateLimit-Reset')); + } + } } From 551cca56bd15b4141ef2bbeaffbd2324773ad1b2 Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 21 Nov 2025 12:15:41 +0100 Subject: [PATCH 08/38] Clean rate limiting class and add clear limiting logic --- backend/config/packages/rate_limiter.php | 13 --- .../Public/Listener/RateLimiterListener.php | 80 ----------------- backend/src/Api/RateLimit/RateLimit.php | 15 ++++ .../src/Api/RateLimit/RateLimitListener.php | 87 +++++++++++++++---- backend/tests/Api/Public/RateLimitTest.php | 8 +- backend/tests/Case/WebTestCase.php | 13 ++- 6 files changed, 100 insertions(+), 116 deletions(-) delete mode 100644 backend/config/packages/rate_limiter.php delete mode 100644 backend/src/Api/Public/Listener/RateLimiterListener.php diff --git a/backend/config/packages/rate_limiter.php b/backend/config/packages/rate_limiter.php deleted file mode 100644 index d6b17532..00000000 --- a/backend/config/packages/rate_limiter.php +++ /dev/null @@ -1,13 +0,0 @@ -rateLimiter() - ->limiter('public_api') - ->policy('fixed_window') - ->limit(30) - ->interval('1 minute'); -}; diff --git a/backend/src/Api/Public/Listener/RateLimiterListener.php b/backend/src/Api/Public/Listener/RateLimiterListener.php deleted file mode 100644 index 96e51cf6..00000000 --- a/backend/src/Api/Public/Listener/RateLimiterListener.php +++ /dev/null @@ -1,80 +0,0 @@ -getPathInfo(), '/api/public'); - } - - public function onKernelRequest(RequestEvent $event): void - { - - $request = $event->getRequest(); - - if (!$this->isPublicApiRequest($request)) { - return; - } - - $limiter = $this->publicApiLimiter->create($request->getClientIp()); - $limit = $limiter->consume(); - - $headers = [ - 'RateLimit-Limit' => $limit->getLimit(), - 'RateLimit-Remaining' => $limit->getRemainingTokens(), - 'RateLimit-Reset' => $limit->getRetryAfter()->getTimestamp() - time(), - ]; - - $request->attributes->set(self::RATE_LIMIT_HEADERS_KEY, $headers); - - if ($limit->isAccepted() === false) { - $response = new Response( - null, - Response::HTTP_TOO_MANY_REQUESTS, - $headers - ); - $event->setResponse($response); - } - - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $response = $event->getResponse(); - - if (!$this->isPublicApiRequest($request)) { - return; - } - - if ($request->attributes->has(self::RATE_LIMIT_HEADERS_KEY)) { - /** @var array $headers */ - $headers = $request->attributes->get(self::RATE_LIMIT_HEADERS_KEY); - foreach ($headers as $key => $value) { - $response->headers->set($key, $value); - } - } - } - -} diff --git a/backend/src/Api/RateLimit/RateLimit.php b/backend/src/Api/RateLimit/RateLimit.php index aad67bff..8d4cd824 100644 --- a/backend/src/Api/RateLimit/RateLimit.php +++ b/backend/src/Api/RateLimit/RateLimit.php @@ -50,6 +50,21 @@ public function apiKey(): array ]; } + /** + * Rate limit for public API per IP. + * 30 per minute per IP + * @return RateLimitConfig + */ + public function publicApi(): array + { + return [ + 'id' => 'public_api', + 'policy' => 'fixed_window', + 'limit' => $this->isDev ? 1000 : 30, + 'interval' => '1 minute', + ]; + } + /** * Rate limit for the POST /subscribers endpoint. * 1 subscribe per email per minute diff --git a/backend/src/Api/RateLimit/RateLimitListener.php b/backend/src/Api/RateLimit/RateLimitListener.php index ae8391ae..d3f5c5be 100644 --- a/backend/src/Api/RateLimit/RateLimitListener.php +++ b/backend/src/Api/RateLimit/RateLimitListener.php @@ -25,13 +25,24 @@ public function __construct( ) { } - private const string RATE_LIMIT_HEADERS_ATTRIBUTE_KEY = 'console_api_rate_limit_headers'; + private const string CONSOLE_RATE_LIMIT_HEADERS_KEY = 'console_api_rate_limit_headers'; + private const string PUBLIC_RATE_LIMIT_HEADERS_KEY = 'public_api_rate_limit_headers'; + + private function isPublicApiRequest(Request $request): bool + { + return str_starts_with($request->getPathInfo(), '/api/public'); + } private function isConsoleApiRequest(Request $request): bool { return str_starts_with($request->getPathInfo(), '/api/console'); } + private function isMachineApiRequest(Request $request): bool + { + return str_starts_with($request->getPathInfo(), '/api/machine'); + } + private function isSubscribePostRequest(Request $request): bool { return $request->getMethod() === 'POST' @@ -79,21 +90,53 @@ public function onController(ControllerEvent $controllerEvent): void $request = $controllerEvent->getRequest(); - // Apply subscribe-specific rate limiting for public form submissions - if ($this->isSubscribePostRequest($request)) { - $this->applySubscribeRateLimit($request); - return; // Skip general rate limiting for this endpoint - } + // Unified rate limiting logic + if ($this->isPublicApiRequest($request)) { + if ($this->isSubscribePostRequest($request)) { + $this->applySubscribeRateLimit($request); + } - if (!$this->isConsoleApiRequest($request)) { + $this->applyPublicApiRateLimit($request); + + } else if ($this->isConsoleApiRequest($request)) { + $this->applyConsoleApiRateLimit($request); + + } else if ($this->isMachineApiRequest($request)) { + // Machine API - no rate limiting return; } + // Other API endpoints - no rate limiting + } + + private function applyPublicApiRateLimit(Request $request): void + { + $limiter = $this->rateLimiterProvider->rateLimiter( + $this->rateLimit->publicApi(), + 'public_ip:' . $request->getClientIp() + ); + $limit = $limiter->consume(); + $resetIn = max($limit->getRetryAfter()->getTimestamp() - time(), 0); + $request->attributes->set(self::PUBLIC_RATE_LIMIT_HEADERS_KEY, [ + 'RateLimit-Limit' => $limit->getLimit(), + 'RateLimit-Remaining' => $limit->getRemainingTokens(), + 'RateLimit-Reset' => $resetIn, + ]); + + if ($limit->isAccepted() === false) { + throw new TooManyRequestsHttpException( + message: 'Rate limit exceeded. Please try again in ' . $resetIn . ' seconds.', + ); + } + } + + private function applyConsoleApiRateLimit(Request $request): void + { $limiter = $this->getRateLimiter($request); $limit = $limiter->consume(); $resetIn = max($limit->getRetryAfter()->getTimestamp() - time(), 0); - $request->attributes->set(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY, [ + $request->attributes->set(self::CONSOLE_RATE_LIMIT_HEADERS_KEY, [ 'X-RateLimit-Limit' => $limit->getLimit(), 'X-RateLimit-Remaining' => $limit->getRemainingTokens(), 'X-RateLimit-Reset' => $resetIn, @@ -142,17 +185,27 @@ public function onResponse(ResponseEvent $responseEvent): void } $request = $responseEvent->getRequest(); - if (!$this->isConsoleApiRequest($request)) { - return; - } - $response = $responseEvent->getResponse(); - if ($request->attributes->has(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY)) { - /** @var array $rateLimitHeaders */ - $rateLimitHeaders = $request->attributes->get(self::RATE_LIMIT_HEADERS_ATTRIBUTE_KEY); - foreach ($rateLimitHeaders as $header => $value) { - $response->headers->set($header, (string)$value); + // Add rate limit headers for Console API + if ($this->isConsoleApiRequest($request)) { + if ($request->attributes->has(self::CONSOLE_RATE_LIMIT_HEADERS_KEY)) { + /** @var array $rateLimitHeaders */ + $rateLimitHeaders = $request->attributes->get(self::CONSOLE_RATE_LIMIT_HEADERS_KEY); + foreach ($rateLimitHeaders as $header => $value) { + $response->headers->set($header, (string)$value); + } + } + } + + // Add rate limit headers for Public API + if ($this->isPublicApiRequest($request)) { + if ($request->attributes->has(self::PUBLIC_RATE_LIMIT_HEADERS_KEY)) { + /** @var array $rateLimitHeaders */ + $rateLimitHeaders = $request->attributes->get(self::PUBLIC_RATE_LIMIT_HEADERS_KEY); + foreach ($rateLimitHeaders as $header => $value) { + $response->headers->set($header, (string)$value); + } } } } diff --git a/backend/tests/Api/Public/RateLimitTest.php b/backend/tests/Api/Public/RateLimitTest.php index 73fd0b66..2f4eb3b2 100644 --- a/backend/tests/Api/Public/RateLimitTest.php +++ b/backend/tests/Api/Public/RateLimitTest.php @@ -2,12 +2,16 @@ namespace App\Tests\Api\Public; -use App\Api\Public\Listener\RateLimiterListener; +use App\Api\RateLimit\RateLimit; +use App\Api\RateLimit\RateLimitListener; +use App\Service\App\RateLimit\RateLimiterProvider; use App\Tests\Case\WebTestCase; use App\Tests\Factory\NewsletterFactory; use PHPUnit\Framework\Attributes\CoversClass; -#[CoversClass(RateLimiterListener::class)] +#[CoversClass(RateLimitListener::class)] +#[CoversClass(RateLimit::class)] +#[CoversClass(RateLimiterProvider::class)] class RateLimitTest extends WebTestCase { public function test_adds_rate_limit_headers(): void diff --git a/backend/tests/Case/WebTestCase.php b/backend/tests/Case/WebTestCase.php index 5b619c9c..26f9faa9 100644 --- a/backend/tests/Case/WebTestCase.php +++ b/backend/tests/Case/WebTestCase.php @@ -170,16 +170,21 @@ public function consoleApi( * @param array $headers */ public function publicApi( - string $method, - string $uri, - array $data = [], - array $headers = [], + string $method, + string $uri, + array $data = [], + array $headers = [], + ?string $clientIp = null, ): Response { $server = [ 'CONTENT_TYPE' => 'application/json', ]; + if ($clientIp !== null) { + $server['REMOTE_ADDR'] = $clientIp; + } + foreach ($headers as $key => $value) { $server['HTTP_' . strtoupper(str_replace('-', '_', $key))] = $value; } From 3f9e50da56f87f94abb7523739ada93e47b48cab Mon Sep 17 00:00:00 2001 From: Supun Wimalasena <44988673+supun-io@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:16:57 +0530 Subject: [PATCH 09/38] fixes #314 (#349) --- backend/src/Entity/Type/MediaFolder.php | 9 +++++-- .../tests/Api/Console/Import/ImportTest.php | 25 +++++++++++++++++++ backend/tests/Service/Import/importsmall.csv | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 backend/tests/Service/Import/importsmall.csv diff --git a/backend/src/Entity/Type/MediaFolder.php b/backend/src/Entity/Type/MediaFolder.php index 63960364..15fd2f78 100644 --- a/backend/src/Entity/Type/MediaFolder.php +++ b/backend/src/Entity/Type/MediaFolder.php @@ -14,7 +14,10 @@ enum MediaFolder: string case EXPORT = 'export'; /** - * @return string[] + * See https://symfony.com/doc/current/reference/constraints/File.html#extensions + * We can explicitly set mime types for specific extensions if needed. + * + * @return array */ public function getAllowedExtensions(): array { @@ -22,7 +25,9 @@ public function getAllowedExtensions(): array return match ($this) { self::ISSUE_IMAGES, self::NEWSLETTER_IMAGES => $imageExtensions, - self::IMPORT, self::EXPORT => ['csv'], + self::IMPORT, self::EXPORT => [ + 'csv' => ['text/csv', 'text/plain'] + ], }; } diff --git a/backend/tests/Api/Console/Import/ImportTest.php b/backend/tests/Api/Console/Import/ImportTest.php index bac2db26..31a431ab 100644 --- a/backend/tests/Api/Console/Import/ImportTest.php +++ b/backend/tests/Api/Console/Import/ImportTest.php @@ -233,4 +233,29 @@ public function test_monthly_import_limit(): void $this->assertNotFalse($content); $this->assertStringContainsString('Monthly import limit reached.', $content); } + + + public function test_import_upload_small(): void + { + $newsletter = NewsletterFactory::createOne(); + + $file = new UploadedFile( + dirname(__DIR__, 3) . '/Service/Import/importsmall.csv', + 'import.csv', + ); + + $response = $this->consoleApi( + $newsletter, + 'POST', + '/imports/upload', + files: [ + 'file' => $file + ], + parameters: [ + 'source' => 'test' + ] + ); + + $this->assertSame(200, $response->getStatusCode()); + } } diff --git a/backend/tests/Service/Import/importsmall.csv b/backend/tests/Service/Import/importsmall.csv new file mode 100644 index 00000000..7f7d4f5e --- /dev/null +++ b/backend/tests/Service/Import/importsmall.csv @@ -0,0 +1 @@ +john@hyvor.com,John \ No newline at end of file From 213fa1b757c8ea616ec19372922d5842cbbb73f5 Mon Sep 17 00:00:00 2001 From: Nadil Karunarathna <113877141+Nadil-K@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:19:22 +0530 Subject: [PATCH 10/38] Adds brand url to sending profiles (#345) * add brand-url backend updates - wip #339 * add brand-url frontend updates - fixes #339 * brand-url at template variables * php test fixes * prettier fix --- backend/migrations/Version20251118102335.php | 7 +++++++ .../Controller/SendingProfileController.php | 7 ++++++- .../SendingProfile/CreateSendingProfileInput.php | 4 +++- .../SendingProfile/UpdateSendingProfileInput.php | 6 ++++-- .../Api/Console/Object/SendingProfileObject.php | 2 ++ backend/src/Entity/SendingProfile.php | 15 +++++++++++++++ .../Dto/UpdateSendingProfileDto.php | 1 + .../SendingProfile/SendingProfileService.php | 12 +++++++++--- .../Service/Template/TemplateVariableService.php | 1 + .../SendingProfile/CreateSendingProfileTest.php | 4 ++++ .../SendingProfile/UpdateSendingProfileTest.php | 3 +++ backend/tests/Factory/SendingProfileFactory.php | 3 ++- .../AddEditSendingProfileModal.svelte | 16 ++++++++++++---- .../sending-profiles/SendingProfileRow.svelte | 11 +++++++++++ .../console/lib/actions/sendingProfileActions.ts | 4 ++++ frontend/src/routes/console/types.ts | 1 + shared/locale/en.json | 3 ++- 17 files changed, 87 insertions(+), 13 deletions(-) diff --git a/backend/migrations/Version20251118102335.php b/backend/migrations/Version20251118102335.php index a2a978a8..b7e75758 100644 --- a/backend/migrations/Version20251118102335.php +++ b/backend/migrations/Version20251118102335.php @@ -44,6 +44,13 @@ public function up(Schema $schema): void ALTER COLUMN private_note TYPE TEXT; SQL ); + + $this->addSql( + <<from_name, $input->reply_to_email, $input->brand_name, - $input->brand_logo + $input->brand_logo, + $input->brand_url ); return $this->json(new SendingProfileObject($sendingProfile)); @@ -104,6 +105,10 @@ public function updateSendingProfile( $updates->brandLogo = $input->brand_logo; } + if ($input->hasProperty('brand_url')) { + $updates->brandUrl = $input->brand_url; + } + if ($input->hasProperty('is_default')) { $updates->isDefault = $input->is_default; } diff --git a/backend/src/Api/Console/Input/SendingProfile/CreateSendingProfileInput.php b/backend/src/Api/Console/Input/SendingProfile/CreateSendingProfileInput.php index 9bb02f50..0d80b50d 100644 --- a/backend/src/Api/Console/Input/SendingProfile/CreateSendingProfileInput.php +++ b/backend/src/Api/Console/Input/SendingProfile/CreateSendingProfileInput.php @@ -24,7 +24,9 @@ class CreateSendingProfileInput #[Assert\Type('string')] public ?string $brand_name; - #[Assert\Length(max: 1024)] #[Assert\Url] public ?string $brand_logo; + + #[Assert\Url] + public ?string $brand_url; } diff --git a/backend/src/Api/Console/Input/SendingProfile/UpdateSendingProfileInput.php b/backend/src/Api/Console/Input/SendingProfile/UpdateSendingProfileInput.php index 4bf6365d..9c5c4504 100644 --- a/backend/src/Api/Console/Input/SendingProfile/UpdateSendingProfileInput.php +++ b/backend/src/Api/Console/Input/SendingProfile/UpdateSendingProfileInput.php @@ -26,10 +26,12 @@ class UpdateSendingProfileInput #[Assert\Type('string')] public ?string $brand_name; - #[Assert\Length(max: 255)] - #[Assert\Type('string')] + #[Assert\Url] public ?string $brand_logo; + #[Assert\Url] + public ?string $brand_url; + #[Assert\IsTrue] public bool $is_default; } diff --git a/backend/src/Api/Console/Object/SendingProfileObject.php b/backend/src/Api/Console/Object/SendingProfileObject.php index b7aac7df..a016e382 100644 --- a/backend/src/Api/Console/Object/SendingProfileObject.php +++ b/backend/src/Api/Console/Object/SendingProfileObject.php @@ -14,6 +14,7 @@ class SendingProfileObject public ?string $reply_to_email; public ?string $brand_name; public ?string $brand_logo; + public ?string $brand_url; public bool $is_default; public bool $is_system; @@ -28,5 +29,6 @@ public function __construct(SendingProfile $sendingProfile) $this->reply_to_email = $sendingProfile->getReplyToEmail(); $this->brand_name = $sendingProfile->getBrandName(); $this->brand_logo = $sendingProfile->getBrandLogo(); + $this->brand_url = $sendingProfile->getBrandUrl(); } } diff --git a/backend/src/Entity/SendingProfile.php b/backend/src/Entity/SendingProfile.php index 816a42b7..02e834aa 100644 --- a/backend/src/Entity/SendingProfile.php +++ b/backend/src/Entity/SendingProfile.php @@ -41,6 +41,9 @@ class SendingProfile #[ORM\Column] private ?string $brand_logo; + #[ORM\Column] + private ?string $brand_url; + #[ORM\Column] private bool $is_default = false; @@ -167,6 +170,18 @@ public function setBrandLogo(?string $brandLogo): static return $this; } + public function getBrandUrl(): ?string + { + return $this->brand_url ?? null; + } + + public function setBrandUrl(?string $brandUrl): static + { + $this->brand_url = $brandUrl; + + return $this; + } + public function getIsDefault(): bool { return $this->is_default; diff --git a/backend/src/Service/SendingProfile/Dto/UpdateSendingProfileDto.php b/backend/src/Service/SendingProfile/Dto/UpdateSendingProfileDto.php index 3d292ee8..0138f559 100644 --- a/backend/src/Service/SendingProfile/Dto/UpdateSendingProfileDto.php +++ b/backend/src/Service/SendingProfile/Dto/UpdateSendingProfileDto.php @@ -14,6 +14,7 @@ class UpdateSendingProfileDto public ?string $replyToEmail; public ?string $brandName; public ?string $brandLogo; + public ?string $brandUrl; public Domain $customDomain; public bool $isDefault; } diff --git a/backend/src/Service/SendingProfile/SendingProfileService.php b/backend/src/Service/SendingProfile/SendingProfileService.php index e3baf16a..f670aaf4 100644 --- a/backend/src/Service/SendingProfile/SendingProfileService.php +++ b/backend/src/Service/SendingProfile/SendingProfileService.php @@ -48,14 +48,15 @@ public function getSendingProfileOfNewsletterById(Newsletter $newsletter, int $i public function createSendingProfile( Newsletter $newsletter, - ?Domain $customDomain, + ?Domain $customDomain, string $fromEmail, ?string $fromName = null, ?string $replyToEmail = null, ?string $brandName = null, ?string $brandLogo = null, - bool $system = false, - bool $flush = true, + ?string $brandUrl = null, + bool $system = false, + bool $flush = true, ): SendingProfile { $sendingProfile = new SendingProfile(); @@ -68,6 +69,7 @@ public function createSendingProfile( $sendingProfile->setReplyToEmail($replyToEmail); $sendingProfile->setBrandName($brandName); $sendingProfile->setBrandLogo($brandLogo); + $sendingProfile->setBrandUrl($brandUrl); $sendingProfile->setIsDefault($this->getSendingProfilesCount($newsletter) === 0); $sendingProfile->setIsSystem($system); @@ -104,6 +106,10 @@ public function updateSendingProfile( $sendingProfile->setBrandLogo($updates->brandLogo); } + if ($updates->hasProperty('brandUrl')) { + $sendingProfile->setBrandUrl($updates->brandUrl); + } + if ($updates->hasProperty('customDomain')) { $sendingProfile->setDomain($updates->customDomain); } diff --git a/backend/src/Service/Template/TemplateVariableService.php b/backend/src/Service/Template/TemplateVariableService.php index f7814d9a..9a840693 100644 --- a/backend/src/Service/Template/TemplateVariableService.php +++ b/backend/src/Service/Template/TemplateVariableService.php @@ -98,6 +98,7 @@ private function setVariablesFromSendingProfile(TemplateVariables $variables, Se $variables->name = $sendingProfile->getFromName() ?: $variables->name; $variables->brand_logo = $sendingProfile->getBrandLogo() ?: $variables->brand_logo; $variables->brand_logo_alt = $sendingProfile->getBrandName() ?: $variables->brand_logo_alt; + $variables->brand_url = $sendingProfile->getBrandUrl() ?: $variables->brand_url; return $variables; } diff --git a/backend/tests/Api/Console/SendingProfile/CreateSendingProfileTest.php b/backend/tests/Api/Console/SendingProfile/CreateSendingProfileTest.php index a44122ff..bf246e5b 100644 --- a/backend/tests/Api/Console/SendingProfile/CreateSendingProfileTest.php +++ b/backend/tests/Api/Console/SendingProfile/CreateSendingProfileTest.php @@ -39,6 +39,7 @@ public function test_create_sending_profile(): void 'reply_to_email' => null, 'brand_name' => null, 'brand_logo' => null, + 'brand_url' => null ], ); @@ -81,6 +82,7 @@ public function test_it_does_not_make_it_default_when_there_is_already_one(): vo 'reply_to_email' => null, 'brand_name' => null, 'brand_logo' => null, + 'brand_url' => null ], ); @@ -107,6 +109,7 @@ public function test_create_sending_profile_domain_not_found(): void 'reply_to_email' => null, 'brand_name' => null, 'brand_logo' => null, + 'brand_url' => null ], ); $this->assertSame(400, $response->getStatusCode()); @@ -133,6 +136,7 @@ public function test_create_sending_profile_domain_not_verified(): void 'reply_to_email' => null, 'brand_name' => null, 'brand_logo' => null, + 'brand_url' => null ], ); $this->assertSame(400, $response->getStatusCode()); diff --git a/backend/tests/Api/Console/SendingProfile/UpdateSendingProfileTest.php b/backend/tests/Api/Console/SendingProfile/UpdateSendingProfileTest.php index 3f4d7730..eb61b6e0 100644 --- a/backend/tests/Api/Console/SendingProfile/UpdateSendingProfileTest.php +++ b/backend/tests/Api/Console/SendingProfile/UpdateSendingProfileTest.php @@ -57,6 +57,7 @@ public function test_update_sending_profile(): void [ 'from_email' => 'thibault@gmail.com', 'brand_name' => 'Hyvor Post', + 'brand_url' => 'https://post.hyvor.com' ] ); @@ -64,12 +65,14 @@ public function test_update_sending_profile(): void $json = $this->getJson(); $this->assertSame('thibault@gmail.com', $json['from_email']); $this->assertSame('Hyvor Post', $json['brand_name']); + $this->assertSame('https://post.hyvor.com', $json['brand_url']); $this->assertSame(false, $json['is_default']); $sendingEmail = $this->em->getRepository(SendingProfile::class)->findOneBy(['id' => $json['id']]); $this->assertInstanceOf(SendingProfile::class, $sendingEmail); $this->assertSame('thibault@gmail.com', $sendingEmail->getFromEmail()); $this->assertSame('Hyvor Post', $sendingEmail->getBrandName()); + $this->assertSame('https://post.hyvor.com', $sendingEmail->getBrandUrl()); $this->assertNotNull($sendingEmail->getDomain()); $this->assertSame($domain2->getId(), $sendingEmail->getDomain()->getId()); $this->assertSame(false, $sendingEmail->getIsDefault()); diff --git a/backend/tests/Factory/SendingProfileFactory.php b/backend/tests/Factory/SendingProfileFactory.php index 799ae9af..0c128019 100644 --- a/backend/tests/Factory/SendingProfileFactory.php +++ b/backend/tests/Factory/SendingProfileFactory.php @@ -42,7 +42,8 @@ protected function defaults(): array 'from_email' => self::faker()->email(), 'reply_to_email' => self::faker()->email(), 'brand_name' => self::faker()->company(), - 'brand_logo' => "https://picsum.photos/200" + 'brand_logo' => "https://picsum.photos/200", + 'brand_url' => self::faker()->url(), ]; } diff --git a/frontend/src/routes/console/(nav)/[subdomain]/settings/sending-profiles/AddEditSendingProfileModal.svelte b/frontend/src/routes/console/(nav)/[subdomain]/settings/sending-profiles/AddEditSendingProfileModal.svelte index c7699bd3..b0e9e971 100644 --- a/frontend/src/routes/console/(nav)/[subdomain]/settings/sending-profiles/AddEditSendingProfileModal.svelte +++ b/frontend/src/routes/console/(nav)/[subdomain]/settings/sending-profiles/AddEditSendingProfileModal.svelte @@ -1,5 +1,5 @@ @@ -37,14 +35,14 @@
- +
- +
- Main Graphic + Main Graphic
diff --git a/frontend/src/routes/(marketing)/@components/ArchiveSite.svelte b/frontend/src/routes/(marketing)/@components/ArchiveSite.svelte index effa1e4f..eda40a60 100644 --- a/frontend/src/routes/(marketing)/@components/ArchiveSite.svelte +++ b/frontend/src/routes/(marketing)/@components/ArchiveSite.svelte @@ -1,6 +1,4 @@ diff --git a/frontend/static/img/automation.svg b/frontend/static/img/automation.svg deleted file mode 100644 index 0470852c..00000000 --- a/frontend/static/img/automation.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/static/img/comprehensive-analytics.svg b/frontend/static/img/comprehensive-analytics.svg deleted file mode 100644 index 431a2138..00000000 --- a/frontend/static/img/comprehensive-analytics.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/static/img/custom-domain.svg b/frontend/static/img/custom-domain.svg deleted file mode 100644 index fdf7a605..00000000 --- a/frontend/static/img/custom-domain.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/static/img/custom-templates.svg b/frontend/static/img/custom-templates.svg deleted file mode 100644 index c890d7d1..00000000 --- a/frontend/static/img/custom-templates.svg +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/static/img/easy-install.svg b/frontend/static/img/easy-install.svg deleted file mode 100755 index 698ca652..00000000 --- a/frontend/static/img/easy-install.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/static/img/embed-newsletter-form.svg b/frontend/static/img/embed-newsletter-form.svg deleted file mode 100644 index 65d64491..00000000 --- a/frontend/static/img/embed-newsletter-form.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/static/img/api-webhooks.svg b/frontend/static/img/home/api-webhooks.svg similarity index 100% rename from frontend/static/img/api-webhooks.svg rename to frontend/static/img/home/api-webhooks.svg diff --git a/frontend/src/routes/(marketing)/img/archive.png b/frontend/static/img/home/archive.png similarity index 100% rename from frontend/src/routes/(marketing)/img/archive.png rename to frontend/static/img/home/archive.png diff --git a/frontend/src/routes/(marketing)/img/console.png b/frontend/static/img/home/console.png similarity index 100% rename from frontend/src/routes/(marketing)/img/console.png rename to frontend/static/img/home/console.png diff --git a/frontend/src/routes/(marketing)/img/custom-email.png b/frontend/static/img/home/custom-email.png similarity index 100% rename from frontend/src/routes/(marketing)/img/custom-email.png rename to frontend/static/img/home/custom-email.png diff --git a/frontend/src/routes/(marketing)/img/custom-email1.png b/frontend/static/img/home/custom-email1.png similarity index 100% rename from frontend/src/routes/(marketing)/img/custom-email1.png rename to frontend/static/img/home/custom-email1.png diff --git a/frontend/src/routes/(marketing)/img/email.png b/frontend/static/img/home/email.png similarity index 100% rename from frontend/src/routes/(marketing)/img/email.png rename to frontend/static/img/home/email.png diff --git a/frontend/src/routes/(marketing)/img/form.png b/frontend/static/img/home/form.png similarity index 100% rename from frontend/src/routes/(marketing)/img/form.png rename to frontend/static/img/home/form.png diff --git a/frontend/static/img/main-graphic.svg b/frontend/static/img/home/main-graphic.svg similarity index 100% rename from frontend/static/img/main-graphic.svg rename to frontend/static/img/home/main-graphic.svg diff --git a/frontend/static/img/migration.svg b/frontend/static/img/home/migration.svg similarity index 100% rename from frontend/static/img/migration.svg rename to frontend/static/img/home/migration.svg diff --git a/frontend/static/img/multiple-segments.svg b/frontend/static/img/home/multiple-segments.svg similarity index 100% rename from frontend/static/img/multiple-segments.svg rename to frontend/static/img/home/multiple-segments.svg diff --git a/frontend/src/routes/(marketing)/img/pp.svg b/frontend/static/img/home/pp.svg similarity index 100% rename from frontend/src/routes/(marketing)/img/pp.svg rename to frontend/static/img/home/pp.svg diff --git a/frontend/src/routes/(marketing)/img/signup.png b/frontend/static/img/home/signup.png similarity index 100% rename from frontend/src/routes/(marketing)/img/signup.png rename to frontend/static/img/home/signup.png diff --git a/frontend/static/img/team-collaboration.svg b/frontend/static/img/home/team-collaboration.svg similarity index 100% rename from frontend/static/img/team-collaboration.svg rename to frontend/static/img/home/team-collaboration.svg From 0995dd4c52f61b81b7ca970fd59ec8dcafa971ad Mon Sep 17 00:00:00 2001 From: Nadil Karunarathna <113877141+Nadil-K@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:08:18 +0530 Subject: [PATCH 19/38] Console API docs (#359) * wip * complets console api docs - fixes #344 * prettier --- .../Console/Controller/TemplateController.php | 4 - .../Console/Object/SubscriberExportObject.php | 9 +- .../Api/Console/Object/SubscriberObject.php | 10 +- .../docs/[...slug]/content/ConsoleApi.svelte | 1084 +++++++++++++++++ .../routes/(marketing)/docs/[...slug]/docs.ts | 32 +- 5 files changed, 1111 insertions(+), 28 deletions(-) create mode 100644 frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte diff --git a/backend/src/Api/Console/Controller/TemplateController.php b/backend/src/Api/Console/Controller/TemplateController.php index 86d51af5..2cb8b625 100644 --- a/backend/src/Api/Console/Controller/TemplateController.php +++ b/backend/src/Api/Console/Controller/TemplateController.php @@ -7,15 +7,11 @@ use App\Api\Console\Input\Template\UpdateTemplateInput; use App\Api\Console\Input\Template\RenderTemplateInput; use App\Api\Console\Object\TemplateObject; -use App\Entity\Issue; use App\Entity\Newsletter; use App\Service\Content\ContentDefaultStyle; -use App\Service\Content\ContentService; -use App\Service\Newsletter\NewsletterDefaults; use App\Service\Template\Dto\UpdateTemplateDto; use App\Service\Template\HtmlTemplateRenderer; use App\Service\Template\TemplateService; -use App\Service\Template\TemplateVariables; use App\Service\Template\TemplateVariableService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; diff --git a/backend/src/Api/Console/Object/SubscriberExportObject.php b/backend/src/Api/Console/Object/SubscriberExportObject.php index b0b2db2d..a975dbc7 100644 --- a/backend/src/Api/Console/Object/SubscriberExportObject.php +++ b/backend/src/Api/Console/Object/SubscriberExportObject.php @@ -3,21 +3,22 @@ namespace App\Api\Console\Object; use App\Entity\SubscriberExport; +use App\Entity\Type\SubscriberExportStatus; class SubscriberExportObject { public int $id; - public string $status; + public int $created_at; + public SubscriberExportStatus $status; public ?string $error_message; public ?string $url; - public int $created_at; public function __construct(SubscriberExport $export, ?string $mediaUrl) { $this->id = $export->getId(); - $this->status = $export->getStatus()->value; + $this->created_at = $export->getCreatedAt()->getTimestamp(); + $this->status = $export->getStatus(); $this->error_message = $export->getErrorMessage(); $this->url = $mediaUrl; - $this->created_at = $export->getCreatedAt()->getTimestamp(); } } \ No newline at end of file diff --git a/backend/src/Api/Console/Object/SubscriberObject.php b/backend/src/Api/Console/Object/SubscriberObject.php index 237fa71a..daba7352 100644 --- a/backend/src/Api/Console/Object/SubscriberObject.php +++ b/backend/src/Api/Console/Object/SubscriberObject.php @@ -3,14 +3,16 @@ namespace App\Api\Console\Object; use App\Entity\Subscriber; +use App\Entity\Type\SubscriberSource; +use App\Entity\Type\SubscriberStatus; class SubscriberObject { public int $id; public string $email; - public string $source; - public string $status; + public SubscriberSource $source; + public SubscriberStatus $status; /** * @var array */ @@ -29,8 +31,8 @@ public function __construct(Subscriber $subscriber) { $this->id = $subscriber->getId(); $this->email = $subscriber->getEmail(); - $this->source = $subscriber->getSource()->value; - $this->status = $subscriber->getStatus()->value; + $this->source = $subscriber->getSource(); + $this->status = $subscriber->getStatus(); $this->list_ids = array_values($subscriber->getLists()->map(fn($list) => $list->getId())->toArray()); $this->subscribe_ip = $subscriber->getSubscribeIp(); $this->is_opted_in = $subscriber->getOptInAt() !== null; diff --git a/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte b/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte new file mode 100644 index 00000000..6ea9428a --- /dev/null +++ b/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte @@ -0,0 +1,1084 @@ + + +

Console API

+ +

+ The Console API allows you to automate your newsletter-related tasks over HTTP with API key + authentication. This is the same API that we internally use at the Console. +

+ +

Getting Started

+ +
    +
  • + Create a Console API key at Console → Settings → API Keys. +
  • +
  • The base URL: https://post.hyvor.com/api/console
  • +
  • + For each request, set Authorization header to + Bearer {''}. +
  • +
  • Available HTTP methods:
  • +
      +
    • GET - Retrieve a resource
    • +
    • POST - Create a resource or perform an action
    • +
    • PUT - Update a resource
    • +
    • DELETE - Remove a resource
    • +
    +
  • + Request params can be set as JSON (recommended) or as + application/x-www-form-urlencoded. +
  • +
  • All endpoints return JSON data. The response will be an object or an array of objects.
  • +
+ + +

+ In this documentation, all objects, request params, and responses are written as Typescript interfaces in order to make type declarations concise. +

+
+ +

Categories

+ +

The Console API endpoints are categorized based on the resource they interact with.

+ +

Jump to each category:

+ + + + + +

Newsletter

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Get newsletter data

+ +GET /newsletter + + + +

Update a newsletter

+ +PATCH /newsletter + + // except id, created_at + type Response = Newsletter + `} +/> + +

Issue

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Get issues

+ +GET /issues + + + +

Create an issue

+ +POST /issues + + + +

Get an issue

+ +GET /issues/{'{id}'} + + + +

Update an issue

+ +PATCH /issues/{'{id}'} + + + +

Delete an issue

+ +DELETE /issues/{'{id}'} + + + +

Send an issue

+ +POST /issues/{'{id}'}/send + + + +

Get issue sends

+ +GET /issues/{'{id}'}/sends + + + +

Lists

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Create a list

+ +POST /lists + + + +

Update a list

+ +PATCH /lists/{'{id}'} + + + +

Delete a list

+ +DELETE /lists/{'{id}'} + + + +

Subscriber

+ +

Endpoints:

+ + + +

Objects:

+ + +

Get subscribers

+ +GET /subscribers + + + +

Create a subscriber

+ +POST /subscribers + + + +

Update a subscriber

+ +PATCH /subscribers/{'{id}'} + +; + } + type Response = Subscriber + `} +/> + +

Delete a subscriber

+ +DELETE /subscribers/{'{id}'} + + + +

Bulk update subscribers

+ +POST /subscribers/bulk + +; // required if action is metadata_update + } + type Response = { + status: string; + message: string; + subscribers: Subscriber[]; + } + `} +/> + +

Subscriber Metadata

+ +

+ Subscriber metadata definitions allow you to define custom fields for subscribers. These fields + can be used to store additional information about subscribers. +

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Create a subscriber metadata definition

+ +POST /subscriber-metadata-definitions + + + + +
    +
  • key can only contain lowercase letters, numbers, and underscores.
  • +
  • Once created, the key cannot be changed.
  • +
+
+ +

Update a subscriber metadata definition

+ +PATCH /subscriber-metadata-definitions/{'{id}'} + + + +

Delete a subscriber metadata definition

+ +DELETE /subscriber-metadata-definitions/{'{id}'} + + + +

Sending Profile

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Get sending profiles

+ +GET /sending-profiles + + + +

Create a sending profile

+ +POST /sending-profiles + + + +

Update a sending profile

+ +PATCH /sending-profiles/{'{id}'} + + + +

Delete a sending profile

+ +DELETE /sending-profiles/{'{id}'} + + + +

Template

+ +Hyvor Post provides a flexible newsletter template system that allows you to customize the +appearance of your newsletters. + +

Endpoints:

+ + + +

Objects:

+ + + +

Get newsletter template

+ +GET /templates + + + +

Update newsletter template

+ +POST /templates/update + + + +

Render newsletter template with content

+ +POST /templates/render + + + +

User

+ +

+ The owner of the newsletter can invite other users as Admins to collaborate on managing the + newsletter. +

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Get user

+ +GET /users + + + +

Delete user

+ +DELETE /users/{'{id}'} + + + +

Get invites

+ +GET /invites + + + +

Create an invite

+ +POST /invites + +

+ You must ask your Admins to create a HYVOR account before sending an invitation. +

+ + + + +
    +
  • + Either username or email of the invitee's HYVOR account is required. +
  • +
+
+ +

Delete an invite

+ +DELETE /invites/{'{id}'} + + + +

Media

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Upload media

+ +POST /media + + + +

Export

+ +

Endpoints:

+ + + +

Objects:

+ + + +

Get subscriber exports

+ +GET /export + + + +

Create a subscriber export

+ +POST /export + + + + + +

Objects

+ +

Newsletter Object

+ + + +

Issue Object

+ + + +

Send Object

+ + + +

List Object

+ + + +

Subscriber Object

+ +; + } + `} +/> + +

Subscriber Metadata Definition Object

+ + + +

Sending Profile Object

+ + + +

Template Object

+ + + +

User Mini Object

+ + + +

User Object

+ + + +

User Invite Object

+ + + +

Media Object

+ + + +

Subscriber Export Object

+ + diff --git a/frontend/src/routes/(marketing)/docs/[...slug]/docs.ts b/frontend/src/routes/(marketing)/docs/[...slug]/docs.ts index c598544b..99efe173 100644 --- a/frontend/src/routes/(marketing)/docs/[...slug]/docs.ts +++ b/frontend/src/routes/(marketing)/docs/[...slug]/docs.ts @@ -4,6 +4,7 @@ import type { Component } from 'svelte'; import SignupForm from './content/SignupForm.svelte'; import Import from './content/Import/Import.svelte'; import Approval from './content/Approval.svelte'; +import ConsoleApi from './content/ConsoleApi.svelte'; export const categories: Category[] = [ { @@ -41,23 +42,22 @@ export const categories: Category[] = [ component: Import } ] + }, + { + name: 'Developer', + pages: [ + // { + // slug: 'webhooks', + // name: 'Webhooks' + // // component: add component name + // }, + { + slug: 'api-console', + name: 'Console API', + component: ConsoleApi + } + ] } - - // { - // name: 'Developer', - // pages: [ - // { - // slug: 'webhooks', - // name: 'Webhooks' - // // component: add component name - // }, - // { - // slug: 'api-console', - // name: 'Console API' - // // component: add component name - // } - //] - //}, ]; export const pages = categories.reduce((acc, category) => acc.concat(category.pages), [] as Page[]); From 9416006fa9be43ddfa68959b1a4cd37b974e47c8 Mon Sep 17 00:00:00 2001 From: Nadil Karunarathna <113877141+Nadil-K@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:32:42 +0530 Subject: [PATCH 20/38] Final fixes before release (#360) * fixes #353 * fixes #355 * fixes #358 * prettier --- .../Console/Controller/IssueController.php | 2 +- .../Console/Controller/TemplateController.php | 2 +- .../Sudo/Controller/ApprovalController.php | 2 +- .../src/Service/Approval/ApprovalService.php | 6 +- .../Console/Issue/GetTestIssueDataTest.php | 61 +++++++++++++++++++ .../Console/Template/UpdateTemplateTest.php | 12 ++-- .../docs/[...slug]/content/ConsoleApi.svelte | 4 +- .../design/lib/actions/templateActions.ts | 4 +- 8 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 backend/tests/Api/Console/Issue/GetTestIssueDataTest.php diff --git a/backend/src/Api/Console/Controller/IssueController.php b/backend/src/Api/Console/Controller/IssueController.php index 39ac7a53..e138dca7 100644 --- a/backend/src/Api/Console/Controller/IssueController.php +++ b/backend/src/Api/Console/Controller/IssueController.php @@ -210,7 +210,7 @@ public function getTestData(Issue $issue): JsonResponse $newsletterUserEmails = array_map(fn($authUser) => $authUser->email, $this->authService->fromIds($newsletterUserIds)); $testSentEmails = $newsletter->getTestSentEmails() ?? []; - $suggestedEmails = array_merge($newsletterUserEmails, $testSentEmails); + $suggestedEmails = array_unique(array_merge($newsletterUserEmails, $testSentEmails)); return $this->json([ 'verified_domains' => array_map(fn($domain) => $domain->getDomain(), $verifiedDomains), diff --git a/backend/src/Api/Console/Controller/TemplateController.php b/backend/src/Api/Console/Controller/TemplateController.php index 2cb8b625..3d6a735d 100644 --- a/backend/src/Api/Console/Controller/TemplateController.php +++ b/backend/src/Api/Console/Controller/TemplateController.php @@ -45,7 +45,7 @@ public function getNewsletterTemplate(Newsletter $newsletter): JsonResponse return $this->json(new TemplateObject($template)); } - #[Route('/templates/update', methods: 'POST')] + #[Route('/templates', methods: 'PATCH')] #[ScopeRequired(Scope::TEMPLATES_WRITE)] public function updateTemplate( Newsletter $newsletter, diff --git a/backend/src/Api/Sudo/Controller/ApprovalController.php b/backend/src/Api/Sudo/Controller/ApprovalController.php index 428c439b..a349a856 100644 --- a/backend/src/Api/Sudo/Controller/ApprovalController.php +++ b/backend/src/Api/Sudo/Controller/ApprovalController.php @@ -43,7 +43,7 @@ public function getApprovals( #[Route('/approvals/{id}', methods: ['POST'])] public function approve(Approval $approval, #[MapRequestPayload] ApproveInput $input): JsonResponse { - $approval = $this->approvalService->changeStatus( + $approval = $this->approvalService->approvalSudoAction( $approval, $input->status, $input->public_note, diff --git a/backend/src/Service/Approval/ApprovalService.php b/backend/src/Service/Approval/ApprovalService.php index 22407ecb..5c84b75c 100644 --- a/backend/src/Service/Approval/ApprovalService.php +++ b/backend/src/Service/Approval/ApprovalService.php @@ -206,7 +206,7 @@ public function updateApproval( return $approval; } - public function changeStatus( + public function approvalSudoAction( Approval $approval, ApprovalStatus $status, ?string $public_note, @@ -230,7 +230,9 @@ public function changeStatus( throw new HttpException(422, "User not found"); } - $this->sendApprovalMail($approval, $status, $user); + if ($status === ApprovalStatus::APPROVED || $status === ApprovalStatus::REJECTED) { + $this->sendApprovalMail($approval, $status, $user); + } $this->em->persist($approval); $this->em->flush(); diff --git a/backend/tests/Api/Console/Issue/GetTestIssueDataTest.php b/backend/tests/Api/Console/Issue/GetTestIssueDataTest.php new file mode 100644 index 00000000..69cb0c1f --- /dev/null +++ b/backend/tests/Api/Console/Issue/GetTestIssueDataTest.php @@ -0,0 +1,61 @@ + 15, + 'username' => 'nadil', + 'name' => 'Nadil Karunarathna', + 'email' => 'nadil@hyvor.com' + ]); + + $newsletter = NewsletterFactory::createOne([ + 'test_sent_emails' => [ + 'nadil@hyvor.com', + 'supun@hyvor.com' + ] + ]); + $issue = IssueFactory::createOne( + [ + 'newsletter' => $newsletter, + 'subject' => 'Test subject', + 'content' => 'Test content', + 'status' => IssueStatus::DRAFT + ] + ); + + UserFactory::createOne([ + 'newsletter' => $newsletter, + 'hyvor_user_id' => 15, + 'role' => UserRole::OWNER + ]); + + $response = $this->consoleApi( + $newsletter, + 'GET', + "/issues/" . $issue->getId() . "/test", + ); + + $this->assertSame(200, $response->getStatusCode()); + $data = $response->getContent(); + $this->assertNotFalse($data); + $json = json_decode($data, true); + $this->assertIsArray($json); + $this->assertIsArray($json['suggested_emails']); + $this->assertCount(2, $json['suggested_emails']); + $this->assertIsArray($json['test_sent_emails']); + $this->assertCount(2, $json['test_sent_emails']); + } +} \ No newline at end of file diff --git a/backend/tests/Api/Console/Template/UpdateTemplateTest.php b/backend/tests/Api/Console/Template/UpdateTemplateTest.php index d385973e..ec698549 100644 --- a/backend/tests/Api/Console/Template/UpdateTemplateTest.php +++ b/backend/tests/Api/Console/Template/UpdateTemplateTest.php @@ -23,8 +23,8 @@ public function test_create_template_valid(): void $response = $this->consoleApi( $newsletter, - 'POST', - '/templates/update', + 'PATCH', + '/templates', ); $this->assertSame(200, $response->getStatusCode()); @@ -51,8 +51,8 @@ public function test_update_template(): void $response = $this->consoleApi( $newsletter, - 'POST', - '/templates/update', + 'PATCH', + '/templates', [ 'template' => ' @@ -98,8 +98,8 @@ public function test_restore_default_template(): void $response = $this->consoleApi( $newsletter, - 'POST', - '/templates/update', + 'PATCH', + '/templates', [ 'template' => null ] diff --git a/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte b/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte index 6ea9428a..d85aef37 100644 --- a/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte +++ b/frontend/src/routes/(marketing)/docs/[...slug]/content/ConsoleApi.svelte @@ -578,7 +578,7 @@ appearance of your newsletters.
  • GET /templates - Get newsletter template
  • - POST /templates/update - Update newsletter template + PATCH /templates - Update newsletter template
  • POST /templates/render - Render newsletter template @@ -606,7 +606,7 @@ appearance of your newsletters.

    Update newsletter template

    -POST /templates/update +PATCH /templates ({ - endpoint: 'templates/update', + return consoleApi.patch({ + endpoint: 'templates', data: { template } From 92b548c1ce79571805076e1adeec232003b0fb90 Mon Sep 17 00:00:00 2001 From: Ishini Senanayake <61535113+IshiniAvindya@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:32:51 +0100 Subject: [PATCH 21/38] hds version updated (#367) * hds version updated hds version updated * updated package-lock.json --- frontend/package-lock.json | 361 +++++++++++++++++- frontend/package.json | 2 +- frontend/src/routes/(marketing)/Header.svelte | 2 +- 3 files changed, 349 insertions(+), 16 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d7dd074..d5f04148 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "frontend", "version": "0.0.1", "dependencies": { - "@hyvor/design": "^1.1.23", + "@hyvor/design": "^1.1.24", "@hyvor/icons": "^1.1.0", "@hyvor/richtext": "^0.0.11", "chart.js": "^4.4.9", @@ -720,10 +720,11 @@ } }, "node_modules/@hyvor/design": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@hyvor/design/-/design-1.1.23.tgz", - "integrity": "sha512-fKl2DBgpvSQK450JGgpuEHnfm3TaqXRgURoJWJYxZ+p2umb/CS+rkBiSryjlHItRm6Vy+BTlvFZN3S17ldyAoQ==", + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/@hyvor/design/-/design-1.1.24.tgz", + "integrity": "sha512-+zp8atBjD141xIK3fjtgm2+uLvUG9eTxImzvUR0o+AFgByI4iBriHRNavNQeXFCIBrAPJfM2EstWotRucBSrug==", "license": "MIT", + "peer": true, "dependencies": { "@fontsource/readex-pro": "^5.0.8", "@hyvor/icons": "^1.1.1", @@ -746,6 +747,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@hyvor/icons/-/icons-1.1.1.tgz", "integrity": "sha512-8g08HWBNsWuCOzQHUqB2jRSv/XzA82NlKWm/x/NVz7G5lQD0wJibJLyzNmlYVDhwQh6TyjKcA2yR1f4eRqC1Ew==", + "peer": true, "peerDependencies": { "svelte": "^5.0.0" } @@ -879,6 +881,302 @@ "@openpanel/sdk": "1.0.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@playwright/test": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", @@ -1217,6 +1515,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz", "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==", "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1277,6 +1576,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1395,6 +1695,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -1612,6 +1913,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1709,7 +2011,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -1750,6 +2052,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1913,6 +2216,19 @@ "node": ">=16.0.0" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/devalue": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz", @@ -2006,6 +2322,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2247,7 +2564,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -2374,8 +2691,7 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -2420,7 +2736,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2430,7 +2746,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -2443,7 +2759,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -2584,7 +2900,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -2656,6 +2972,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2755,7 +3078,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -2840,6 +3163,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2924,6 +3248,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -2944,6 +3269,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -2964,6 +3290,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -2997,6 +3324,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz", "integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -3273,6 +3601,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.6.tgz", "integrity": "sha512-RnyO9VXI85Bmsf4b8AuQFBKFYL3LKUl+ZrifOjvlrQoboAROj5IITVLK1yOXBjwUWUn2BI5cfmurktgCzuZ5QA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3409,6 +3738,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3438,7 +3768,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -3499,6 +3829,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3546,6 +3877,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3651,6 +3983,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index dc8c5f3d..a20a1fff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "vite": "^6.0.0" }, "dependencies": { - "@hyvor/design": "^1.1.23", + "@hyvor/design": "^1.1.24", "@hyvor/icons": "^1.1.0", "@hyvor/richtext": "^0.0.11", "chart.js": "^4.4.9", diff --git a/frontend/src/routes/(marketing)/Header.svelte b/frontend/src/routes/(marketing)/Header.svelte index 4fbf8555..65a03e7c 100644 --- a/frontend/src/routes/(marketing)/Header.svelte +++ b/frontend/src/routes/(marketing)/Header.svelte @@ -6,7 +6,7 @@ import IconBoxArrowUpRight from '@hyvor/icons/IconBoxArrowUpRight'; -
    +
    {#snippet center()}
    @@ -50,126 +47,6 @@ - - --> - - -
    + diff --git a/frontend/src/routes/(marketing)/@components/Letter.svelte b/frontend/src/routes/(marketing)/@components/Letter.svelte index aa3fa191..7019855e 100644 --- a/frontend/src/routes/(marketing)/@components/Letter.svelte +++ b/frontend/src/routes/(marketing)/@components/Letter.svelte @@ -39,13 +39,6 @@ padding: 100px 0; } - /* larger screens */ - /* @media (min-width: 1200px) { - .letter-section { - padding: 150px 0; - } - } */ - .wrap { z-index: 1; font-style: normal; @@ -70,7 +63,6 @@ line-height: 60px; margin-top: 0; margin-bottom: 0; - /* color: #b7f9ff; */ } p { @@ -80,10 +72,6 @@ line-height: 32px; } - /* p :global(strong) { - color: #b7f9ff; - } */ - @media (max-width: 768px) { .letter-section { padding: 50px 0; diff --git a/frontend/src/routes/(marketing)/@components/SignupForm.svelte b/frontend/src/routes/(marketing)/@components/SignupForm.svelte index e5f59479..7af79ae9 100644 --- a/frontend/src/routes/(marketing)/@components/SignupForm.svelte +++ b/frontend/src/routes/(marketing)/@components/SignupForm.svelte @@ -144,11 +144,9 @@ @keyframes heroEnter { 0% { - /* //transform: translateX(30px) translateY(0) translateZ(20px); */ transform: translateX(300px) translateY(0) translateZ(20px); } 100% { - /* //transform: translateX(0) translateY(0) translateZ(0); */ transform: translateX(0) translateY(0) translateZ(0); opacity: 1; filter: blur(0); @@ -158,14 +156,11 @@ @keyframes heroEnterDelayed { 0% { transform: translateX(200px) translateY(30px) translateZ(10px); - /* transform: translateX(0) translateY(30px) translateZ(10px); */ opacity: 0.1; } 100% { - /* transform: translateX(0) translateY(0) translateZ(0); */ opacity: 1; transform: translateX(0) translateY(0) translateZ(0); - /* filter: blur(0); */ } } @@ -185,13 +180,6 @@ subtleFloat 8s ease-in-out infinite 2s; } - /* .another-browser { - animation: - heroEnterDelayed 3s ease-out, - subtleFloat 6s ease-in-out infinite 3s reverse; - /* differed the appearance and float animation a bit using different timings - } */ - @media (max-width: 992px) { .signup-form { flex-direction: column; @@ -238,7 +226,7 @@ .bento-grid { grid-template-columns: 1fr; gap: 20px; - justify-items: center; + /* justify-items: center; */ } .int { @@ -253,7 +241,7 @@ } h2 { - font-size: 16px; + font-size: 18px; max-width: 90%; } @@ -264,12 +252,12 @@ @media (max-width: 480px) { h1 { - font-size: 30px; + font-size: 40px; } h2 { - font-size: 15px; - padding-top: 5px; + font-size: 18px; + padding-top: 20px; } .bento-grid { @@ -278,7 +266,7 @@ } .int { - font-size: 16px; + font-size: 18px; gap: 10px; } } diff --git a/frontend/src/routes/(marketing)/@components/TrialChecks.svelte b/frontend/src/routes/(marketing)/@components/TrialChecks.svelte index fec6ec42..4bedadd3 100644 --- a/frontend/src/routes/(marketing)/@components/TrialChecks.svelte +++ b/frontend/src/routes/(marketing)/@components/TrialChecks.svelte @@ -53,11 +53,9 @@ justify-content: center; flex-direction: column; align-items: center; - /* width: auto; */ } .check { - /* width: auto; */ width: 300px; max-width: 100%; } diff --git a/frontend/src/routes/(marketing)/@components/TrialSignUp.svelte b/frontend/src/routes/(marketing)/@components/TrialSignUp.svelte index 885464f6..2b567355 100644 --- a/frontend/src/routes/(marketing)/@components/TrialSignUp.svelte +++ b/frontend/src/routes/(marketing)/@components/TrialSignUp.svelte @@ -22,11 +22,6 @@ {title}
    -
    -->
    - + -
    +
    .outer-box { background-color: var(--accent-light); - /* padding: 60px 0; */ + padding: 60px 0; } .feature-wrapper { margin: 70px 40px; @@ -135,11 +135,12 @@ margin: 0; } - @media (max-width: 768px) { + @media (max-width: 978px) { .feature-wrapper { grid-template-columns: 1fr; gap: 1.5rem; margin: 2rem auto; + width: auto; } .feature { @@ -162,7 +163,8 @@ @media (max-width: 480px) { .feature-wrapper { - padding: 0 0.5rem; + padding: 0 1rem; + width: auto; } .feature { diff --git a/frontend/src/routes/(marketing)/Footer.svelte b/frontend/src/routes/(marketing)/Footer.svelte index cafc208a..3abc5c80 100644 --- a/frontend/src/routes/(marketing)/Footer.svelte +++ b/frontend/src/routes/(marketing)/Footer.svelte @@ -69,5 +69,9 @@ .row :global(.links) { align-items: center; } + + :global(.footer-top) { + text-align: center; + } } diff --git a/frontend/src/routes/(marketing)/pricing/PricingPlan.svelte b/frontend/src/routes/(marketing)/pricing/PricingPlan.svelte index eecfa82d..ffea2a8d 100644 --- a/frontend/src/routes/(marketing)/pricing/PricingPlan.svelte +++ b/frontend/src/routes/(marketing)/pricing/PricingPlan.svelte @@ -313,4 +313,13 @@ font-size: 15px; color: var(--text-light); } + + /* Responsive */ + @media (max-width: 480px) { + .plan-toggle, + .plan-yearly-only { + width: 100%; + flex-direction: column; + } + } diff --git a/frontend/src/routes/(marketing)/pricing/PricingPlans.svelte b/frontend/src/routes/(marketing)/pricing/PricingPlans.svelte index 89a6726f..6032fa60 100644 --- a/frontend/src/routes/(marketing)/pricing/PricingPlans.svelte +++ b/frontend/src/routes/(marketing)/pricing/PricingPlans.svelte @@ -91,6 +91,7 @@ @media (max-width: 992px) { .plans { flex-direction: column; + flex-wrap: unset !important; } } diff --git a/frontend/src/routes/(marketing)/privacy/+page.svelte b/frontend/src/routes/(marketing)/privacy/+page.svelte index 8a8ba9fa..cfe90273 100644 --- a/frontend/src/routes/(marketing)/privacy/+page.svelte +++ b/frontend/src/routes/(marketing)/privacy/+page.svelte @@ -116,10 +116,6 @@

    5. Data Sharing

      -
    • - Your data is shared with service providers such as email delivery to provide the Hyvor - Post services. -
    • Your data can be shared with law enforcement or regulatory bodies when required by law or to protect our rights. @@ -129,7 +125,9 @@

      6. Data Processors

      We use GDPR-compliant sub-processors (e.g., email delivery providers) to send newsletters - and ensure platform stability. A full list is available at https://hyvor.com/sub. + and ensure platform stability. A full list is available at sub-processors of HYVOR.


      diff --git a/frontend/static/img/home/signup.png b/frontend/static/img/home/signup.png index 66913ab29c4e9ba598d1e6445407f7350e6ee1b3..e70b19c0ad42acc33d6c0185e1f9e14212879d42 100644 GIT binary patch literal 54315 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 literal 53391 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%0Dj=G_2J0m<~}-t;AZ> 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 From b48ed8c8f3752305fd2180572a1b234c00a56803 Mon Sep 17 00:00:00 2001 From: Nadil Karunarathna <113877141+Nadil-K@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:09:08 +0530 Subject: [PATCH 26/38] fix-branding-for-newsletter-name (#377) --- backend/src/Service/Template/TemplateVariableService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Service/Template/TemplateVariableService.php b/backend/src/Service/Template/TemplateVariableService.php index 9a840693..958c6062 100644 --- a/backend/src/Service/Template/TemplateVariableService.php +++ b/backend/src/Service/Template/TemplateVariableService.php @@ -95,7 +95,7 @@ public function variablesFromSend(Send $send): TemplateVariables private function setVariablesFromSendingProfile(TemplateVariables $variables, SendingProfile $sendingProfile): TemplateVariables { - $variables->name = $sendingProfile->getFromName() ?: $variables->name; + $variables->name = $sendingProfile->getBrandName() ?: ($sendingProfile->getFromName() ?: $variables->name); $variables->brand_logo = $sendingProfile->getBrandLogo() ?: $variables->brand_logo; $variables->brand_logo_alt = $sendingProfile->getBrandName() ?: $variables->brand_logo_alt; $variables->brand_url = $sendingProfile->getBrandUrl() ?: $variables->brand_url; From adececb35cac076dcb0271e0b5c08e6f5afb8b48 Mon Sep 17 00:00:00 2001 From: Nadil Karunarathna Date: Tue, 9 Dec 2025 13:06:17 +0530 Subject: [PATCH 27/38] hide lists when one list is avaliable --- embed/src/form/Form.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embed/src/form/Form.svelte b/embed/src/form/Form.svelte index a22a77a2..b6076ae1 100644 --- a/embed/src/form/Form.svelte +++ b/embed/src/form/Form.svelte @@ -226,7 +226,7 @@
      {#each lists as list (list.id)}