diff --git a/backend/app/DomainObjects/Generated/WebhookDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/WebhookDomainObjectAbstract.php index 5527c28818..660f7a66f5 100644 --- a/backend/app/DomainObjects/Generated/WebhookDomainObjectAbstract.php +++ b/backend/app/DomainObjects/Generated/WebhookDomainObjectAbstract.php @@ -13,6 +13,7 @@ abstract class WebhookDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr final public const ID = 'id'; final public const USER_ID = 'user_id'; final public const EVENT_ID = 'event_id'; + final public const ORGANIZER_ID = 'organizer_id'; final public const ACCOUNT_ID = 'account_id'; final public const URL = 'url'; final public const EVENT_TYPES = 'event_types'; @@ -27,7 +28,8 @@ abstract class WebhookDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr protected int $id; protected int $user_id; - protected int $event_id; + protected ?int $event_id = null; + protected ?int $organizer_id = null; protected int $account_id; protected string $url; protected array|string $event_types; @@ -46,6 +48,7 @@ public function toArray(): array 'id' => $this->id ?? null, 'user_id' => $this->user_id ?? null, 'event_id' => $this->event_id ?? null, + 'organizer_id' => $this->organizer_id ?? null, 'account_id' => $this->account_id ?? null, 'url' => $this->url ?? null, 'event_types' => $this->event_types ?? null, @@ -82,17 +85,28 @@ public function getUserId(): int return $this->user_id; } - public function setEventId(int $event_id): self + public function setEventId(?int $event_id): self { $this->event_id = $event_id; return $this; } - public function getEventId(): int + public function getEventId(): ?int { return $this->event_id; } + public function setOrganizerId(?int $organizer_id): self + { + $this->organizer_id = $organizer_id; + return $this; + } + + public function getOrganizerId(): ?int + { + return $this->organizer_id; + } + public function setAccountId(int $account_id): self { $this->account_id = $account_id; diff --git a/backend/app/Http/Actions/Organizers/Webhooks/CreateOrganizerWebhookAction.php b/backend/app/Http/Actions/Organizers/Webhooks/CreateOrganizerWebhookAction.php new file mode 100644 index 0000000000..603bfde2ad --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/CreateOrganizerWebhookAction.php @@ -0,0 +1,43 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $webhook = $this->createWebhookHandler->handle( + new CreateWebhookDTO( + url: $request->validated('url'), + eventTypes: $request->validated('event_types'), + eventId: null, + organizerId: $organizerId, + userId: $this->getAuthenticatedUser()->getId(), + accountId: $this->getAuthenticatedAccountId(), + status: WebhookStatus::fromName($request->validated('status')), + ) + ); + + return $this->resourceResponse( + resource: WebhookResource::class, + data: $webhook + ); + } +} diff --git a/backend/app/Http/Actions/Organizers/Webhooks/DeleteOrganizerWebhookAction.php b/backend/app/Http/Actions/Organizers/Webhooks/DeleteOrganizerWebhookAction.php new file mode 100644 index 0000000000..cd157307fa --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/DeleteOrganizerWebhookAction.php @@ -0,0 +1,30 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $this->deleteWebhookHandler->handle( + webhookId: $webhookId, + eventId: null, + organizerId: $organizerId, + ); + + return $this->deletedResponse(); + } +} diff --git a/backend/app/Http/Actions/Organizers/Webhooks/EditOrganizerWebhookAction.php b/backend/app/Http/Actions/Organizers/Webhooks/EditOrganizerWebhookAction.php new file mode 100644 index 0000000000..6bc51334cb --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/EditOrganizerWebhookAction.php @@ -0,0 +1,44 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $webhook = $this->editWebhookHandler->handle( + new EditWebhookDTO( + webhookId: $webhookId, + url: $request->validated('url'), + eventTypes: $request->validated('event_types'), + eventId: null, + organizerId: $organizerId, + userId: $this->getAuthenticatedUser()->getId(), + accountId: $this->getAuthenticatedAccountId(), + status: WebhookStatus::fromName($request->validated('status')), + ) + ); + + return $this->resourceResponse( + resource: WebhookResource::class, + data: $webhook + ); + } +} diff --git a/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookAction.php b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookAction.php new file mode 100644 index 0000000000..cb7ea4ad25 --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookAction.php @@ -0,0 +1,34 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $webhook = $this->getWebhookHandler->handle( + webhookId: $webhookId, + eventId: null, + organizerId: $organizerId + ); + + return $this->resourceResponse( + resource: WebhookResource::class, + data: $webhook + ); + } +} diff --git a/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookLogsAction.php b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookLogsAction.php new file mode 100644 index 0000000000..720728f6b2 --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhookLogsAction.php @@ -0,0 +1,39 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $webhookLogs = $this->getWebhookLogsHandler->handle( + webhookId: $webhookId, + eventId: null, + organizerId: $organizerId, + ); + + $webhookLogs = $webhookLogs->sortBy(function (WebhookLogDomainObject $webhookLog) { + return $webhookLog->getId(); + }, SORT_REGULAR, true); + + return $this->resourceResponse( + resource: WebhookLogResource::class, + data: $webhookLogs + ); + } +} diff --git a/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhooksAction.php b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhooksAction.php new file mode 100644 index 0000000000..f6e7e51ef1 --- /dev/null +++ b/backend/app/Http/Actions/Organizers/Webhooks/GetOrganizerWebhooksAction.php @@ -0,0 +1,34 @@ +isActionAuthorized($organizerId, OrganizerDomainObject::class); + + $webhooks = $this->getWebhooksHandler->handler( + accountId: $this->getAuthenticatedAccountId(), + eventId: null, + organizerId: $organizerId + ); + + return $this->resourceResponse( + resource: WebhookResource::class, + data: $webhooks + ); + } +} diff --git a/backend/app/Http/Actions/Webhooks/CreateWebhookAction.php b/backend/app/Http/Actions/Webhooks/CreateWebhookAction.php index 7d01420015..a61d3acf62 100644 --- a/backend/app/Http/Actions/Webhooks/CreateWebhookAction.php +++ b/backend/app/Http/Actions/Webhooks/CreateWebhookAction.php @@ -27,10 +27,10 @@ public function __invoke(int $eventId, UpsertWebhookRequest $request): JsonRespo new CreateWebhookDTO( url: $request->validated('url'), eventTypes: $request->validated('event_types'), - eventId: $eventId, userId: $this->getAuthenticatedUser()->getId(), accountId: $this->getAuthenticatedAccountId(), status: WebhookStatus::fromName($request->validated('status')), + eventId: $eventId, ) ); diff --git a/backend/app/Http/Actions/Webhooks/DeleteWebhookAction.php b/backend/app/Http/Actions/Webhooks/DeleteWebhookAction.php index 508c0fa3d0..b807d47195 100644 --- a/backend/app/Http/Actions/Webhooks/DeleteWebhookAction.php +++ b/backend/app/Http/Actions/Webhooks/DeleteWebhookAction.php @@ -20,8 +20,8 @@ public function __invoke(int $eventId, int $webhookId): Response $this->isActionAuthorized($eventId, EventDomainObject::class); $this->deleteWebhookHandler->handle( - $eventId, - $webhookId, + webhookId: $webhookId, + eventId: $eventId, ); return $this->deletedResponse(); diff --git a/backend/app/Jobs/Event/Webhook/DispatchEventWebhookJob.php b/backend/app/Jobs/Event/Webhook/DispatchEventWebhookJob.php new file mode 100644 index 0000000000..0f73e76e8f --- /dev/null +++ b/backend/app/Jobs/Event/Webhook/DispatchEventWebhookJob.php @@ -0,0 +1,31 @@ +dispatchEventWebhook( + eventType: $this->eventType, + eventId: $this->eventId, + ); + } +} diff --git a/backend/app/Models/Organizer.php b/backend/app/Models/Organizer.php index b174f9727d..9eb57f2bdc 100644 --- a/backend/app/Models/Organizer.php +++ b/backend/app/Models/Organizer.php @@ -21,4 +21,9 @@ public function organizer_settings(): HasOne { return $this->hasOne(OrganizerSetting::class); } + + public function webhooks(): HasMany + { + return $this->hasMany(Webhook::class); + } } diff --git a/backend/app/Models/Webhook.php b/backend/app/Models/Webhook.php index 7d2520b752..1c85c17c4b 100644 --- a/backend/app/Models/Webhook.php +++ b/backend/app/Models/Webhook.php @@ -32,6 +32,11 @@ public function event(): BelongsTo return $this->belongsTo(Event::class); } + public function organizer(): BelongsTo + { + return $this->belongsTo(Organizer::class); + } + public function account(): BelongsTo { return $this->belongsTo(Account::class); diff --git a/backend/app/Repository/Eloquent/WebhookRepository.php b/backend/app/Repository/Eloquent/WebhookRepository.php index ec2778eaea..dffca0b3b5 100644 --- a/backend/app/Repository/Eloquent/WebhookRepository.php +++ b/backend/app/Repository/Eloquent/WebhookRepository.php @@ -2,9 +2,11 @@ namespace HiEvents\Repository\Eloquent; +use HiEvents\DomainObjects\Status\WebhookStatus; use HiEvents\DomainObjects\WebhookDomainObject; use HiEvents\Models\Webhook; use HiEvents\Repository\Interfaces\WebhookRepositoryInterface; +use Illuminate\Support\Collection; /** * @extends BaseRepository @@ -20,4 +22,24 @@ public function getDomainObject(): string { return WebhookDomainObject::class; } + + public function findEnabledByEventId(int $eventId): Collection + { + $results = $this->model::query() + ->where('status', WebhookStatus::ENABLED->name) + ->where(function ($query) use ($eventId) { + $query->where('event_id', $eventId) + ->orWhere('organizer_id', function ($subquery) use ($eventId) { + $subquery->select('organizer_id') + ->from('events') + ->where('id', $eventId) + ->limit(1); + }); + }) + ->get(); + + $this->resetModel(); + + return $this->handleResults($results); + } } diff --git a/backend/app/Repository/Interfaces/WebhookRepositoryInterface.php b/backend/app/Repository/Interfaces/WebhookRepositoryInterface.php index 49a0dac742..a2fd914f14 100644 --- a/backend/app/Repository/Interfaces/WebhookRepositoryInterface.php +++ b/backend/app/Repository/Interfaces/WebhookRepositoryInterface.php @@ -3,10 +3,12 @@ namespace HiEvents\Repository\Interfaces; use HiEvents\DomainObjects\WebhookDomainObject; +use Illuminate\Support\Collection; /** * @extends RepositoryInterface */ interface WebhookRepositoryInterface extends RepositoryInterface { + public function findEnabledByEventId(int $eventId): Collection; } diff --git a/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php b/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php index 0ebd327e10..7b86a00011 100644 --- a/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php +++ b/backend/app/Services/Application/Handlers/Event/CreateEventHandler.php @@ -9,8 +9,10 @@ use HiEvents\Exceptions\OrganizerNotFoundException; use HiEvents\Services\Application\Handlers\Event\DTO\CreateEventDTO; use HiEvents\Services\Domain\Event\CreateEventService; -use HiEvents\Services\Domain\Organizer\OrganizerFetchService; use HiEvents\Services\Domain\ProductCategory\CreateProductCategoryService; +use HiEvents\Services\Domain\Organizer\OrganizerFetchService; +use HiEvents\Jobs\Event\Webhook\DispatchEventWebhookJob; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use Illuminate\Database\DatabaseManager; use Throwable; @@ -65,6 +67,11 @@ private function createEvent(CreateEventDTO $eventData): EventDomainObject $this->createProductCategoryService->createDefaultProductCategory($newEvent); + DispatchEventWebhookJob::dispatch( + $newEvent->getId(), + DomainEventType::EVENT_CREATED, + ); + return $newEvent; } } diff --git a/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php b/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php index 0ff2ded0b7..8f284ff631 100644 --- a/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php +++ b/backend/app/Services/Application/Handlers/Event/UpdateEventHandler.php @@ -12,6 +12,8 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Application\Handlers\Event\DTO\UpdateEventDTO; use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService; +use HiEvents\Jobs\Event\Webhook\DispatchEventWebhookJob; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use Illuminate\Database\DatabaseManager; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Throwable; @@ -97,6 +99,11 @@ private function getUpdateEvent(UpdateEventDTO $eventData): EventDomainObject $this->dispatcher->dispatchEvent(new EventUpdateEvent($event)); + DispatchEventWebhookJob::dispatch( + $event->getId(), + DomainEventType::EVENT_UPDATED, + ); + return $event; } diff --git a/backend/app/Services/Application/Handlers/Event/UpdateEventStatusHandler.php b/backend/app/Services/Application/Handlers/Event/UpdateEventStatusHandler.php index 175e1e3045..4d43879404 100644 --- a/backend/app/Services/Application/Handlers/Event/UpdateEventStatusHandler.php +++ b/backend/app/Services/Application/Handlers/Event/UpdateEventStatusHandler.php @@ -7,6 +7,9 @@ use HiEvents\Repository\Interfaces\AccountRepositoryInterface; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Services\Application\Handlers\Event\DTO\UpdateEventStatusDTO; +use HiEvents\DomainObjects\Status\EventStatus; +use HiEvents\Jobs\Event\Webhook\DispatchEventWebhookJob; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use Illuminate\Database\DatabaseManager; use Psr\Log\LoggerInterface; use Throwable; @@ -60,9 +63,20 @@ private function updateEventStatus(UpdateEventStatusDTO $updateEventStatusDTO): 'status' => $updateEventStatusDTO->status ]); - return $this->eventRepository->findFirstWhere([ + $event = $this->eventRepository->findFirstWhere([ 'id' => $updateEventStatusDTO->eventId, 'account_id' => $updateEventStatusDTO->accountId, ]); + + $eventType = $updateEventStatusDTO->status === EventStatus::ARCHIVED->name + ? DomainEventType::EVENT_ARCHIVED + : DomainEventType::EVENT_UPDATED; + + DispatchEventWebhookJob::dispatch( + $event->getId(), + $eventType, + ); + + return $event; } } diff --git a/backend/app/Services/Application/Handlers/Webhook/CreateWebhookHandler.php b/backend/app/Services/Application/Handlers/Webhook/CreateWebhookHandler.php index 35160fffb0..a8f283a164 100644 --- a/backend/app/Services/Application/Handlers/Webhook/CreateWebhookHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/CreateWebhookHandler.php @@ -31,6 +31,7 @@ private function createWebhook(CreateWebhookDTO $upsertWebhookDTO): WebhookDomai ->setUrl($upsertWebhookDTO->url) ->setEventTypes($upsertWebhookDTO->eventTypes) ->setEventId($upsertWebhookDTO->eventId) + ->setOrganizerId($upsertWebhookDTO->organizerId) ->setUserId($upsertWebhookDTO->userId) ->setAccountId($upsertWebhookDTO->accountId) ->setStatus($upsertWebhookDTO->status->value); diff --git a/backend/app/Services/Application/Handlers/Webhook/DTO/CreateWebhookDTO.php b/backend/app/Services/Application/Handlers/Webhook/DTO/CreateWebhookDTO.php index 2c834e336a..1ee6d9e216 100644 --- a/backend/app/Services/Application/Handlers/Webhook/DTO/CreateWebhookDTO.php +++ b/backend/app/Services/Application/Handlers/Webhook/DTO/CreateWebhookDTO.php @@ -10,10 +10,11 @@ class CreateWebhookDTO extends BaseDTO public function __construct( public string $url, public array $eventTypes, - public int $eventId, public int $userId, public int $accountId, public WebhookStatus $status, + public ?int $eventId = null, + public ?int $organizerId = null, ) { } diff --git a/backend/app/Services/Application/Handlers/Webhook/DTO/EditWebhookDTO.php b/backend/app/Services/Application/Handlers/Webhook/DTO/EditWebhookDTO.php index 0c3fdf9e86..5c23e15615 100644 --- a/backend/app/Services/Application/Handlers/Webhook/DTO/EditWebhookDTO.php +++ b/backend/app/Services/Application/Handlers/Webhook/DTO/EditWebhookDTO.php @@ -10,19 +10,21 @@ public function __construct( public int $webhookId, string $url, array $eventTypes, - int $eventId, int $userId, int $accountId, WebhookStatus $status, + ?int $eventId = null, + ?int $organizerId = null, ) { parent::__construct( url: $url, eventTypes: $eventTypes, - eventId: $eventId, userId: $userId, accountId: $accountId, status: $status, + eventId: $eventId, + organizerId: $organizerId, ); } } diff --git a/backend/app/Services/Application/Handlers/Webhook/DeleteWebhookHandler.php b/backend/app/Services/Application/Handlers/Webhook/DeleteWebhookHandler.php index 6fbc127a18..6a8edb9096 100644 --- a/backend/app/Services/Application/Handlers/Webhook/DeleteWebhookHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/DeleteWebhookHandler.php @@ -17,28 +17,29 @@ public function __construct( { } - public function handle(int $eventId, int $webhookId): void + public function handle(int $webhookId, ?int $eventId = null, ?int $organizerId = null): void { - $this->databaseManager->transaction(function () use ($eventId, $webhookId) { - $webhook = $this->webhookRepository->findFirstWhere([ - 'id' => $webhookId, - 'event_id' => $eventId, - ]); + $this->databaseManager->transaction(function () use ($eventId, $webhookId, $organizerId) { + $where = ['id' => $webhookId]; + if ($eventId !== null) { + $where['event_id'] = $eventId; + } + if ($organizerId !== null) { + $where['organizer_id'] = $organizerId; + } + + $webhook = $this->webhookRepository->findFirstWhere($where); if (!$webhook) { throw new ResourceNotFoundException(__( - key: 'Webhook not found for ID: :webhookId and event ID: :eventId', + key: 'Webhook not found for ID: :webhookId', replace: [ 'webhookId' => $webhookId, - 'eventId' => $eventId, ] )); } - $this->webhookRepository->deleteWhere([ - 'id' => $webhookId, - 'event_id' => $eventId, - ]); + $this->webhookRepository->deleteWhere($where); $this->webhookLogRepository ->deleteOldLogs($webhookId, numberToKeep: 0); diff --git a/backend/app/Services/Application/Handlers/Webhook/EditWebhookHandler.php b/backend/app/Services/Application/Handlers/Webhook/EditWebhookHandler.php index 3f3c86efe8..9d76c441ed 100644 --- a/backend/app/Services/Application/Handlers/Webhook/EditWebhookHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/EditWebhookHandler.php @@ -20,18 +20,22 @@ public function __construct( public function handle(EditWebhookDTO $dto): WebhookDomainObject { return $this->databaseManager->transaction(function () use ($dto) { + $where = ['id' => $dto->webhookId]; + if ($dto->eventId !== null) { + $where['event_id'] = $dto->eventId; + } + if ($dto->organizerId !== null) { + $where['organizer_id'] = $dto->organizerId; + } + /** @var WebhookDomainObject $webhook */ - $webhook = $this->webhookRepository->findFirstWhere([ - 'id' => $dto->webhookId, - 'event_id' => $dto->eventId, - ]); + $webhook = $this->webhookRepository->findFirstWhere($where); if (!$webhook) { throw new ResourceNotFoundException(__( - key: 'Webhook not found for ID: :webhookId and event ID: :eventId', + key: 'Webhook not found for ID: :webhookId', replace: [ 'webhookId' => $dto->webhookId, - 'eventId' => $dto->eventId, ] )); } diff --git a/backend/app/Services/Application/Handlers/Webhook/GetWebhookHandler.php b/backend/app/Services/Application/Handlers/Webhook/GetWebhookHandler.php index 8d4eafc0d9..2d63ff111f 100644 --- a/backend/app/Services/Application/Handlers/Webhook/GetWebhookHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/GetWebhookHandler.php @@ -13,13 +13,18 @@ public function __construct( { } - public function handle(int $eventId, int $webhookId): WebhookDomainObject + public function handle(int $webhookId, ?int $eventId = null, ?int $organizerId = null): WebhookDomainObject { + $where = ['id' => $webhookId]; + if ($eventId !== null) { + $where['event_id'] = $eventId; + } + if ($organizerId !== null) { + $where['organizer_id'] = $organizerId; + } + return $this->webhookRepository->findFirstWhere( - where: [ - 'id' => $webhookId, - 'event_id' => $eventId, - ] + where: $where ); } } diff --git a/backend/app/Services/Application/Handlers/Webhook/GetWebhookLogsHandler.php b/backend/app/Services/Application/Handlers/Webhook/GetWebhookLogsHandler.php index e3e6a37828..80cd5a3194 100644 --- a/backend/app/Services/Application/Handlers/Webhook/GetWebhookLogsHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/GetWebhookLogsHandler.php @@ -16,13 +16,18 @@ public function __construct( { } - public function handle(int $eventId, int $webhookId): LengthAwarePaginator + public function handle(int $webhookId, ?int $eventId = null, ?int $organizerId = null): LengthAwarePaginator { + $where = ['id' => $webhookId]; + if ($eventId !== null) { + $where['event_id'] = $eventId; + } + if ($organizerId !== null) { + $where['organizer_id'] = $organizerId; + } + $webhook = $this->webhookRepository->findFirstWhere( - where: [ - 'id' => $webhookId, - 'event_id' => $eventId, - ] + where: $where ); if (!$webhook) { diff --git a/backend/app/Services/Application/Handlers/Webhook/GetWebhooksHandler.php b/backend/app/Services/Application/Handlers/Webhook/GetWebhooksHandler.php index 1ae708df0a..c68b99ffad 100644 --- a/backend/app/Services/Application/Handlers/Webhook/GetWebhooksHandler.php +++ b/backend/app/Services/Application/Handlers/Webhook/GetWebhooksHandler.php @@ -14,13 +14,18 @@ public function __construct( { } - public function handler(int $accountId, int $eventId): Collection + public function handler(int $accountId, ?int $eventId = null, ?int $organizerId = null): Collection { + $where = ['account_id' => $accountId]; + if ($eventId !== null) { + $where['event_id'] = $eventId; + } + if ($organizerId !== null) { + $where['organizer_id'] = $organizerId; + } + return $this->webhookRepository->findWhere( - where: [ - 'account_id' => $accountId, - 'event_id' => $eventId, - ], + where: $where, orderAndDirections: [ new OrderAndDirection('id', OrderAndDirection::DIRECTION_DESC), ] diff --git a/backend/app/Services/Domain/CreateWebhookService.php b/backend/app/Services/Domain/CreateWebhookService.php index 7ea1b663a7..8d2701ad0f 100644 --- a/backend/app/Services/Domain/CreateWebhookService.php +++ b/backend/app/Services/Domain/CreateWebhookService.php @@ -14,9 +14,7 @@ class CreateWebhookService public function __construct( private readonly WebhookRepositoryInterface $webhookRepository, private readonly LoggerInterface $logger, - ) - { - } + ) {} public function createWebhook(WebhookDomainObject $webhookDomainObject): WebhookDomainObject { @@ -26,6 +24,7 @@ public function createWebhook(WebhookDomainObject $webhookDomainObject): Webhook WebhookDomainObjectAbstract::ACCOUNT_ID => $webhookDomainObject->getAccountId(), WebhookDomainObjectAbstract::STATUS => $webhookDomainObject->getStatus(), WebhookDomainObjectAbstract::EVENT_ID => $webhookDomainObject->getEventId(), + WebhookDomainObjectAbstract::ORGANIZER_ID => $webhookDomainObject->getOrganizerId(), WebhookDomainObjectAbstract::USER_ID => $webhookDomainObject->getUserId(), WebhookDomainObjectAbstract::SECRET => Str::random(32), ]); diff --git a/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php b/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php index dbaf38aecd..690dadd394 100644 --- a/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php +++ b/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php @@ -12,6 +12,10 @@ enum DomainEventType: string case PRODUCT_UPDATED = 'product.updated'; case PRODUCT_DELETED = 'product.deleted'; + case EVENT_CREATED = 'event.created'; + case EVENT_UPDATED = 'event.updated'; + case EVENT_ARCHIVED = 'event.archived'; + case ORDER_CREATED = 'order.created'; case ORDER_UPDATED = 'order.updated'; case ORDER_MARKED_AS_PAID = 'order.marked_as_paid'; diff --git a/backend/app/Services/Infrastructure/Webhook/WebhookDispatchService.php b/backend/app/Services/Infrastructure/Webhook/WebhookDispatchService.php index 5790c015c1..25fe3369ed 100644 --- a/backend/app/Services/Infrastructure/Webhook/WebhookDispatchService.php +++ b/backend/app/Services/Infrastructure/Webhook/WebhookDispatchService.php @@ -6,7 +6,6 @@ use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\DomainObjects\ProductPriceDomainObject; use HiEvents\DomainObjects\QuestionAndAnswerViewDomainObject; -use HiEvents\DomainObjects\Status\WebhookStatus; use HiEvents\DomainObjects\TaxAndFeesDomainObject; use HiEvents\DomainObjects\WebhookDomainObject; use HiEvents\Repository\Eloquent\Value\Relationship; @@ -15,13 +14,14 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Repository\Interfaces\WebhookRepositoryInterface; +use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Resources\Attendee\AttendeeResource; +use HiEvents\Resources\Event\EventResource; use HiEvents\Resources\CheckInList\AttendeeCheckInResource; use HiEvents\Resources\Order\OrderResource; use HiEvents\Resources\Product\ProductResource; use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Collection; use Psr\Log\LoggerInterface; use Spatie\WebhookServer\WebhookCall; @@ -34,10 +34,22 @@ public function __construct( private readonly ProductRepositoryInterface $productRepository, private readonly AttendeeRepositoryInterface $attendeeRepository, private readonly AttendeeCheckInRepositoryInterface $attendeeCheckInRepository, + private readonly EventRepositoryInterface $eventRepository, ) { } + public function dispatchEventWebhook(DomainEventType $eventType, int $eventId): void + { + $event = $this->eventRepository->findById($eventId); + + $this->dispatchWebhook( + eventType: $eventType, + payload: new EventResource($event), + eventId: $eventId, + ); + } + public function dispatchAttendeeWebhook(DomainEventType $eventType, int $attendeeId): void { $attendee = $this->attendeeRepository @@ -132,11 +144,7 @@ public function dispatchOrderWebhook(DomainEventType $eventType, int $orderId): private function dispatchWebhook(DomainEventType $eventType, JsonResource $payload, int $eventId): void { - /** @var Collection $webhooks */ - $webhooks = $this->webhookRepository->findWhere([ - 'event_id' => $eventId, - 'status' => WebhookStatus::ENABLED->name, - ]) + $webhooks = $this->webhookRepository->findEnabledByEventId($eventId) ->filter(fn(WebhookDomainObject $webhook) => in_array($eventType->value, $webhook->getEventTypes(), true)); foreach ($webhooks as $webhook) { diff --git a/backend/app/Services/Infrastructure/Webhook/WebhookResponseHandlerService.php b/backend/app/Services/Infrastructure/Webhook/WebhookResponseHandlerService.php index 1f1bd332c1..5c2ccfd560 100644 --- a/backend/app/Services/Infrastructure/Webhook/WebhookResponseHandlerService.php +++ b/backend/app/Services/Infrastructure/Webhook/WebhookResponseHandlerService.php @@ -15,9 +15,7 @@ public function __construct( private readonly LoggerInterface $logger, private readonly WebhookLogRepositoryInterface $webhookLogRepository, private readonly DatabaseManager $databaseManager, - ) - { - } + ) {} public function handleResponse( int $eventId, @@ -25,12 +23,10 @@ public function handleResponse( string $eventType, array $payload, ?Response $response - ): void - { + ): void { $this->databaseManager->transaction(function () use ($payload, $eventType, $eventId, $webhookId, $response) { $webhook = $this->webhookRepository->findFirstWhere([ 'id' => $webhookId, - 'event_id' => $eventId, ]); if (!$webhook) { @@ -49,8 +45,8 @@ public function handleResponse( ], where: [ 'id' => $webhookId, - 'event_id' => $eventId, - ]); + ] + ); $this->webhookLogRepository->create([ 'webhook_id' => $webhook->getId(), diff --git a/backend/database/migrations/2026_02_22_115000_add_organizer_id_to_webhooks_table.php b/backend/database/migrations/2026_02_22_115000_add_organizer_id_to_webhooks_table.php new file mode 100644 index 0000000000..fba97e757c --- /dev/null +++ b/backend/database/migrations/2026_02_22_115000_add_organizer_id_to_webhooks_table.php @@ -0,0 +1,24 @@ +foreignId('organizer_id')->nullable()->constrained()->onDelete('cascade'); + $table->foreignId('event_id')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('webhooks', static function (Blueprint $table) { + $table->dropForeign(['organizer_id']); + $table->dropColumn('organizer_id'); + $table->foreignId('event_id')->nullable(false)->change(); + }); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 61c631ef32..09ca424e49 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -115,6 +115,12 @@ use HiEvents\Http\Actions\Organizers\Settings\PartialUpdateOrganizerSettingsAction; use HiEvents\Http\Actions\Organizers\Stats\GetOrganizerStatsAction; use HiEvents\Http\Actions\Organizers\UpdateOrganizerStatusAction; +use HiEvents\Http\Actions\Organizers\Webhooks\CreateOrganizerWebhookAction; +use HiEvents\Http\Actions\Organizers\Webhooks\DeleteOrganizerWebhookAction; +use HiEvents\Http\Actions\Organizers\Webhooks\EditOrganizerWebhookAction; +use HiEvents\Http\Actions\Organizers\Webhooks\GetOrganizerWebhookAction; +use HiEvents\Http\Actions\Organizers\Webhooks\GetOrganizerWebhookLogsAction; +use HiEvents\Http\Actions\Organizers\Webhooks\GetOrganizerWebhooksAction; use HiEvents\Http\Actions\ProductCategories\CreateProductCategoryAction; use HiEvents\Http\Actions\ProductCategories\DeleteProductCategoryAction; use HiEvents\Http\Actions\ProductCategories\EditProductCategoryAction; @@ -275,6 +281,12 @@ function (Router $router): void { $router->patch('/organizers/{organizer_id}/settings', PartialUpdateOrganizerSettingsAction::class); $router->get('/organizers/{organizer_id}/reports/{report_type}', GetOrganizerReportAction::class); $router->get('/organizers/{organizer_id}/reports/{report_type}/export', ExportOrganizerReportAction::class); + $router->post('/organizers/{organizer_id}/webhooks', CreateOrganizerWebhookAction::class); + $router->get('/organizers/{organizer_id}/webhooks', GetOrganizerWebhooksAction::class); + $router->put('/organizers/{organizer_id}/webhooks/{webhook_id}', EditOrganizerWebhookAction::class); + $router->get('/organizers/{organizer_id}/webhooks/{webhook_id}', GetOrganizerWebhookAction::class); + $router->delete('/organizers/{organizer_id}/webhooks/{webhook_id}', DeleteOrganizerWebhookAction::class); + $router->get('/organizers/{organizer_id}/webhooks/{webhook_id}/logs', GetOrganizerWebhookLogsAction::class); // Email Templates - Organizer level $router->get('/organizers/{organizerId}/email-templates', GetOrganizerEmailTemplatesAction::class); diff --git a/frontend/src/api/organizer-webhook.client.ts b/frontend/src/api/organizer-webhook.client.ts new file mode 100644 index 0000000000..574f2e511b --- /dev/null +++ b/frontend/src/api/organizer-webhook.client.ts @@ -0,0 +1,34 @@ +import {GenericDataResponse, IdParam, Webhook, WebhookLog} from "../types"; +import {api} from "./client"; + +export interface OrganizerWebhookRequest { + url: string; + event_types: string[]; + status: 'ENABLED' | 'PAUSED'; +} + +export const organizerWebhookClient = { + get: async (organizerId: IdParam, webhookId: IdParam) => { + return await api.get>(`organizers/${organizerId}/webhooks/${webhookId}`); + }, + + create: async (organizerId: IdParam, webhook: OrganizerWebhookRequest) => { + return await api.post>(`organizers/${organizerId}/webhooks`, webhook); + }, + + all: async (organizerId: IdParam) => { + return await api.get>(`organizers/${organizerId}/webhooks`); + }, + + logs: async (organizerId: IdParam, webhookId: IdParam) => { + return await api.get>(`organizers/${organizerId}/webhooks/${webhookId}/logs`); + }, + + delete: async (organizerId: IdParam, webhookId: IdParam) => { + return await api.delete(`organizers/${organizerId}/webhooks/${webhookId}`); + }, + + update: async (organizerId: IdParam, webhookId: IdParam, webhook: OrganizerWebhookRequest) => { + return await api.put>(`organizers/${organizerId}/webhooks/${webhookId}`, webhook); + }, +} diff --git a/frontend/src/components/common/OrganizerWebhookTable/index.tsx b/frontend/src/components/common/OrganizerWebhookTable/index.tsx new file mode 100644 index 0000000000..a34f1ca8a0 --- /dev/null +++ b/frontend/src/components/common/OrganizerWebhookTable/index.tsx @@ -0,0 +1,291 @@ +import { + Anchor, + Badge, + Button, + Group, + Menu, + Paper, + Popover, + Stack, + Table as MantineTable, + Text, + Tooltip +} from '@mantine/core'; +import { + IconBolt, + IconClipboardList, + IconClockHour4, + IconDotsVertical, + IconPencil, + IconPlus, + IconTrash +} from '@tabler/icons-react'; +import { Table, TableHead } from '../Table'; +import classes from '../WebhookTable/WebhookTable.module.scss'; +import { IdParam, Webhook } from '../../../types'; +import { confirmationDialog } from '../../../utilites/confirmationDialog'; +import Truncate from '../Truncate'; +import { relativeDate } from "../../../utilites/dates.ts"; +import { useDisclosure } from "@mantine/hooks"; +import { useState } from "react"; +import { t, Trans } from "@lingui/macro"; +import { EditOrganizerWebhookModal } from "../../modals/EditOrganizerWebhookModal"; +import { useDeleteOrganizerWebhook } from "../../../mutations/useDeleteOrganizerWebhook.ts"; +import { useParams } from "react-router"; +import { showError, showSuccess } from "../../../utilites/notifications.tsx"; +import { NoResultsSplash } from "../NoResultsSplash"; +import { OrganizerWebhookLogsModal } from "../../modals/OrganizerWebhookLogsModal"; + +interface OrganizerWebhookTableProps { + webhooks: Webhook[]; + openCreateModal: () => void; +} + +export const OrganizerWebhookTable = ({ webhooks, openCreateModal }: OrganizerWebhookTableProps) => { + const { organizerId } = useParams(); + const [editModalOpen, { open: openEditModal, close: closeEditModal }] = useDisclosure(false); + const [logsModalOpen, { open: openLogsModal, close: closeLogsModal }] = useDisclosure(false); + const [selectedWebhookId, setSelectedWebhookId] = useState(); + const deleteMutation = useDeleteOrganizerWebhook(); + + const handleDelete = (webhookId: IdParam) => { + deleteMutation.mutate({ organizerId: organizerId as IdParam, webhookId }, { + onSuccess: () => showSuccess(t`Webhook deleted successfully`), + onError: (error) => showError(error.message) + }); + } + + const EventTypeDisplay = ({ webhook }: { webhook: Webhook }) => { + const eventTypes = webhook.event_types; + + if (!eventTypes || eventTypes.length === 0) { + return <>-; + } + + const eventCount = eventTypes.length; + + return ( +
+ + {eventTypes.map((type) => ( +
{type}
+ ))} +
+ } + > + + {eventCount > 1 ? {eventCount} events : eventTypes[0]} + + + + ); + }; + + const ActionMenu = ({ webhook }: { webhook: Webhook }) => ( + + + + + + + + {t`Manage`} + } + onClick={() => { + setSelectedWebhookId(webhook.id as IdParam); + openEditModal(); + }} + > + {t`Edit webhook`} + + } + onClick={() => { + setSelectedWebhookId(webhook.id as IdParam); + openLogsModal(); + }} + > + {t`View logs`} + + + {t`Danger zone`} + } + onClick={() => { + confirmationDialog( + t`Are you sure you want to delete this webhook?`, + () => handleDelete(webhook.id as IdParam) + ); + }} + > + {t`Delete webhook`} + + + + + ); + + + const ResponseDisplay = ({ webhook }: { webhook: Webhook }) => { + if (webhook.last_response_code === null || webhook.last_response_code === undefined) { + return ( + + + + {t`No responses yet`} + + + ); + } + + const isSuccess = (webhook.last_response_code >= 200 && webhook.last_response_code < 300) && webhook.last_response_code !== 0; + const statusColor = isSuccess ? 'green' : 'red'; + const statusText = isSuccess ? t`Success` : t`Error`; + + return ( + + + + } + > + {statusText} {webhook.last_response_code > 0 ? `- ${webhook.last_response_code}` : ''} + + + + + + + + {t`Response Details`} + + {webhook.last_response_code > 0 ? webhook.last_response_code : t`No response`} + + + + {webhook.last_response_body && ( + + + {webhook.last_response_body} + + + )} + + + + ); + }; + + if (webhooks.length === 0) { + return ( + + +

+ Webhooks instantly notify external services when events happen, like adding a new attendee + to your CRM or mailing list upon registration, ensuring seamless automation. +

+

+ Use third-party services like Zapier,{' '} + IFTTT or Make to + create custom workflows and automate tasks. +

+
+ + + )} + /> + ); + } + + return ( + <> + + + + {t`URL`} + {t`Event Types`} + {t`Status`} + {t`Last Response`} + {t`Last Triggered`} + + + + + {webhooks.map((webhook) => ( + + + + + + + + + + {webhook.status} + + + + + + + + {webhook.last_triggered_at ? relativeDate(webhook.last_triggered_at as string) : t`Never`} + + + + + + + ))} + +
+ {logsModalOpen && selectedWebhookId && ( + + )} + + {(editModalOpen && selectedWebhookId) && ( + + )} + + ); +}; diff --git a/frontend/src/components/forms/WebhookForm/index.tsx b/frontend/src/components/forms/WebhookForm/index.tsx index cb91629614..878240a526 100644 --- a/frontend/src/components/forms/WebhookForm/index.tsx +++ b/frontend/src/components/forms/WebhookForm/index.tsx @@ -1,8 +1,8 @@ -import {TextInput} from "@mantine/core"; -import {t} from "@lingui/macro"; -import {UseFormReturnType} from "@mantine/form"; -import {CustomSelect, ItemProps} from "../../common/CustomSelect"; -import {IconBolt, IconWebhook, IconWebhookOff} from "@tabler/icons-react"; +import { TextInput } from "@mantine/core"; +import { t } from "@lingui/macro"; +import { UseFormReturnType } from "@mantine/form"; +import { CustomSelect, ItemProps } from "../../common/CustomSelect"; +import { IconBolt, IconWebhook, IconWebhookOff } from "@tabler/icons-react"; interface WebhookFormProps { form: UseFormReturnType<{ @@ -12,16 +12,16 @@ interface WebhookFormProps { }>; } -export const WebhookForm = ({form}: WebhookFormProps) => { +export const WebhookForm = ({ form }: WebhookFormProps) => { const statusOptions: ItemProps[] = [ { - icon: , + icon: , label: t`Enabled`, value: 'ENABLED', description: t`Webhook will send notifications`, }, { - icon: , + icon: , label: t`Paused`, value: 'PAUSED', description: t`Webhook will not send notifications`, @@ -30,79 +30,97 @@ export const WebhookForm = ({form}: WebhookFormProps) => { const eventTypeOptions: ItemProps[] = [ { - icon: , + icon: , label: t`Product Created`, value: 'product.created', description: t`When a new product is created`, }, { - icon: , + icon: , + label: t`Event Created`, + value: 'event.created', + description: t`When a new event is created`, + }, + { + icon: , + label: t`Event Updated`, + value: 'event.updated', + description: t`When an event is updated`, + }, + { + icon: , + label: t`Event Archived`, + value: 'event.archived', + description: t`When an event is archived`, + }, + { + icon: , label: t`Product Updated`, value: 'product.updated', description: t`When a product is updated`, }, { - icon: , + icon: , label: t`Product Deleted`, value: 'product.deleted', description: t`When a product is deleted`, }, { - icon: , + icon: , label: t`Order Created`, value: 'order.created', description: t`When a new order is created`, }, { - icon: , + icon: , label: t`Order Updated`, value: 'order.updated', description: t`When an order is updated`, }, { - icon: , + icon: , label: t`Order Marked as Paid`, value: 'order.marked_as_paid', description: t`When an order is marked as paid`, }, { - icon: , + icon: , label: t`Order Refunded`, value: 'order.refunded', description: t`When an order is refunded`, }, { - icon: , + icon: , label: t`Order Cancelled`, value: 'order.cancelled', description: t`When an order is cancelled`, }, { - icon: , + icon: , label: t`Attendee Created`, value: 'attendee.created', description: t`When a new attendee is created`, }, { - icon: , + icon: , label: t`Attendee Updated`, value: 'attendee.updated', description: t`When an attendee is updated`, }, { - icon: , + icon: , label: t`Attendee Cancelled`, value: 'attendee.cancelled', description: t`When an attendee is cancelled`, }, { - icon: , + icon: , label: t`Check-in Created`, value: 'checkin.created', description: t`When an attendee is checked in`, }, { - icon: , + icon: , label: t`Check-in Deleted`, value: 'checkin.deleted', description: t`When a check-in is deleted`, diff --git a/frontend/src/components/layouts/OrganizerLayout/index.tsx b/frontend/src/components/layouts/OrganizerLayout/index.tsx index 3e66cb94ef..92dba3a636 100644 --- a/frontend/src/components/layouts/OrganizerLayout/index.tsx +++ b/frontend/src/components/layouts/OrganizerLayout/index.tsx @@ -13,51 +13,52 @@ import { IconPaint, IconSettings, IconShare, - IconUsersGroup + IconUsersGroup, + IconWebhook } from "@tabler/icons-react"; -import {t} from "@lingui/macro"; -import {BreadcrumbItem, NavItem} from "../AppLayout/types.ts"; +import { t } from "@lingui/macro"; +import { BreadcrumbItem, NavItem } from "../AppLayout/types.ts"; import AppLayout from "../AppLayout"; -import {NavLink, useLocation, useParams} from "react-router"; -import {Button, Modal, Stack, Text} from "@mantine/core"; -import {useGetOrganizer} from "../../../queries/useGetOrganizer.ts"; -import {useState} from "react"; -import {CreateEventModal} from "../../modals/CreateEventModal"; -import {TopBarButton} from "../../common/TopBarButton"; +import { NavLink, useLocation, useParams } from "react-router"; +import { Button, Modal, Stack, Text } from "@mantine/core"; +import { useGetOrganizer } from "../../../queries/useGetOrganizer.ts"; +import { useState } from "react"; +import { CreateEventModal } from "../../modals/CreateEventModal"; +import { TopBarButton } from "../../common/TopBarButton"; import classes from "./OrganizerLayout.module.scss"; -import {CalloutConfig, SidebarCalloutQueue} from "../../common/SidebarCallout/SidebarCalloutQueue"; -import {InviteUserModal} from "../../modals/InviteUserModal"; -import {useDisclosure, useMediaQuery} from "@mantine/hooks"; -import {SwitchOrganizerModal} from "../../modals/SwitchOrganizerModal"; -import {useGetOrganizers} from "../../../queries/useGetOrganizers.ts"; -import {useGetAccount} from "../../../queries/useGetAccount.ts"; -import {StripeConnectButton} from "../../common/StripeConnectButton"; -import {ShareModal} from "../../modals/ShareModal"; -import {organizerHomepageUrl} from "../../../utilites/urlHelper"; -import {useUpdateOrganizerStatus} from "../../../mutations/useUpdateOrganizerStatus.ts"; -import {confirmationDialog} from "../../../utilites/confirmationDialog.tsx"; -import {showError, showSuccess} from "../../../utilites/notifications.tsx"; -import {useResendEmailConfirmation} from "../../../mutations/useResendEmailConfirmation.ts"; -import {useGetMe} from "../../../queries/useGetMe.ts"; +import { CalloutConfig, SidebarCalloutQueue } from "../../common/SidebarCallout/SidebarCalloutQueue"; +import { InviteUserModal } from "../../modals/InviteUserModal"; +import { useDisclosure, useMediaQuery } from "@mantine/hooks"; +import { SwitchOrganizerModal } from "../../modals/SwitchOrganizerModal"; +import { useGetOrganizers } from "../../../queries/useGetOrganizers.ts"; +import { useGetAccount } from "../../../queries/useGetAccount.ts"; +import { StripeConnectButton } from "../../common/StripeConnectButton"; +import { ShareModal } from "../../modals/ShareModal"; +import { organizerHomepageUrl } from "../../../utilites/urlHelper"; +import { useUpdateOrganizerStatus } from "../../../mutations/useUpdateOrganizerStatus.ts"; +import { confirmationDialog } from "../../../utilites/confirmationDialog.tsx"; +import { showError, showSuccess } from "../../../utilites/notifications.tsx"; +import { useResendEmailConfirmation } from "../../../mutations/useResendEmailConfirmation.ts"; +import { useGetMe } from "../../../queries/useGetMe.ts"; const OrganizerLayout = () => { - const {organizerId} = useParams(); + const { organizerId } = useParams(); const location = useLocation(); - const {data: organizer} = useGetOrganizer(organizerId); + const { data: organizer } = useGetOrganizer(organizerId); const [showCreateEventModal, setShowCreateEventModal] = useState(false); - const [createModalOpen, {open: openCreateModal, close: closeCreateModal}] = useDisclosure(false); - const [switchOrganizerModalOpen, {open: openSwitchModal, close: closeSwitchModal}] = useDisclosure(false); - const [shareModalOpen, {open: openShareModal, close: closeShareModal}] = useDisclosure(false); + const [createModalOpen, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false); + const [switchOrganizerModalOpen, { open: openSwitchModal, close: closeSwitchModal }] = useDisclosure(false); + const [shareModalOpen, { open: openShareModal, close: closeShareModal }] = useDisclosure(false); const [emailVerificationModalOpen, { open: openEmailVerificationModal, close: closeEmailVerificationModal }] = useDisclosure(false); - const {data: organizerResposne} = useGetOrganizers(); + const { data: organizerResposne } = useGetOrganizers(); const organizers = organizerResposne?.data; - const {data: account} = useGetAccount(); + const { data: account } = useGetAccount(); const resendEmailConfirmationMutation = useResendEmailConfirmation(); const [emailConfirmationResent, setEmailConfirmationResent] = useState(false); - const {data: me} = useGetMe(); + const { data: me } = useGetMe(); const isUserEmailVerfied = me?.is_email_verified; const isMobile = useMediaQuery('(max-width: 768px)'); @@ -71,8 +72,8 @@ const OrganizerLayout = () => { isActive: () => false, showWhen: () => organizers && organizers.length > 1, }, - {label: 'Overview'}, - {link: 'dashboard', label: t`Organizer Dashboard`, icon: IconDashboard}, + { label: 'Overview' }, + { link: 'dashboard', label: t`Organizer Dashboard`, icon: IconDashboard }, { link: 'reports', label: t`Reports`, @@ -80,12 +81,15 @@ const OrganizerLayout = () => { isActive: (isActive) => isActive || location.pathname.includes('/report/') }, - {label: t`Manage`}, - {link: 'events', label: t`Events`, icon: IconCalendar}, - {link: 'settings', label: t`Settings`, icon: IconSettings}, + { label: t`Manage` }, + { link: 'events', label: t`Events`, icon: IconCalendar }, + { link: 'settings', label: t`Settings`, icon: IconSettings }, - {label: t`Tools`}, - {link: 'organizer-homepage-designer', label: t`Homepage Designer`, icon: IconPaint}, + { label: t`Tools` }, + { link: 'organizer-homepage-designer', label: t`Homepage Designer`, icon: IconPaint }, + + { label: t`Integrations` }, + { link: 'webhooks', label: t`Webhooks`, icon: IconWebhook }, ]; const handleEmailConfirmationResend = () => { @@ -149,7 +153,7 @@ const OrganizerLayout = () => { className={classes.createEventBreadcrumb} onClick={() => setShowCreateEventModal(true)} > - {t`Create Event`} + {t`Create Event`} ), } @@ -157,10 +161,10 @@ const OrganizerLayout = () => { const callouts: CalloutConfig[] = [ { - icon: , + icon: , heading: t`Invite Your Team`, description: t`Collaborate with your team to create amazing events together.`, - buttonIcon: , + buttonIcon: , buttonText: t`Invite Team Members`, onClick: () => { openCreateModal(); @@ -171,7 +175,7 @@ const OrganizerLayout = () => { if (account && !account?.stripe_connect_setup_complete) { callouts.unshift({ - icon: , + icon: , heading: t`Connect Stripe`, description: t`Connect your Stripe account to accept payments for tickets and products.`, storageKey: `stripe-callout-dismissed`, @@ -179,7 +183,7 @@ const OrganizerLayout = () => { } + buttonIcon={} buttonText={t`Connect Stripe`} className={classes.calloutButton} /> @@ -198,9 +202,9 @@ const OrganizerLayout = () => { : - } - rightSection={} + leftSection={organizer?.status === 'DRAFT' ? : + } + rightSection={} > {organizer?.status === 'DRAFT' ? {t`Draft`} { )} - sidebarFooter={} + sidebarFooter={} /> - {createModalOpen && } + {createModalOpen && } {switchOrganizerModalOpen && - } + } {organizer && shareModalOpen && ( { + const { organizerId } = useParams(); + const errorHandler = useFormErrorResponseHandler(); + + const form = useForm({ + initialValues: { + url: '', + event_types: [], + status: 'ENABLED' + }, + validate: { + url: (value) => { + if (!value) return t`URL is required`; + try { + new URL(value); + return null; + } catch { + return t`Please enter a valid URL`; + } + }, + event_types: (value) => value.length === 0 ? t`At least one event type must be selected` : null, + } + }); + + const createMutation = useCreateOrganizerWebhook(); + + const handleSubmit = (requestData: OrganizerWebhookRequest) => { + createMutation.mutate({ + organizerId: organizerId as IdParam, + webhook: requestData + }, { + onSuccess: () => { + showSuccess(t`Webhook created successfully`); + onClose(); + }, + onError: (error) => errorHandler(form, error), + }); + } + + return ( + +
+ + + +
+ ); +} diff --git a/frontend/src/components/modals/EditOrganizerWebhookModal/index.tsx b/frontend/src/components/modals/EditOrganizerWebhookModal/index.tsx new file mode 100644 index 0000000000..778af26c5e --- /dev/null +++ b/frontend/src/components/modals/EditOrganizerWebhookModal/index.tsx @@ -0,0 +1,92 @@ +import { GenericModalProps, IdParam } from "../../../types.ts"; +import { Modal } from "../../common/Modal"; +import { t } from "@lingui/macro"; +import { WebhookForm } from "../../forms/WebhookForm"; +import { useForm } from "@mantine/form"; +import { Alert, Button, Center, Loader } from "@mantine/core"; +import { showSuccess } from "../../../utilites/notifications.tsx"; +import { useParams } from "react-router"; +import { useFormErrorResponseHandler } from "../../../hooks/useFormErrorResponseHandler.tsx"; +import { useGetOrganizerWebhook } from "../../../queries/useGetOrganizerWebhook.ts"; +import { useEditOrganizerWebhook } from "../../../mutations/useEditOrganizerWebhook.ts"; +import { useEffect } from "react"; +import { OrganizerWebhookRequest } from "../../../api/organizer-webhook.client.ts"; + +interface EditWebhookModalProps { + webhookId: IdParam; +} + +export const EditOrganizerWebhookModal = ({ + onClose, + webhookId +}: GenericModalProps & EditWebhookModalProps) => { + const { organizerId } = useParams(); + const errorHandler = useFormErrorResponseHandler(); + const { data: webhook, error: webhookError, isLoading: webhookLoading } = useGetOrganizerWebhook(organizerId as IdParam, webhookId); + const form = useForm({ + initialValues: { + url: '', + event_types: [], + status: 'ENABLED', + } + }); + const editMutation = useEditOrganizerWebhook(); + + const handleSubmit = (requestData: OrganizerWebhookRequest) => { + editMutation.mutate( + { + organizerId: organizerId as IdParam, + webhook: requestData, + webhookId: webhookId, + }, + { + onSuccess: () => { + showSuccess(t`Successfully updated Webhook`); + onClose(); + }, + onError: (error) => { + errorHandler(form, error); + }, + } + ); + }; + + useEffect(() => { + if (webhook && webhook.data && webhook.data.data) { + form.setValues({ + url: webhook.data.data.url, + event_types: webhook.data.data.event_types, + status: webhook.data.data.status, + }); + } + }, [webhook]); + + return ( + + {webhookLoading && ( +
+ +
+ )} + + {!!webhookError && ( + + {t`Failed to load Webhook`} + + )} + + {webhook && ( +
+ + + + )} +
+ ); +}; diff --git a/frontend/src/components/modals/OrganizerWebhookLogsModal/OrganizerWebhookLogsModal.module.scss b/frontend/src/components/modals/OrganizerWebhookLogsModal/OrganizerWebhookLogsModal.module.scss new file mode 100644 index 0000000000..0643d6c795 --- /dev/null +++ b/frontend/src/components/modals/OrganizerWebhookLogsModal/OrganizerWebhookLogsModal.module.scss @@ -0,0 +1,37 @@ +.logEntry { + cursor: pointer; + transition: all 0.2s ease; + border-radius: 8px; +} + +.logEntryExpanded { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.chevronIcon { + transition: transform 0.2s ease; +} + +.chevronIconExpanded { + transform: rotate(90deg); +} + +.statusIcon { + border-radius: 50%; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; +} + +.codeBlock { + border-radius: 6px; + max-height: 300px; + overflow: auto; + background-color: var(--mantine-color-gray-0); +} + +.noLogsAlert { + text-align: center; +} diff --git a/frontend/src/components/modals/OrganizerWebhookLogsModal/index.tsx b/frontend/src/components/modals/OrganizerWebhookLogsModal/index.tsx new file mode 100644 index 0000000000..ad21f551fc --- /dev/null +++ b/frontend/src/components/modals/OrganizerWebhookLogsModal/index.tsx @@ -0,0 +1,180 @@ +import { useParams } from "react-router"; +import { useGetOrganizerWebhookLogs } from "../../../queries/useGetOrganizerWebhookLogs"; +import { Modal } from "../../common/Modal"; +import { t } from "@lingui/macro"; +import { Center } from "../../common/Center"; +import { Alert, Badge, Code, Collapse, Group, Loader, Paper, Stack, Text } from "@mantine/core"; +import { IconCheck, IconChevronRight, IconX } from '@tabler/icons-react'; +import { GenericModalProps, IdParam } from "../../../types.ts"; +import { useState } from "react"; +import { relativeDate } from "../../../utilites/dates.ts"; +import classes from "./OrganizerWebhookLogsModal.module.scss"; + +interface WebhookLog { + id: IdParam; + webhook_id: IdParam; + payload?: string; + response_code?: number; + response_body?: string; + event_type: string; + created_at: string; +} + +interface WebhookLogsModalProps extends GenericModalProps { + webhookId: IdParam; +} + +const LogEntry = ({ log }: { log: WebhookLog }) => { + const [detailsOpen, setDetailsOpen] = useState(false); + + const getStatusColor = (code?: number) => { + if (!code) return 'gray'; + if (code >= 200 && code < 300) return 'green'; + if (code >= 300 && code < 400) return 'blue'; + return 'red'; + }; + + const formatContent = (content?: string) => { + if (!content) return ''; + + try { + return JSON.stringify(JSON.parse(content), null, 2); + } catch (e) { + return content; + } + }; + + const statusColor = getStatusColor(log.response_code); + + return ( + setDetailsOpen(!detailsOpen)} + className={`${classes.logEntry} ${detailsOpen ? classes.logEntryExpanded : ''}`} + style={{ + borderLeft: `4px solid var(--mantine-color-${statusColor}-6)` + }} + > + + +
+ +
+
+ + + {log.event_type} + + + {log.response_code || t`No Response`} + + + + {relativeDate(log.created_at)} + +
+
+ {log.response_code && ( +
+ {log.response_code >= 200 && log.response_code < 300 ? + : + + } +
+ )} +
+ + + + {log.payload && ( +
+ {t`Payload`}: + + {formatContent(log.payload)} + +
+ )} + + {log.response_body && ( +
+ {t`Response`}: + + {formatContent(log.response_body)} + +
+ )} +
+
+
+ ); +}; + +export const OrganizerWebhookLogsModal = ({ onClose, webhookId }: WebhookLogsModalProps) => { + const { organizerId } = useParams(); + const logsQuery = useGetOrganizerWebhookLogs(organizerId as IdParam, webhookId); + const logs = logsQuery.data?.data?.data; + + return ( + + {logsQuery.isLoading && ( +
+ + + {t`Loading webhook logs...`} + +
+ )} + + {!!logsQuery.error && ( + } + radius="md" + > + {logsQuery.error.message} + + )} + + {logs && logs.length === 0 && !logsQuery.isLoading && ( + +

+ {t`No logs found`} +

+

+ {t`No webhook events have been recorded for this endpoint yet. Events will appear here once they are triggered.`} +

+
+ )} + + {logs && logs.length > 0 && ( + <> + {logs.map((log: WebhookLog) => ( + + ))} + + )} +
+ ); +}; diff --git a/frontend/src/components/routes/organizer/Settings/index.tsx b/frontend/src/components/routes/organizer/Settings/index.tsx index c423b7ec17..6db5f17f7e 100644 --- a/frontend/src/components/routes/organizer/Settings/index.tsx +++ b/frontend/src/components/routes/organizer/Settings/index.tsx @@ -1,24 +1,24 @@ -import {SeoSettings} from "./Sections/SeoSettings"; +import { SeoSettings } from "./Sections/SeoSettings"; import BasicSettings from "./Sections/BasicSettings"; -import {SocialLinks} from "./Sections/SocialLinks"; -import {AddressSettings} from "./Sections/AddressSettings"; +import { SocialLinks } from "./Sections/SocialLinks"; +import { AddressSettings } from "./Sections/AddressSettings"; import EmailTemplateSettings from "./Sections/EmailTemplateSettings"; -import {EventDefaults} from "./Sections/EventDefaults"; -import {PlatformFeesSettings} from "./Sections/PlatformFeesSettings"; -import {PageBody} from "../../../common/PageBody"; -import {PageTitle} from "../../../common/PageTitle"; -import {t} from "@lingui/macro"; -import {Box, Group, NavLink as MantineNavLink, Stack} from "@mantine/core"; -import {IconBrandGoogleAnalytics, IconInfoCircle, IconMapPin, IconShare, IconMail, IconCalendarEvent, IconPercentage} from "@tabler/icons-react"; -import {useMediaQuery} from "@mantine/hooks"; -import {useMemo, useState} from "react"; -import {Card} from "../../../common/Card"; -import {useParams} from "react-router"; -import {useGetAccount} from "../../../../queries/useGetAccount.ts"; +import { EventDefaults } from "./Sections/EventDefaults"; +import { PlatformFeesSettings } from "./Sections/PlatformFeesSettings"; +import { PageBody } from "../../../common/PageBody"; +import { PageTitle } from "../../../common/PageTitle"; +import { t } from "@lingui/macro"; +import { Box, Group, NavLink as MantineNavLink, Stack } from "@mantine/core"; +import { IconBrandGoogleAnalytics, IconInfoCircle, IconMapPin, IconShare, IconMail, IconCalendarEvent, IconPercentage } from "@tabler/icons-react"; +import { useMediaQuery } from "@mantine/hooks"; +import { useMemo, useState } from "react"; +import { Card } from "../../../common/Card"; +import { useParams } from "react-router"; +import { useGetAccount } from "../../../../queries/useGetAccount.ts"; const Settings = () => { const { organizerId } = useParams(); - const {data: account} = useGetAccount(); + const { data: account } = useGetAccount(); const isSaasMode = account?.is_saas_mode_enabled; const SECTIONS = useMemo(() => { @@ -84,19 +84,19 @@ const Settings = () => { const handleClick = (sectionId: string) => { setActiveSection(sectionId); - document.getElementById(sectionId)?.scrollIntoView({behavior: 'smooth'}); + document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth' }); }; const sideMenu = ( - + {SECTIONS.map((section) => ( } + leftSection={} onClick={() => handleClick(section.id)} /> ))} @@ -104,9 +104,9 @@ const Settings = () => { ); - const content = SECTIONS.map(({id, component: Component}) => ( -
- + const content = SECTIONS.map(({ id, component: Component }) => ( +
+
)); @@ -116,10 +116,10 @@ const Settings = () => { {isLargeScreen ? ( - + {sideMenu} - {content} + {content} ) : ( diff --git a/frontend/src/components/routes/organizer/Webhooks/index.tsx b/frontend/src/components/routes/organizer/Webhooks/index.tsx new file mode 100644 index 0000000000..c773455d11 --- /dev/null +++ b/frontend/src/components/routes/organizer/Webhooks/index.tsx @@ -0,0 +1,62 @@ +import { t } from "@lingui/macro"; +import { Badge, Button, Group, Box } from "@mantine/core" +import { IconPlus } from "@tabler/icons-react"; +import { Card } from "../../../common/Card"; +import { useDisclosure } from "@mantine/hooks"; +import { OrganizerWebhookTable } from "../../../common/OrganizerWebhookTable"; +import { useGetOrganizerWebhooks } from "../../../../queries/useGetOrganizerWebhooks"; +import { useParams } from "react-router"; +import { CreateOrganizerWebhookModal } from "../../../modals/CreateOrganizerWebhookModal"; +import { TableSkeleton } from "../../../common/TableSkeleton"; +import { IdParam } from "../../../../types"; +import { PageBody } from "../../../common/PageBody"; +import { PageTitle } from "../../../common/PageTitle"; + +export default function Webhooks() { + const { organizerId } = useParams(); + const [createModalOpen, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false); + const webhooksQuery = useGetOrganizerWebhooks(organizerId as IdParam); + const webhooks = webhooksQuery.data?.data?.data; + + const getWebhookCountText = () => { + if (!webhooks) return t`Loading Webhooks`; + if (webhooks.length === 0) return t`No Active Webhooks`; + if (webhooks.length === 1) return t`1 Active Webhook`; + return t`${webhooks.length} Active Webhooks`; + }; + + return ( + + {t`Webhooks`} + + + + + + {getWebhookCountText()} + + + +
+ + + {webhooks && ()} +
+ + {createModalOpen && ( + + )} +
+
+ ); +} diff --git a/frontend/src/mutations/useCreateOrganizerWebhook.ts b/frontend/src/mutations/useCreateOrganizerWebhook.ts new file mode 100644 index 0000000000..cc922bdd3a --- /dev/null +++ b/frontend/src/mutations/useCreateOrganizerWebhook.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { organizerWebhookClient, OrganizerWebhookRequest } from "../api/organizer-webhook.client.ts"; +import { IdParam } from "../types.ts"; +import { GET_ORGANIZER_WEBHOOKS_QUERY_KEY } from "../queries/useGetOrganizerWebhooks.ts"; + +export const useCreateOrganizerWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ organizerId, webhook }: { + organizerId: IdParam, + webhook: OrganizerWebhookRequest + }) => organizerWebhookClient.create(organizerId, webhook), + + onSuccess: () => queryClient.invalidateQueries({ queryKey: [GET_ORGANIZER_WEBHOOKS_QUERY_KEY] }) + }); +} diff --git a/frontend/src/mutations/useDeleteOrganizerWebhook.ts b/frontend/src/mutations/useDeleteOrganizerWebhook.ts new file mode 100644 index 0000000000..4aacd12915 --- /dev/null +++ b/frontend/src/mutations/useDeleteOrganizerWebhook.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { IdParam } from "../types"; +import { organizerWebhookClient } from "../api/organizer-webhook.client"; +import { GET_ORGANIZER_WEBHOOKS_QUERY_KEY } from "../queries/useGetOrganizerWebhooks"; + +export const useDeleteOrganizerWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ organizerId, webhookId }: { + organizerId: IdParam, + webhookId: IdParam + }) => organizerWebhookClient.delete(organizerId, webhookId), + + onSuccess: () => queryClient.invalidateQueries({ queryKey: [GET_ORGANIZER_WEBHOOKS_QUERY_KEY] }) + }); +} diff --git a/frontend/src/mutations/useEditOrganizerWebhook.ts b/frontend/src/mutations/useEditOrganizerWebhook.ts new file mode 100644 index 0000000000..e45caa7178 --- /dev/null +++ b/frontend/src/mutations/useEditOrganizerWebhook.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { organizerWebhookClient, OrganizerWebhookRequest } from "../api/organizer-webhook.client.ts"; +import { IdParam } from "../types.ts"; +import { GET_ORGANIZER_WEBHOOKS_QUERY_KEY } from "../queries/useGetOrganizerWebhooks.ts"; + +export const useEditOrganizerWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ organizerId, webhookId, webhook }: { + organizerId: IdParam, + webhookId: IdParam, + webhook: OrganizerWebhookRequest + }) => organizerWebhookClient.update(organizerId, webhookId, webhook), + + onSuccess: () => queryClient.invalidateQueries({ queryKey: [GET_ORGANIZER_WEBHOOKS_QUERY_KEY] }) + }); +} diff --git a/frontend/src/queries/useGetOrganizerWebhook.ts b/frontend/src/queries/useGetOrganizerWebhook.ts new file mode 100644 index 0000000000..b4eb56a0bb --- /dev/null +++ b/frontend/src/queries/useGetOrganizerWebhook.ts @@ -0,0 +1,13 @@ +import { organizerWebhookClient } from "../api/organizer-webhook.client.ts"; +import { useQuery } from "@tanstack/react-query"; +import { IdParam } from "../types.ts"; + +export const GET_ORGANIZER_WEBHOOK_QUERY_KEY = 'getOrganizerWebhook'; + +export const useGetOrganizerWebhook = (organizerId: IdParam, webhookId: IdParam) => { + return useQuery({ + queryKey: [GET_ORGANIZER_WEBHOOK_QUERY_KEY, organizerId, webhookId], + queryFn: async () => await organizerWebhookClient.get(organizerId, webhookId), + enabled: !!webhookId + }); +} diff --git a/frontend/src/queries/useGetOrganizerWebhookLogs.ts b/frontend/src/queries/useGetOrganizerWebhookLogs.ts new file mode 100644 index 0000000000..45aa645aa2 --- /dev/null +++ b/frontend/src/queries/useGetOrganizerWebhookLogs.ts @@ -0,0 +1,13 @@ +import { organizerWebhookClient } from "../api/organizer-webhook.client.ts"; +import { useQuery } from "@tanstack/react-query"; +import { IdParam } from "../types.ts"; + +export const GET_ORGANIZER_WEBHOOK_LOGS_QUERY_KEY = 'getOrganizerWebhookLogs'; + +export const useGetOrganizerWebhookLogs = (organizerId: IdParam, webhookId: IdParam) => { + return useQuery({ + queryKey: [GET_ORGANIZER_WEBHOOK_LOGS_QUERY_KEY, organizerId, webhookId], + queryFn: async () => await organizerWebhookClient.logs(organizerId, webhookId), + enabled: !!webhookId + }); +} diff --git a/frontend/src/queries/useGetOrganizerWebhooks.ts b/frontend/src/queries/useGetOrganizerWebhooks.ts new file mode 100644 index 0000000000..ec8cef8c7c --- /dev/null +++ b/frontend/src/queries/useGetOrganizerWebhooks.ts @@ -0,0 +1,12 @@ +import { organizerWebhookClient } from "../api/organizer-webhook.client.ts"; +import { useQuery } from "@tanstack/react-query"; +import { IdParam } from "../types.ts"; + +export const GET_ORGANIZER_WEBHOOKS_QUERY_KEY = 'getOrganizerWebhooks'; + +export const useGetOrganizerWebhooks = (organizerId: IdParam) => { + return useQuery({ + queryKey: [GET_ORGANIZER_WEBHOOKS_QUERY_KEY, organizerId], + queryFn: async () => await organizerWebhookClient.all(organizerId), + }); +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 34df497443..5eaa8aa13b 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,10 +1,10 @@ -import {Navigate, RouteObject} from "react-router"; +import { Navigate, RouteObject } from "react-router"; import ErrorPage from "./error-page.tsx"; -import {useEffect, useState} from "react"; -import {useGetMe} from "./queries/useGetMe.ts"; -import {publicEventRouteLoader} from "./routeLoaders/publicEventRouteLoader.ts"; -import {publicOrganizerRouteLoader} from "./routeLoaders/publicOrganizerRouteLoader.ts"; -import {organizerPreviewRouteLoader} from "./routeLoaders/organizerPreviewRouteLoader.ts"; +import { useEffect, useState } from "react"; +import { useGetMe } from "./queries/useGetMe.ts"; +import { publicEventRouteLoader } from "./routeLoaders/publicEventRouteLoader.ts"; +import { publicOrganizerRouteLoader } from "./routeLoaders/publicOrganizerRouteLoader.ts"; +import { organizerPreviewRouteLoader } from "./routeLoaders/organizerPreviewRouteLoader.ts"; const Root = () => { const [redirectPath, setRedirectPath] = useState(null); @@ -19,102 +19,102 @@ const Root = () => { }, [me.isFetched]); if (redirectPath) { - return ; + return ; } }; export const router: RouteObject[] = [ { path: "", - element: , - errorElement: + element: , + errorElement: }, { path: "auth", async lazy() { const AuthLayout = await import("./components/layouts/AuthLayout"); - return {Component: AuthLayout.default}; + return { Component: AuthLayout.default }; }, - errorElement: , + errorElement: , children: [ { path: "login", async lazy() { const Login = await import("./components/routes/auth/Login"); - return {Component: Login.default}; + return { Component: Login.default }; }, }, { path: "register", async lazy() { const Register = await import("./components/routes/auth/Register"); - return {Component: Register.default}; + return { Component: Register.default }; } }, { path: "forgot-password", async lazy() { const ForgotPassword = await import("./components/routes/auth/ForgotPassword"); - return {Component: ForgotPassword.default}; + return { Component: ForgotPassword.default }; } }, { path: "reset-password/:token", async lazy() { const ResetPassword = await import("./components/routes/auth/ResetPassword"); - return {Component: ResetPassword.default}; + return { Component: ResetPassword.default }; } }, { path: "accept-invitation/:token", async lazy() { const AcceptInvitation = await import("./components/routes/auth/AcceptInvitation"); - return {Component: AcceptInvitation.default}; + return { Component: AcceptInvitation.default }; } } ] }, { path: "manage", - errorElement: , + errorElement: , async lazy() { const DefaultLayout = await import("./components/layouts/DefaultLayout"); - return {Component: DefaultLayout.default}; + return { Component: DefaultLayout.default }; }, children: [ { path: "events/:eventsState?", async lazy() { const Dashboard = await import("./components/routes/events/Dashboard"); - return {Component: Dashboard.default}; + return { Component: Dashboard.default }; }, }, { path: "account", async lazy() { const ManageAccount = await import("./components/routes/account/ManageAccount"); - return {Component: ManageAccount.default}; + return { Component: ManageAccount.default }; } }, { path: "profile", async lazy() { const ManageProfile = await import("./components/routes/profile/ManageProfile"); - return {Component: ManageProfile.default}; + return { Component: ManageProfile.default }; } }, { path: "profile/confirm-email-change/:token", async lazy() { const ConfirmEmailChange = await import("./components/routes/profile/ConfirmEmailChange"); - return {Component: ConfirmEmailChange.default}; + return { Component: ConfirmEmailChange.default }; } }, { path: "profile/confirm-email-address/:token", async lazy() { const ConfirmEmailAddress = await import("./components/routes/profile/ConfirmEmailAddress"); - return {Component: ConfirmEmailAddress.default}; + return { Component: ConfirmEmailAddress.default }; } }, ] @@ -123,147 +123,147 @@ export const router: RouteObject[] = [ path: "welcome", async lazy() { const WelcomeLayout = await import("./components/layouts/WelcomeLayout"); - return {Component: WelcomeLayout.default}; + return { Component: WelcomeLayout.default }; }, - errorElement: , + errorElement: , children: [ { path: "", async lazy() { const Welcome = await import("./components/routes/welcome"); - return {Component: Welcome.default}; + return { Component: Welcome.default }; } }, ] }, { path: "admin", - errorElement: , + errorElement: , async lazy() { const AdminLayout = await import("./components/layouts/Admin"); - return {Component: AdminLayout.default}; + return { Component: AdminLayout.default }; }, children: [ { path: "", async lazy() { const Dashboard = await import("./components/routes/admin/Dashboard"); - return {Component: Dashboard.default}; + return { Component: Dashboard.default }; } }, { path: "accounts", async lazy() { const Accounts = await import("./components/routes/admin/Accounts"); - return {Component: Accounts.default}; + return { Component: Accounts.default }; } }, { path: "accounts/:accountId", async lazy() { const AccountDetail = await import("./components/routes/admin/Accounts/AccountDetail"); - return {Component: AccountDetail.default}; + return { Component: AccountDetail.default }; } }, { path: "users", async lazy() { const Users = await import("./components/routes/admin/Users"); - return {Component: Users.default}; + return { Component: Users.default }; } }, { path: "events", async lazy() { const Events = await import("./components/routes/admin/Events"); - return {Component: Events.default}; + return { Component: Events.default }; } }, { path: "orders", async lazy() { const Orders = await import("./components/routes/admin/Orders"); - return {Component: Orders.default}; + return { Component: Orders.default }; } }, { path: "attribution", async lazy() { const Attribution = await import("./components/routes/admin/Attribution"); - return {Component: Attribution.default}; + return { Component: Attribution.default }; } }, { path: "configurations", async lazy() { const Configurations = await import("./components/routes/admin/Configurations"); - return {Component: Configurations.default}; + return { Component: Configurations.default }; } }, { path: "failed-jobs", async lazy() { const FailedJobs = await import("./components/routes/admin/FailedJobs"); - return {Component: FailedJobs.default}; + return { Component: FailedJobs.default }; } }, { path: "messages", async lazy() { const Messages = await import("./components/routes/admin/Messages"); - return {Component: Messages.default}; + return { Component: Messages.default }; } } ] }, { path: "account", - errorElement: , + errorElement: , async lazy() { const DefaultLayout = await import("./components/layouts/DefaultLayout"); - return {Component: DefaultLayout.default}; + return { Component: DefaultLayout.default }; }, children: [ { path: "", async lazy() { const ManageAccount = await import("./components/routes/account/ManageAccount"); - return {Component: ManageAccount.default}; + return { Component: ManageAccount.default }; }, children: [ { path: "settings", async lazy() { const AccountSettings = await import("./components/routes/account/ManageAccount/sections/AccountSettings"); - return {Component: AccountSettings.default}; + return { Component: AccountSettings.default }; } }, { path: "taxes-and-fees", async lazy() { const TaxSettings = await import("./components/routes/account/ManageAccount/sections/TaxSettings"); - return {Component: TaxSettings.default}; + return { Component: TaxSettings.default }; } }, { path: "event-defaults", async lazy() { const EventDefaultsSettings = await import("./components/routes/account/ManageAccount/sections/EventDefaultsSettings"); - return {Component: EventDefaultsSettings.default}; + return { Component: EventDefaultsSettings.default }; } }, { path: "users", async lazy() { const Users = await import("./components/routes/account/ManageAccount/sections/Users"); - return {Component: Users.default}; + return { Component: Users.default }; } }, { path: "payment", async lazy() { const PaymentSettings = await import("./components/routes/account/ManageAccount/sections/PaymentSettings"); - return {Component: PaymentSettings.default}; + return { Component: PaymentSettings.default }; } }, ] @@ -274,50 +274,57 @@ export const router: RouteObject[] = [ path: "/manage/organizer/:organizerId?", async lazy() { const Dashboard = await import("./components/layouts/OrganizerLayout"); - return {Component: Dashboard.default}; + return { Component: Dashboard.default }; }, - errorElement: , + errorElement: , children: [ { path: "dashboard?", async lazy() { const OrganizerDashboard = await import("./components/routes/organizer/OrganizerDashboard"); - return {Component: OrganizerDashboard.default}; + return { Component: OrganizerDashboard.default }; } }, { path: "events/:eventsState?", async lazy() { const Events = await import("./components/routes/organizer/Events"); - return {Component: Events.default}; + return { Component: Events.default }; } }, { path: "settings", async lazy() { const Settings = await import("./components/routes/organizer/Settings"); - return {Component: Settings.default}; + return { Component: Settings.default }; } }, { path: "organizer-homepage-designer", async lazy() { const OrganizerHomepageDesigner = await import("./components/routes/organizer/OrganizerHomepageDesigner"); - return {Component: OrganizerHomepageDesigner.default}; + return { Component: OrganizerHomepageDesigner.default }; + } + }, + { + path: "webhooks", + async lazy() { + const Webhooks = await import("./components/routes/organizer/Webhooks"); + return { Component: Webhooks.default }; } }, { path: "reports", async lazy() { const OrganizerReports = await import("./components/routes/organizer/Reports"); - return {Component: OrganizerReports.default}; + return { Component: OrganizerReports.default }; } }, { path: "report/:reportType", async lazy() { const OrganizerReportLayout = await import("./components/routes/organizer/Reports/ReportLayout"); - return {Component: OrganizerReportLayout.default}; + return { Component: OrganizerReportLayout.default }; } } ], @@ -326,148 +333,148 @@ export const router: RouteObject[] = [ path: "/manage/event/:eventId", async lazy() { const EventLayout = await import("./components/layouts/Event"); - return {Component: EventLayout.default}; + return { Component: EventLayout.default }; }, - errorElement: , + errorElement: , children: [ { path: "", async lazy() { const EventDashboard = await import("./components/routes/event/EventDashboard"); - return {Component: EventDashboard.default}; + return { Component: EventDashboard.default }; } }, { path: "dashboard", async lazy() { const EventDashboard = await import("./components/routes/event/EventDashboard"); - return {Component: EventDashboard.default}; + return { Component: EventDashboard.default }; } }, { path: "reports", async lazy() { const Reports = await import("./components/routes/event/Reports"); - return {Component: Reports.default}; + return { Component: Reports.default }; }, }, { path: "report/:reportType", async lazy() { const ReportLayout = await import("./components/routes/event/Reports/ReportLayout"); - return {Component: ReportLayout.default}; + return { Component: ReportLayout.default }; }, }, { path: "products", async lazy() { const Products = await import("./components/routes/event/products"); - return {Component: Products.default}; + return { Component: Products.default }; } }, { path: "attendees", async lazy() { const Attendees = await import("./components/routes/event/attendees"); - return {Component: Attendees.default}; + return { Component: Attendees.default }; } }, { path: "questions", async lazy() { const Questions = await import("./components/routes/event/questions"); - return {Component: Questions.default}; + return { Component: Questions.default }; } }, { path: "orders", async lazy() { const Orders = await import("./components/routes/event/orders"); - return {Component: Orders.default}; + return { Component: Orders.default }; } }, { path: "promo-codes", async lazy() { const PromoCodes = await import("./components/routes/event/promo-codes"); - return {Component: PromoCodes.default}; + return { Component: PromoCodes.default }; } }, { path: "affiliates", async lazy() { const Affiliates = await import("./components/routes/event/Affiliates"); - return {Component: Affiliates.default}; + return { Component: Affiliates.default }; } }, { path: "check-in", async lazy() { const CheckIn = await import("./components/routes/event/CheckInLists"); - return {Component: CheckIn.default}; + return { Component: CheckIn.default }; } }, { path: "messages", async lazy() { const Messages = await import("./components/routes/event/messages"); - return {Component: Messages.default}; + return { Component: Messages.default }; } }, { path: "settings", async lazy() { const Settings = await import("./components/routes/event/Settings"); - return {Component: Settings.default}; + return { Component: Settings.default }; } }, { path: "widget", async lazy() { const Widget = await import("./components/routes/event/widget"); - return {Component: Widget.default}; + return { Component: Widget.default }; } }, { path: "homepage-designer", async lazy() { const HomepageDesigner = await import("./components/routes/event/HomepageDesigner"); - return {Component: HomepageDesigner.default}; + return { Component: HomepageDesigner.default }; } }, { path: "ticket-designer", async lazy() { const TicketDesigner = await import("./components/routes/event/TicketDesigner"); - return {Component: TicketDesigner.default}; + return { Component: TicketDesigner.default }; } }, { path: "getting-started", async lazy() { const GettingStarted = await import("./components/routes/event/GettingStarted"); - return {Component: GettingStarted.default}; + return { Component: GettingStarted.default }; } }, { path: "sold-out-waitlist", async lazy() { const SoldOutWaitlist = await import("./components/routes/event/SoldOutWaitlist"); - return {Component: SoldOutWaitlist.default}; + return { Component: SoldOutWaitlist.default }; } }, { path: "capacity-assignments", async lazy() { const CapacityAssignments = await import("./components/routes/event/CapacityAssignments"); - return {Component: CapacityAssignments.default}; + return { Component: CapacityAssignments.default }; } }, { path: "webhooks", async lazy() { const Webhooks = await import("./components/routes/event/Webhooks"); - return {Component: Webhooks.default}; + return { Component: Webhooks.default }; } } ] @@ -477,32 +484,32 @@ export const router: RouteObject[] = [ loader: publicOrganizerRouteLoader, async lazy() { const PublicOrganizer = await import("./components/layouts/PublicOrganizer"); - return {Component: PublicOrganizer.default}; + return { Component: PublicOrganizer.default }; }, - errorElement: , + errorElement: , }, { path: "/events/:organizerId/:organizerSlug/past-events", loader: publicOrganizerRouteLoader, async lazy() { const PublicOrganizer = await import("./components/layouts/PublicOrganizer"); - return {Component: PublicOrganizer.default}; + return { Component: PublicOrganizer.default }; }, - errorElement: , + errorElement: , }, { path: "/e/:eventId/:eventSlug", async lazy() { const EventHomepage = await import("./components/layouts/EventHomepage"); - return {Component: EventHomepage.default}; + return { Component: EventHomepage.default }; }, - errorElement: , + errorElement: , }, { path: "/event/:eventId/preview", async lazy() { const EventHomepagePreview = await import("./components/layouts/EventHomepagePreview"); - return {Component: EventHomepagePreview.default}; + return { Component: EventHomepagePreview.default }; }, }, { @@ -510,7 +517,7 @@ export const router: RouteObject[] = [ loader: organizerPreviewRouteLoader, async lazy() { const OrganizerHomepagePreview = await import("./components/layouts/OrganizerHomepagePreview"); - return {Component: OrganizerHomepagePreview.default}; + return { Component: OrganizerHomepagePreview.default }; }, }, { @@ -518,52 +525,52 @@ export const router: RouteObject[] = [ loader: publicEventRouteLoader, async lazy() { const PublicEvent = await import("./components/layouts/PublicEvent"); - return {Component: PublicEvent.default}; + return { Component: PublicEvent.default }; }, - errorElement: , + errorElement: , }, { path: "/widget/:eventId", async lazy() { const ProductWidget = await import("./components/layouts/ProductWidget"); - return {Component: ProductWidget.default}; + return { Component: ProductWidget.default }; }, - errorElement: , + errorElement: , }, { path: "/checkout/:eventId", async lazy() { const Checkout = await import("./components/layouts/Checkout"); - return {Component: Checkout.default}; + return { Component: Checkout.default }; }, - errorElement: , + errorElement: , children: [ { path: ":orderShortId/details", async lazy() { const CollectInformation = await import("./components/routes/product-widget/CollectInformation"); - return {Component: CollectInformation.default}; + return { Component: CollectInformation.default }; } }, { path: ":orderShortId/payment", async lazy() { const Payment = await import("./components/routes/product-widget/Payment"); - return {Component: Payment.default}; + return { Component: Payment.default }; } }, { path: ":orderShortId/summary", async lazy() { const OrderSummaryAndProducts = await import("./components/routes/product-widget/OrderSummaryAndProducts"); - return {Component: OrderSummaryAndProducts.default}; + return { Component: OrderSummaryAndProducts.default }; } }, { path: ":orderShortId/payment_return", async lazy() { const PaymentReturn = await import("./components/routes/product-widget/PaymentReturn"); - return {Component: PaymentReturn.default}; + return { Component: PaymentReturn.default }; } }, ] @@ -572,49 +579,49 @@ export const router: RouteObject[] = [ path: "/order/:eventId/:orderShortId/print", async lazy() { const PrintOrder = await import("./components/routes/product-widget/PrintOrder"); - return {Component: PrintOrder.default}; + return { Component: PrintOrder.default }; }, - errorElement: + errorElement: }, { path: "/product/:eventId/:attendeeShortId/print", async lazy() { const PrintProduct = await import("./components/routes/product-widget/PrintProduct"); - return {Component: PrintProduct.default}; + return { Component: PrintProduct.default }; }, - errorElement: + errorElement: }, { path: "/manage/event/:eventId/ticket-designer/print", async lazy() { const TicketDesignerPrint = await import("./components/routes/event/TicketDesigner/TicketDesignerPrint"); - return {Component: TicketDesignerPrint.default}; + return { Component: TicketDesignerPrint.default }; }, - errorElement: + errorElement: }, { path: "/product/:eventId/:attendeeShortId", async lazy() { const AttendeeProductAndInformation = await import("./components/routes/product-widget/AttendeeProductAndInformation"); - return {Component: AttendeeProductAndInformation.default}; + return { Component: AttendeeProductAndInformation.default }; }, - errorElement: + errorElement: }, { path: "/check-in/:checkInListShortId", async lazy() { const CheckIn = await import("./components/layouts/CheckIn"); - return {Component: CheckIn.default}; + return { Component: CheckIn.default }; }, - errorElement: , + errorElement: , }, { path: "/my-tickets/:token", async lazy() { const MyTickets = await import("./components/routes/my-tickets"); - return {Component: MyTickets.default}; + return { Component: MyTickets.default }; }, - errorElement: , + errorElement: , } ];