diff --git a/README.md b/README.md
index e97fe183..2b9fb476 100644
--- a/README.md
+++ b/README.md
@@ -72,22 +72,33 @@ $apiClient->setAccessToken($accessToken)
});
```
-Отправить пользователя на страницу авторизации можно 2мя способами:
-1. Отрисовав кнопку на сайт:
+Доступные методы отправки на страницу авторизации пользователя
+1. Кнопка установки существующей интеграции по client_id
```php
-$apiClient->getOAuthClient()->getOAuthButton(
- [
- 'title' => 'Установить интеграцию',
- 'compact' => true,
- 'class_name' => 'className',
- 'color' => 'default',
- 'error_callback' => 'handleOauthError',
- 'state' => $state,
- ]
- );
+// data-client-id будет взят из API клиента
+$apiClient->getOAuthClient()->getOAuthButton([
+ 'title' => 'Установить интеграцию',
+ 'compact' => true,
+ 'class_name' => 'className',
+ 'color' => 'default',
+ 'error_callback' => 'handleOauthError',
+ 'state' => $state,
+]);
```
-2. Отправив пользователя на страницу авторизации
+2. Кнопка с метаданными для создания внешней интеграции. Для каждой установки создается отдельная интеграция
+
+```php
+// redirect_uri будет взят из API клиента
+$apiClient->getOAuthClient()->getOAuthButton([
+ 'is_metadata' => true, // указываем, что передаем метаданные в кнопку. По умолчанию - false
+ 'secrets_uri' => 'https://your-domain.com/secrets'
+ 'title' => 'Создать интеграцию',
+ // ... другие параметры
+]);
+```
+
+3. Прямой редирект на страницу авторизации
```php
$authorizationUrl = $apiClient->getOAuthClient()->getAuthorizeUrl([
'state' => $state,
diff --git a/examples/oauth_button_actions.php b/examples/oauth_button_actions.php
new file mode 100644
index 00000000..f5bdfb74
--- /dev/null
+++ b/examples/oauth_button_actions.php
@@ -0,0 +1,52 @@
+getOAuthClient()->getOAuthButton(
+ [
+ 'title' => 'title_tmedvedevv',
+ 'compact' => false,
+ 'is_kommo' => true,
+ 'class_name' => 'classNameTmedvedevv',
+ 'color' => 'red',
+ 'mode' => 'popup',
+ 'error_callback' => 'handleOauthError'
+ ]
+);
+
+var_dump($buttonByClientId);
+
+echo "\n=== Генерация кнопки с метаданными, необходимые для создания внешней интеграции ===\n";
+
+// Генерируем кнопку с метаданными для внешней интеграции
+// client_id из API клиента будет проигнорирован т.к создается новая интеграция в аккаунте
+// После установки приходит 2 хука.
+
+$buttonMetadata = $apiClient->getOAuthClient()->getOAuthButton([
+ 'title' => 'title_tmedvedevv',
+ 'compact' => true,
+ 'is_kommo' => false,
+ 'class_name' => 'classNameTmedvedevv',
+ 'color' => 'red',
+ 'mode' => 'popup',
+ 'error_callback' => 'handleOauthError',
+ 'scopes' => ['crm'],
+ 'is_metadata' => true, // Указываем, поскольку нужна кнопка с передачей метаданных. По умолчанию false
+ 'secrets_uri' => $secretsUri // url куда будут отправлены данные по интеграции
+]);
+
+var_dump($buttonMetadata);
diff --git a/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php b/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php
new file mode 100644
index 00000000..e2dac98c
--- /dev/null
+++ b/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php
@@ -0,0 +1,10 @@
+ '#D84315',
];
+ /**
+ * Доступные scopes для кнопки с метаданными, необходимые для создания внешней интеграции.
+ */
+ public const METADATA_BUTTON_AVAILABLE_SCOPES = ['crm', 'notifications'];
+
protected const REQUEST_TIMEOUT = 15;
/**
@@ -333,24 +338,38 @@ public function getResourceOwner(AccessTokenInterface $accessToken): ResourceOwn
/**
* Доступные значения для options:
+ * string class_name
* string title
* bool compact
- * string class_name
* string color
* string state
* string error_callback
+ * string mode
+ * bool is_kommo - Использовать Kommo вместо amoCRM
+ * bool is_metadata - Создать кнопку с передачей метаданных для установки внешней интеграции
+ *
+ * Для кнопки с метаданными:
+ * string name - Название интеграции
+ * string secrets_uri - Адрес, куда будет отправлен webhook с client_id, state и секретным ключом интеграции
+ * string description - Описание интеграции
+ * array scopes - Запрашиваемые права
+ * string logo - URL логотипа
*
* @param array $options
*
- * @return string
- * @throws BadTypeException
+ * @return string HTML код кнопки
+ * @throws AmoCRMOAuthButtonConfigurationException
*/
public function getOAuthButton(array $options = []): string
{
if (isset($options['color']) && !array_key_exists($options['color'], self::BUTTON_COLORS)) {
- throw new BadTypeException('Invalid color selected');
+ throw new AmoCRMOAuthButtonConfigurationException(
+ 'Cannot create OAuth button: Invalid color selected'
+ );
}
+ $isMetadata = $options['is_metadata'] ?? false;
+ $clientId = $this->oauthProvider->getClientId();
$title = $options['title'] ?? 'Установить интеграцию';
$compact = isset($options['compact']) && $options['compact'] ? 'true' : 'false';
$className = $options['class_name'] ?? 'className';
@@ -371,7 +390,8 @@ public function getOAuthButton(array $options = []): string
? 'https://www.kommo.com/auth/button.min.js'
: 'https://www.amocrm.ru/auth/button.min.js';
- return '
+ if ($isMetadata === false) {
+ return '
';
+ }
+
+ $secretsUri = $options['secrets_uri'] ?? '';
+ $redirectUri = $this->redirectUri;
+
+ // Должны быть заполнены т.к при передаче метаданных после установки приходит два хука
+ // с данными по самой интеграции и с данными для получения токенов.
+ if (empty($secretsUri) || empty($redirectUri)) {
+ throw new AmoCRMOAuthButtonConfigurationException(
+ 'Cannot create metadata OAuth button: For metadata OAuth button,
+ both secrets_uri and redirect_uri must be configured'
+ );
+ }
+
+ $description = $options['description'] ?? 'Описание интеграции';
+ $name = $options['name'] ?? 'Название интеграции';
+ $logo = $options['logo'] ?? 'https://example.com/amocrm_logo.png';
+
+ if (!isset($options['scopes'])) {
+ $scopes = self::METADATA_BUTTON_AVAILABLE_SCOPES;
+ } else {
+ if (!is_array($options['scopes'])) {
+ throw new AmoCRMOAuthButtonConfigurationException(
+ 'scopes parameter must be an array.'
+ );
+ }
+
+ $scopes = $options['scopes'];
+ }
+
+ $scopes = array_filter($scopes);
+
+ if (empty($scopes)) {
+ $scopes = self::METADATA_BUTTON_AVAILABLE_SCOPES;
+ }
+
+ $invalidScopes = array_diff($scopes, self::METADATA_BUTTON_AVAILABLE_SCOPES);
+ if (!empty($invalidScopes)) {
+ throw new AmoCRMOAuthButtonConfigurationException(sprintf(
+ 'Invalid scopes: %s. Available scopes: %s',
+ implode(', ', $invalidScopes),
+ implode(', ', self::METADATA_BUTTON_AVAILABLE_SCOPES)
+ ));
+ }
+
+ $scopes = implode(',', $scopes);
+
+ return '
+
+
';
}
/**
diff --git a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php
new file mode 100644
index 00000000..f4bdbbf9
--- /dev/null
+++ b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php
@@ -0,0 +1,239 @@
+oauthClient = new AmoCRMOAuth(
+ 'test_client_id',
+ 'test_secret',
+ 'https://example.com/redirect'
+ );
+ }
+
+ /**
+ * Проверяем генерацию обычной кнопки с client_id
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testRegularButtonWithClientId(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([]);
+ $this->assertStringContainsString('data-client-id="test_client_id"', $result);
+ $this->assertStringContainsString('data-title="Установить интеграцию"', $result);
+ $this->assertStringContainsString('data-class-name="className"', $result);
+ }
+
+
+ /**
+ * Проверяем кастомные параметры для обычной кнопки
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testRegularButtonWithCustomOptions(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([
+ 'title' => 'Custom Title',
+ 'class_name' => 'custom-class',
+ 'color' => 'red',
+ 'compact' => false,
+ 'mode' => 'popup'
+ ]);
+
+ $this->assertStringContainsString('data-title="Custom Title"', $result);
+ $this->assertStringContainsString('data-class-name="custom-class"', $result);
+ $this->assertStringContainsString('data-color="red"', $result);
+ $this->assertStringContainsString('data-compact="false"', $result);
+ $this->assertStringContainsString('data-mode="popup"', $result);
+ }
+
+ /**
+ * Проверяем ошибку при невалидном цвете
+ * @return void
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testInvalidColorThrowsException(): void
+ {
+ $this->expectException(AmoCRMOAuthButtonConfigurationException::class);
+ $this->expectExceptionMessage('Invalid color selected');
+ $this->oauthClient->getOAuthButton(['color' => 'invalid_color']);
+ }
+
+
+ /**
+ * Тест 4: Проверяем генерацию metadata кнопки
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonGeneration(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test Integration',
+ 'description' => 'Test Description',
+ 'scopes' => ['crm', 'notifications'],
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+
+ $this->assertStringNotContainsString('data-client-id', $result);
+ $this->assertStringContainsString('data-name="Test Integration"', $result);
+ $this->assertStringContainsString('data-description="Test Description"', $result);
+ $this->assertStringContainsString('data-scopes="crm,notifications"', $result);
+ $this->assertStringContainsString('data-secrets_uri="https://secrets.com"', $result);
+ }
+
+ /**
+ * Должно упасть, если не передали redirect_uri
+ * @return void
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonWithoutNameThrowsException(): void
+ {
+ $oauthClient = new AmoCRMOAuth(
+ 'test_client_id',
+ 'test_secret',
+ null
+ );
+
+ $this->expectException(AmoCRMOAuthButtonConfigurationException::class);
+
+
+ $oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'description' => 'Test',
+ 'scopes' => ['crm'],
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+ }
+
+ /**
+ * Проверяем ошибку при невалидных scopes
+ * @return void
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonWithInvalidScopesThrowsException(): void
+ {
+ $this->expectException(AmoCRMOAuthButtonConfigurationException::class);
+ $this->expectExceptionMessage('Invalid scopes');
+
+ $this->oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'scopes' => ['invalid_scope', 'another_invalid'],
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+ }
+
+ /**
+ * Проверяем ошибку когда scopes не массив
+ * @return void
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonWithStringScopesThrowsException(): void
+ {
+ $this->expectException(AmoCRMOAuthButtonConfigurationException::class);
+ $this->expectExceptionMessage('scopes parameter must be an array');
+
+ $this->oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'scopes' => 'crm,notifications',
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+ }
+
+ /**
+ * Проверяем использование дефолтных scopes когда не переданы
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonUsesDefaultScopes(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'secrets_uri' => 'https://secrets.com'
+ // должны использоваться дефолтные scopes - crm, notifications
+ ]);
+
+ $defaultScopes = implode(',', AmoCRMOAuth::METADATA_BUTTON_AVAILABLE_SCOPES);
+ $this->assertStringContainsString('data-scopes="' . $defaultScopes . '"', $result);
+ }
+
+ /**
+ * Проверяем поддержку Kommo
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testKommoButtonGeneration(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([
+ 'is_kommo' => true,
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'scopes' => ['crm'],
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+
+ $this->assertStringContainsString('class="kommo_oauth"', $result);
+ $this->assertStringContainsString('kommo.com', $result);
+ }
+
+ /**
+ * Проверяем ошибку при отсутствии secrets_uri для metadata кнопки
+ * @return void
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonWithoutSecretsUriThrowsException(): void
+ {
+ $this->expectException(AmoCRMOAuthButtonConfigurationException::class);
+ $this->expectExceptionMessage('secrets_uri');
+
+ // Создаем клиент без secrets_uri
+ $clientWithoutSecrets = new AmoCRMOAuth(
+ 'test_client_id',
+ 'test_secret',
+ 'https://redirect.com'
+ );
+
+ $clientWithoutSecrets->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'scopes' => ['crm']
+ ]);
+ }
+
+ /**
+ * Проверяем работу с пустым массивом scopes
+ * Должны подставиться дефолтные scopes
+ * @throws AmoCRMOAuthButtonConfigurationException
+ */
+ public function testMetadataButtonWithEmptyScopesArrayUsesDefaults(): void
+ {
+ $result = $this->oauthClient->getOAuthButton([
+ 'is_metadata' => true,
+ 'name' => 'Test',
+ 'description' => 'Test',
+ 'scopes' => [], // Пустой массив
+ 'secrets_uri' => 'https://secrets.com'
+ ]);
+
+ $defaultScopes = implode(',', AmoCRMOAuth::METADATA_BUTTON_AVAILABLE_SCOPES);
+ $this->assertStringContainsString('data-scopes="' . $defaultScopes . '"', $result);
+ }
+}
diff --git a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php
similarity index 98%
rename from tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php
rename to tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php
index 2d9f205a..37a1102d 100644
--- a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php
+++ b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php
@@ -10,7 +10,7 @@
use PHPUnit\Framework\TestCase;
use ReflectionClass;
-class AmoCRMOAuth extends TestCase
+class AmoCRMOAuthTest extends TestCase
{
public function testCreateValidAtConstraintReturnsConstraintInstance()
{