├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── bitrix24.php ├── database └── migrations │ ├── 2024_11_15_151001_create_b24_apps_table.php │ └── 2024_11_15_151002_create_b24_users_table.php ├── phpunit.xml ├── resources └── views │ ├── index.blade.php │ ├── install-fail.blade.php │ └── install.blade.php ├── routes ├── b24app.php ├── b24appFrontRequest.php ├── b24appUser.php └── web.php └── src ├── Adapters └── EventDispatcherAdapter.php ├── Application └── Local │ └── Infrastructure │ └── Database │ ├── AppAuthDatabaseStorage.php │ └── UserAuthDatabaseStorage.php ├── Bitrix24ApiClient.php ├── Bitrix24App.php ├── Bitrix24ServiceProvider.php ├── Bitrix24User.php ├── Http ├── Controllers │ └── Bitrix24 │ │ ├── AppController.stub │ │ ├── Events │ │ ├── OnApplicationInstallController.stub │ │ └── OnApplicationUninstallController.stub │ │ └── InstallController.stub └── Middleware │ ├── B24AppMiddleware.php │ ├── B24AppUserMiddleware.php │ └── B24AuthUserMiddleware.php ├── Listeners └── PortalDomainUrlChangedListener.php └── Models ├── B24App.php └── B24User.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 X3Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Пакет Laravel для удобной работы с REST API Битрикс24 и написания приложений. 2 | 3 | Включает в себя: 4 | - Миграции для сбора статистики запросов и сохранения авторизации(токенов) пользователей 5 | - Роуты в зависимости от типа приложения и запросов к нему 6 | - Шаблоны для установки и работы приложения 7 | - Проверку статуса порталов на которые было установлено приложение 8 | - Автоматическое обновление токенов пользователей 9 | 10 | Установка 11 | 12 | ```injectablephp 13 | composer require x3group-dev/bitrix24-api-laravel 14 | ``` 15 | 16 | Выполнить публикацию (скопируются routes, blade, базовые контроллеры) 17 | ```injectablephp 18 | php artisan vendor:publish --provider="X3Group\Bitrix24\Bitrix24ServiceProvider" 19 | ``` 20 | 21 | Выполнить миграции 22 | ```injectablephp 23 | php artisan migrate 24 | ``` 25 | 26 | В адреса приложений вписываем 27 | 28 | Приложение: 29 | ```injectablephp 30 | https://host/app 31 | ``` 32 | Установка приложения: 33 | ```injectablephp 34 | https://host/install 35 | ``` 36 | 37 | в файл .env добавляем и заполняем своими данными 38 | ```injectablephp 39 | BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID= 40 | BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET= 41 | # вместо crm,user_brief укажите скоупы приложения 42 | BITRIX24_PHP_SDK_APPLICATION_SCOPE="crm,user_brief" 43 | BITRIX24_LOG_MAX_FILES=3 44 | ``` 45 | 46 | В сборку фронта добавить проброс авторизации в заголовках, чтобы работали роуты b24appFrontRequest 47 | 48 | ```injectablephp 49 | BX24.ready(async function () { 50 | await BX24.init(async function () { 51 | window.axios.defaults.headers.common['X-b24api-access-token'] = BX24.getAuth().access_token; 52 | window.axios.defaults.headers.common['X-b24api-refresh-token'] = BX24.getAuth().refresh_token; 53 | window.axios.defaults.headers.common['X-b24api-domain'] = BX24.getAuth().domain; 54 | window.axios.defaults.headers.common['X-b24api-member-id'] = BX24.getAuth().member_id; 55 | window.axios.defaults.headers.common['X-b24api-expires-in'] = BX24.getAuth().expires_in; 56 | }); 57 | }); 58 | ``` 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x3group-dev/bitrix24-api-laravel", 3 | "description": "b24 rest api", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "x3Group", 9 | "email": "dev@x3group.ru" 10 | }, 11 | { 12 | "name": "MrDeff", 13 | "email": "e.pedan@gmail.com", 14 | "role": "Developer" 15 | }, 16 | { 17 | "name": "Maxim Yugov", 18 | "email": "maxim.s.yugov@gmail.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "keywords": ["Laravel", "Bitrix", "Bitrix24"], 23 | "require": { 24 | "illuminate/support": "^11.0|^12.0", 25 | "bitrix24/b24phpsdk": "^1.1" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "~9.0", 29 | "orchestra/testbench": "~7" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "X3Group\\Bitrix24\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "X3Group\\Bitrix24\\Tests\\": "tests" 39 | } 40 | }, 41 | "extra": { 42 | "laravel": { 43 | "providers": [ 44 | "X3Group\\Bitrix24\\Bitrix24ServiceProvider" 45 | ], 46 | "aliases": { 47 | "Bitrix24ApiLaravel": "Bitrix24" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/bitrix24.php: -------------------------------------------------------------------------------- 1 | env('BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID'), 5 | 'client_secret' => env('BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET'), 6 | 'scope' => env('BITRIX24_PHP_SDK_APPLICATION_SCOPE'), 7 | 'log_max_files' => env('BITRIX24_LOG_MAX_FILES', 3), 8 | ]; 9 | -------------------------------------------------------------------------------- /database/migrations/2024_11_15_151001_create_b24_apps_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('member_id')->unique(); 15 | $table->string('domain'); 16 | $table->string('application_token')->nullable(); 17 | $table->string('access_token'); 18 | $table->string('refresh_token'); 19 | $table->integer('expires_in'); 20 | $table->integer('expires'); 21 | 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('b24_apps'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_11_15_151002_create_b24_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('member_id'); 15 | 16 | $table->foreign('member_id') 17 | ->references('member_id') 18 | ->on('b24_apps') 19 | ->cascadeOnDelete(); 20 | 21 | $table->string('domain'); 22 | $table->integer('user_id'); 23 | $table->boolean('is_admin'); 24 | $table->string('access_token'); 25 | $table->string('refresh_token'); 26 | $table->integer('expires_in'); 27 | $table->integer('expires'); 28 | 29 | $table->timestamps(); 30 | 31 | $table->unique(['member_id', 'user_id']); 32 | }); 33 | } 34 | 35 | public function down(): void 36 | { 37 | Schema::dropIfExists('b24_users'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Приложение 7 | 8 | 9 | 10 |
11 | Приложение работает! 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/install-fail.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Установка 7 | 8 | 9 | Ошибка при установке приложения. Повторите попытку позже, либо обратитесь в тех поддержку 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/install.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Установка 7 | 12 | 13 | 14 | Установка успешно завершена 15 | 16 | 17 | -------------------------------------------------------------------------------- /routes/b24app.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::post('/onApplicationInstall', [OnApplicationInstallController::class, 'handle']); 16 | Route::post('/onApplicationUninstall', [DemoOnApplicationUninstallController::class, 'handle']); 17 | }); 18 | -------------------------------------------------------------------------------- /routes/b24appFrontRequest.php: -------------------------------------------------------------------------------- 1 | dispatcher = resolve('events'); 17 | } 18 | 19 | public function dispatch(object $event, ?string $eventName = null): object 20 | { 21 | $r = $this->dispatcher->dispatch($event); 22 | 23 | return $event; 24 | } 25 | 26 | public function listen(array|Closure|QueuedClosure|string $events, array|Closure|QueuedClosure|null|string $listener): void 27 | { 28 | $this->dispatcher->listen($events, $listener); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/Local/Infrastructure/Database/AppAuthDatabaseStorage.php: -------------------------------------------------------------------------------- 1 | where('member_id', $this->memberId) 25 | ->first(); 26 | 27 | if (!$b24app) { 28 | throw new \Exception('Application is not installed'); 29 | } 30 | 31 | return LocalAppAuth::initFromArray([ 32 | 'auth_token' => [ 33 | 'access_token' => $b24app->access_token, 34 | 'refresh_token' => $b24app->refresh_token, 35 | 'expires' => $b24app->expires, 36 | ], 37 | 'domain_url' => "https://{$b24app->domain}", 38 | 'application_token' => $b24app->application_token, 39 | ]); 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function getApplicationToken(): ?string 46 | { 47 | $b24app = B24App::query() 48 | ->where('member_id', $this->memberId) 49 | ->first(); 50 | 51 | return $b24app?->application_token ?? null; 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | public function saveRenewedToken(RenewedAuthToken $renewedAuthToken): void 58 | { 59 | $b24app = B24App::query() 60 | ->where('member_id', $renewedAuthToken->memberId) 61 | ->first(); 62 | 63 | if (!$b24app) { 64 | throw new \Exception('App token not found'); 65 | } 66 | 67 | $b24app->access_token = $renewedAuthToken->authToken->accessToken; 68 | $b24app->refresh_token = $renewedAuthToken->authToken->refreshToken; 69 | $b24app->expires_in = $renewedAuthToken->authToken->expiresIn ?? 3600; 70 | $b24app->expires = $renewedAuthToken->authToken->expires; 71 | 72 | $b24app->save(); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function save(LocalAppAuth $localAppAuth): void 79 | { 80 | $expiresIn = $localAppAuth->getAuthToken()->expiresIn; 81 | 82 | if ($expiresIn === null) { 83 | $expiresIn = now(); 84 | $expiresIn->addSeconds($localAppAuth->getAuthToken()->expires); 85 | $expiresIn = $expiresIn->timestamp; 86 | } 87 | 88 | $b24api = B24App::query() 89 | ->where('member_id', $this->memberId) 90 | ->first(); 91 | 92 | if ($b24api === null) { 93 | B24App::query() 94 | ->create([ 95 | 'access_token' => $localAppAuth->getAuthToken()->accessToken, 96 | 'refresh_token' => $localAppAuth->getAuthToken()->refreshToken, 97 | 'expires' => $expiresIn, 98 | 'expires_in' => $localAppAuth->getAuthToken()->expires, 99 | 'application_token' => $localAppAuth->getApplicationToken(), 100 | 'domain' => $localAppAuth->getDomainUrl(), 101 | 'member_id' => $this->memberId, 102 | ]); 103 | } else { 104 | $b24api->access_token = $localAppAuth->getAuthToken()->accessToken; 105 | $b24api->refresh_token = $localAppAuth->getAuthToken()->refreshToken; 106 | $b24api->expires = $expiresIn; 107 | $b24api->expires_in = $localAppAuth->getAuthToken()->expires; 108 | $b24api->application_token = $localAppAuth->getApplicationToken(); 109 | $b24api->domain = $localAppAuth->getDomainUrl(); 110 | 111 | $b24api->save(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Application/Local/Infrastructure/Database/UserAuthDatabaseStorage.php: -------------------------------------------------------------------------------- 1 | with(B24App::class) 27 | ->where('member_id', $this->memberId) 28 | ->where('user_id', $this->userId) 29 | ->first(); 30 | 31 | if (!$b24user) { 32 | throw new \Exception('User token not found'); 33 | } 34 | 35 | return LocalAppAuth::initFromArray([ 36 | 'auth_token' => [ 37 | 'access_token' => $b24user->access_token, 38 | 'refresh_token' => $b24user->refresh_token, 39 | 'expires' => $b24user->expires, 40 | ], 41 | 'domain_url' => "https://{$b24user->domain}", 42 | 'application_token' => $b24user->b24app()->application_token, 43 | ]); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function getApplicationToken(): ?string 50 | { 51 | $b24user = B24User::query() 52 | ->with(B24App::class) 53 | ->where('member_id', $this->memberId) 54 | ->where('user_id', $this->userId) 55 | ->first(); 56 | 57 | return $b24user?->b24app()->application_token ?? null; 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function saveRenewedToken(RenewedAuthToken $renewedAuthToken): void 64 | { 65 | $b24user = B24User::query() 66 | ->where('member_id', $renewedAuthToken->memberId) 67 | ->where('user_id', $this->userId) 68 | ->first(); 69 | 70 | if (!$b24user) { 71 | throw new \Exception('User token not found'); 72 | } 73 | 74 | $b24user->access_token = $renewedAuthToken->authToken->accessToken; 75 | $b24user->refresh_token = $renewedAuthToken->authToken->refreshToken; 76 | $b24user->expires_in = $renewedAuthToken->authToken->expiresIn ?? 3600; 77 | $b24user->expires = $renewedAuthToken->authToken->expires; 78 | 79 | $b24user->save(); 80 | } 81 | 82 | /** 83 | * @inheritDoc 84 | */ 85 | public function save(LocalAppAuth $localAppAuth): void 86 | { 87 | $expiresIn = $localAppAuth->getAuthToken()->expiresIn; 88 | 89 | if ($expiresIn === null) { 90 | $expiresIn = now(); 91 | $expiresIn->addSeconds($localAppAuth->getAuthToken()->expires); 92 | $expiresIn = $expiresIn->timestamp; 93 | } 94 | 95 | $b24api = B24User::query() 96 | ->where('member_id', $this->memberId) 97 | ->where('user_id', $this->userId) 98 | ->first(); 99 | 100 | if ($b24api === null) { 101 | B24User::query() 102 | ->create([ 103 | 'access_token' => $localAppAuth->getAuthToken()->accessToken, 104 | 'refresh_token' => $localAppAuth->getAuthToken()->refreshToken, 105 | 'expires' => $expiresIn, 106 | 'expires_in' => $localAppAuth->getAuthToken()->expires, 107 | 'domain' => $localAppAuth->getDomainUrl(), 108 | 'member_id' => $this->memberId, 109 | ]); 110 | } else { 111 | $b24api->access_token = $localAppAuth->getAuthToken()->accessToken; 112 | $b24api->refresh_token = $localAppAuth->getAuthToken()->refreshToken; 113 | $b24api->expires = $expiresIn; 114 | $b24api->expires_in = $localAppAuth->getAuthToken()->expires; 115 | $b24api->domain = $localAppAuth->getDomainUrl(); 116 | 117 | $b24api->save(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Bitrix24ApiClient.php: -------------------------------------------------------------------------------- 1 | memberId = $memberId; 25 | 26 | $applicationProfile = new ApplicationProfile( 27 | clientId: config('bitrix24.client_id'), 28 | clientSecret: config('bitrix24.client_secret'), 29 | scope: Scope::initFromString(config('bitrix24.scope')) 30 | ); 31 | 32 | $b24api = B24App::query() 33 | ->where('member_id', $memberId) 34 | ->first(); 35 | 36 | $authToken = new AuthToken( 37 | accessToken: $b24api->access_token, 38 | refreshToken: $b24api->refresh_token, 39 | expires: $b24api->expires, 40 | expiresIn: $b24api->expires_in, 41 | ); 42 | 43 | /** @var EventDispatcher $eventDispatcher */ 44 | $eventDispatcher = resolve('appEvents'); 45 | 46 | $app = new ServiceBuilderFactory( 47 | eventDispatcher: $eventDispatcher, 48 | log: resolve('b24log', [ 49 | 'memberId' => $memberId 50 | ]), 51 | ); 52 | 53 | $this->api = $app->init( 54 | applicationProfile: $applicationProfile, 55 | authToken: $authToken, 56 | bitrix24DomainUrl: "https://{$b24api->domain}", 57 | ); 58 | } 59 | 60 | public function getMemberId(): string 61 | { 62 | return $this->memberId; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Bitrix24ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'x3group'); 53 | // $this->loadViewsFrom(__DIR__.'/../resources/views', 'x3group'); 54 | // $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 55 | // $this->loadRoutesFrom(__DIR__.'/routes.php'); 56 | 57 | // Publishing is only necessary when using the CLI. 58 | if ($this->app->runningInConsole()) { 59 | $this->bootForConsole(); 60 | } 61 | 62 | $application = $kernel->getApplication(); 63 | $router = $application->make(Router::class); 64 | 65 | /** 66 | * Защита для приложений типа: использует только API 67 | */ 68 | $router->middlewareGroup('b24app', [ 69 | EncryptCookies::class, 70 | AddQueuedCookiesToResponse::class, 71 | StartSession::class, 72 | ShareErrorsFromSession::class, 73 | SubstituteBindings::class, 74 | B24AppMiddleware::class, 75 | ]); 76 | 77 | /** 78 | * Первичный вход на приложение, сохранение авторизации пользователя (laravel), 79 | * авторизация его в рамках приложения и laravel 80 | * 81 | * При включенных ThirdParty cookie авторизация б24 берется из сессии 82 | * Хождение в рамках приложения с отключенной проверкой CsrfToken 83 | * 84 | * Для приложений с интерфейсом 85 | */ 86 | $router->middlewareGroup('b24appUser', [ 87 | EncryptCookies::class, 88 | AddQueuedCookiesToResponse::class, 89 | StartSession::class, 90 | ShareErrorsFromSession::class, 91 | SubstituteBindings::class, 92 | B24AppUserMiddleware::class, 93 | ]); 94 | 95 | /** 96 | * Запросы из фронта приложения с передачей авторизации через header X-b24api-access-token X-b24api-domain X-b24api-member-id 97 | * авторизует пользователя и делает запрос от него 98 | */ 99 | $router->middlewareGroup('b24appFrontRequest', [ 100 | EncryptCookies::class, 101 | AddQueuedCookiesToResponse::class, 102 | StartSession::class, 103 | ShareErrorsFromSession::class, 104 | SubstituteBindings::class, 105 | B24AuthUserMiddleware::class, 106 | ]); 107 | 108 | $router->group(['middleware' => 'b24app'], function () { 109 | if (file_exists(base_path('routes/b24app.php'))) 110 | $this->loadRoutesFrom(base_path('routes/b24app.php')); 111 | }); 112 | 113 | $router->group(['middleware' => 'b24appUser'], function () { 114 | if (file_exists(base_path('routes/b24appUser.php'))) 115 | $this->loadRoutesFrom(base_path('routes/b24appUser.php')); 116 | }); 117 | 118 | $router->group(['middleware' => 'b24appFrontRequest'], function () { 119 | if (file_exists(base_path('routes/b24appFrontRequest.php'))) 120 | $this->loadRoutesFrom(base_path('routes/b24appFrontRequest.php')); 121 | }); 122 | 123 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); 124 | 125 | $this->publishes([ 126 | __DIR__ . '/../routes/b24app.php' => base_path('routes/b24app.php'), 127 | __DIR__ . '/../routes/b24appUser.php' => base_path('routes/b24appUser.php'), 128 | __DIR__ . '/../routes/b24appFrontRequest.php' => base_path('routes/b24appFrontRequest.php'), 129 | 130 | __DIR__ . '/../resources/views' => resource_path('views/b24api'), 131 | 132 | __DIR__ . '/Http/Controllers/Bitrix24/AppController.stub' => base_path('app/Http/Controllers/Bitrix24/AppController.php'), 133 | __DIR__ . '/Http/Controllers/Bitrix24/InstallController.stub' => base_path('app/Http/Controllers/Bitrix24/InstallController.php'), 134 | 135 | __DIR__ . '/Http/Controllers/Bitrix24/Events/OnApplicationInstallController.stub' => base_path('app/Http/Controllers/Bitrix24/Events/OnApplicationInstallController.php'), 136 | __DIR__ . '/Http/Controllers/Bitrix24/Events/OnApplicationUninstallController.stub' => base_path('app/Http/Controllers/Bitrix24/Events/OnApplicationUninstallController.php'), 137 | ], 'bitrix24-routes'); 138 | 139 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 140 | } 141 | 142 | /** 143 | * Register any package services. 144 | * 145 | * @return void 146 | */ 147 | public function register(): void 148 | { 149 | $this->mergeConfigFrom(__DIR__.'/../config/bitrix24.php', 'bitrix24'); 150 | 151 | $this->app->bind('appEvents', function () { 152 | $eventDispatcher = new EventDispatcherAdapter(); 153 | $eventDispatcher->listen(AuthTokenRenewedEvent::class, function (AuthTokenRenewedEvent $event) { 154 | /** @var AppAuthDatabaseStorage $appAuthStorage */ 155 | $appAuthStorage = resolve(AppAuthDatabaseStorage::class, [ 156 | 'memberId' => $event->getRenewedToken()->memberId, 157 | ]); 158 | $appAuthStorage->saveRenewedToken($event->getRenewedToken()); 159 | }); 160 | //$eventDispatcher->listen(PortalDomainUrlChangedEvent::class, function (PortalDomainUrlChangedEvent $event) { 161 | // \logger('change url event'); 162 | // $listener = new PortalDomainUrlChangedListener(); 163 | // $listener->handle($event); 164 | //}); 165 | 166 | return $eventDispatcher; 167 | }); 168 | 169 | $this->app->bind('userEvents', function (Application $app, array $parameters) { 170 | $eventDispatcher = new EventDispatcherAdapter(); 171 | 172 | if (isset($parameters['memberId']) && isset($parameters['userId'])) { 173 | $eventDispatcher->listen( 174 | events: AuthTokenRenewedEvent::class, 175 | listener: function (AuthTokenRenewedEvent $event) use ($parameters) { 176 | 177 | resolve(UserAuthDatabaseStorage::class, [ 178 | 'memberId' => $event->getRenewedToken()->memberId, 179 | 'userId' => $parameters['userId'], 180 | ])->saveRenewedToken($event->getRenewedToken()); 181 | }); 182 | } 183 | 184 | return $eventDispatcher; 185 | }); 186 | 187 | $this->app->bind('b24log', function (Application $app, array $parameters) { 188 | $memberId = $parameters['memberId']; 189 | $domain = $parameters['domain'] ?? 'unknown'; 190 | 191 | /** @var B24App $b24app */ 192 | $b24app = B24App::query() 193 | ->where('member_id', $memberId) 194 | ->first(); 195 | 196 | if ($b24app) { 197 | $domain = $b24app->domain; 198 | } 199 | 200 | $logger = new Logger('b24log'); 201 | $logger->pushHandler(new RotatingFileHandler( 202 | filename: storage_path('logs/b24api/' . $domain . '-' . $memberId . '/b24api.log'), 203 | maxFiles: config('bitrix24.log_max_files'), 204 | )); 205 | 206 | return $logger; 207 | }); 208 | 209 | $this->app->bind('bitrix24user', function (Application $app, array $parameters) { 210 | return new Bitrix24User($parameters['memberId'], $parameters['userId']); 211 | }); 212 | 213 | $this->app->bind('bitrix24app', function (Application $app, array $parameters) { 214 | return new Bitrix24App($parameters['memberId']); 215 | }); 216 | 217 | $this->app->bind(AppAuthDatabaseStorage::class, function (Application $app, array $parameters) { 218 | return new AppAuthDatabaseStorage($parameters['memberId']); 219 | }); 220 | 221 | $this->app->bind(ApplicationProfile::class, function () { 222 | return new ApplicationProfile( 223 | clientId: config('bitrix24.client_id'), 224 | clientSecret: config('bitrix24.client_secret'), 225 | scope: Scope::initFromString(config('bitrix24.scope')) 226 | ); 227 | }); 228 | 229 | $this->app->bind(Bitrix24ApiClient::class, function () { 230 | // 231 | $applicationProfile = new ApplicationProfile( 232 | clientId: config('bitrix24.client_id'), 233 | clientSecret: config('bitrix24.client_secret'), 234 | scope: Scope::initFromString(config('bitrix24.scope')) 235 | ); 236 | 237 | $memberId = null; 238 | 239 | $request = Request::createFromGlobals(); 240 | 241 | if ($request->has('auth') && !empty($request->input('auth')['member_id'])) { 242 | $memberId = $request->input('auth')['member_id']; 243 | } elseif ($request->has('member_id') && !empty($request->input('member_id'))) { 244 | $memberId = $request->input('member_id'); 245 | } 246 | 247 | if (is_null($memberId)) { 248 | throw new \Exception('Request has no member_id'); 249 | } 250 | 251 | $b24api = B24App::query() 252 | ->where('member_id', $memberId) 253 | ->first(); 254 | 255 | $authToken = new AuthToken( 256 | accessToken: $b24api->access_token, 257 | refreshToken: $b24api->refresh_token, 258 | expires: $b24api->expires, 259 | expiresIn: $b24api->expires_in, 260 | ); 261 | 262 | $app = new ServiceBuilderFactory( 263 | eventDispatcher: resolve('appEvents'), 264 | log: resolve('b24log', [ 265 | 'memberId' => $memberId 266 | ]), 267 | ); 268 | 269 | $appClient = $app->init( 270 | applicationProfile: $applicationProfile, 271 | authToken: $authToken, 272 | bitrix24DomainUrl: "https://{$b24api->domain}", 273 | ); 274 | 275 | // User 276 | $userClient = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest( 277 | placementRequest: Request::createFromGlobals(), 278 | applicationProfile: $applicationProfile, 279 | eventDispatcher: new EventDispatcherAdapter(), 280 | ); 281 | 282 | return new Bitrix24ApiClient( 283 | app: $appClient, 284 | user: $userClient, 285 | ); 286 | }); 287 | 288 | $this->app->bind(ApiClient::class, function (Application $app, array $parameters) { 289 | return new ApiClient( 290 | credentials: new Credentials( 291 | webhookUrl: null, 292 | authToken: new AuthToken( 293 | accessToken: $parameters['accessToken'], 294 | refreshToken: $parameters['refreshToken'], 295 | expires: $parameters['expires'], 296 | expiresIn: $parameters['expiresIn'], 297 | ), 298 | applicationProfile: new ApplicationProfile( 299 | clientId: config('bitrix24.client_id'), 300 | clientSecret: config('bitrix24.client_secret'), 301 | scope: Scope::initFromString(config('bitrix24.scope')) 302 | ), 303 | domainUrl: "https://{$parameters['domain']}", 304 | ), 305 | client: HttpClient::create(), 306 | requestIdGenerator: new DefaultRequestIdGenerator(), 307 | apiLevelErrorHandler: new ApiLevelErrorHandler(resolve('b24log', [ 308 | 'memberId' => $parameters['memberId'] 309 | ])), 310 | logger: resolve('b24log', [ 311 | 'memberId' => $parameters['memberId'] 312 | ]), 313 | ); 314 | }); 315 | 316 | Event::listen(PortalDomainUrlChangedEvent::class, PortalDomainUrlChangedListener::class); 317 | } 318 | 319 | /** 320 | * Get the services provided by the provider. 321 | * 322 | * @return array 323 | */ 324 | public function provides(): array 325 | { 326 | return ['bitrix24']; 327 | } 328 | 329 | /** 330 | * Console-specific booting. 331 | * 332 | * @return void 333 | */ 334 | protected function bootForConsole(): void 335 | { 336 | // Publishing the configuration file. 337 | $this->publishes([ 338 | __DIR__.'/../config/bitrix24.php' => config_path('bitrix24.php'), 339 | ], 'bitrix24.config'); 340 | 341 | // Publishing the views. 342 | /*$this->publishes([ 343 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/x3group'), 344 | ], 'bitrix24.views');*/ 345 | 346 | // Publishing assets. 347 | /*$this->publishes([ 348 | __DIR__.'/../resources/assets' => public_path('vendor/x3group'), 349 | ], 'bitrix24.assets');*/ 350 | 351 | // Publishing the translation files. 352 | /*$this->publishes([ 353 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/x3group'), 354 | ], 'bitrix24.lang');*/ 355 | 356 | // Registering package commands. 357 | // $this->commands([]); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/Bitrix24User.php: -------------------------------------------------------------------------------- 1 | memberId = $memberId; 26 | $this->userId = $userId; 27 | 28 | $factory = new ServiceBuilderFactory( 29 | eventDispatcher: resolve('userEvents', [ 30 | 'memberId' => $memberId, 31 | 'userId' => $userId, 32 | ]), 33 | log: resolve('b24log', [ 34 | 'memberId' => $memberId 35 | ]), 36 | ); 37 | 38 | $applicationProfile = new ApplicationProfile( 39 | clientId: config('bitrix24.client_id'), 40 | clientSecret: config('bitrix24.client_secret'), 41 | scope: Scope::initFromString(config('bitrix24.scope')) 42 | ); 43 | 44 | /** @var B24User $b24user */ 45 | $b24user = B24User::query() 46 | ->where('member_id', $memberId) 47 | ->where('user_id', $userId) 48 | ->first(); 49 | 50 | if (empty($b24user)) { 51 | throw new \Exception('User not found'); 52 | } 53 | 54 | $authToken = new AuthToken( 55 | accessToken: $b24user->access_token, 56 | refreshToken: $b24user->refresh_token, 57 | expires: $b24user->expires, 58 | expiresIn: $b24user->expires_in, 59 | ); 60 | 61 | $this->api = $factory->init( 62 | applicationProfile: $applicationProfile, 63 | authToken: $authToken, 64 | bitrix24DomainUrl: "https://{$b24user->domain}", 65 | ); 66 | } 67 | 68 | public function getMemberId(): string 69 | { 70 | return $this->memberId; 71 | } 72 | 73 | public function getUserId(): int 74 | { 75 | return $this->userId; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Http/Controllers/Bitrix24/AppController.stub: -------------------------------------------------------------------------------- 1 | input('auth')['member_id']; 14 | $applicationToken = $request->input('auth')['application_token']; 15 | 16 | $b24app = B24App::query() 17 | ->where('member_id', $memberId) 18 | ->whereNull('application_token') 19 | ->first(); 20 | 21 | if ($b24app) { 22 | $b24app->application_token = $applicationToken; 23 | $b24app->save(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Controllers/Bitrix24/Events/OnApplicationUninstallController.stub: -------------------------------------------------------------------------------- 1 | input('auth')['member_id']; 20 | 21 | $b24app = B24App::query() 22 | ->where('member_id', $memberId) 23 | ->first(); 24 | 25 | if ($b24app === null) { 26 | return; 27 | } 28 | 29 | try { 30 | RemoteEventsFabric::init(resolve('b24log', [ 31 | 'memberId' => $memberId 32 | ])) 33 | ->createEvent( 34 | request: $request, 35 | applicationToken: $b24app->application_token, 36 | ); 37 | 38 | $b24app->delete(); 39 | } catch (WrongSecuritySignatureException $e) { 40 | return; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Controllers/Bitrix24/InstallController.stub: -------------------------------------------------------------------------------- 1 | listen(AuthTokenRenewedEvent::class, function (AuthTokenRenewedEvent $authTokenRenewedEvent): void { 28 | /** @var AppAuthDatabaseStorage $appAuthStorage */ 29 | $appAuthStorage = resolve(AppAuthDatabaseStorage::class, [ 30 | 'memberId' => $authTokenRenewedEvent->getRenewedToken()->memberId, 31 | ]); 32 | $appAuthStorage->saveRenewedToken($authTokenRenewedEvent->getRenewedToken()); 33 | }); 34 | 35 | $b24 = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest( 36 | placementRequest: $request, 37 | applicationProfile: new ApplicationProfile( 38 | clientId: config('bitrix24.client_id'), 39 | clientSecret: config('bitrix24.client_secret'), 40 | scope: Scope::initFromString(config('bitrix24.scope')), 41 | ), 42 | eventDispatcher: $eventDispatcher, 43 | logger: resolve('b24log', [ 44 | 'memberId' => $request->input('member_id'), 45 | 'domain' => $request->input('DOMAIN') 46 | ]), 47 | ); 48 | 49 | $currentB24UserId = $b24->getMainScope() 50 | ->main() 51 | ->getCurrentUserProfile() 52 | ->getUserProfile() 53 | ->ID; 54 | 55 | $authToken = new AuthToken( 56 | accessToken: $request->input('AUTH_ID'), 57 | refreshToken: $request->input('REFRESH_ID'), 58 | expires: (int)$request->input('AUTH_EXPIRES'), 59 | ); 60 | 61 | $localAppAuth = new LocalAppAuth( 62 | authToken: $authToken, 63 | domainUrl: $request->input('DOMAIN'), 64 | applicationToken: null, 65 | ); 66 | 67 | $memberId = $request->input('member_id'); 68 | 69 | $storage = new AppAuthDatabaseStorage($memberId); 70 | $storage->save($localAppAuth); 71 | 72 | $b24->getMainScope()->eventManager()->unbindAllEventHandlers(); 73 | 74 | $b24->getMainScope()->eventManager()->bindEventHandlers([ 75 | new EventHandlerMetadata( 76 | code: OnApplicationInstall::CODE, 77 | handlerUrl: config('app.url') . '/events/onApplicationInstall', 78 | userId: $currentB24UserId, 79 | ), 80 | new EventHandlerMetadata( 81 | code: OnApplicationUninstall::CODE, 82 | handlerUrl: config('app.url') . '/events/onApplicationUninstall', 83 | userId: $currentB24UserId, 84 | ), 85 | ]); 86 | 87 | // your code here 88 | 89 | return view('b24api.install'); 90 | } catch (\Throwable $e) { 91 | return view('b24api.install-fail'); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Http/Middleware/B24AppMiddleware.php: -------------------------------------------------------------------------------- 1 | has('member_id') || $request->has('auth') && isset($request->post('auth')['member_id']) && !empty($request->post('auth')['member_id'])) { 12 | // 13 | } else { 14 | return response()->json(['error' => 'memberId is null'], 406); 15 | } 16 | 17 | return $next($request); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Middleware/B24AppUserMiddleware.php: -------------------------------------------------------------------------------- 1 | post('member_id'); 18 | if (empty($memberId)) { 19 | return response()->json(['error' => 'memberId is null'], 406); 20 | } 21 | $reLogin = false; 22 | if (!auth()->check()) { 23 | $reLogin = true; 24 | } elseif ((auth()->user()->getMemberId() != $memberId)) { 25 | $reLogin = true; 26 | } else { 27 | if (is_null(auth()->user()->expires) || time() >= auth()->user()->expires) { 28 | $reLogin = true; 29 | } 30 | } 31 | 32 | if ($reLogin) { 33 | if (!$request->post('AUTH_ID')) 34 | return response()->json(['error' => 'AUTH_ID is null'], 406); 35 | 36 | try { 37 | $b24 = ServiceBuilderFactory::createServiceBuilderFromPlacementRequest( 38 | placementRequest: $request, 39 | applicationProfile: new ApplicationProfile( 40 | clientId: config('bitrix24.client_id'), 41 | clientSecret: config('bitrix24.client_secret'), 42 | scope: Scope::initFromString(config('bitrix24.scope')) 43 | ), 44 | eventDispatcher: new EventDispatcherAdapter(), 45 | logger: resolve('b24log', [ 46 | 'memberId' => $memberId, 47 | 'domain' => $request->input('DOMAIN'), 48 | ]), 49 | ); 50 | 51 | $profile = $b24->getMainScope()->main()->getCurrentUserProfile()->getUserProfile(); 52 | 53 | $userFind = B24User::query() 54 | ->where('user_id', $profile->ID) 55 | ->where('member_id', $memberId) 56 | ->first(); 57 | 58 | if ($userFind) { 59 | $userFind->update([ 60 | 'access_token' => $request->post('AUTH_ID'), 61 | 'refresh_token' => $request->post('REFRESH_ID'), 62 | 'domain' => $request->get('DOMAIN'), 63 | 'is_admin' => $profile->ADMIN, 64 | 'expires' => time() + (int)$request->post('AUTH_EXPIRES') - 600, 65 | 'expires_in' => 3600, 66 | ]); 67 | } else { 68 | $userFind = B24User::query() 69 | ->create([ 70 | 'user_id' => $profile->ID, 71 | 'member_id' => $request->post('member_id'), 72 | 'access_token' => $request->post('AUTH_ID'), 73 | 'refresh_token' => $request->post('REFRESH_ID'), 74 | 'application_token' => $request->post('APP_SID'), 75 | 'domain' => $request->get('DOMAIN'), 76 | 'is_admin' => $profile->ADMIN, 77 | 'expires' => time() + (int)$request->post('AUTH_EXPIRES') - 600, 78 | 'expires_in' => 3600, 79 | ]); 80 | } 81 | 82 | auth()->login($userFind); 83 | if (!auth()->check()) { 84 | return response()->json(['error' => 'Unauthorized, auth failed'], 401); 85 | } 86 | Context::addHidden('memberId', $memberId); 87 | Context::addHidden('userId', $userFind->user_id); 88 | } catch (\Exception $e) { 89 | return response()->json(['error' => $e->getMessage()], 401); 90 | } 91 | } 92 | 93 | return $next($request); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/Middleware/B24AuthUserMiddleware.php: -------------------------------------------------------------------------------- 1 | header('X-b24api-member-id'); 17 | if (empty($memberId)) { 18 | return response()->json(['error' => 'memberId is null'], 406); 19 | } 20 | 21 | $domain = $request->header('X-b24api-domain'); 22 | if (empty($domain)) { 23 | return response()->json(['error' => 'domain is null'], 406); 24 | } 25 | 26 | $accessToken = $request->header('X-b24api-access-token'); 27 | if (empty($accessToken)) { 28 | return response()->json(['error' => 'access token is null'], 406); 29 | } 30 | 31 | if (!auth()->check() || (auth()->user()->getMemberId() != $memberId)) { 32 | try { 33 | $applicationProfile = new ApplicationProfile( 34 | clientId: config('bitrix24.client_id'), 35 | clientSecret: config('bitrix24.client_secret'), 36 | scope: Scope::initFromString(config('bitrix24.scope')) 37 | ); 38 | 39 | $authToken = new AuthToken( 40 | accessToken: $request->header('X-b24api-access-token'), 41 | refreshToken: $request->header('X-b24api-refresh-token'), 42 | expires: $request->header('X-b24api-expires-in'), 43 | expiresIn: 3600, 44 | ); 45 | 46 | $factory = new ServiceBuilderFactory( 47 | eventDispatcher: resolve('userEvents', [ 48 | 'memberId' => $memberId, 49 | ]), 50 | log: resolve('b24log', [ 51 | 'memberId' => $memberId 52 | ]), 53 | ); 54 | 55 | $b24 = $factory->init( 56 | applicationProfile: $applicationProfile, 57 | authToken: $authToken, 58 | bitrix24DomainUrl: "https://{$request->header('X-b24api-domain')}", 59 | ); 60 | 61 | $profile = $b24->getUserScope()->user()->current()->user(); 62 | 63 | $user = B24User::query() 64 | ->where('member_id', $memberId) 65 | ->where('user_id', $profile->ID) 66 | ->first(); 67 | 68 | if (!$user) { 69 | throw new \Exception('User not found'); 70 | } 71 | 72 | auth()->login($user); 73 | } catch (\Exception $e) { 74 | return response()->json(['error' => $e->getMessage()], 401); 75 | } 76 | } 77 | return $next($request); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Listeners/PortalDomainUrlChangedListener.php: -------------------------------------------------------------------------------- 1 | where('domain', $event->getOldDomainUrlHost()) 20 | ->update([ 21 | 'domain' => $event->getNewDomainUrlHost(), 22 | ]); 23 | 24 | B24User::query() 25 | ->where('domain', $event->getOldDomainUrlHost()) 26 | ->update([ 27 | 'domain' => $event->getNewDomainUrlHost() 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Models/B24App.php: -------------------------------------------------------------------------------- 1 | 'boolean', 32 | ]; 33 | 34 | public function getMemberId(){ 35 | return $this->member_id; 36 | } 37 | 38 | public function b24app(): BelongsTo 39 | { 40 | return $this->belongsTo(B24App::class, 'member_id', 'member_id'); 41 | } 42 | } 43 | --------------------------------------------------------------------------------