Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namecheap support #51

Merged
merged 22 commits into from
Oct 2, 2024
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
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ OAUTH_TOKEN_URL=
OAUTH_USERINFO_URL=
OAUTH_SCOPE=

# Typically your IP address, this envvar is required for
# some connectors that need to be provided with your host's
# outgoing IP address.
OUTGOING_IP=

LIMITED_FEATURES=false
LIMIT_MAX_WATCHLIST=0
LIMIT_MAX_WATCHLIST_DOMAINS=0
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ yarn-error.log
phpstan.neon
###< phpstan/phpstan ###

# Eclipse
.project
.buildpath
/.settings/

public/images/*.png
public/content/*.md
public/favicon.ico
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The table below lists the supported API connector providers:
|:---------:|---------------------------------------------------------------|:---------:|
| OVH | https://api.ovh.com | **Yes** |
| GANDI | https://api.gandi.net/docs/domains/ | **Yes** |
| NAMECHEAP | https://www.namecheap.com/support/api/methods/domains/create/ | |
| NAMECHEAP | https://www.namecheap.com/support/api/methods/domains/create/ | **Yes** |

If a domain has expired and a connector is linked to the Watchlist, then Domain Watchdog will try to order it via the
connector provider's API.
Expand Down
16 changes: 16 additions & 0 deletions assets/components/tracking/connector/ConnectorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
</Form.Item>
</>
}
{
provider === ConnectorProvider.NAMECHEAP && <>
<Form.Item
label={t`Username`}
name={['authData', 'ApiUser']}
>
<Input autoComplete='off'></Input>
</Form.Item>
<Form.Item
label={t`API key`}
name={['authData', 'ApiKey']}
>
<Input autoComplete='off'></Input>
</Form.Item>
</>
}

{
provider !== undefined && <>
Expand Down
3 changes: 2 additions & 1 deletion assets/utils/api/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {request} from "./index";

export enum ConnectorProvider {
OVH = 'ovh',
GANDI = 'gandi'
GANDI = 'gandi',
NAMECHEAP = 'namecheap'
}

export type Connector = {
Expand Down
4 changes: 4 additions & 0 deletions assets/utils/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const helpGetTokenLink = (provider?: string) => {
return <Typography.Link target='_blank' href="https://admin.gandi.net/organizations/account/pat">
{t`Retrieve a Personal Access Token from your customer account on the Provider's website`}
</Typography.Link>
case ConnectorProvider.NAMECHEAP:
return <Typography.Link target='_blank' href="https://ap.www.namecheap.com/settings/tools/apiaccess/">
{t`Retreive an API key and whitelist this instance's IP address on Namecheap's website`}
</Typography.Link>
default:
return <></>

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
"symfony/zulip-notifier": "7.1.*",
"symfonycasts/verify-email-bundle": "*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"twig/twig": "^2.12|^3.0",
"ext-simplexml": "*"
},
"config": {
"allow-plugins": {
Expand Down
3 changes: 3 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ parameters:
limit_max_watchlist_domains: '%env(int:LIMIT_MAX_WATCHLIST_DOMAINS)%'
limit_max_watchlist_webhooks: '%env(int:LIMIT_MAX_WATCHLIST_WEBHOOKS)%'

outgoing_ip: '%env(string:OUTGOING_IP)%'

services:
# default configuration for services in *this* file
_defaults:
Expand All @@ -25,6 +27,7 @@ services:
bind:
$mailerSenderEmail: '%mailer_sender_email%'
$mailerSenderName: '%mailer_sender_name%'
$outgoingIp: '%outgoing_ip%'

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Expand Down
9 changes: 6 additions & 3 deletions src/Config/ConnectorProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

namespace App\Config;

use App\Config\Provider\GandiProvider;
use App\Config\Provider\OvhProvider;
use App\Service\Connector\GandiProvider;
use App\Service\Connector\NamecheapProvider;
use App\Service\Connector\OvhProvider;

enum ConnectorProvider: string
{
case OVH = 'ovh';
case GANDI = 'gandi';
case NAMECHEAP = 'namecheap';

public function getConnectorProvider(): string
{
return match ($this) {
ConnectorProvider::OVH => OvhProvider::class,
ConnectorProvider::GANDI => GandiProvider::class
ConnectorProvider::GANDI => GandiProvider::class,
ConnectorProvider::NAMECHEAP => NamecheapProvider::class,
};
}
}
15 changes: 9 additions & 6 deletions src/Controller/ConnectorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Controller;

use App\Config\Provider\AbstractProvider;
use App\Entity\Connector;
use App\Entity\User;
use App\Service\Connector\AbstractProvider;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
Expand All @@ -20,7 +22,9 @@ class ConnectorController extends AbstractController
public function __construct(
private readonly SerializerInterface $serializer,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger
private readonly LoggerInterface $logger,
#[Autowire(service: 'service_container')]
private ContainerInterface $locator
) {
}

Expand Down Expand Up @@ -71,10 +75,9 @@ public function createConnector(Request $request, HttpClientInterface $client):
throw new BadRequestHttpException('Provider not found');
}

/** @var AbstractProvider $connectorProviderClass */
$connectorProviderClass = $provider->getConnectorProvider();

$authData = $connectorProviderClass::verifyAuthData($connector->getAuthData(), $client);
/** @var AbstractProvider $providerClient */
$providerClient = $this->locator->get($provider->getConnectorProvider());
$authData = $providerClient->verifyAuthData($connector->getAuthData());
$connector->setAuthData($authData);

$this->logger->info('User {username} authentication data with the {provider} provider has been validated.', [
Expand Down
18 changes: 8 additions & 10 deletions src/Controller/WatchListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Controller;

use App\Config\Provider\AbstractProvider;
use App\Entity\Connector;
use App\Entity\Domain;
use App\Entity\DomainEntity;
Expand All @@ -12,6 +11,7 @@
use App\Notifier\TestChatNotification;
use App\Repository\WatchListRepository;
use App\Service\ChatNotificationService;
use App\Service\Connector\AbstractProvider;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\ORMException;
Expand All @@ -27,21 +27,20 @@
use Eluceo\iCal\Presentation\Component\Property;
use Eluceo\iCal\Presentation\Component\Property\Value\TextValue;
use Eluceo\iCal\Presentation\Factory\CalendarFactory;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Sabre\VObject\EofException;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class WatchListController extends AbstractController
{
Expand All @@ -50,10 +49,9 @@ public function __construct(
private readonly EntityManagerInterface $em,
private readonly WatchListRepository $watchListRepository,
private readonly LoggerInterface $logger,
private readonly HttpClientInterface $httpClient,
private readonly CacheItemPoolInterface $cacheItemPool,
private readonly KernelInterface $kernel,
private readonly ChatNotificationService $chatNotificationService
private readonly ChatNotificationService $chatNotificationService,
#[Autowire(service: 'service_container')]
private ContainerInterface $locator
) {
}

Expand Down Expand Up @@ -182,9 +180,9 @@ private function verifyConnector(WatchList $watchList, ?Connector $connector): v

$connectorProviderClass = $connector->getProvider()->getConnectorProvider();
/** @var AbstractProvider $connectorProvider */
$connectorProvider = new $connectorProviderClass($connector->getAuthData(), $this->httpClient, $this->cacheItemPool, $this->kernel);
$connectorProvider = $this->locator->get($connectorProviderClass);

$connectorProvider::verifyAuthData($connector->getAuthData(), $this->httpClient); // We want to check if the tokens are OK
$connectorProvider->authenticate($connector->getAuthData());
$supported = $connectorProvider->isSupported(...$watchList->getDomains()->toArray());

if (!$supported) {
Expand Down
15 changes: 8 additions & 7 deletions src/MessageHandler/OrderDomainHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\MessageHandler;

use App\Config\Provider\AbstractProvider;
use App\Entity\Domain;
use App\Entity\WatchList;
use App\Message\OrderDomain;
Expand All @@ -11,16 +10,17 @@
use App\Repository\DomainRepository;
use App\Repository\WatchListRepository;
use App\Service\ChatNotificationService;
use App\Service\Connector\AbstractProvider;
use App\Service\StatService;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Mime\Address;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsMessageHandler]
final readonly class OrderDomainHandler
Expand All @@ -33,12 +33,12 @@ public function __construct(
private WatchListRepository $watchListRepository,
private DomainRepository $domainRepository,
private KernelInterface $kernel,
private HttpClientInterface $client,
private CacheItemPoolInterface $cacheItemPool,
private MailerInterface $mailer,
private LoggerInterface $logger,
private StatService $statService,
private ChatNotificationService $chatNotificationService
private ChatNotificationService $chatNotificationService,
#[Autowire(service: 'service_container')]
private ContainerInterface $locator
) {
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
}
Expand Down Expand Up @@ -72,7 +72,8 @@ public function __invoke(OrderDomain $message): void
$connectorProviderClass = $provider->getConnectorProvider();

/** @var AbstractProvider $connectorProvider */
$connectorProvider = new $connectorProviderClass($connector->getAuthData(), $this->client, $this->cacheItemPool, $this->kernel);
$connectorProvider = $this->locator->get($connectorProviderClass);
$connectorProvider->authenticate($connector->getAuthData());

$connectorProvider->orderDomain($domain, $this->kernel->isDebug());
$this->statService->incrementStat('stats.domain.purchased');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
<?php

namespace App\Config\Provider;
namespace App\Service\Connector;

use App\Entity\Domain;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* The typical flow of a provider will go as follows:
*
* MyProvider $provider; // gotten from DI
* $provider->authenticate($authData);
* $provider->orderDomain($domain, $dryRun);
*/
abstract class AbstractProvider
{
protected array $authData;

public function __construct(
protected array $authData,
protected HttpClientInterface $client,
protected CacheItemPoolInterface $cacheItemPool,
protected KernelInterface $kernel
protected CacheItemPoolInterface $cacheItemPool
) {
}

abstract public static function verifyAuthData(array $authData, HttpClientInterface $client): array;
/**
* @param array $authData raw authentication data as supplied by the user
*
* @return array a cleaned up version of the authentication data
*/
abstract public function verifyAuthData(array $authData): array;

/**
* @throws \Exception when the registrar denies the authentication
*/
abstract public function assertAuthentication(): void; // TODO use dedicated exception type

abstract public function orderDomain(Domain $domain, bool $dryRun): void;

Expand Down Expand Up @@ -55,6 +69,15 @@ public function isSupported(Domain ...$domainList): bool
return true;
}

/**
* @throws \Exception
*/
public function authenticate(array $authData): void
{
$this->authData = $this->verifyAuthData($authData);
$this->assertAuthentication();
}

abstract protected function getCachedTldList(): CacheItemInterface;

abstract protected function getSupportedTldList(): array;
Expand Down
Loading
Loading