Skip to content

Commit

Permalink
Add UI for webhooks (#269)
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdekker committed May 7, 2023
1 parent 5377bb6 commit 7eb2355
Show file tree
Hide file tree
Showing 35 changed files with 1,123 additions and 47 deletions.
1 change: 1 addition & 0 deletions docker/php-fpm/cron/notifications
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ HOME=/app
*/5 * * * * www-data php bin/console revisions:fetch 2>&1
*/5 * * * * www-data php -dmax_execution_time=0 bin/console revisions:validate 2>&1
15 4 * * * www-data php bin/console code-inspection:cleanup 2>&1
30 5 * * * www-data php bin/console webhook:cleanup 2>&1
54 changes: 54 additions & 0 deletions migrations/Version20230506160659.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230506160659 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(
'CREATE TABLE webhook_repository (webhook_id INT NOT NULL, repository_id INT NOT NULL, INDEX IDX_57821885C9BA60B (webhook_id), ' .
'INDEX IDX_578218850C9D4F7 (repository_id), ' .
'PRIMARY KEY(webhook_id, repository_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'
);
$this->addSql(
'ALTER TABLE webhook_repository ADD CONSTRAINT FK_57821885C9BA60B FOREIGN KEY (webhook_id) REFERENCES webhook (id) ON DELETE CASCADE'
);
$this->addSql(
'ALTER TABLE webhook_repository ' .
'ADD CONSTRAINT FK_578218850C9D4F7 FOREIGN KEY (repository_id) REFERENCES repository (id) ON DELETE CASCADE'
);
$this->addSql('ALTER TABLE webhook DROP FOREIGN KEY FK_8A74175650C9D4F7');
$this->addSql('DROP INDEX IDX_8A74175650C9D4F7 ON webhook');
$this->addSql('INSERT INTO webhook_repository SELECT `id` AS webhook_id, repository_id FROM webhook');
$this->addSql('ALTER TABLE webhook DROP repository_id');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE webhook_repository DROP FOREIGN KEY FK_57821885C9BA60B');
$this->addSql('ALTER TABLE webhook_repository DROP FOREIGN KEY FK_578218850C9D4F7');
$this->addSql('DROP TABLE webhook_repository');
$this->addSql('ALTER TABLE webhook ADD repository_id INT DEFAULT NULL');
$this->addSql(
'ALTER TABLE webhook ' .
'ADD CONSTRAINT FK_8A74175650C9D4F7 FOREIGN KEY (repository_id) REFERENCES repository (id) ON UPDATE NO ACTION ON DELETE NO ACTION'
);
$this->addSql('CREATE INDEX IDX_8A74175650C9D4F7 ON webhook (repository_id)');
}
}
31 changes: 31 additions & 0 deletions migrations/Version20230506201821.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230506201821 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE INDEX create_timestamp_idx ON webhook_activity (create_timestamp)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX create_timestamp_idx ON webhook_activity');
}
}
31 changes: 31 additions & 0 deletions src/Command/Webhook/WebhookCleanUpCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace DR\Review\Command\Webhook;

use DR\Review\Repository\Webhook\WebhookActivityRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand('webhook:cleanup', "Clean up all the webhook activity from 2 weeks and older.")]
class WebhookCleanUpCommand extends Command
{
public function __construct(private readonly WebhookActivityRepository $activityRepository)
{
parent::__construct();
}

/**
* @inheritDoc
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$removed = $this->activityRepository->cleanUp(strtotime("-2 weeks"));

$output->writeln("Removed " . $removed . " webhook activities");

return self::SUCCESS;
}
}
35 changes: 35 additions & 0 deletions src/Controller/App/Admin/DeleteWebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);

namespace DR\Review\Controller\App\Admin;

use DR\Review\Controller\AbstractController;
use DR\Review\Entity\Webhook\Webhook;
use DR\Review\Repository\Webhook\WebhookRepository;
use DR\Review\Security\Role\Roles;
use DR\Review\ViewModel\App\Admin\EditWebhookViewModel;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class DeleteWebhookController extends AbstractController
{
public function __construct(private WebhookRepository $webhookRepository)
{
}

/**
* @return array<string, EditWebhookViewModel>|RedirectResponse
*/
#[Route('/app/admin/webhook/{id<\d+>}', self::class, methods: ['DELETE'])]
#[IsGranted(Roles::ROLE_ADMIN)]
public function __invoke(#[MapEntity] Webhook $webhook): array|RedirectResponse
{
$this->webhookRepository->remove($webhook, true);

$this->addFlash('success', 'webhook.successful.removed');

return $this->refererRedirect(WebhooksController::class);
}
}
52 changes: 52 additions & 0 deletions src/Controller/App/Admin/WebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);

namespace DR\Review\Controller\App\Admin;

use DR\Review\Controller\AbstractController;
use DR\Review\Entity\Webhook\Webhook;
use DR\Review\Form\Webhook\EditWebhookFormType;
use DR\Review\Repository\Webhook\WebhookRepository;
use DR\Review\Security\Role\Roles;
use DR\Review\ViewModel\App\Admin\EditWebhookViewModel;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class WebhookController extends AbstractController
{
public function __construct(private WebhookRepository $webhookRepository)
{
}

/**
* @return array<string, EditWebhookViewModel>|RedirectResponse
*/
#[Route('/app/admin/webhook/{id<\d+>?}', self::class, methods: ['GET', 'POST'])]
#[Template('app/admin/edit_webhook.html.twig')]
#[IsGranted(Roles::ROLE_ADMIN)]
public function __invoke(Request $request, #[MapEntity] ?Webhook $webhook): array|RedirectResponse
{
if ($webhook === null && $request->attributes->get('id') !== null) {
throw new NotFoundHttpException('Webhook not found');
}

$webhook ??= (new Webhook())->setEnabled(true)->setRetries(3)->setVerifySsl(true);

$form = $this->createForm(EditWebhookFormType::class, ['webhook' => $webhook]);
$form->handleRequest($request);
if ($form->isSubmitted() === false || $form->isValid() === false) {
return ['editWebhookModel' => new EditWebhookViewModel($webhook, $form->createView())];
}

$this->webhookRepository->save($webhook, true);

$this->addFlash('success', 'webhook.successful.saved');

return $this->redirectToRoute(WebhooksController::class);
}
}
30 changes: 30 additions & 0 deletions src/Controller/App/Admin/WebhooksController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace DR\Review\Controller\App\Admin;

use DR\Review\Controller\AbstractController;
use DR\Review\Security\Role\Roles;
use DR\Review\ViewModel\App\Admin\WebhooksViewModel;
use DR\Review\ViewModelProvider\WebhooksViewModelProvider;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class WebhooksController extends AbstractController
{
public function __construct(private readonly WebhooksViewModelProvider $viewModelProvider)
{
}

/**
* @return array<string, WebhooksViewModel>
*/
#[Route('/app/admin/webhooks', self::class, methods: 'GET')]
#[Template('app/admin/webhooks.html.twig')]
#[IsGranted(Roles::ROLE_ADMIN)]
public function __invoke(): array
{
return ['webhooksViewModel' => $this->viewModelProvider->getWebhooksViewModel()];
}
}
39 changes: 32 additions & 7 deletions src/Entity/Webhook/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,18 @@ class Webhook
#[ORM\Column(type: 'json', nullable: true)]
private array $headers = [];

#[ORM\ManyToOne(targetEntity: Repository::class)]
private ?Repository $repository = null;
/** @phpstan-var Collection<int, Repository> */
#[ORM\ManyToMany(targetEntity: Repository::class)]
private Collection $repositories;

/** @phpstan-var Collection<int, WebhookActivity> */
#[ORM\OneToMany(mappedBy: 'webhook', targetEntity: WebhookActivity::class, cascade: ['persist', 'remove'], orphanRemoval: false)]
private Collection $activities;

public function __construct()
{
$this->activities = new ArrayCollection();
$this->repositories = new ArrayCollection();
$this->activities = new ArrayCollection();
}

public function setId(int $id): self
Expand Down Expand Up @@ -123,14 +125,37 @@ public function setHeaders(array $headers): self
return $this;
}

public function getRepository(): ?Repository
public function setHeader(string $key, ?string $value): self
{
return $this->repository;
if ($value === null) {
unset($this->headers[$key]);
} else {
$this->headers[$key] = $value;
}

return $this;
}

/**
* @return Collection<int, Repository>
*/
public function getRepositories(): Collection
{
return $this->repositories;
}

public function addRepository(Repository $repository): self
{
if (!$this->repositories->contains($repository)) {
$this->repositories->add($repository);
}

return $this;
}

public function setRepository(?Repository $repository): self
public function removeRepository(Repository $repository): self
{
$this->repository = $repository;
$this->repositories->removeElement($repository);

return $this;
}
Expand Down
1 change: 1 addition & 0 deletions src/Entity/Webhook/WebhookActivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use DR\Review\Repository\Webhook\WebhookActivityRepository;

#[ORM\Entity(repositoryClass: WebhookActivityRepository::class)]
#[ORM\Index(columns: ['create_timestamp'], name: 'create_timestamp_idx')]
class WebhookActivity
{
#[ORM\Id]
Expand Down
32 changes: 32 additions & 0 deletions src/Form/Webhook/EditWebhookFormType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace DR\Review\Form\Webhook;

use DR\Review\Controller\App\Admin\WebhookController;
use DR\Review\Entity\Webhook\Webhook;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class EditWebhookFormType extends AbstractType
{
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}

/**
* @inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
/** @var array{webhook: Webhook|null} $data */
$data = $options['data'];

$builder->setAction($this->urlGenerator->generate(WebhookController::class, ['id' => $data['webhook']?->getId()]));
$builder->setMethod('POST');
$builder->add('webhook', WebhookType::class, ['label' => false]);
$builder->add('save', SubmitType::class, ['label' => 'save']);
}
}
Loading

0 comments on commit 7eb2355

Please sign in to comment.