Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use HiEvents\DomainObjects\Status\WebhookStatus;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\Request\Webhook\UpsertWebhookRequest;
use HiEvents\Resources\Webhook\WebhookResource;
use HiEvents\Resources\Webhook\WebhookResourceWithSecret;
use HiEvents\Services\Application\Handlers\Webhook\CreateWebhookHandler;
use HiEvents\Services\Application\Handlers\Webhook\DTO\CreateWebhookDTO;
use Illuminate\Http\JsonResponse;
Expand Down Expand Up @@ -36,7 +36,7 @@ public function __invoke(int $organizerId, UpsertWebhookRequest $request): JsonR
);

return $this->resourceResponse(
resource: WebhookResource::class,
resource: WebhookResourceWithSecret::class,
data: $webhook
);
}
Expand Down
4 changes: 2 additions & 2 deletions backend/app/Http/Actions/Webhooks/CreateWebhookAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use HiEvents\DomainObjects\Status\WebhookStatus;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\Request\Webhook\UpsertWebhookRequest;
use HiEvents\Resources\Webhook\WebhookResource;
use HiEvents\Resources\Webhook\WebhookResourceWithSecret;
use HiEvents\Services\Application\Handlers\Webhook\CreateWebhookHandler;
use HiEvents\Services\Application\Handlers\Webhook\DTO\CreateWebhookDTO;
use Illuminate\Http\JsonResponse;
Expand Down Expand Up @@ -35,7 +35,7 @@ public function __invoke(int $eventId, UpsertWebhookRequest $request): JsonRespo
);

return $this->resourceResponse(
resource: WebhookResource::class,
resource: WebhookResourceWithSecret::class,
data: $webhook
);
}
Expand Down
18 changes: 18 additions & 0 deletions backend/app/Resources/Webhook/WebhookResourceWithSecret.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace HiEvents\Resources\Webhook;

use HiEvents\DomainObjects\WebhookDomainObject;

/**
* @mixin WebhookDomainObject
*/
class WebhookResourceWithSecret extends WebhookResource
{
public function toArray($request): array
{
return array_merge(parent::toArray($request), [
'secret' => $this->getSecret(),
]);
}
}
54 changes: 54 additions & 0 deletions backend/tests/Unit/Resources/Webhook/WebhookResourceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Tests\Unit\Resources\Webhook;

use HiEvents\DomainObjects\WebhookDomainObject;
use HiEvents\Resources\Webhook\WebhookResource;
use HiEvents\Resources\Webhook\WebhookResourceWithSecret;
use Illuminate\Http\Request;
use Tests\TestCase;

class WebhookResourceTest extends TestCase
{
private function createWebhookDomainObject(): WebhookDomainObject
{
return (new WebhookDomainObject())
->setId(1)
->setUrl('https://example.com/webhook')
->setEventTypes(['order.created', 'attendee.created'])
->setStatus('ENABLED')
->setSecret('test-secret-key-1234567890abcdef');
}

public function test_webhook_resource_excludes_secret(): void
{
$webhook = $this->createWebhookDomainObject();
$resource = (new WebhookResource($webhook))->toArray(Request::create('/'));

$this->assertArrayNotHasKey('secret', $resource);
$this->assertEquals(1, $resource['id']);
$this->assertEquals('https://example.com/webhook', $resource['url']);
$this->assertEquals('ENABLED', $resource['status']);
}

public function test_webhook_resource_with_secret_includes_secret(): void
{
$webhook = $this->createWebhookDomainObject();
$resource = (new WebhookResourceWithSecret($webhook))->toArray(Request::create('/'));

$this->assertArrayHasKey('secret', $resource);
$this->assertEquals('test-secret-key-1234567890abcdef', $resource['secret']);
}

public function test_webhook_resource_with_secret_includes_all_base_fields(): void
{
$webhook = $this->createWebhookDomainObject();
$resource = (new WebhookResourceWithSecret($webhook))->toArray(Request::create('/'));

$this->assertEquals(1, $resource['id']);
$this->assertEquals('https://example.com/webhook', $resource['url']);
$this->assertEquals(['order.created', 'attendee.created'], $resource['event_types']);
$this->assertEquals('ENABLED', $resource['status']);
$this->assertEquals('test-secret-key-1234567890abcdef', $resource['secret']);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { useState } from "react";
import { GenericModalProps, IdParam } from "../../../types";
import { Modal } from "../../common/Modal";
import { t } from "@lingui/macro";
import { t, Trans } from "@lingui/macro";
import { WebhookForm } from "../../forms/WebhookForm";
import { useForm } from "@mantine/form";
import { Button } from "@mantine/core";
import { Alert, Button, Group, TextInput } from "@mantine/core";
import { useCreateOrganizerWebhook } from "../../../mutations/useCreateOrganizerWebhook";
import { showSuccess } from "../../../utilites/notifications";
import { useParams } from "react-router";
import { useFormErrorResponseHandler } from "../../../hooks/useFormErrorResponseHandler";
import { OrganizerWebhookRequest } from "../../../api/organizer-webhook.client.ts";
import { CopyButton } from "../../common/CopyButton";
import { IconInfoCircle } from "@tabler/icons-react";

export const CreateOrganizerWebhookModal = ({ onClose }: GenericModalProps) => {
const { organizerId } = useParams();
const errorHandler = useFormErrorResponseHandler();
const [secret, setSecret] = useState<string | null>(null);

const form = useForm<OrganizerWebhookRequest>({
initialValues: {
Expand Down Expand Up @@ -41,14 +45,36 @@ export const CreateOrganizerWebhookModal = ({ onClose }: GenericModalProps) => {
organizerId: organizerId as IdParam,
webhook: requestData
}, {
onSuccess: () => {
onSuccess: (response) => {
showSuccess(t`Webhook created successfully`);
onClose();
setSecret(response.data.data.secret);
},
onError: (error) => errorHandler(form, error),
});
}

if (secret) {
return (
<Modal
opened
onClose={onClose}
heading={t`Webhook Signing Secret`}
>
<Alert icon={<IconInfoCircle/>} color="yellow" mb="md">
<Trans>This is the only time the signing secret will be shown. Please copy it now and store it securely.</Trans>
</Alert>
<TextInput
value={secret}
readOnly
rightSection={<CopyButton value={secret}/>}
/>
<Button fullWidth mt="xl" onClick={onClose}>
{t`Done`}
</Button>
</Modal>
);
}

return (
<Modal
opened
Expand Down
34 changes: 30 additions & 4 deletions frontend/src/components/modals/CreateWebhookModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import {useState} from "react";
import {GenericModalProps, IdParam} from "../../../types";
import {Modal} from "../../common/Modal";
import {t} from "@lingui/macro";
import {t, Trans} from "@lingui/macro";
import {WebhookForm} from "../../forms/WebhookForm";
import {useForm} from "@mantine/form";
import {Button} from "@mantine/core";
import {Alert, Button, Group, TextInput} from "@mantine/core";
import {useCreateWebhook} from "../../../mutations/useCreateWebhook";
import {showSuccess} from "../../../utilites/notifications";
import {useParams} from "react-router";
import {useFormErrorResponseHandler} from "../../../hooks/useFormErrorResponseHandler";
import {WebhookRequest} from "../../../api/webhook.client.ts";
import {CopyButton} from "../../common/CopyButton";
import {IconInfoCircle} from "@tabler/icons-react";

export const CreateWebhookModal = ({onClose}: GenericModalProps) => {
const {eventId} = useParams();
const errorHandler = useFormErrorResponseHandler();
const [secret, setSecret] = useState<string | null>(null);

const form = useForm<WebhookRequest>({
initialValues: {
Expand Down Expand Up @@ -41,14 +45,36 @@ export const CreateWebhookModal = ({onClose}: GenericModalProps) => {
eventId: eventId as IdParam,
webhook: requestData
}, {
onSuccess: () => {
onSuccess: (response) => {
showSuccess(t`Webhook created successfully`);
onClose();
setSecret(response.data.data.secret);
},
onError: (error) => errorHandler(form, error),
});
}

if (secret) {
return (
<Modal
opened
onClose={onClose}
heading={t`Webhook Signing Secret`}
>
<Alert icon={<IconInfoCircle/>} color="yellow" mb="md">
<Trans>This is the only time the signing secret will be shown. Please copy it now and store it securely.</Trans>
</Alert>
<TextInput
value={secret}
readOnly
rightSection={<CopyButton value={secret}/>}
/>
<Button fullWidth mt="xl" onClick={onClose}>
{t`Done`}
</Button>
</Modal>
);
}

return (
<Modal
opened
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/de.js

Large diffs are not rendered by default.

Loading
Loading