Skip to content

Commit 1cda411

Browse files
TatevikGrtatevikg1
andauthored
ISSUE-345: privileges, analytics (#149)
* ISSUE-345: admin privileges * ISSUE-345: dev version * ISSUE-345: check manage subscribers privilege * ISSUE-345: check manage campaigns privilege * ISSUE-345: analytics controller * ISSUE-345: campaign stat normalizer * ISSUE-345: view open stat normalizer * ISSUE-345: return 400 on validation exception * ISSUE-345: test fix * ISSUE-345: make it more specific * ISSUE-345: style fix * ISSUE-345: refactor * ISSUE-345: top domains stats * ISSUE-345: top local parts stats * ISSUE-345: style * ISSUE-345: note * ISSUE-345: do not publish fos * ISSUE-345: AccessDeniedException * ISSUE-345: version * ISSUE-345: test fix --------- Co-authored-by: Tatevik <[email protected]>
1 parent 6dbbee9 commit 1cda411

36 files changed

+2321
-54
lines changed

composer.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
{
1717
"name": "Xheni Myrtaj",
1818
"email": "[email protected]",
19-
"role": "Maintainer"
19+
"role": "Former developer"
2020
},
2121
{
2222
"name": "Oliver Klee",
2323
"email": "[email protected]",
2424
"role": "Former developer"
25+
},
26+
{
27+
"name": "Tatevik Grigoryan",
28+
"email": "[email protected]",
29+
"role": "Maintainer"
2530
}
2631
],
2732
"support": {
@@ -31,7 +36,7 @@
3136
},
3237
"require": {
3338
"php": "^8.1",
34-
"phplist/core": "v5.0.0-alpha7",
39+
"phplist/core": "v5.0.0-alpha8",
3540
"friendsofsymfony/rest-bundle": "*",
3641
"symfony/test-pack": "^1.0",
3742
"symfony/process": "^6.4",
@@ -93,7 +98,6 @@
9398
"symfony-tests-dir": "tests",
9499
"phplist/core": {
95100
"bundles": [
96-
"FOS\\RestBundle\\FOSRestBundle",
97101
"PhpList\\RestBundle\\PhpListRestBundle"
98102
],
99103
"routes": {
@@ -111,6 +115,11 @@
111115
"resource": "@PhpListRestBundle/Messaging/Controller/",
112116
"type": "attribute",
113117
"prefix": "/api/v2"
118+
},
119+
"rest-api-analitics": {
120+
"resource": "@PhpListRestBundle/Statistics/Controller/",
121+
"type": "attribute",
122+
"prefix": "/api/v2"
114123
}
115124
}
116125
}

config/services/controllers.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ services:
2424
autowire: true
2525
autoconfigure: true
2626
public: true
27+
28+
PhpList\RestBundle\Statistics\Controller\:
29+
resource: '../src/Statistics/Controller'
30+
tags: [ 'controller.service_arguments' ]
31+
autowire: true
32+
autoconfigure: true
33+
public: true

config/services/managers.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,16 @@ services:
3939
PhpList\Core\Domain\Subscription\Service\SubscriberCsvExporter:
4040
autowire: true
4141
autoconfigure: true
42+
43+
PhpList\Core\Domain\Analytics\Service\Manager\LinkTrackManager:
44+
autowire: true
45+
autoconfigure: true
46+
47+
PhpList\Core\Domain\Analytics\Service\Manager\UserMessageViewManager:
48+
autowire: true
49+
autoconfigure: true
50+
51+
PhpList\Core\Domain\Analytics\Service\AnalyticsService:
52+
autowire: true
53+
autoconfigure: true
54+

config/services/normalizers.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,19 @@ services:
6969
PhpList\RestBundle\Subscription\Serializer\SubscribersExportRequestNormalizer:
7070
tags: [ 'serializer.normalizer' ]
7171
autowire: true
72+
73+
PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer:
74+
tags: [ 'serializer.normalizer' ]
75+
autowire: true
76+
77+
PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer:
78+
tags: [ 'serializer.normalizer' ]
79+
autowire: true
80+
81+
PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer:
82+
tags: [ 'serializer.normalizer' ]
83+
autowire: true
84+
85+
PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer:
86+
tags: [ 'serializer.normalizer' ]
87+
autowire: true

config/services/providers.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ services:
77
PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider:
88
autowire: true
99
autoconfigure: true
10+
11+
PhpList\RestBundle\Messaging\Service\CampaignService:
12+
autowire: true
13+
autoconfigure: true

src/Common/Controller/BaseController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Symfony\Component\HttpFoundation\Request;
1212
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1313

14+
/** @SuppressWarnings(PHPMD.NumberOfChildren) */
1415
abstract class BaseController extends AbstractController
1516
{
1617
protected Authentication $authentication;

src/Common/EventListener/ExceptionListener.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
namespace PhpList\RestBundle\Common\EventListener;
66

77
use Exception;
8+
use PhpList\Core\Domain\Identity\Exception\AdminAttributeCreationException;
89
use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException;
910
use Symfony\Component\HttpFoundation\JsonResponse;
1011
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1112
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1213
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
14+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
15+
use Symfony\Component\Validator\Exception\ValidatorException;
1316

1417
class ExceptionListener
1518
{
@@ -34,6 +37,21 @@ public function onKernelException(ExceptionEvent $event): void
3437
'message' => $exception->getMessage(),
3538
], $exception->getStatusCode());
3639
$event->setResponse($response);
40+
} elseif ($exception instanceof AdminAttributeCreationException) {
41+
$response = new JsonResponse([
42+
'message' => $exception->getMessage(),
43+
], $exception->getStatusCode());
44+
$event->setResponse($response);
45+
} elseif ($exception instanceof ValidatorException) {
46+
$response = new JsonResponse([
47+
'message' => $exception->getMessage(),
48+
], 400);
49+
$event->setResponse($response);
50+
} elseif ($exception instanceof AccessDeniedException) {
51+
$response = new JsonResponse([
52+
'message' => $exception->getMessage(),
53+
], 403);
54+
$event->setResponse($response);
3755
} elseif ($exception instanceof Exception) {
3856
$response = new JsonResponse([
3957
'message' => $exception->getMessage(),

src/Identity/OpenApi/SwaggerSchemasRequest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@
3636
type: 'boolean',
3737
example: false
3838
),
39+
new OA\Property(
40+
property: 'privileges',
41+
description: 'Array of privileges where keys are privilege names and values are booleans',
42+
properties: [
43+
new OA\Property(property: 'subscribers', type: 'boolean', example: true),
44+
new OA\Property(property: 'campaigns', type: 'boolean', example: false),
45+
new OA\Property(property: 'statistics', type: 'boolean', example: true),
46+
new OA\Property(property: 'settings', type: 'boolean', example: false),
47+
],
48+
type: 'object',
49+
example: ['subscribers' => true, 'campaigns' => false, 'statistics' => true, 'settings' => false]
50+
),
3951
],
4052
type: 'object'
4153
)]
@@ -68,6 +80,18 @@
6880
type: 'boolean',
6981
example: false
7082
),
83+
new OA\Property(
84+
property: 'privileges',
85+
description: 'Array of privileges where keys are privilege names and values are booleans',
86+
properties: [
87+
new OA\Property(property: 'subscribers', type: 'boolean', example: true),
88+
new OA\Property(property: 'campaigns', type: 'boolean', example: false),
89+
new OA\Property(property: 'statistics', type: 'boolean', example: true),
90+
new OA\Property(property: 'settings', type: 'boolean', example: false),
91+
],
92+
type: 'object',
93+
example: ['subscribers' => true, 'campaigns' => false, 'statistics' => true, 'settings' => false]
94+
),
7195
],
7296
type: 'object'
7397
)]

src/Identity/Request/CreateAdministratorRequest.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PhpList\Core\Domain\Identity\Model\Administrator;
88
use PhpList\Core\Domain\Identity\Model\Dto\CreateAdministratorDto;
9+
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
910
use PhpList\RestBundle\Common\Request\RequestInterface;
1011
use PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmail;
1112
use PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginName;
@@ -31,13 +32,26 @@ class CreateAdministratorRequest implements RequestInterface
3132
#[Assert\Type('bool')]
3233
public bool $superUser = false;
3334

35+
/**
36+
* Array of privileges where keys are privilege names (from PrivilegeFlag enum) and values are booleans.
37+
* Example: ['subscribers' => true, 'campaigns' => false, 'statistics' => true, 'settings' => false]
38+
*/
39+
#[Assert\Type('array')]
40+
#[Assert\All([
41+
'constraints' => [
42+
new Assert\Type(['type' => 'bool']),
43+
],
44+
])]
45+
public array $privileges = [];
46+
3447
public function getDto(): CreateAdministratorDto
3548
{
3649
return new CreateAdministratorDto(
37-
$this->loginName,
38-
$this->password,
39-
$this->email,
40-
$this->superUser
50+
loginName: $this->loginName,
51+
password: $this->password,
52+
email: $this->email,
53+
isSuperUser: $this->superUser,
54+
privileges: $this->privileges
4155
);
4256
}
4357
}

src/Identity/Request/UpdateAdministratorRequest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PhpList\Core\Domain\Identity\Model\Administrator;
88
use PhpList\Core\Domain\Identity\Model\Dto\UpdateAdministratorDto;
9+
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
910
use PhpList\RestBundle\Common\Request\RequestInterface;
1011
use PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmail;
1112
use PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginName;
@@ -29,6 +30,18 @@ class UpdateAdministratorRequest implements RequestInterface
2930
#[Assert\Type('bool')]
3031
public ?bool $superAdmin = null;
3132

33+
/**
34+
* Array of privileges where keys are privilege names (from PrivilegeFlag enum) and values are booleans.
35+
* Example: ['subscribers' => true, 'campaigns' => false, 'statistics' => true, 'settings' => false]
36+
*/
37+
#[Assert\Type('array')]
38+
#[Assert\All([
39+
'constraints' => [
40+
new Assert\Type(['type' => 'bool']),
41+
],
42+
])]
43+
public array $privileges = [];
44+
3245
public function getDto(): UpdateAdministratorDto
3346
{
3447
return new UpdateAdministratorDto(
@@ -37,6 +50,7 @@ public function getDto(): UpdateAdministratorDto
3750
password: $this->password,
3851
email: $this->email,
3952
superAdmin: $this->superAdmin,
53+
privileges: $this->privileges
4054
);
4155
}
4256
}

src/Identity/Serializer/AdministratorNormalizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function normalize($object, string $format = null, array $context = []):
2626
'login_name' => $object->getLoginName(),
2727
'email' => $object->getEmail(),
2828
'super_admin' => $object->isSuperUser(),
29+
'privileges' => $object->getPrivileges()->all(),
2930
'created_at' => $object->getCreatedAt()?->format(DateTimeInterface::ATOM),
3031
];
3132
}

src/Messaging/Controller/CampaignController.php

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,13 @@
55
namespace PhpList\RestBundle\Messaging\Controller;
66

77
use OpenApi\Attributes as OA;
8-
use PhpList\Core\Domain\Messaging\Model\Filter\MessageFilter;
98
use PhpList\Core\Domain\Messaging\Model\Message;
10-
use PhpList\Core\Domain\Messaging\Service\MessageManager;
119
use PhpList\Core\Security\Authentication;
1210
use PhpList\RestBundle\Common\Controller\BaseController;
13-
use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider;
1411
use PhpList\RestBundle\Common\Validator\RequestValidator;
1512
use PhpList\RestBundle\Messaging\Request\CreateMessageRequest;
1613
use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest;
17-
use PhpList\RestBundle\Messaging\Serializer\MessageNormalizer;
14+
use PhpList\RestBundle\Messaging\Service\CampaignService;
1815
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
1916
use Symfony\Component\HttpFoundation\JsonResponse;
2017
use Symfony\Component\HttpFoundation\Request;
@@ -29,21 +26,15 @@
2926
#[Route('/campaigns', name: 'campaign_')]
3027
class CampaignController extends BaseController
3128
{
32-
private MessageNormalizer $normalizer;
33-
private MessageManager $messageManager;
34-
private PaginatedDataProvider $paginatedProvider;
29+
private CampaignService $campaignService;
3530

3631
public function __construct(
3732
Authentication $authentication,
3833
RequestValidator $validator,
39-
MessageNormalizer $normalizer,
40-
MessageManager $messageManager,
41-
PaginatedDataProvider $paginatedProvider,
34+
CampaignService $campaignService,
4235
) {
4336
parent::__construct($authentication, $validator);
44-
$this->normalizer = $normalizer;
45-
$this->messageManager = $messageManager;
46-
$this->paginatedProvider = $paginatedProvider;
37+
$this->campaignService = $campaignService;
4738
}
4839

4940
#[Route('', name: 'get_list', methods: ['GET'])]
@@ -103,12 +94,10 @@ public function __construct(
10394
)]
10495
public function getMessages(Request $request): JsonResponse
10596
{
106-
$authUer = $this->requireAuthentication($request);
107-
108-
$filter = (new MessageFilter())->setOwner($authUer);
97+
$authUser = $this->requireAuthentication($request);
10998

11099
return $this->json(
111-
$this->paginatedProvider->getPaginatedList($request, $this->normalizer, Message::class, $filter),
100+
$this->campaignService->getMessages($request, $authUser),
112101
Response::HTTP_OK
113102
);
114103
}
@@ -157,11 +146,7 @@ public function getMessage(
157146
): JsonResponse {
158147
$this->requireAuthentication($request);
159148

160-
if (!$message) {
161-
throw $this->createNotFoundException('Campaign not found.');
162-
}
163-
164-
return $this->json($this->normalizer->normalize($message), Response::HTTP_OK);
149+
return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK);
165150
}
166151

167152
#[Route('', name: 'create', methods: ['POST'])]
@@ -216,15 +201,17 @@ public function getMessage(
216201
),
217202
]
218203
)]
219-
public function createMessage(Request $request, MessageNormalizer $normalizer): JsonResponse
204+
public function createMessage(Request $request): JsonResponse
220205
{
221206
$authUser = $this->requireAuthentication($request);
222207

223208
/** @var CreateMessageRequest $createMessageRequest */
224209
$createMessageRequest = $this->validator->validate($request, CreateMessageRequest::class);
225-
$data = $this->messageManager->createMessage($createMessageRequest->getDto(), $authUser);
226210

227-
return $this->json($normalizer->normalize($data), Response::HTTP_CREATED);
211+
return $this->json(
212+
$this->campaignService->createMessage($createMessageRequest, $authUser),
213+
Response::HTTP_CREATED
214+
);
228215
}
229216

230217
#[Route('/{messageId}', name: 'update', requirements: ['messageId' => '\d+'], methods: ['PUT'])]
@@ -291,14 +278,13 @@ public function updateMessage(
291278
): JsonResponse {
292279
$authUser = $this->requireAuthentication($request);
293280

294-
if (!$message) {
295-
throw $this->createNotFoundException('Campaign not found.');
296-
}
297281
/** @var UpdateMessageRequest $updateMessageRequest */
298282
$updateMessageRequest = $this->validator->validate($request, UpdateMessageRequest::class);
299-
$data = $this->messageManager->updateMessage($updateMessageRequest->getDto(), $message, $authUser);
300283

301-
return $this->json($this->normalizer->normalize($data), Response::HTTP_OK);
284+
return $this->json(
285+
$this->campaignService->updateMessage($updateMessageRequest, $authUser, $message),
286+
Response::HTTP_OK
287+
);
302288
}
303289

304290
#[Route('/{messageId}', name: 'delete', requirements: ['messageId' => '\d+'], methods: ['DELETE'])]
@@ -348,13 +334,9 @@ public function deleteMessage(
348334
Request $request,
349335
#[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null
350336
): JsonResponse {
351-
$this->requireAuthentication($request);
352-
353-
if (!$message) {
354-
throw $this->createNotFoundException('Campaign not found.');
355-
}
337+
$authUser = $this->requireAuthentication($request);
356338

357-
$this->messageManager->delete($message);
339+
$this->campaignService->deleteMessage($authUser, $message);
358340

359341
return $this->json(null, Response::HTTP_NO_CONTENT);
360342
}

0 commit comments

Comments
 (0)