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

Implémenter l'API Gandi en tant que Provider #28

Merged
merged 8 commits into from
Aug 6, 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
25 changes: 24 additions & 1 deletion assets/components/tracking/ConnectorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,35 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
>
<Select options={ovhPricingMode} optionFilterProp="label"/>
</Form.Item>
</>
}
{
provider === ConnectorProvider.GANDI && <>
<Form.Item
label={t`Personal Access Token (PAT)`}
name={['authData', 'token']}
rules={[{required: true, message: t`Required`}]}>
<Input autoComplete='off'/>
</Form.Item>
<Form.Item
label={t`Organization sharing ID`}
name={['authData', 'sharingId']}
help={<Typography.Text
type='secondary'>{t`It indicates the organization that will pay for the ordered product`}</Typography.Text>}
required={false}>
<Input autoComplete='off' placeholder='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'/>
</Form.Item>
</>
}

{
provider !== undefined && <>
<Form.Item
valuePropName="checked"
label={t`API Terms of Service`}
name={['authData', 'acceptConditions']}
rules={[{required: true, message: t`Required`}]}
style={{marginTop: '3em'}}
>
<Checkbox
required={true}>
Expand Down Expand Up @@ -124,7 +148,6 @@ export function ConnectorForm({form, onCreate}: { form: FormInstance, onCreate:
</>
}


<Form.Item style={{marginTop: 10}}>
<Space>
<Button type="primary" htmlType="submit">
Expand Down
3 changes: 2 additions & 1 deletion assets/utils/api/connectors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {request} from "./index";

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

export type Connector = {
Expand Down
8 changes: 8 additions & 0 deletions assets/utils/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ export const helpGetTokenLink = (provider?: string) => {
href="https://api.ovh.com/createToken/index.cgi?GET=/order/cart&GET=/order/cart/*&POST=/order/cart&POST=/order/cart/*&DELETE=/order/cart/*">
{t`Retrieve a set of tokens from your customer account on the Provider's website`}
</Typography.Link>

case ConnectorProvider.GANDI:
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>
default:
return <></>

}
}

export const tosHyperlink = (provider?: string) => {
switch (provider) {
case ConnectorProvider.OVH:
return 'https://www.ovhcloud.com/fr/terms-and-conditions/contracts/'
case ConnectorProvider.GANDI:
return 'https://www.gandi.net/en/contracts/terms-of-service'
default:
return ''
}
Expand Down
5 changes: 4 additions & 1 deletion src/Config/Connector/ConnectorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace App\Config\Connector;

use App\Entity\Domain;
use Symfony\Contracts\HttpClient\HttpClientInterface;

interface ConnectorInterface
{
public static function verifyAuthData(array $authData): array;
public function __construct(array $authData, HttpClientInterface $client);

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

public static function verifyAuthData(array $authData, HttpClientInterface $client);
}
133 changes: 133 additions & 0 deletions src/Config/Connector/GandiConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace App\Config\Connector;

use App\Entity\Domain;
use http\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

readonly class GandiConnector implements ConnectorInterface
{
private const BASE_URL = 'https://api.gandi.net/v5';

public function __construct(private array $authData, private HttpClientInterface $client)
{
}

/**
* Order a domain name with the Gandi API.
*
* @throws \Exception
* @throws TransportExceptionInterface
* @throws DecodingExceptionInterface
*/
public function orderDomain(Domain $domain, bool $dryRun = false): void
{
if (!$domain->getDeleted()) {
throw new InvalidArgumentException('The domain name still appears in the WHOIS database');
}

$ldhName = $domain->getLdhName();
if (!$ldhName) {
throw new InvalidArgumentException('Domain name cannot be null');
}

$authData = self::verifyAuthData($this->authData, $this->client);

$user = $this->client->request('GET', '/v5/organization/user-info', (new HttpOptions())
->setAuthBearer($authData['token'])
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->toArray()
)->toArray();

$httpOptions = (new HttpOptions())
->setAuthBearer($authData['token'])
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->setHeader('Dry-Run', $dryRun ? '1' : '0')
->setJson([
'fqdn' => $ldhName,
'owner' => [
'email' => $user['email'],
'given' => $user['firstname'],
'family' => $user['lastname'],
'streetaddr' => $user['streetaddr'],
'zip' => $user['zip'],
'city' => $user['city'],
'state' => $user['state'],
'phone' => $user['phone'],
'country' => $user['country'],
'type' => 'individual',
],
'tld_period' => 'golive',
]);

if (array_key_exists('sharingId', $authData)) {
$httpOptions->setQuery([
'sharing_id' => $authData['sharingId'],
]);
}

$res = $this->client->request('POST', '/domain/domains', $httpOptions->toArray());

if ((!$dryRun && Response::HTTP_ACCEPTED !== $res->getStatusCode())
|| ($dryRun && Response::HTTP_OK !== $res->getStatusCode())) {
throw new \Exception($res->toArray()['message']);
}
}

/**
* @throws TransportExceptionInterface
*/
public static function verifyAuthData(array $authData, HttpClientInterface $client): array
maelgangloff marked this conversation as resolved.
Show resolved Hide resolved
{
$token = $authData['token'];

$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
$waiveRetractationPeriod = $authData['waiveRetractationPeriod'];

if (!is_string($token) || empty($token)
|| (array_key_exists('sharingId', $authData) && !is_string($authData['sharingId']))
) {
throw new BadRequestHttpException('Bad authData schema');
}

if (true !== $acceptConditions
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod) {
throw new HttpException(451, 'The user has not given explicit consent');
}

$response = $client->request('GET', '/v5/organization/user-info', (new HttpOptions())
->setAuthBearer($token)
->setHeader('Accept', 'application/json')
->setBaseUri(self::BASE_URL)
->toArray()
);

if (Response::HTTP_OK !== $response->getStatusCode()) {
throw new BadRequestHttpException('The status of these credentials is not valid');
}

$authDataReturned = [
'token' => $token,
'acceptConditions' => $acceptConditions,
'ownerLegalAge' => $ownerLegalAge,
'waiveRetractationPeriod' => $waiveRetractationPeriod,
];

if (array_key_exists('sharingId', $authData)) {
$authDataReturned['sharingId'] = $authData['sharingId'];
}

return $authDataReturned;
}
}
22 changes: 13 additions & 9 deletions src/Config/Connector/OvhConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use App\Entity\Domain;
use Ovh\Api;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Contracts\HttpClient\HttpClientInterface;

readonly class OvhConnector implements ConnectorInterface
{
Expand All @@ -30,7 +32,7 @@
],
];

public function __construct(private array $authData)
public function __construct(private array $authData, private HttpClientInterface $client)
{
}

Expand All @@ -39,8 +41,8 @@ public function __construct(private array $authData)
*
* @throws \Exception
*/
public function orderDomain(Domain $domain, bool $dryRun = false
): void {
public function orderDomain(Domain $domain, bool $dryRun = false): void
{
if (!$domain->getDeleted()) {
throw new \Exception('The domain name still appears in the WHOIS database');
}
Expand All @@ -50,7 +52,7 @@ public function orderDomain(Domain $domain, bool $dryRun = false
throw new \Exception('Domain name cannot be null');
}

$authData = self::verifyAuthData($this->authData);
$authData = self::verifyAuthData($this->authData, $this->client);

$acceptConditions = $authData['acceptConditions'];
$ownerLegalAge = $authData['ownerLegalAge'];
Expand Down Expand Up @@ -118,7 +120,7 @@ public function orderDomain(Domain $domain, bool $dryRun = false
/**
* @throws \Exception
*/
public static function verifyAuthData(array $authData): array
public static function verifyAuthData(array $authData, HttpClientInterface $client): array
{
$appKey = $authData['appKey'];
$appSecret = $authData['appSecret'];
Expand All @@ -137,14 +139,16 @@ public static function verifyAuthData(array $authData): array
|| !is_string($apiEndpoint) || empty($apiEndpoint)
|| !is_string($ovhSubsidiary) || empty($ovhSubsidiary)
|| !is_string($pricingMode) || empty($pricingMode)

|| true !== $acceptConditions
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod
) {
throw new BadRequestHttpException('Bad authData schema');
}

if (true !== $acceptConditions
maelgangloff marked this conversation as resolved.
Show resolved Hide resolved
|| true !== $ownerLegalAge
|| true !== $waiveRetractationPeriod) {
throw new HttpException(451, 'The user has not given explicit consent');
}

$conn = new Api(
$appKey,
$appSecret,
Expand Down
12 changes: 12 additions & 0 deletions src/Config/ConnectorProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@

namespace App\Config;

use App\Config\Connector\GandiConnector;
use App\Config\Connector\OvhConnector;

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

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

namespace App\Controller;

use App\Config\Connector\OvhConnector;
use App\Config\ConnectorProvider;
use App\Config\Connector\ConnectorInterface;
use App\Entity\Connector;
use App\Entity\User;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class ConnectorController extends AbstractController
{
Expand Down Expand Up @@ -43,6 +43,7 @@ public function getConnector(): Collection

/**
* @throws \Exception
* @throws TransportExceptionInterface
*/
#[Route(
path: '/api/connectors',
Expand All @@ -53,7 +54,7 @@ public function getConnector(): Collection
],
methods: ['POST']
)]
public function createConnector(Request $request): Connector
public function createConnector(Request $request, HttpClientInterface $client): Connector
{
$connector = $this->serializer->deserialize($request->getContent(), Connector::class, 'json', ['groups' => 'connector:create']);
/** @var User $user */
Expand All @@ -67,17 +68,21 @@ public function createConnector(Request $request): Connector
'provider' => $provider->value,
]);

if (ConnectorProvider::OVH === $provider) {
$authData = OvhConnector::verifyAuthData($connector->getAuthData());
$connector->setAuthData($authData);

$this->logger->info('User {username} authentication data with the OVH provider has been validated.', [
'username' => $user->getUserIdentifier(),
]);
} else {
throw new BadRequestHttpException('Unknown provider');
if (null === $provider) {
throw new \Exception('Provider not found');
}

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

$authData = $connectorProviderClass::verifyAuthData($connector->getAuthData(), $client);
$connector->setAuthData($authData);

$this->logger->info('User {username} authentication data with the {provider} provider has been validated.', [
'username' => $user->getUserIdentifier(),
'provider' => $provider->value,
]);

$this->logger->info('The new API connector requested by {username} has been successfully registered.', [
'username' => $user->getUserIdentifier(),
]);
Expand Down
Loading
Loading