From 130ce1bbac10e3c5cb2a140004c21ca4f73a5cc4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 22 May 2025 14:00:17 +0200 Subject: [PATCH] wip: refactor watchlist triggers --- .../watchlist/UpdateWatchlistButton.tsx | 1 + .../tracking/watchlist/WatchlistForm.tsx | 38 +++++++++++++++++-- assets/pages/tracking/WatchlistPage.tsx | 11 +----- assets/utils/api/index.ts | 13 +++++-- assets/utils/api/watchlist.ts | 11 +++++- src/Entity/WatchList.php | 5 +++ src/Entity/WatchListTrigger.php | 33 +++++++++++++--- src/Service/ChatNotificationService.php | 3 +- src/State/WatchListUpdateProcessor.php | 26 ++++++++----- 9 files changed, 108 insertions(+), 33 deletions(-) diff --git a/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx b/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx index 2d8529f..68c36d3 100644 --- a/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx +++ b/assets/components/tracking/watchlist/UpdateWatchlistButton.tsx @@ -62,6 +62,7 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors} }} connectors={connectors} isCreation={false} + watchList={watchlist} /> diff --git a/assets/components/tracking/watchlist/WatchlistForm.tsx b/assets/components/tracking/watchlist/WatchlistForm.tsx index 6b816ee..7f174ea 100644 --- a/assets/components/tracking/watchlist/WatchlistForm.tsx +++ b/assets/components/tracking/watchlist/WatchlistForm.tsx @@ -2,12 +2,12 @@ import type { FormInstance, SelectProps} from 'antd' import {Button, Form, Input, Select, Space, Tag, Tooltip, Typography} from 'antd' import {t} from 'ttag' import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from '@ant-design/icons' -import React from 'react' +import React, {useState} from 'react' import type {Connector} from '../../../utils/api/connectors' import {rdapEventDetailTranslation, rdapEventNameTranslation} from '../../../utils/functions/rdapTranslation' import {actionToColor} from '../../../utils/functions/actionToColor' import {actionToIcon} from '../../../utils/functions/actionToIcon' -import type {EventAction} from '../../../utils/api' +import {EventAction, putWatchlistTrigger, Watchlist} from '../../../utils/api' import {formItemLayoutWithOutLabel} from "../../../utils/providers" type TagRender = SelectProps['tagRender'] @@ -23,11 +23,12 @@ const formItemLayout = { } } -export function WatchlistForm({form, connectors, onFinish, isCreation}: { +export function WatchlistForm({form, connectors, onFinish, isCreation, watchList}: { form: FormInstance connectors: Array onFinish: (values: { domains: string[], triggers: string[], token: string }) => void - isCreation: boolean + isCreation: boolean, + watchList?: Watchlist, }) { const rdapEventNameTranslated = rdapEventNameTranslation() const rdapEventDetailTranslated = rdapEventDetailTranslation() @@ -59,6 +60,32 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: { ) } + const [triggersLoading, setTriggersLoading] = useState(false); + + const createTrigger = async (event: string) => { + if (isCreation) return + + setTriggersLoading(true); + await putWatchlistTrigger(watchList!.token, { // FIXME this 500s + watchList: watchList!['@id'], + event, + action: 'email', + }); + await putWatchlistTrigger(watchList!.token, { + watchList: watchList!['@id'], + event, + action: 'chat', + }); + setTriggersLoading(false); + }; + + const removeTrigger = async (event: string) => { + if (isCreation) return + + setTriggersLoading(true); + // TODO + }; + return (
({ value: e, title: rdapEventDetailTranslated[e as keyof typeof rdapEventDetailTranslated] || undefined, diff --git a/assets/pages/tracking/WatchlistPage.tsx b/assets/pages/tracking/WatchlistPage.tsx index 52904da..a334b54 100644 --- a/assets/pages/tracking/WatchlistPage.tsx +++ b/assets/pages/tracking/WatchlistPage.tsx @@ -21,18 +21,10 @@ interface FormValuesType { const getRequestDataFromForm = (values: FormValuesType) => { const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase()) - let triggers = values.triggers.map(t => ({event: t, action: 'email'})) - if (values.dsn !== undefined) { - triggers = [...triggers, ...values.triggers.map(t => ({ - event: t, - action: 'chat' - }))] - } return { name: values.name, domains: domainsURI, - triggers, connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined, dsn: values.dsn } @@ -91,7 +83,8 @@ export default function WatchlistPage() { {(connectors != null) && (watchlists != null) && watchlists.length > 0 && } diff --git a/assets/utils/api/index.ts b/assets/utils/api/index.ts index e09e14c..403a6ec 100644 --- a/assets/utils/api/index.ts +++ b/assets/utils/api/index.ts @@ -16,7 +16,7 @@ export type EventAction = | 'enum validation expiration' | string -export type TriggerAction = 'email' | string +export type TriggerAction = 'email' | 'chat' export interface Event { action: EventAction @@ -74,19 +74,26 @@ export interface User { roles: string[] } +export interface WatchlistTrigger { + event: EventAction + action: TriggerAction + watchList?: string +} + export interface WatchlistRequest { name?: string domains: string[] - triggers: Array<{ event: EventAction, action: TriggerAction }> + triggers?: Array connector?: string dsn?: string[] } export interface Watchlist { + '@id': string name?: string token: string domains: Domain[] - triggers?: Array<{ event: EventAction, action: string }> + triggers?: Array dsn?: string[] connector?: { id: string diff --git a/assets/utils/api/watchlist.ts b/assets/utils/api/watchlist.ts index eaa6648..1af0d5d 100644 --- a/assets/utils/api/watchlist.ts +++ b/assets/utils/api/watchlist.ts @@ -1,4 +1,4 @@ -import type { TrackedDomains, Watchlist, WatchlistRequest} from './index' +import type {TrackedDomains, Watchlist, WatchlistRequest, WatchlistTrigger} from './index' import {request} from './index' interface WatchlistList { @@ -56,3 +56,12 @@ export async function getTrackedDomainList(params: { page: number, itemsPerPage: }) return response.data } + +export async function putWatchlistTrigger(watchListToken: string, watchListTrigger: WatchlistTrigger): Promise { + const response = await request({ + method: 'PUT', + url: `watchlists/${watchListToken}/triggers`, + data: watchListTrigger, + }); + return response.data; +} diff --git a/src/Entity/WatchList.php b/src/Entity/WatchList.php index 66f4ed5..174eaf8 100644 --- a/src/Entity/WatchList.php +++ b/src/Entity/WatchList.php @@ -2,10 +2,12 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use App\Repository\WatchListRepository; @@ -90,6 +92,7 @@ use Symfony\Component\Uid\Uuid; security: 'object.user == user', name: 'update', processor: WatchListUpdateProcessor::class, + extraProperties: ['standard_put' => false], ), new Delete( security: 'object.user == user' @@ -101,10 +104,12 @@ class WatchList #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchLists')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] public ?User $user = null; + #[ORM\Id] #[ORM\Column(type: 'uuid')] #[Groups(['watchlist:item', 'watchlist:list', 'watchlist:token'])] private string $token; + /** * @var Collection */ diff --git a/src/Entity/WatchListTrigger.php b/src/Entity/WatchListTrigger.php index 162632f..3569096 100644 --- a/src/Entity/WatchListTrigger.php +++ b/src/Entity/WatchListTrigger.php @@ -3,7 +3,11 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Put; use App\Config\TriggerAction; use App\Repository\EventTriggerRepository; use Doctrine\ORM\Mapping as ORM; @@ -12,28 +16,45 @@ use Symfony\Component\Serializer\Attribute\Groups; #[ORM\Entity(repositoryClass: EventTriggerRepository::class)] #[ApiResource( uriTemplate: '/watchlists/{watchListId}/triggers/{action}/{event}', + operations: [ + new Get(), + new GetCollection( + uriTemplate: '/watchlists/{watchListId}/triggers', + uriVariables: [ + 'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class), + ], + ), + new Put( + uriTemplate: '/watchlists/{watchListId}/triggers', + uriVariables: [ + 'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class), + ], + ), + new Delete(), + ], uriVariables: [ 'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class), 'action' => 'action', 'event' => 'event', ], + security: 'object.getWatchList().user == user', )] class WatchListTrigger { #[ORM\Id] - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: false)] #[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])] - private ?string $event = null; + private ?string $event; #[ORM\Id] - #[ORM\ManyToOne(targetEntity: WatchList::class, cascade: ['persist'], inversedBy: 'watchListTriggers')] + #[ORM\ManyToOne(targetEntity: WatchList::class, inversedBy: 'watchListTriggers')] #[ORM\JoinColumn(referencedColumnName: 'token', nullable: false, onDelete: 'CASCADE')] - private ?WatchList $watchList = null; + private ?WatchList $watchList; #[ORM\Id] - #[ORM\Column(enumType: TriggerAction::class)] + #[ORM\Column(nullable: false, enumType: TriggerAction::class)] #[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])] - private ?TriggerAction $action = null; + private ?TriggerAction $action; public function getEvent(): ?string { diff --git a/src/Service/ChatNotificationService.php b/src/Service/ChatNotificationService.php index aed091b..a9c553a 100644 --- a/src/Service/ChatNotificationService.php +++ b/src/Service/ChatNotificationService.php @@ -22,7 +22,8 @@ readonly class ChatNotificationService public function sendChatNotification(WatchList $watchList, DomainWatchdogNotification $notification): void { $webhookDsn = $watchList->getWebhookDsn(); - if (null === $webhookDsn || 0 === count($webhookDsn)) { + + if (empty($webhookDsn)) { return; } diff --git a/src/State/WatchListUpdateProcessor.php b/src/State/WatchListUpdateProcessor.php index b7cdfe4..9c1395f 100644 --- a/src/State/WatchListUpdateProcessor.php +++ b/src/State/WatchListUpdateProcessor.php @@ -7,11 +7,13 @@ use ApiPlatform\Metadata\Post; use ApiPlatform\State\ProcessorInterface; use App\Entity\Domain; use App\Entity\WatchList; +use App\Entity\WatchListTrigger; use App\Notifier\TestChatNotification; use App\Repository\DomainRepository; use App\Service\ChatNotificationService; use App\Service\Connector\AbstractProvider; use App\Service\RDAPService; +use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -27,17 +29,18 @@ class WatchListUpdateProcessor implements ProcessorInterface { public function __construct( private readonly DomainRepository $domainRepository, - private readonly RDAPService $RDAPService, - private readonly KernelInterface $kernel, - private readonly Security $security, - private readonly RateLimiterFactory $rdapRequestsLimiter, - private readonly ParameterBagInterface $parameterBag, + private readonly RDAPService $RDAPService, + private readonly KernelInterface $kernel, + private readonly Security $security, + private readonly RateLimiterFactory $rdapRequestsLimiter, + private readonly ParameterBagInterface $parameterBag, #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] - private readonly ProcessorInterface $persistProcessor, - private readonly LoggerInterface $logger, + private readonly ProcessorInterface $persistProcessor, + private readonly LoggerInterface $logger, private readonly ChatNotificationService $chatNotificationService, #[Autowire(service: 'service_container')] - private readonly ContainerInterface $locator, + private readonly ContainerInterface $locator, + private readonly EntityManagerInterface $entityManager, ) {} @@ -50,14 +53,15 @@ class WatchListUpdateProcessor implements ProcessorInterface */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { - dd($data); $user = $this->security->getUser(); + $data->setUser($user); if ($this->parameterBag->get('limited_features')) { if ($data->getDomains()->count() > (int) $this->parameterBag->get('limit_max_watchlist_domains')) { $this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [ 'username' => $user->getUserIdentifier(), ]); + throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist'); } @@ -85,6 +89,7 @@ class WatchListUpdateProcessor implements ProcessorInterface $this->logger->notice('User {username} tried to update a Watchlist. The maximum number of webhooks has been reached.', [ 'username' => $user->getUserIdentifier(), ]); + throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist'); } } @@ -97,6 +102,7 @@ class WatchListUpdateProcessor implements ProcessorInterface 'username' => $user->getUserIdentifier(), 'connector' => $connector->getId(), ]); + throw new AccessDeniedHttpException('You cannot create a Watchlist with a connector that does not belong to you'); } @@ -104,6 +110,7 @@ class WatchListUpdateProcessor implements ProcessorInterface foreach ($data->getDomains()->getIterator() as $domain) { if ($domain->getDeleted()) { $ldhName = $domain->getLdhName(); + throw new BadRequestHttpException("To add a connector, no domain in this Watchlist must have already expired ($ldhName)"); } } @@ -120,6 +127,7 @@ class WatchListUpdateProcessor implements ProcessorInterface 'username' => $user->getUserIdentifier(), 'connector' => $connector->getId(), ]); + throw new BadRequestHttpException('This connector does not support all TLDs in this Watchlist'); } }