Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
142e809
Add new sudo sections
Nolab0 Feb 24, 2026
41c1048
Add tests
Nolab0 Feb 24, 2026
d31cdc1
Remove logic from container
Nolab0 Feb 24, 2026
1cf677d
Fix phpstan
Nolab0 Feb 24, 2026
316092c
Fix prettier
Nolab0 Feb 24, 2026
09cb932
Use sudo object factory
Nolab0 Mar 3, 2026
42c8b4c
Improve frontend
Nolab0 Mar 3, 2026
68c41a8
Fix frontend issues
Nolab0 Mar 3, 2026
62139a2
Fix svelte imports
Nolab0 Mar 3, 2026
9f699e9
Add sudo roles and auth
Nolab0 Apr 3, 2026
d23d073
Add name filter
Nolab0 Apr 3, 2026
226c5a8
Improve sudo UI and filters
Nolab0 Apr 3, 2026
9c5f5d8
Improve issue sudo
Nolab0 Apr 3, 2026
c114c6e
PhPstan
Nolab0 Apr 3, 2026
a8b2482
fix: user invitation (#431)
supun-io Feb 23, 2026
9a95b54
fix assertNotNull (#436)
supun-io Feb 26, 2026
a2a635a
Add webhook signature verification (#421)
Nolab0 Mar 2, 2026
c691e8e
Spellings and Grammar check (marketing pages) (#442)
IshiniAvindya Mar 22, 2026
ff01df4
validate email of test issue
Nadil-K Mar 25, 2026
3799b5b
rate limit for test issue
Nadil-K Mar 25, 2026
3d296a0
phpstan fixes
Nadil-K Mar 25, 2026
be220f5
remove org migration command
Nadil-K Mar 25, 2026
20f347c
test_emails_sent
Nadil-K Mar 25, 2026
b9ccc32
Merge branch 'main' into 337-sudo-new-sections
Nolab0 Apr 7, 2026
dfb9bed
prettier
Nolab0 Apr 7, 2026
204ab28
minor
supun-io Apr 10, 2026
9af85a9
Create a new migration instead of adding a new one
Nolab0 Apr 25, 2026
80a10ec
Add subdomain sudo features
Nolab0 Apr 25, 2026
325f49e
Improve issue sudo
Nolab0 Apr 25, 2026
8f3f70d
prettier
Nolab0 Apr 25, 2026
60941b1
monitor sudo permissions
supun-io Apr 27, 2026
92abe4d
remove read blogs
supun-io Apr 27, 2026
ee94ea8
bring org logic to the controller
supun-io Apr 27, 2026
0c2781a
phpstan
supun-io Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"doctrine/orm": "^3.5.0",
"dunglas/doctrine-json-odm": "^1.4.2",
"fakerphp/faker": "^1.24.1",
"hyvor/internal": "4.0.3",
"hyvor/internal": "^4.0.10",
"hyvor/phrosemirror": "^1.0.5",
"league/flysystem-aws-s3-v3": "^3.29",
"nelmio/cors-bundle": "^2.5",
Expand Down
1,069 changes: 539 additions & 530 deletions backend/composer.lock

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions backend/config/packages/internal.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
<?php

use App\Service\Sudo\SudoPermission;
use App\Service\Sudo\SudoRole;
use Symfony\Component\DependencyInjection\Loader\Configurator\App;

return static function (\Symfony\Config\InternalConfig $internal): void {

$internal->component('post');
$internal->i18n(['default' => 'en']);

};
return App::config([
'internal' => [
'component' => 'post',
'i18n' => [
'default' => 'en',
],
'sudo' => [
'permission_enum' => SudoPermission::class,
'role_enum' => SudoRole::class,
]
],
]);
51 changes: 29 additions & 22 deletions backend/config/reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,29 +208,29 @@
* initial_marking?: list<scalar|Param|null>,
* events_to_dispatch?: list<string|Param>|null,
* places?: list<array{ // Default: []
* name: scalar|Param|null,
* metadata?: list<mixed>,
* name?: scalar|Param|null,
* metadata?: array<string, mixed>,
* }>,
* transitions: list<array{ // Default: []
* name: string|Param,
* transitions?: list<array{ // Default: []
* name?: string|Param,
* guard?: string|Param, // An expression to block the transition.
* from?: list<array{ // Default: []
* place: string|Param,
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* to?: list<array{ // Default: []
* place: string|Param,
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* weight?: int|Param, // Default: 1
* metadata?: list<mixed>,
* metadata?: array<string, mixed>,
* }>,
* metadata?: list<mixed>,
* metadata?: array<string, mixed>,
* }>,
* },
* router?: bool|array{ // Router configuration
* enabled?: bool|Param, // Default: false
* resource: scalar|Param|null,
* resource?: scalar|Param|null,
* type?: scalar|Param|null,
* cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%"
* default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null
Expand Down Expand Up @@ -360,10 +360,10 @@
* mapping?: array{
* paths?: list<scalar|Param|null>,
* },
* default_context?: list<mixed>,
* default_context?: array<string, mixed>,
* named_serializers?: array<string, array{ // Default: []
* name_converter?: scalar|Param|null,
* default_context?: list<mixed>,
* default_context?: array<string, mixed>,
* include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true
* include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true
* }>,
Expand Down Expand Up @@ -427,7 +427,7 @@
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool|Param, // Default: true
* routing?: array<string, array{ // Default: []
* routing?: array<string, string|array{ // Default: []
* senders?: list<scalar|Param|null>,
* }>,
* serializer?: array{
Expand All @@ -440,7 +440,7 @@
* transports?: array<string, string|array{ // Default: []
* dsn?: scalar|Param|null,
* serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null
* options?: list<mixed>,
* options?: array<string, mixed>,
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* retry_strategy?: string|array{
* service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null
Expand All @@ -462,7 +462,7 @@
* allow_no_senders?: bool|Param, // Default: true
* },
* middleware?: list<string|array{ // Default: []
* id: scalar|Param|null,
* id?: scalar|Param|null,
* arguments?: list<mixed>,
* }>,
* }>,
Expand Down Expand Up @@ -634,7 +634,7 @@
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
* storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
* policy: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
* policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
* limiters?: list<scalar|Param|null>,
* limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
* interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
Expand Down Expand Up @@ -679,7 +679,7 @@
* enabled?: bool|Param, // Default: false
* message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus"
* routing?: array<string, array{ // Default: []
* service: scalar|Param|null,
* service?: scalar|Param|null,
* secret?: scalar|Param|null, // Default: ""
* }>,
* },
Expand All @@ -694,7 +694,7 @@
* dbal?: array{
* default_connection?: scalar|Param|null,
* types?: array<string, string|array{ // Default: []
* class: scalar|Param|null,
* class?: scalar|Param|null,
* commented?: bool|Param, // Deprecated: The doctrine-bundle type commenting features were removed; the corresponding config parameter was deprecated in 2.0 and will be dropped in 3.0.
* }>,
* driver_schemes?: array<string, scalar|Param|null>,
Expand Down Expand Up @@ -910,7 +910,7 @@
* datetime_functions?: array<string, scalar|Param|null>,
* },
* filters?: array<string, string|array{ // Default: []
* class: scalar|Param|null,
* class?: scalar|Param|null,
* enabled?: bool|Param, // Default: false
* parameters?: array<string, mixed>,
* }>,
Expand Down Expand Up @@ -954,6 +954,10 @@
* folder?: scalar|Param|null, // Default: "%kernel.project_dir%/../shared/locale"
* default?: scalar|Param|null, // Default: "en-US"
* },
* sudo?: array{
* permission_enum?: string|Param, // Default: null
* role_enum?: string|Param, // Default: null
* },
* }
* @psalm-type DamaDoctrineTestConfig = array{
* enable_static_connection?: mixed, // Default: true
Expand Down Expand Up @@ -1045,7 +1049,7 @@
* use_microseconds?: scalar|Param|null, // Default: true
* channels?: list<scalar|Param|null>,
* handlers?: array<string, array{ // Default: []
* type: scalar|Param|null,
* type?: scalar|Param|null,
* id?: scalar|Param|null,
* enabled?: bool|Param, // Default: true
* priority?: scalar|Param|null, // Default: 0
Expand Down Expand Up @@ -1192,7 +1196,7 @@
* headers?: list<scalar|Param|null>,
* mailer?: scalar|Param|null, // Default: null
* email_prototype?: string|array{
* id: scalar|Param|null,
* id?: scalar|Param|null,
* method?: scalar|Param|null, // Default: null
* },
* lazy?: bool|Param, // Default: true
Expand Down Expand Up @@ -1301,7 +1305,7 @@
* use_underscore?: bool|Param, // Default: true
* unordered_list_markers?: list<scalar|Param|null>,
* },
* ...<mixed>
* ...<string, mixed>
* },
* }
* @psalm-type ZenstruckMessengerMonitorConfig = array{
Expand Down Expand Up @@ -1402,7 +1406,10 @@ final class App
*/
public static function config(array $config): array
{
return AppReference::config($config);
/** @var ConfigType $config */
$config = AppReference::config($config);

return $config;
}
}

Expand Down
30 changes: 30 additions & 0 deletions backend/migrations/Version20260424091842.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260424091842 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add role column to sudo_users table';
}

public function up(Schema $schema): void
{
$this->addSql(<<<SQL
ALTER TABLE sudo_users ADD COLUMN role TEXT NOT NULL DEFAULT 'sudo';
SQL
);
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE sudo_users DROP COLUMN role');
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function confirm(Request $request): JsonResponse
throw new BadRequestHttpException('Invalid confirmation token.');
}

if (!$data || !is_array($data) || !isset($data['subscriber_id'], $data['expires_at'])) {
if (!$data || !is_array($data) || !isset($data['subscriber_id'], $data['expires_at']) || !is_int($data['subscriber_id'])) {
throw new BadRequestHttpException('Invalid confirmation token.');
}

Expand Down
7 changes: 4 additions & 3 deletions backend/src/Api/Root/MessengerMonitorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace App\Api\Root;

use App\Service\Sudo\SudoPermission;
use Hyvor\Internal\Bundle\Api\SudoPermissionRequired;
use Symfony\Component\Routing\Attribute\Route;
use Zenstruck\Messenger\Monitor\Controller\MessengerMonitorController as BaseMessengerMonitorController;

#[Route('/messenger')]
class MessengerMonitorController extends BaseMessengerMonitorController
{
}
#[SudoPermissionRequired(SudoPermission::ACCESS_SUDO)]
class MessengerMonitorController extends BaseMessengerMonitorController {}
50 changes: 0 additions & 50 deletions backend/src/Api/Sudo/Authorization/SudoAuthorizationListener.php

This file was deleted.

3 changes: 3 additions & 0 deletions backend/src/Api/Sudo/Controller/ApprovalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
use App\Entity\Approval;
use App\Entity\Type\ApprovalStatus;
use App\Service\Approval\ApprovalService;
use App\Service\Sudo\SudoPermission;
use Hyvor\Internal\Bundle\Api\SudoPermissionRequired;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Attribute\Route;

#[SudoPermissionRequired(SudoPermission::ACCESS_SUDO)]
class ApprovalController extends AbstractController
{
public function __construct(
Expand Down
59 changes: 59 additions & 0 deletions backend/src/Api/Sudo/Controller/IssueController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace App\Api\Sudo\Controller;

use App\Entity\Issue;
use App\Entity\Type\IssueStatus;
use App\Service\Issue\IssueService;
use App\Service\Sudo\SudoPermission;
use Hyvor\Internal\Bundle\Api\SudoPermissionRequired;
use Hyvor\Internal\Bundle\Api\SudoObject\SudoObjectFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

#[SudoPermissionRequired(SudoPermission::ACCESS_SUDO)]
class IssueController extends AbstractController
{
public function __construct(
private IssueService $issueService,
private SudoObjectFactory $sudoObjectFactory,
)
{
}

#[Route('/issues', methods: ['GET'])]
public function getIssues(Request $request): JsonResponse
{
$newsletterId = $request->query->has('newsletter_id') ? $request->query->getInt('newsletter_id') : null;
$status = $request->query->has('status')
? IssueStatus::tryFrom($request->query->getString('status'))
: null;
$limit = $request->query->getInt('limit', 50);
$offset = $request->query->getInt('offset', 0);

$relationships = [Issue::class => ['newsletter']];

return new JsonResponse(
array_map(
fn($issue) => $this->sudoObjectFactory->create($issue, $relationships),
$this->issueService->getIssuesGlobal($newsletterId, $status, $limit, $offset)
)
);
}

#[Route('/issues/{id}', methods: ['GET'])]
public function getIssue(int $id): JsonResponse
{
$issue = $this->issueService->getIssueGlobal($id);

if (!$issue) {
throw $this->createNotFoundException();
}

return new JsonResponse(
$this->sudoObjectFactory->create($issue, [Issue::class => ['newsletter']])
);
}
}
Loading
Loading