From 17fab5f363bbdd54445f693a575a45883b5674a3 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Sun, 4 Jun 2023 03:35:16 +0200 Subject: [PATCH] GitLab's integration webhooks support --- config/packages/security.yaml | 2 +- config/services.yaml | 3 +- ...mpose-prod.yml => docker-compose-split.yml | 0 docker-compose.yml | 13 +- public/packeton/img/logo/bitbucket.png | Bin 0 -> 1490 bytes public/packeton/img/logo/gitea.png | Bin 0 -> 4351 bytes public/packeton/img/logo/github.png | Bin 0 -> 6393 bytes public/packeton/img/logo/gitlab.png | Bin 0 -> 4323 bytes public/packeton/js/layout.js | 5 + src/Controller/Api/ApiController.php | 48 +++- .../OAuth/IntegrationController.php | 25 +- src/Controller/PackageController.php | 2 +- src/DependencyInjection/Configuration.php | 15 +- src/Entity/OAuthIntegration.php | 12 + src/Entity/PackageSerializedTrait.php | 10 + src/Event/PackageEvent.php | 22 ++ src/EventListener/IntegrationListener.php | 57 ++++ src/Form/Type/IntegrationSettingsType.php | 3 +- src/Integrations/AppInterface.php | 10 +- src/Integrations/Base/AppIntegrationTrait.php | 28 +- .../Base/BaseIntegrationTrait.php | 38 ++- src/Integrations/Github/GitHubIntegration.php | 2 +- src/Integrations/Github/GithubResultPager.php | 8 +- src/Integrations/Gitlab/GitLabIntegration.php | 260 +++++++++++++++++- .../Gitlab/GitLabOAuth2Factory.php | 15 + src/Integrations/IntegrationRegistry.php | 3 + src/Integrations/Model/AppConfig.php | 10 + src/Integrations/Model/IntegrationUtils.php | 27 ++ src/Menu/MenuBuilder.php | 7 +- src/Model/AutoHookUser.php | 13 +- src/Model/PackageManager.php | 9 + src/Model/PatUserScores.php | 5 + src/Security/Api/ApiTokenAuthenticator.php | 6 +- .../Token/IntegrationTokenChecker.php | 5 +- src/Security/Token/PatTokenChecker.php | 3 +- src/Twig/PackagistExtension.php | 4 +- templates/integration/connect.html.twig | 45 +++ templates/integration/index.html.twig | 33 ++- templates/integration/list.html.twig | 38 +++ translations/messages.en.yml | 2 + 40 files changed, 718 insertions(+), 70 deletions(-) rename docker-compose-prod.yml => docker-compose-split.yml (100%) create mode 100644 public/packeton/img/logo/bitbucket.png create mode 100644 public/packeton/img/logo/gitea.png create mode 100644 public/packeton/img/logo/github.png create mode 100644 public/packeton/img/logo/gitlab.png create mode 100644 src/Event/PackageEvent.php create mode 100644 src/EventListener/IntegrationListener.php create mode 100644 templates/integration/connect.html.twig create mode 100644 templates/integration/list.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index aa433b82..25ac82f2 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -72,7 +72,7 @@ security: # Maintainers - { path: (^(/users/(.+)/packages))+, roles: ROLE_MAINTAINER } - { path: (^(/users/(.+)/favorites))+, roles: ROLE_MAINTAINER } - - { path: (^(/metadata/changes.json$|/explore|/jobs/|/archive/))+, roles: ROLE_MAINTAINER } + - { path: (^(/metadata/changes.json$|/explore|/jobs/|/archive/|/api/hooks/))+, roles: ROLE_MAINTAINER } # Secured part of the site # This config requires being logged for the whole site and having the admin role for the admin part. diff --git a/config/services.yaml b/config/services.yaml index 837110c5..64dac3ca 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -219,6 +219,7 @@ services: - '@Packeton\Security\Token\DefaultTokenChecker' calls: - [addTokenChecker, ['@Packeton\Security\Token\PatTokenChecker']] - - [addTokenChecker, ['@Packeton\Security\Token\PatTokenChecker']] + - [addTokenChecker, ['@Packeton\Security\Token\JwtTokenChecker']] + - [addTokenChecker, ['@Packeton\Security\Token\IntegrationTokenChecker']] Symfony\Component\HttpClient\NoPrivateNetworkHttpClient: ~ diff --git a/docker-compose-prod.yml b/docker-compose-split.yml similarity index 100% rename from docker-compose-prod.yml rename to docker-compose-split.yml diff --git a/docker-compose.yml b/docker-compose.yml index cad57e28..eb2983f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,16 +5,21 @@ services: build: context: . image: packeton/packeton:latest + restart: unless-stopped container_name: packagist hostname: packagist environment: - ADMIN_USER: admin - ADMIN_PASSWORD: 123456 - ADMIN_EMAIL: admin@example.com +# ADMIN_USER: admin +# ADMIN_PASSWORD: 123456 +# ADMIN_EMAIL: admin@example.com TRUSTED_PROXIES: 172.16.0.0/12 # Default SQLite - # DATABASE_URL: "mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4" + DATABASE_URL: "mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4" + +# MAILER_DSN: smtp://user:pass@smtp.mailgun.org:587 +# MAILER_FROM: Packeton ports: +# setup nginx reverse proxy for ssl - '127.0.0.1:8088:80' volumes: - .docker:/data diff --git a/public/packeton/img/logo/bitbucket.png b/public/packeton/img/logo/bitbucket.png new file mode 100644 index 0000000000000000000000000000000000000000..06a68b9bf550b9cfab2c484288a5743bd9d3bd52 GIT binary patch literal 1490 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 literal 0 HcmV?d00001 diff --git a/public/packeton/img/logo/github.png b/public/packeton/img/logo/github.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 GIT binary patch literal 6393 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 Hu0mjfZ$b4` literal 0 HcmV?d00001 diff --git a/public/packeton/img/logo/gitlab.png b/public/packeton/img/logo/gitlab.png new file mode 100644 index 0000000000000000000000000000000000000000..aeb653f1d7c8fd9e6d539ddcac9395a2d933fb50 GIT binary patch literal 4323 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 PDFEg-Zy95+dp!9czY1r( literal 0 HcmV?d00001 diff --git a/public/packeton/js/layout.js b/public/packeton/js/layout.js index 5001c610..85248146 100644 --- a/public/packeton/js/layout.js +++ b/public/packeton/js/layout.js @@ -90,4 +90,9 @@ if (typeof select2.select2 === 'function') { select2.select2(); } + + let tooltip = $('[data-toggle="tooltip"]'); + if (typeof tooltip.tooltip === 'function') { + tooltip.tooltip(); + } })(jQuery, humane); diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index ec120c18..c7f3bb01 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -11,6 +11,8 @@ use Packeton\Entity\Package; use Packeton\Entity\User; use Packeton\Entity\Webhook; +use Packeton\Integrations\IntegrationRegistry; +use Packeton\Model\AutoHookUser; use Packeton\Model\DownloadManager; use Packeton\Model\PackageManager; use Packeton\Service\Scheduler; @@ -35,7 +37,9 @@ public function __construct( protected DownloadManager $downloadManager, protected LoggerInterface $logger, protected ValidatorInterface $validator, - ){} + protected IntegrationRegistry $integrations + ) { + } #[Route('/api/create-package', name: 'generic_create', methods: ['POST'])] public function createPackageAction(Request $request): Response @@ -65,6 +69,8 @@ public function createPackageAction(Request $request): Response $em = $this->registry->getManager(); $em->persist($package); $em->flush(); + + $this->container->get(PackageManager::class)->insetPackage($package); } catch (\Exception $e) { $this->logger->critical($e->getMessage(), ['exception', $e]); return new JsonResponse(['status' => 'error', 'message' => 'Error saving package'], 500); @@ -78,15 +84,23 @@ public function createPackageAction(Request $request): Response #[Route('/api/hooks/{alias}/{id}', name: 'api_integration_postreceive')] public function integrationHook(Request $request, string $alias, #[Vars] OAuthIntegration $oauth): Response { - // parse the payload - $payload = $this->getJsonPayload($request); + if ($alias !== $oauth->getAlias()) { + return new JsonResponse(['error' => "App $alias is not found"], 409); + } + + $response = $this->receiveIntegrationHook($request, $oauth); + if (null !== $response) { + return new JsonResponse($response, $response['code'] ?? 200); + } + + return $this->updatePackageAction($request, fallback: true); } #[Route('/api/github', name: 'github_postreceive')] #[Route('/api/bitbucket', name: 'bitbucket_postreceive')] #[Route('/api/update-package', name: 'generic_postreceive')] #[Route('/api/update-package/{name}', name: 'generic_named_postreceive', requirements: ['name' => '%package_name_regex%'])] - public function updatePackageAction(Request $request, #[Vars] Package $package = null): Response + public function updatePackageAction(Request $request, #[Vars] Package $package = null, bool $fallback = false): Response { // parse the payload $payload = $this->getJsonPayload($request); @@ -95,6 +109,12 @@ public function updatePackageAction(Request $request, #[Vars] Package $package = return new JsonResponse(['status' => 'error', 'message' => 'Missing payload parameter'], 406); } + // May helpfully for GitLab Packagist Integrations. Replacement for group webhooks that enabled only for PAID EE version + // See docs how to use GitLab Integrations + if (false === $fallback && $this->getUser() instanceof AutoHookUser && null !== ($response = $this->receiveIntegrationHook($request))) { + return new JsonResponse($response, $response['code'] ?? 200); + } + $packages = [$package]; // Get from query parameter. if ($packageNames = $request->get('composer_package_name')) { @@ -332,6 +352,26 @@ protected function receivePost(Request $request, $url, $urlRegex) return $this->schedulePostJobs($packages); } + protected function receiveIntegrationHook(Request $request, OAuthIntegration $oauth = null): ?array + { + $user = $this->getUser(); + if (null === $oauth && $user instanceof AutoHookUser) { + $oauth = $this->registry->getRepository(OAuthIntegration::class)->find((int) $user->getHookIdentifier()); + if (null === $oauth) { + return null; + } + } + + try { + $app = $this->integrations->findApp($oauth->getAlias()); + return $app->receiveHooks($request, $this->getJsonPayload($request), $oauth); + } catch (\Throwable $e) { + $this->logger->error($e->getMessage(), ['e' => $e]); + } + + return null; + } + /** * @param Package[] $packages * @return Response diff --git a/src/Controller/OAuth/IntegrationController.php b/src/Controller/OAuth/IntegrationController.php index 5dc00ce0..4cdc4b54 100644 --- a/src/Controller/OAuth/IntegrationController.php +++ b/src/Controller/OAuth/IntegrationController.php @@ -12,6 +12,8 @@ use Packeton\Integrations\Exception\NotFoundAppException; use Packeton\Integrations\IntegrationRegistry; use Packeton\Integrations\AppInterface; +use Packeton\Integrations\Model\IntegrationUtils; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -23,13 +25,22 @@ class IntegrationController extends AbstractController public function __construct( protected IntegrationRegistry $integrations, protected ManagerRegistry $registry, + protected LoggerInterface $logger ) { } #[Route('', name: 'integration_list')] public function listAction(): Response { + $integrations = $this->integrations->findAllApps(); + return $this->render('integration/list.html.twig', ['integrations' => $integrations]); + } + #[Route('/connect', name: 'integration_connect')] + public function connect(): Response + { + $integrations = $this->integrations->findAllApps(); + return $this->render('integration/connect.html.twig', ['integrations' => $integrations]); } #[Route('/{alias}/{id}', name: 'integration_index')] @@ -100,16 +111,22 @@ public function connectOrg(Request $request, string $alias, #[Vars] OAuthIntegra $client = $this->getClient($alias, $oauth); $oauth->setConnected($org, $connected = !$oauth->isConnected($org)); - $this->registry->getManager()->flush(); + $response = ['connected' => $connected]; try { - $connected ? $client->addOrgHook($oauth, $org) : $client->removeOrgHook($oauth, $org); + $status = $connected ? $client->addOrgHook($oauth, $org) : $client->removeOrgHook($oauth, $org); + if (is_array($status)) { + $oauth->setWebhookInfo($org, $status); + $response += $status; + } } catch (\Throwable $e) { - return new JsonResponse(['error' => $e->getMessage()] + $response, 400); + $this->logger->error($e->getMessage(), ['e' => $e]); + $response += ['error' => IntegrationUtils::castError($e), 'code' => 409]; } - return new JsonResponse($response, 200); + $this->registry->getManager()->flush(); + return new JsonResponse($response, $response['code'] ?? 200); } protected function getClient($alias, OAuthIntegration $oauth = null): AppInterface diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index d383b245..ad8446ae 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -129,7 +129,7 @@ public function submitPackageAction(Request $req, string $type = null): Response $em->persist($package); $em->flush(); - $this->providerManager->insertPackage($package); + $this->container->get(PackageManager::class)->insetPackage($package); $this->addFlash('success', $package->getName().' has been added to the package list, the repository will now be crawled.'); return new RedirectResponse($this->generateUrl('view_package', ['name' => $package->getName()])); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 9ac13ab1..1572ecbc 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -217,6 +217,7 @@ private function addIntegrationSection(ArrayNodeDefinition $rootNode, array $fac $nodeBuilder->children() ->booleanNode('enabled')->defaultTrue()->end() ->scalarNode('base_url')->end() + ->scalarNode('webhook_url')->info('Static current host')->end() ->scalarNode('svg_logo')->end() ->scalarNode('logo')->end() ->scalarNode('login_title')->end() @@ -267,15 +268,25 @@ private function defaultIconsData(): array { return [ 'github' => [ - 'logo' => null, + 'logo' => '/packeton/img/logo/github.png', 'svg_logo' => 'svg/github.html.twig', 'login_title' => 'Login with GitHub', ], 'gitlab' => [ - 'logo' => null, + 'logo' => '/packeton/img/logo/gitlab.png', 'svg_logo' => 'svg/gitlab.html.twig', 'login_title' => 'Login with GitLab', ], + 'gitea' => [ + 'logo' => '/packeton/img/logo/gitea.png', + 'svg_logo' => 'svg/gitea.html.twig', + 'login_title' => 'Login with Gitea', + ], + 'bitbucket' => [ + 'logo' => '/packeton/img/logo/bitbucket.png', + 'svg_logo' => 'svg/gitea.html.twig', + 'login_title' => 'Login with Bitbucket', + ] ]; } } diff --git a/src/Entity/OAuthIntegration.php b/src/Entity/OAuthIntegration.php index bdec1036..5d262dd7 100644 --- a/src/Entity/OAuthIntegration.php +++ b/src/Entity/OAuthIntegration.php @@ -127,6 +127,18 @@ public function getEnabledOrganizations(): array return $this->getSerialized('enabled_org', 'array', []); } + public function setWebhookInfo(string|int $orgs, array $info): self + { + $info += ['status' => true, 'error' => null, 'id' => null]; + return $this->setSerialized("web_hook_$orgs", $info); + } + + public function getWebhookInfo(string|int $orgs): ?array + { + $info = $this->getSerialized("web_hook_$orgs", 'array'); + return is_array($info) ? $info + ['status' => null, 'error' => null, 'id' => null] : null; + } + public function isConnected(string|int $name): bool { return in_array((string)$name, $this->getEnabledOrganizations()); diff --git a/src/Entity/PackageSerializedTrait.php b/src/Entity/PackageSerializedTrait.php index d49b7710..b8d3fc0d 100644 --- a/src/Entity/PackageSerializedTrait.php +++ b/src/Entity/PackageSerializedTrait.php @@ -36,6 +36,16 @@ public function setExcludedGlob(?string $glob): void $this->setSerializedField('excludedGlob', $glob); } + public function getWebhookInfo(): ?array + { + return $this->serializedData['webhook_info'] ?? null; + } + + public function setWebhookInfo(?array $info): void + { + $this->setSerializedField('webhook_info', $info); + } + public function isSkipNotModifyTag(): ?bool { return (bool)($this->serializedData['skip_empty_tag'] ?? null); diff --git a/src/Event/PackageEvent.php b/src/Event/PackageEvent.php new file mode 100644 index 00000000..dd576b6c --- /dev/null +++ b/src/Event/PackageEvent.php @@ -0,0 +1,22 @@ +package; + } +} diff --git a/src/EventListener/IntegrationListener.php b/src/EventListener/IntegrationListener.php new file mode 100644 index 00000000..e347b60a --- /dev/null +++ b/src/EventListener/IntegrationListener.php @@ -0,0 +1,57 @@ +getPackage(); + if (null === ($oauth = $package->getIntegration()) || empty($package->getExternalRef())) { + return; + } + + try { + $app = $this->integrations->findApp($oauth->getAlias()); + $info = $app->addHook($oauth, $package->getExternalRef()); + if ($info['status'] ?? false) { + $package->setAutoUpdated(true); + } + } catch (\Throwable $e) { + $info = ['status' => false, 'error' => IntegrationUtils::castError($e, $app ?? null)]; + } + + $package->setWebhookInfo($info); + } + + #[AsEventListener(event: 'packageRemove')] + public function onPackageDelete(UpdaterEvent $event): void + { + $package = $event->getPackage(); + if (null === ($oauth = $package->getIntegration()) || empty($package->getExternalRef())) { + return; + } + + try { + $app = $this->integrations->findApp($oauth->getAlias(), false); + $app->removeHook($oauth, $package->getExternalRef()); + } catch (\Throwable $e) { + } + } +} diff --git a/src/Form/Type/IntegrationSettingsType.php b/src/Form/Type/IntegrationSettingsType.php index b22d6add..9b1df71a 100644 --- a/src/Form/Type/IntegrationSettingsType.php +++ b/src/Form/Type/IntegrationSettingsType.php @@ -5,6 +5,7 @@ namespace Packeton\Form\Type; use Packeton\Entity\OAuthIntegration; +use Packeton\Util\PacketonUtils; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -16,7 +17,7 @@ class IntegrationSettingsType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $repos = $options['repos']; - $repos = array_combine(array_column($repos, 'label'), array_column($repos, 'name')); + $repos = PacketonUtils::buildChoices($repos, 'label', 'name'); $builder ->add('globFilter', TextareaType::class, [ diff --git a/src/Integrations/AppInterface.php b/src/Integrations/AppInterface.php index 57410937..7e15a0ad 100644 --- a/src/Integrations/AppInterface.php +++ b/src/Integrations/AppInterface.php @@ -38,15 +38,15 @@ public function organizations(App $accessToken): array; public function cacheClear(string|int $appId): void; - public function addHook(App $accessToken, int|string $repoId): void; + public function addHook(App $accessToken, int|string $repoId): ?array; - public function removeHook(App $accessToken, int|string $repoId): void; + public function removeHook(App $accessToken, int|string $repoId): ?array; - public function addOrgHook(App $accessToken, int|string $orgId): void; + public function addOrgHook(App $accessToken, int|string $orgId): ?array; - public function removeOrgHook(App $accessToken, int|string $orgId): void; + public function removeOrgHook(App $accessToken, int|string $orgId): ?array; - public function receiveHooks(Request $request, array $payload): bool; + public function receiveHooks(Request $request, ?array $payload, App $app): ?array; public function findApps(): array; diff --git a/src/Integrations/Base/AppIntegrationTrait.php b/src/Integrations/Base/AppIntegrationTrait.php index b7b91ae8..39d99f38 100644 --- a/src/Integrations/Base/AppIntegrationTrait.php +++ b/src/Integrations/Base/AppIntegrationTrait.php @@ -13,6 +13,8 @@ trait AppIntegrationTrait { + protected $ownOrg = ['name' => 'Own profile', 'identifier' => '@self']; + protected \Redis $redis; protected LockFactory $lock; protected ManagerRegistry $registry; @@ -79,30 +81,34 @@ public function organizations(OAuthIntegration $accessToken): array return []; } - public function addHook(array|OAuthIntegration $accessToken, int|string $repositoryId): void + public function receiveHooks(Request $request, ?array $payload, OAuthIntegration $accessToken): ?array { + return null; } - public function receiveHooks(Request $request, array $payload): bool + public function findApps(): array { - return false; + return $this->registry->getRepository(OAuthIntegration::class)->findBy(['alias' => $this->name]); } - public function findApps(): array + public function addHook(array|OAuthIntegration $accessToken, int|string $repositoryId): ?array { - return $this->registry->getRepository(OAuthIntegration::class)->findBy(['alias' => $this->name]); + return null; } - public function removeHook(OAuthIntegration $accessToken, int|string $repositoryId): void + public function removeHook(OAuthIntegration $accessToken, int|string $repositoryId): ?array { + return null; } - public function addOrgHook(OAuthIntegration $accessToken, int|string $orgId): void + public function addOrgHook(OAuthIntegration $accessToken, int|string $orgId): ?array { + return null; } - public function removeOrgHook(OAuthIntegration $accessToken, int|string $orgId): void + public function removeOrgHook(OAuthIntegration $accessToken, int|string $orgId): ?array { + return null; } public function zipballDownload(OAuthIntegration $accessToken, string $reference): string @@ -114,8 +120,9 @@ public function authenticateIO(OAuthIntegration $accessToken, IOInterface $io, C { } - protected function getCached(string|int $appId, string $key, bool $withCache = true, callable $callback = null): mixed + protected function getCached(string|int|OAuthIntegration $appId, string $key, bool $withCache = true, callable $callback = null): mixed { + $appId = $appId instanceof OAuthIntegration ? $appId->getId() : $appId; if (true === $withCache) { try { $result = $this->redis->hGet("oauthapp:{$this->name}:$appId", $key); @@ -131,8 +138,9 @@ protected function getCached(string|int $appId, string $key, bool $withCache = t return $result; } - protected function setCached(string|int $appId, string $key, mixed $value): void + protected function setCached(string|int|OAuthIntegration $appId, string $key, mixed $value): void { + $appId = $appId instanceof OAuthIntegration ? $appId->getId() : $appId; $this->redis->hSet("oauthapp:{$this->name}:$appId", $key, json_encode($value)); } } diff --git a/src/Integrations/Base/BaseIntegrationTrait.php b/src/Integrations/Base/BaseIntegrationTrait.php index f2031cc2..65a33527 100644 --- a/src/Integrations/Base/BaseIntegrationTrait.php +++ b/src/Integrations/Base/BaseIntegrationTrait.php @@ -16,27 +16,43 @@ trait BaseIntegrationTrait /** * {@inheritdoc} */ - public function getConfig(OAuthIntegration $app = null): AppConfig + public function getConfig(OAuthIntegration $app = null, bool $details = false): AppConfig { - return new AppConfig($this->config + $this->getConfigApp($app)); + return new AppConfig($this->config + $this->getConfigApp($app, $details)); } /** * {@inheritdoc} */ - protected function getConfigApp(OAuthIntegration $app = null): array + protected function getConfigApp(OAuthIntegration $app = null, bool $details = false): array { - if (null === $app) { - return []; + $config = []; + if ($app instanceof OAuthIntegration) { + $params = ['alias' => $this->name, 'id' => $app->getId(), 'token' => $app->getHookToken()]; + if (($baseUrl = $this->config['webhook_url'] ?? null)) { + $baseUrl = rtrim($baseUrl, '/'); + $components = parse_url($baseUrl); + if (isset($components['path'])) { + $config['hook_url'] = $baseUrl . "?token={$app->getHookToken()}"; + } else { + $config['hook_url'] = $baseUrl . $this->router->generate('api_integration_postreceive', $params); + } + } else { + $config['hook_url'] = $this->router->generate('api_integration_postreceive', $params, 0); + } } - $base = $this->router->generate('home', referenceType: UrlGeneratorInterface::ABSOLUTE_URL); - $hookUrl = $this->router->generate('api_integration_postreceive', ['alias' => $this->name, 'id' => $app->getId(), 'token' => $app->getHookToken()], 0); + if ($app || $details) { + $config['redirect_urls'] = $this->getRedirectUrls(); + } - return [ - 'redirect_urls' => [$base], - 'hook_url' => $hookUrl - ]; + return $config; + } + + public function getRedirectUrls(): array + { + $base = $this->router->generate('home', referenceType: UrlGeneratorInterface::ABSOLUTE_URL); + return [$base]; } protected function getAuthorizationResponse(string $baseUrl, array $options, string $route = 'oauth_check'): RedirectResponse diff --git a/src/Integrations/Github/GitHubIntegration.php b/src/Integrations/Github/GitHubIntegration.php index aa69da1a..cc546d02 100644 --- a/src/Integrations/Github/GitHubIntegration.php +++ b/src/Integrations/Github/GitHubIntegration.php @@ -109,7 +109,7 @@ public function repositories(OAuthIntegration $app): array $callback = function() use ($app, &$accessToken, &$url) { $accessToken ??= $this->refreshToken($app); $param = $this->getApiHeaders($accessToken); - $pager = new GithubResultPager($this->httpClient, 'GET', $this->getApiUrl($url), $param); + $pager = new GithubResultPager($this->httpClient, $this->getApiUrl($url), $param); return $pager->all(); }; diff --git a/src/Integrations/Github/GithubResultPager.php b/src/Integrations/Github/GithubResultPager.php index 70e2e149..0ec61e23 100644 --- a/src/Integrations/Github/GithubResultPager.php +++ b/src/Integrations/Github/GithubResultPager.php @@ -11,20 +11,20 @@ #[Exclude] class GithubResultPager { - public static $perPage = 100; + public $perPage = 100; public function __construct( private readonly HttpClientInterface $httpClient, - private readonly string $method, private readonly string $url, - private readonly array $params + private readonly array $params, + private readonly string $method = 'GET' ) { } public function all(): array { $params = $this->params; - $params['query']['per_page'] = self::$perPage; + $params['query']['per_page'] ??= $this->perPage; $processed = []; $url = $this->url; diff --git a/src/Integrations/Gitlab/GitLabIntegration.php b/src/Integrations/Gitlab/GitLabIntegration.php index f7593845..8edb5f2c 100644 --- a/src/Integrations/Gitlab/GitLabIntegration.php +++ b/src/Integrations/Gitlab/GitLabIntegration.php @@ -4,13 +4,19 @@ namespace Packeton\Integrations\Gitlab; +use Composer\Config; +use Composer\IO\IOInterface; use Doctrine\Persistence\ManagerRegistry; +use Packeton\Entity\OAuthIntegration; +use Packeton\Entity\OAuthIntegration as App; use Packeton\Entity\User; use Packeton\Integrations\AppInterface; use Packeton\Integrations\Base\AppIntegrationTrait; use Packeton\Integrations\Base\BaseIntegrationTrait; +use Packeton\Integrations\Github\GithubResultPager; use Packeton\Integrations\IntegrationInterface; use Packeton\Integrations\LoginInterface; +use Packeton\Integrations\Model\IntegrationUtils; use Packeton\Integrations\Model\OAuth2State; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -19,6 +25,7 @@ use Symfony\Component\Lock\LockFactory; use Symfony\Component\Routing\Generator\UrlGeneratorInterface as UG; use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; class GitLabIntegration implements IntegrationInterface, LoginInterface, AppInterface @@ -132,9 +139,7 @@ public function fetchUser(Request|array $request, array $options = [], array &$a { $accessToken ??= $request instanceof Request ? $this->getAccessToken($request) : $request; - $params = $this->getApiHeaders($accessToken); - $response = $this->httpClient->request('GET', $this->getApiUrl('/user'), $params); - $response = $response->toArray(); + $response = $this->makeApiRequest($accessToken, 'GET', '/user'); $response['user_identifier'] = $response['email']; $response['external_id'] = isset($response['id']) ? "{$this->name}:{$response['id']}" : null; @@ -158,9 +163,256 @@ public function createUser(array $data): User return $user; } + /** + * {@inheritdoc} + */ + public function organizations(App $app): array + { + $organizations = $this->getCached($app, 'orgs', callback: function () use ($app) { + $accessToken = $this->refreshToken($app); + return $this->makeCGetRequest($accessToken, '/groups', ['query' => 30]); + }); + + $orgs = array_map(fn ($org) => $org + ['identifier' => $org['id'], 'logo' => $org['avatar_url'] ?? null, 'name' => $org['name'] ?? $org['id']], $organizations); + return array_merge([$this->ownOrg], $orgs); + } + + /** + * {@inheritdoc} + */ + public function repositories(App $app): array + { + $organizations = $app->getEnabledOrganizations(); + $organizations = array_diff($organizations, ['@self']); + + $allRepos = []; + if ($app->isConnected('@self')) { + $ownRepos = $this->getCached($app, 'repos:self', callback: function () use ($app) { + $accessToken = $this->refreshToken($app); + return $this->makeCGetRequest($accessToken, '/projects', ['query' => ['membership' => true, 'owned' => true]]); + }); + + $allRepos = array_merge($allRepos, $ownRepos); + } + + foreach ($organizations as $organization) { + $orgRepos = $this->getCached($app, "repos:$organization", callback: function () use ($app, $organization) { + $accessToken = $this->refreshToken($app); + + try { + return $this->makeCGetRequest($accessToken, "/groups/{$organization}/projects", ['query' => ['membership' => true, 'owned' => true]]); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['e' => $e]); + return []; + } + }); + + $allRepos = array_merge($allRepos, $orgRepos); + } + + return $this->formatRepos($allRepos); + } + + protected function formatRepos(array $repos): array + { + $repos = array_map(function ($repo) { + $required = [ + 'name' => $repo['path_with_namespace'], + 'label' => $repo['path_with_namespace'], + 'ext_ref' => $this->name.':'.$repo['id'], + 'url' => $repo['http_url_to_repo'] ?? $repo['web_url'], + 'ssh_url' => $repo['ssh_url_to_repo'] ?? null, + ]; + + return $required + $repo; + }, $repos); + + $unique = []; + foreach ($repos as $repo) { + $unique[$repo['name']] = $repo; + } + + return array_values($unique); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrls(): array + { + return [ + $this->router->generate('oauth_install', ['alias' => $this->name], UG::ABSOLUTE_URL), + $this->router->generate('oauth_check', ['alias' => $this->name], UG::ABSOLUTE_URL), + ]; + } + + public function findHooks(App|array $accessToken, int|string $orgId, ?bool $isRepo = null, ?string $url = null): ?array + { + $orgId = str_replace($this->name.':', '', (string)$orgId, $count); + $isRepo ??= $count > 0; + + $accessToken = $this->refreshToken($accessToken); + try { + $list = $this->makeCGetRequest($accessToken, $isRepo ? "/projects/$orgId/hooks" : "/groups/$orgId/hooks"); + } catch (\Exception $e) { + return $url ? null : []; + } + + if (null !== $url) { + $list = array_filter($list, fn ($u) => $u['url'] === $url); + return reset($list) ?: null; + } + + return $list; + } + + /** + * {@inheritdoc} + */ + public function addOrgHook(App $app, int|string $orgId): ?array + { + return $this->doAddHook($app, $orgId, false); + } + + /** + * {@inheritdoc} + */ + public function removeOrgHook(App $app, int|string $orgId): ?array + { + return $this->doRemoveHook($app, $orgId, false); + } + + /** + * {@inheritdoc} + */ + public function addHook(App $app, int|string $orgId): ?array + { + return $this->doAddHook($app, $orgId, true); + } + + /** + * {@inheritdoc} + */ + public function removeHook(App $app, int|string $orgId): ?array + { + return $this->doRemoveHook($app, $orgId, true); + } + + /** + * {@inheritdoc} + */ + public function authenticateIO(OAuthIntegration $oauth2, IOInterface $io, Config $config, string $repoUrl = null): void + { + $token = $this->refreshToken($oauth2); + $urls = parse_url($this->baseUrl); + + $params = [ + '_driver' => 'gitlab', + '_no_api' => !($useApi = IntegrationUtils::useApiPref($this->getConfig(), $oauth2)), + 'gitlab-domains' => ['gitlab.com', $urls['host']], + 'gitlab-oauth' => [$urls['host'] => $token['access_token']], + ]; + + if (false === $useApi) { + $params['_driver'] = 'git'; + } + + $params += $this->config['composer_config'] ?? []; + $config->merge(['config' => $params]); + $io->loadConfiguration($config); + } + + protected function doAddHook(App $app, int|string $orgId, bool $isRepo): ?array + { + if ('@self' === $orgId) { + return null; + } + + $orgId = str_replace($this->name.':', '', (string)$orgId); + $accessToken = $this->refreshToken($app); + $url = $this->getConfig($app)->getHookUrl(); + if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) { + return ['status' => true, 'id' => $hook['id']]; + } + + try { + $body = ['url' => $url, 'push_events' => true, 'tag_push_events' => true, 'merge_requests_events' => true]; + $response = $this->makeApiRequest($accessToken, 'POST', $isRepo ? "/projects/$orgId/hooks" : "/groups/$orgId/hooks", ['json' => $body]); + if (isset($response['id'])) { + return ['status' => true, 'id' => $response['id']]; + } + } catch (\Exception $e) { + if ($e instanceof HttpExceptionInterface && $e->getResponse()->getStatusCode() === 404 && false === $isRepo) { + $statusMessage = 'Notice. GitLab allow Groups webhooks only for EE paid plan. But you may manually setup ' + . "Packagist integration with target on packeton host (without path), username \"token\" and token \"{$app->getHookToken()}\". " + . "See documentation for details"; + } + + return ['status' => false, 'error' => IntegrationUtils::castError($e), 'status_message' => $statusMessage ?? null]; + } + + if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) { + return ['status' => true, 'id' => $hook['id']]; + } + + return null; + } + + /** + * {@inheritdoc} + */ + protected function doRemoveHook(App $app, int|string $orgId, bool $isRepo): ?array + { + if ('@self' === $orgId) { + return null; + } + + $accessToken = $this->refreshToken($app); + $url = $this->getConfig($app)->getHookUrl(); + + $id = false === $isRepo ? ($app->getWebhookInfo($orgId)['id'] ?? null) : null; + if ($id !== null) { + try { + $this->makeApiRequest($accessToken, 'DELETE', $isRepo ? "/projects/$orgId/hooks/$id" : "/groups/$orgId/hooks/$id"); + return []; + } catch (\Exception $e) { + } + } + + if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) { + $id = $hook['id']; + try { + $this->makeApiRequest($accessToken, 'DELETE', $isRepo ? "/projects/$orgId/hooks/$id" : "/groups/$orgId/hooks/$id"); + return []; + } catch (\Exception $e) { + return ['status' => false, 'error' => IntegrationUtils::castError($e), 'id' => $id]; + } + } + + return []; + } + protected function getApiUrl(string $path): string { - return $this->baseUrl . '/api/v4' . $path; + $apiVer = $this->config['api_version'] ?? 'v4'; + return $this->baseUrl . '/api/' . $apiVer . $path; + } + + protected function makeApiRequest(array $token, string $method, string $path, array $params = []): array + { + $params = array_merge_recursive($this->getApiHeaders($token), $params); + $response = $this->httpClient->request($method, $this->getApiUrl($path), $params); + $content = $response->getContent(); + $content = $content ? json_decode($content, true) : []; + + return is_array($content) ? $content : []; + } + + protected function makeCGetRequest(array $token, string $path, array $params = []): array + { + $params = array_merge_recursive($this->getApiHeaders($token), $params); + $paginator = new GithubResultPager($this->httpClient, $this->getApiUrl($path), $params); + return $paginator->all(); } protected function getApiHeaders(array $token, array $default = []): array diff --git a/src/Integrations/Gitlab/GitLabOAuth2Factory.php b/src/Integrations/Gitlab/GitLabOAuth2Factory.php index 8172a953..6ed7be7e 100644 --- a/src/Integrations/Gitlab/GitLabOAuth2Factory.php +++ b/src/Integrations/Gitlab/GitLabOAuth2Factory.php @@ -6,6 +6,7 @@ use Packeton\Integrations\Factory\OAuth2FactoryInterface; use Packeton\Integrations\Factory\OAuth2FactoryTrait; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\Attribute\Exclude; #[Exclude] @@ -15,4 +16,18 @@ class GitLabOAuth2Factory implements OAuth2FactoryInterface protected $key = 'gitlab'; use OAuth2FactoryTrait; + + /** + * {@inheritdoc} + */ + public function addConfiguration(NodeDefinition $node): void + { + $builder = $node->children(); + + $builder + ->scalarNode('client_id')->end() + ->scalarNode('client_secret')->end() + ->scalarNode('api_version')->example('v4')->end(); + + } } diff --git a/src/Integrations/IntegrationRegistry.php b/src/Integrations/IntegrationRegistry.php index e78948c9..f90bd09e 100644 --- a/src/Integrations/IntegrationRegistry.php +++ b/src/Integrations/IntegrationRegistry.php @@ -74,6 +74,9 @@ public function findApp(string $name, bool $check = true): AppInterface return $app; } + /** + * @return AppInterface[] + */ public function findAllApps(): array { $apps = []; diff --git a/src/Integrations/Model/AppConfig.php b/src/Integrations/Model/AppConfig.php index 3c284233..101ca520 100644 --- a/src/Integrations/Model/AppConfig.php +++ b/src/Integrations/Model/AppConfig.php @@ -56,6 +56,16 @@ public function roles(): array return $this->defaultRoles; } + public function getLogo(): ?string + { + return $this->config['logo'] ?? null; + } + + public function getTitle(): ?string + { + return $this->config['title'] ?? null; + } + public function getName(): string { return $this->config['name']; diff --git a/src/Integrations/Model/IntegrationUtils.php b/src/Integrations/Model/IntegrationUtils.php index 994add89..a625afca 100644 --- a/src/Integrations/Model/IntegrationUtils.php +++ b/src/Integrations/Model/IntegrationUtils.php @@ -5,7 +5,9 @@ namespace Packeton\Integrations\Model; use Packeton\Entity\OAuthIntegration; +use Packeton\Entity\OAuthIntegration as App; use Packeton\Integrations\AppInterface; +use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; class IntegrationUtils { @@ -25,6 +27,31 @@ public static function findUrl(string $externalId, OAuthIntegration $oauth, AppI return self::clonePref($app->getConfig(), $oauth) === 'clone_ssh' && isset($repo['ssh_url']) ? $repo['ssh_url'] : $repo['url']; } + public static function castError(\Throwable $e, App|array $app = null): string + { + $msg = ''; + $app = $app instanceof App ? $app->getAccessToken() : $app; + if ($e instanceof HttpExceptionInterface) { + try { + $msg = $e->getResponse()->getContent(false); + $msg = trim(substr($msg, 0, 512)); + } catch (\Throwable) { + } + } + + if (empty($msg)) { + $msg = $e->getMessage(); + } + + if (isset($app['access_token']) && is_string($app['access_token'])) { + $msg = str_replace($app['access_token'], '***', $msg); + } + if (isset($app['refresh_token']) && is_string($app['refresh_token'])) { + $msg = str_replace($app['refresh_token'], '***', $msg); + } + return $msg; + } + public static function clonePref(AppConfig $config, OAuthIntegration $oauth): string { return $oauth->getClonePreference() ?: $config->clonePref(); diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php index 91bdf52a..aa9bd91b 100644 --- a/src/Menu/MenuBuilder.php +++ b/src/Menu/MenuBuilder.php @@ -5,6 +5,7 @@ use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; use Packeton\Entity\User; +use Packeton\Integrations\IntegrationRegistry; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -16,7 +17,8 @@ public function __construct( private readonly FactoryInterface $factory, private readonly TokenStorageInterface $tokenStorage, private readonly TranslatorInterface $translator, - private readonly AuthorizationCheckerInterface $checker + private readonly AuthorizationCheckerInterface $checker, + private readonly IntegrationRegistry $integrations ) {} public function createUserMenu() @@ -51,6 +53,9 @@ public function createAdminMenu() $menu->addChild($this->translator->trans('menu.ssh_keys'), ['label' => 'menu.ssh_keys_icon', 'route' => 'user_add_sshkey', 'extras' => ['safe_label' => true]]); $menu->addChild($this->translator->trans('menu.webhooks'), ['label' => 'menu.webhooks_icon', 'route' => 'webhook_index', 'extras' => ['safe_label' => true]]); $menu->addChild($this->translator->trans('menu.proxies'), ['label' => 'menu.proxies_icon', 'route' => 'proxies_list', 'extras' => ['safe_label' => true]]); + if ($this->integrations->getNames()) { + $menu->addChild($this->translator->trans('menu.integrations'), ['label' => 'menu.integrations_icon', 'route' => 'integration_list', 'extras' => ['safe_label' => true]]); + } return $menu; } diff --git a/src/Model/AutoHookUser.php b/src/Model/AutoHookUser.php index c0eabd7d..82a06c0f 100644 --- a/src/Model/AutoHookUser.php +++ b/src/Model/AutoHookUser.php @@ -6,8 +6,17 @@ use Symfony\Component\Security\Core\User\UserInterface; -final class AutoHookUser implements UserInterface +class AutoHookUser implements UserInterface { + public function __construct(protected string|int $hookIdentifier) + { + } + + public function getHookIdentifier(): string|int + { + return $this->hookIdentifier; + } + /** * {@inheritdoc} */ @@ -28,6 +37,6 @@ public function eraseCredentials() */ public function getUserIdentifier(): string { - return 'token_hooks'; + return 'token_hooks'.$this->hookIdentifier; } } diff --git a/src/Model/PackageManager.php b/src/Model/PackageManager.php index 17be7189..facda502 100644 --- a/src/Model/PackageManager.php +++ b/src/Model/PackageManager.php @@ -11,6 +11,7 @@ use Packeton\Composer\PackagistFactory; use Packeton\Entity\User; use Packeton\Entity\Version; +use Packeton\Event\PackageEvent; use Packeton\Event\UpdaterEvent; use Packeton\Package\InMemoryDumper; use Packeton\Repository\VersionRepository; @@ -41,6 +42,14 @@ public function __construct( ) { } + public function insetPackage(Package $package): void + { + $this->providerManager->insertPackage($package); + $this->dispatcher->dispatch(new PackageEvent($package), PackageEvent::PACKAGE_CREATE); + $em = $this->doctrine->getManager(); + $em->flush(); + } + public function deletePackage(Package $package) { /** @var VersionRepository $versionRepo */ diff --git a/src/Model/PatUserScores.php b/src/Model/PatUserScores.php index 3de2abaf..0877ca00 100644 --- a/src/Model/PatUserScores.php +++ b/src/Model/PatUserScores.php @@ -37,4 +37,9 @@ public static function getAllowedRoutes(string|array $scores): array return array_unique($routes); } + + public static function isAllowed(string|array $scores, $route): bool + { + return in_array($route, self::getAllowedRoutes($scores), true); + } } diff --git a/src/Security/Api/ApiTokenAuthenticator.php b/src/Security/Api/ApiTokenAuthenticator.php index 67ac5c25..d1b556f0 100644 --- a/src/Security/Api/ApiTokenAuthenticator.php +++ b/src/Security/Api/ApiTokenAuthenticator.php @@ -108,10 +108,8 @@ public function supports(Request $request): bool } if ($username = $request->query->get('token')) { - $username = \explode(':', $username); - if (2 === \count($username)) { - return true; - } + $token = \explode(':', $username); + return 2 === \count($token) || strlen($username) > 32; } return false; diff --git a/src/Security/Token/IntegrationTokenChecker.php b/src/Security/Token/IntegrationTokenChecker.php index c8a9ce93..683dcbeb 100644 --- a/src/Security/Token/IntegrationTokenChecker.php +++ b/src/Security/Token/IntegrationTokenChecker.php @@ -7,6 +7,7 @@ use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\OAuthIntegration; use Packeton\Model\AutoHookUser; +use Packeton\Model\PatUserScores; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; @@ -42,7 +43,7 @@ public function loadUserByToken(string $username, string $token, callable $userL $apiToken = $token ? $this->registry->getRepository(OAuthIntegration::class)->findOneBy(['hookSecret' => $token]) : null; if ($apiToken instanceof OAuthIntegration) { - return new AutoHookUser(); + return new AutoHookUser($apiToken->getId()); } throw new BadCredentialsException('Bad credentials'); @@ -54,7 +55,7 @@ public function loadUserByToken(string $username, string $token, callable $userL public function checkAccess(Request $request, UserInterface $user = null): void { $route = $request->attributes->get('_route'); - if ($route !== 'api_integration_postreceive') { + if ($route !== 'api_integration_postreceive' && !PatUserScores::isAllowed('webhooks', $route)) { throw new CustomUserMessageAccountStatusException('Integration access token allowed only for web hooks route "api_integration_postreceive"'); } } diff --git a/src/Security/Token/PatTokenChecker.php b/src/Security/Token/PatTokenChecker.php index 3432b6ae..eee2d41d 100644 --- a/src/Security/Token/PatTokenChecker.php +++ b/src/Security/Token/PatTokenChecker.php @@ -86,8 +86,7 @@ public function checkAccess(Request $request, UserInterface $user): void throw new BadCredentialsException('Bad credentials.'); } - $allowed = PatUserScores::getAllowedRoutes($user->getScores()); - if (!in_array($route, $allowed, true)) { + if (!PatUserScores::isAllowed($user->getScores(), $route)) { throw new CustomUserMessageAccountStatusException('This access token does not grant access for this route'); } } diff --git a/src/Twig/PackagistExtension.php b/src/Twig/PackagistExtension.php index 1e791b57..50d2923c 100644 --- a/src/Twig/PackagistExtension.php +++ b/src/Twig/PackagistExtension.php @@ -188,8 +188,8 @@ public function getZipballs() }, $data); } - public function generateGravatarHash($email) + public function generateGravatarHash($email): string { - return md5(strtolower($email)); + return md5(strtolower((string)$email)); } } diff --git a/templates/integration/connect.html.twig b/templates/integration/connect.html.twig new file mode 100644 index 00000000..b76b0d7a --- /dev/null +++ b/templates/integration/connect.html.twig @@ -0,0 +1,45 @@ +{% extends "layout.html.twig" %} + +{% block title %}Integration Connect - {{ parent() }}{% endblock %} + +{% block content %} +
+
+

Available integrations

+ +

+ To use OAuth2 API synchronization with on-premises versions of GitHub, Bitbucket, Gitea or GitLab you need + Authorizing Packeton APP with your account remote VCS account. + You can also use them to login to Packeton via OAuth2. +

+ +

+ Packeton use Symfony Http client, if you need to change http preference you may configure it globally + https://symfony.com/doc/current/http_client.html +

+
+ + {% for integration in integrations %} + {% set config = integration.getConfig(null, true) %} + +
+
+ {% if config.logo %} + connect + {% endif %} + {{ config.title ?? config.name|capitalize }} +
+ + +
+
+ +
+ Connect +
+
+ {% endfor %} + +
+
+{% endblock %} diff --git a/templates/integration/index.html.twig b/templates/integration/index.html.twig index 1e57f91c..352f231d 100644 --- a/templates/integration/index.html.twig +++ b/templates/integration/index.html.twig @@ -61,19 +61,44 @@


{% for org in orgs %} -
+ {% set isConn = oauth.isConnected(org['identifier']) %} + {% set webhookInfo = oauth.getWebhookInfo(org['identifier']) %} +
{% if org['logo'] is defined and org['logo'] %} - + logo + {% else %} + logo {% endif %} {{ org['name'] }} - {% if org['identifier'] == '@self' %}{% endif %} + + {% if isConn and webhookInfo %} + {% if webhookInfo['status'] ?? true %} + Hook enabled + {% else %} + Hook error + {% endif %} + {% endif %} + +
- {% if oauth.connected(org['identifier']) %} + {% if isConn %} {% else %} diff --git a/templates/integration/list.html.twig b/templates/integration/list.html.twig new file mode 100644 index 00000000..cd92f4de --- /dev/null +++ b/templates/integration/list.html.twig @@ -0,0 +1,38 @@ +{% extends "layout.html.twig" %} + +{% block title %}Integrations - {{ parent() }}{% endblock %} + +{% block content %} +
+ Install Integration +
+
+ {% if integrations|length > 0 %} + + {% endif %} +
+{% endblock %} diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 9ccf57e5..45bc48a6 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -28,6 +28,7 @@ menu: ssh_keys: Credentials webhooks: Webhooks proxies: Composer Proxies + integrations: Integrations profile_icon: 'Profile' logout_icon: 'Logout' @@ -42,6 +43,7 @@ menu: break_line_icon: '
' proxies_icon: 'Composer Proxies' settings_icon: 'Settings' + integrations_icon: 'Integrations' security: login: