7 Commits
1.0 ... 2.0

Author SHA1 Message Date
830ebbcdd3 feat: sync, webhook, tokens refresh, fillable, key config 2022-05-19 13:24:54 +02:00
4156db2cdc wip 2022-05-18 17:40:18 +02:00
7b3a698daa fix: model trait 2021-06-21 18:30:42 +02:00
3b79a880ae feat: custom attributes sync; update: default User model 2021-06-21 18:24:30 +02:00
14b47b6f6e Update 'composer.json' 2021-06-21 18:13:19 +02:00
eda52c3d65 Update 'README.md' 2021-06-21 18:11:25 +02:00
Maxime Renou
7e13ddcf67 update guzzle 2020-12-09 14:00:25 +01:00
13 changed files with 435 additions and 424 deletions

View File

@@ -7,9 +7,6 @@ The Bluesquare Connect package allows you to use its OAuth server and sync its r
Update your `composer.json`:
```
"require": {
"bluesquare/laravel-connect": "dev-master"
}
"repositories": [
{
"type": "vcs",
@@ -21,7 +18,7 @@ Update your `composer.json`:
Install the package:
```bash
composer update bluesquare/laravel-connect
composer require bluesquare/laravel-connect "1.2"
```
Finally, update your `.env` with your client's credentials:

View File

@@ -35,8 +35,8 @@
}
},
"require": {
"guzzlehttp/guzzle": "^6.5",
"php": "^7.2"
"guzzlehttp/guzzle": "^7.3",
"php": "^7.3|^8.0"
},
"prefer-stable": true
}

143
composer.lock generated
View File

@@ -4,41 +4,47 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "98b09ff09c6392aadb0f56bb9dc51a96",
"content-hash": "9819197b9702d3ffb6483686d1005f4a",
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "6.5.2",
"version": "7.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
"reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79",
"reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"php": ">=5.5"
"guzzlehttp/promises": "^1.4",
"guzzlehttp/psr7": "^1.7",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"psr/log": "^1.1"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.5-dev"
"dev-master": "7.1-dev"
}
},
"autoload": {
@@ -58,6 +64,11 @@
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "Guzzle is a PHP HTTP client library",
@@ -68,30 +79,50 @@
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"time": "2019-12-23T11:57:10+00:00"
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://github.com/alexeyshockov",
"type": "github"
},
{
"url": "https://github.com/gmponos",
"type": "github"
}
],
"time": "2020-10-10T11:47:56+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
"reference": "60d379c243457e073cff02bc323a2a86cb355631"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
"reference": "60d379c243457e073cff02bc323a2a86cb355631",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
@@ -122,20 +153,20 @@
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
"time": "2020-09-30T07:37:28+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.6.1",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a"
"reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3",
"reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3",
"shasum": ""
},
"require": {
@@ -148,15 +179,15 @@
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
"zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6-dev"
"dev-master": "1.7-dev"
}
},
"autoload": {
@@ -193,7 +224,56 @@
"uri",
"url"
],
"time": "2019-07-01T23:21:34+00:00"
"time": "2020-09-30T07:37:11+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"time": "2020-06-29T06:28:15+00:00"
},
{
"name": "psr/http-message",
@@ -288,10 +368,13 @@
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-stable": true,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
"platform": {
"php": "^7.2"
},
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

View File

@@ -5,7 +5,7 @@ return [
/**
* OAuth model
*/
'model' => \App\User::class,
'model' => \App\Models\User::class,
/**
* Route that redirects to Bluesquare Connect
@@ -13,18 +13,19 @@ return [
'login_url' => '/connect/authorize',
/**
* Use post-login remember cookie
*/
'login_remember' => true,
/**
* OAuth callback URL
*/
'redirect_url' => env('BCONNECT_REDIRECT', 'http://localhost:8000/connect/callback'),
/**
* OAuth client id
* OAuth client identifiers
*/
'client_id' => env('BCONNECT_CLIENT_ID', null),
/**
* OAuth client secret
*/
'client_secret' => env('BCONNECT_CLIENT_SECRET', null),
/**

View File

@@ -0,0 +1,45 @@
<?php
namespace Bluesquare\Connect\Commands;
use Bluesquare\Connect\Connect;
use Bluesquare\Connect\ConnectException;
use Bluesquare\Connect\Traits\HasConnectTokens;
use Illuminate\Console\Command;
class RefreshTokens extends Command
{
protected $signature = 'connect:refresh';
protected $description = 'Refresh Bluesquare Connect tokens';
public function handle(Connect $connect)
{
$class = config('bconnect.model');
$has_fields = in_array(HasConnectTokens::class, class_uses($class));
if (! $has_fields) {
throw new ConnectException("$class does not implement HasConnectTokens");
}
$class::query()->chunks(10, function ($models) use ($connect) {
$models->each(function ($model) use ($connect) {
if (! empty($model->connect_refresh_token) && $model->connect_expires_at <= now()->addHour()) {
try {
$tokens = $connect->getAccessTokenFromRefreshToken($model->connect_refresh_token);
$connect->updateUserConnectData($model, $tokens);
$model->save();
}
catch (\Exception $exception) {
$this->warn("Failed to refresh model tokens", $model->toArray());
}
}
});
});
$this->info("Tokens refreshed");
return 0;
}
}

View File

@@ -3,41 +3,45 @@
namespace Bluesquare\Connect\Commands;
use Bluesquare\Connect\Connect;
use Bluesquare\Connect\ConnectException;
use Bluesquare\Connect\Traits\HasConnectTokens;
use Illuminate\Console\Command;
class Sync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'connect:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronize Bluesquare Connect resources';
protected $description = 'Sync Bluesquare Connect users';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(Connect $connect)
{
$connect->syncAll();
$this->call('connect:refresh');
$class = config('bconnect.model');
$has_fields = in_array(HasConnectTokens::class, class_uses($class));
if (! $has_fields) {
throw new ConnectException("$class does not implement HasConnectTokens");
}
$class::query()->chunks(10, function ($models) use ($connect) {
$models->each(function ($model) use ($connect) {
try {
if (! empty($model->connect_access_token)) {
$data = $connect->getUserData($model->connect_access_token);
$connect->updateUserData($model, $data);
$model->save();
}
}
catch (\Exception $exception) {
$this->warn("Failed to sync model data", $model->toArray());
}
});
});
$this->info("Models synced");
return 0;
}
}

View File

@@ -2,8 +2,9 @@
namespace Bluesquare\Connect;
use Bluesquare\Connect\Traits\HasConnectSync;
use Bluesquare\Connect\Traits\HasConnectData;
use Bluesquare\Connect\Traits\HasConnectTokens;
use Bluesquare\Connect\Traits\HasConnectWebhook;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
@@ -14,59 +15,13 @@ use Psr\Http\Message\StreamInterface;
class Connect
{
protected static $resources = [
'Role',
'Company',
'Team',
'User',
'UserTeam'
];
protected static $foreignKeys = [
'role_id' => 'Role',
'company_id' => 'Company',
'team_id' => 'Team',
'user_id' => 'User',
'user_teams_id' => 'UserTeam'
];
protected $app;
protected $synchronized = [];
public function __construct($app)
{
$this->app = $app;
}
// User config
public function setSynchronized($models)
{
$items = [];
foreach ($models as $model)
{
if (!in_array(HasConnectSync::class, class_uses($model)))
throw new ConnectException("$model does not implement HasConnectSync trait.");
$class = explode('\\', $model);
$resource = $model::$connectResource ?? end($class);
$items[$resource] = $model;
}
$synchronized = [];
foreach (self::$resources as $resourceType) { // Re-ordering
foreach ($items as $resource => $model) {
if ($resource == $resourceType)
$synchronized[$resource] = $model;
}
}
$this->synchronized = $synchronized;
}
// API
/**
@@ -76,7 +31,7 @@ class Connect
* @return array
* @throws ConnectException
*/
public function request($method, $uri, $data = null, $auth = true): array
public function request($method, $uri, $data = null, $access_token = null): array
{
$url = $this->getUrl();
$url = $url . '/' . trim($uri, '/');
@@ -89,11 +44,8 @@ class Connect
]
];
if ($auth === true) {
$config['headers']['Authorization'] = 'Bearer ' . $this->getAccessToken();
}
elseif ($auth !== false) {
$config['headers']['Authorization'] = 'Bearer ' . $auth;
if (! is_null($access_token)) {
$config['headers']['Authorization'] = 'Bearer ' . $access_token;
}
if (! is_null($data)) {
@@ -110,12 +62,13 @@ class Connect
return json_decode($body, true);
} catch(\Exception $e) {
$this->deleteAccessToken();
$this->flushTokens();
throw new ConnectException($e->getMessage());
}
}
// OAuth (user)
// Authorization flow
public function redirect($state = null)
{
@@ -123,9 +76,12 @@ class Connect
$state = Str::random();
$states = session()->get('connect_states');
if (! is_array($states))
$states = [];
$states[] = $state;
session()->put('connect_states', $states);
$query = http_build_query([
@@ -137,106 +93,80 @@ class Connect
]);
$url = $this->getUrl() . '/oauth/authorize?' . $query;
return redirect()->to($url);
}
public function checkState(Request $request)
{
if (!session()->has('connect_states')) {
Log::debug("Missing session states");
if (! session()->has('connect_states'))
return false;
}
$states = session()->get('connect_states');
if (!is_array($states)) {
Log::debug("Invalid session state");
if (! is_array($states))
return false;
}
if (!$request->has('state') || !in_array($request->state, $states)) {
Log::debug("Missing valid state in request");
if (! $request->has('state') || ! in_array($request->state, $states))
return false;
}
unset($states[array_search($request->state, $states)]);
session()->put('connect_states', $states);
return true;
}
public function loginFromCallback(Request $request, $redirect_to = '/')
{
if (!$this->checkState($request))
if (! $this->checkState($request) || ! $request->has('code'))
return redirect('/');
// Code check
if (!$request->has('code')) {
Log::debug("Missing authorization code");
return redirect('/');
}
// Access token
$expires_at = now();
$connect_data = $this->getAccessTokenFromAuthorizationCode($request->code);
$connect_data = $this->getAccessTokenFromAuthorizationCode($request->get('code'));
$connect_data['expires_at'] = $expires_at->addSeconds($connect_data['expires_in']);
$model_data = $this->getUserData($connect_data['access_token']);
$model = config('bconnect.model');
// User data
if (in_array($model, $this->synchronized)) {
$user = $model::findConnectResource($model_data['id']) ?? new $model;
}
else {
$user = $model::where('email', $model_data['email'])->first() ?? new $model;
}
$model_data = $this->convertForeignKeys($model_data);
$user->fill($model_data);
if (in_array($model, $this->synchronized))
$user->{$model::$connectColumnId} = $model_data['id'];
$user_data = $this->getUserData($connect_data['access_token']);
$user = $this->sync('create', $user_data);
$this->updateUserConnectData($user, $connect_data);
$user->save();
if (in_array($model, $this->synchronized))
$user = $model::findConnectResource($model_data['id']);
// Login
$this->updateUserConnectData($user, $connect_data);
auth()->login($user, true);
auth()->login($user, config('bconnect.login_remember', true));
return redirect($redirect_to);
}
// OAuth methods
public function getAccessTokenFromAuthorizationCode($code)
{
$data = $this->request('post', 'oauth/token', [
return $this->request('post', 'oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => config('bconnect.client_id'),
'client_secret' => config('bconnect.client_secret'),
'scope' => config('bconnect.user_scopes'),
'redirect_uri' => config('bconnect.redirect_url'),
'code' => $code
], false);
return $data;
]);
}
public function getAccessTokenFromRefreshToken($refresh_token)
{
$data = $this->request('post', 'oauth/token', [
return $this->request('post', 'oauth/token', [
'grant_type' => 'refresh_token',
'client_id' => config('bconnect.client_id'),
'client_secret' => config('bconnect.client_secret'),
'scope' => config('bconnect.user_scopes'),
'redirect_uri' => config('bconnect.redirect_url'),
'refresh_token' => $refresh_token
], false);
return $data;
]);
}
public function getUserData($access_token)
@@ -244,164 +174,98 @@ class Connect
return $this->request('get', 'api/user', null, $access_token);
}
public function updateUserConnectData($user, $data)
public function getUserAccessToken($model)
{
if (!in_array(HasConnectTokens::class, class_uses(get_class($user))))
return false;
$user->connect_access_token = $data['access_token'];
$user->connect_refresh_token = $data['refresh_token'];
$user->connect_expires_at = $data['expires_at'];
return $user->save();
}
public function getUserAccessToken($user)
{
$has_fields = in_array(HasConnectTokens::class, class_uses(get_class($user)));
$class = get_class($model);
$has_fields = in_array(HasConnectTokens::class, class_uses($class));
if (! $has_fields) {
throw new ConnectException("User class does not implement HasConnectTokens");
throw new ConnectException("$class does not implement HasConnectTokens");
}
if ($user->connect_expires_at <= now()) {
$connect_data = $this->getAccessTokenFromRefreshToken($user->connect_refresh_token);
$this->updateUserConnectData($user, $connect_data);
if ($model->connect_expires_at <= now()->addHour()) {
$connect_data = $this->getAccessTokenFromRefreshToken($model->connect_refresh_token);
$this->updateUserConnectData($model, $connect_data);
return $connect_data['access_token'];
}
return $user->connect_access_token;
return $model->connect_access_token;
}
// OAuth (client)
// Sync
public function getAccessToken()
public function sync(string $event, array $data)
{
$access_token = cache()->get('bconnect.access_token');
$access_token_expiration = cache()->get('bconnect.access_token_expiration');
$class = config('bconnect.model');
if ($access_token && $access_token_expiration > time() + 60) {
return $access_token;
if (in_array($event, ['update', 'delete'])) {
$model = $this->resolveUser($data);
if (! $model->exists() && $event === 'delete')
return $model;
}
else {
$hasSoftDeletes = in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses($class));
$model = $this->resolveUser($data, $hasSoftDeletes);
if ($model->exists()) {
if ($hasSoftDeletes) {
$event = 'restore';
} else {
$event = 'update';
}
}
}
$data = $this->request('post', '/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => config('bconnect.client_id'),
'client_secret' => config('bconnect.client_secret'),
'scope' => config('bconnect.client_scopes')
], false);
cache()->set('bconnect.access_token', $data['access_token']);
cache()->set('bconnect.access_token_expiration', time() + $data['expires_in']);
return $data['access_token'];
if (in_array(HasConnectWebhook::class, class_uses($class))) {
$method = 'onConnect' . ucfirst($event);
$model->$method($data);
} else {
$this->updateUserData($model, $data);
$model->save();
}
public function deleteAccessToken()
return $model;
}
public function updateUserConnectData($user, $data)
{
cache()->delete('bconnect.access_token');
cache()->delete('bconnect.access_token_expiration');
if (in_array(HasConnectTokens::class, class_uses(get_class($user))))
$user->fillConnectTokens($data);
}
// Webhook handler
/**
* @param Request $request
* @return bool
*/
public function handleWebhook(Request $request)
public function updateUserData($user, $data)
{
$data = $request->validate([
'connectEventType' => 'required|in:created,updated,deleted',
'connectResourceType' => 'required',
'connectResourceTable' => 'required',
'connectResourceData' => 'required|array',
'connectResourceData.id' => 'required'
]);
if (!array_key_exists($data['connectResourceType'], $this->synchronized))
return false;
$model = $this->synchronized[$data['connectResourceType']];
$method = $this->getEventMethod($data['connectEventType']);
$data = $data['connectResourceData'];
try {
$data = $this->get($data['connectResourceType'], $data['connectResourceData']['id']);
if ($data['connectEventType'] == 'deleted') {
abort(403, "This resource still exists.");
}
} catch (\Exception $e) {
if ($data['connectEventType'] != 'deleted') {
abort(404, "Could not retrieve this resource.");
}
if (in_array(HasConnectData::class, class_uses(get_class($user))))
$user->fillConnectData($data);
}
$data = $this->convertForeignKeys($data);
$model::$method($data['id'], $data);
return true;
}
// Resources getters
public function getAll($resourceType)
protected function resolveUser($data, $withTrashed = false)
{
return $this->request('get', "api/resources/$resourceType");
$class = config('bconnect.model');
$query = $class::query();
if ($withTrashed)
$query->withTrashed();
$model = new $class;
if (in_array(HasConnectData::class, class_uses($class))) {
$id = $model->getConnectIdentifier();
$origin = is_array($id) ? $id[0] : $id;
$target = is_array($id) ? $id[1] : $id;
$model = $query->where($target, $data[$origin])->first() ?? $model;
$model->$target = $data[$origin];
} else {
$model = $query->where('email', $data['email'])->first() ?? $model;
if (! $model->exists())
$model->email = $data['email'];
}
public function get($resourceType, $resourceId)
{
return $this->request('get', "api/resources/$resourceType/$resourceId");
}
// Resources syncing
public function syncAll($resourceTypes = null)
{
$resourceTypes = $resourceTypes ?? $this->synchronized;
foreach ($resourceTypes as $resourceType)
{
$resourceType = $this->resolveResourceType($resourceType);
if (!array_key_exists($resourceType, $this->synchronized))
throw new ConnectException("Resource $resourceType not declared as synchronized.");
$resources = $this->getAll($resourceType);
$model = $this->synchronized[$resourceType];
$identifiers = [];
foreach ($resources as $data)
{
$identifiers[] = intval($data['id']);
$this->sync($resourceType, $data['id'], $data);
}
foreach ($model::all() as $item)
{
if (!in_array(intval($item->{$model::$connectColumnId}), $identifiers))
$model::onConnectResourceDoesNotExist($item);
}
}
}
public function sync($resourceType, $resourceId, $resourceData = null)
{
$resourceType = $this->resolveResourceType($resourceType);
if (is_null($resourceData)) {
$resourceData = $this->get($resourceType, $resourceId);
}
$model = $this->synchronized[$resourceType];
$item = $model::findConnectResource($resourceId);
$method = $this->getEventMethod($item ? 'updated' : 'created');
$data = $this->convertForeignKeys($resourceData);
$model::$method($resourceId, $data);
return $model;
}
// Routing
@@ -425,44 +289,14 @@ class Connect
// Misc
protected function resolveResourceType($class)
protected function flushTokens()
{
if (in_array($class, $this->synchronized))
return array_flip($this->synchronized)[$class];
return $class;
}
protected function resolveResourceModel($class)
{
if (array_key_exists($class, $this->synchronized))
return $this->synchronized[$class];
return $class;
}
protected function getEventMethod($event)
{
return 'onConnectResource' . ucfirst($event);
session()->forget('bconnect.access_token');
session()->forget('bconnect.access_token_expiration');
}
protected function getUrl()
{
return config('bconnect.url') ?? 'https://connect.bluesquare.io';
}
protected function convertForeignKeys($data)
{
foreach (self::$foreignKeys as $key => $resourceType)
{
if (!array_key_exists($key, $data)) continue;
if (!array_key_exists($resourceType, $this->synchronized)) continue;
$model = $this->resolveResourceModel($resourceType);
$record = $model::findConnectResource($data[$key]);
$data[$key] = $record ? $record->id : null;
}
return $data;
}
}

View File

@@ -2,70 +2,54 @@
namespace Bluesquare\Connect;
use Bluesquare\Connect\Commands\RefreshTokens;
use Bluesquare\Connect\Commands\Sync;
use Illuminate\Support\ServiceProvider;
class ConnectServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// Config
$this->mergeConfigFrom(
__DIR__ . '/../config/bconnect.php',
'bconnect'
);
// Singletons
$this->mergeConfigFrom($this->path('config/bconnect.php'), 'bconnect');
$this->app->singleton(Connect::class, function ($app) {
return new Connect($app);
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Config
$config_path = $this->path('config/bconnect.php');
$views_path = $this->path('resources/views/connect');
$this->publishes([
__DIR__ . '/../config/bconnect.php' => config_path('bconnect.php')
$config_path => config_path('bconnect.php'),
$views_path => resource_path('views/vendor/connect'),
]);
// Translations
$this->loadTranslationsFrom($this->path('resources/translations'), 'connect');
$this->loadTranslationsFrom(__DIR__.'/../resources/translations', 'connect');
$this->loadViewsFrom($this->path('resources/views/connect'), 'connect');
// Views
$this->loadViewsFrom(__DIR__.'/../resources/views/connect', 'connect');
$this->publishes([
__DIR__.'/../resources/views/connect' => resource_path('views/vendor/connect'),
if ($this->app->runningInConsole()) {
$this->commands([
RefreshTokens::class,
Sync::class,
]);
}
if (method_exists($this, 'loadViewComponentsAs')) {
// Laravel 7+
if (method_exists($this, 'loadViewComponentsAs')) {
$this->loadViewComponentsAs('connect', [
\Bluesquare\Connect\View\Components\Button::class
]);
}
}
// Commands
// Misc
if ($this->app->runningInConsole()) {
$this->commands([
Sync::class
]);
}
private function path($path = '')
{
return __DIR__ . "/../$path";
}
}

View File

@@ -20,6 +20,19 @@ class ConnectController extends Controller
public function webhook(Request $request, Connect $connect)
{
return $connect->handleWebhook($request);
$hash = sha1(config('bconnect.client_secret') . date('Y-m-d'));
if ($request->header('x-connect-hash') !== $hash)
abort(403);
$data = $request->validate([
'event_type' => 'required|in:create,update,delete',
'connect_data' => 'required|array',
'connect_data.*' => 'nullable',
'connect_data.id' => 'required',
'connect_data.email' => 'required_if:event_type,create|required_if:event_type,update',
]);
$connect->sync($data['event_type'], $data['connect_data']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Bluesquare\Connect\Traits;
use Illuminate\Support\Facades\Log;
trait HasConnectData
{
public function getConnectIdentifier()
{
return $this->connectIdentifier ?? 'connect_id';
}
public function fillConnectData(array $data)
{
$touched = [];
$fillable = $this->connectFillable ?? [];
foreach ($fillable as $origin => $targets) {
$value = is_string($origin) ? $data[$origin] : $data[$targets];
$targets = is_string($origin) && is_array($targets) ? $targets : [$targets];
foreach ($targets as $target) {
$parts = explode('|', $target);
$target = $parts[0];
$currentValue = $value ?? ($parts[1] ?? null);
$target_model = $this;
$parts = explode('.', $target);
foreach ($parts as $i => $property) {
if ($i < count($parts) - 1) {
$target_model = $target_model->$property;
continue;
}
if ($target_model !== $this)
$touched[] = $target_model;
$target_model->$property = $currentValue;
}
}
}
foreach ($touched as $model)
$model->save();
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace Bluesquare\Connect\Traits;
trait HasConnectSync
{
abstract function fill(array $attributes);
abstract function save();
abstract function delete();
public static $connectResource;
public static $connectColumnId = 'connect_resource_id';
public static function findConnectResource($id)
{
return self::query()->where(self::$connectColumnId, $id)->first();
}
public static function onConnectResourceCreated($id, $data)
{
$record = self::findConnectResource($id) ?? new self;
$record->fill($data); // TODO
$record->{self::$connectColumnId} = $id;
return $record->save();
}
public static function onConnectResourceUpdated($id, $data)
{
return self::onConnectResourceCreated($id, $data);
}
public static function onConnectResourceDeleted($id, $data = null)
{
$record = self::findConnectResource($id);
return $record ? $record->forceDelete() : false;
}
public static function onConnectResourceDoesNotExist($record)
{
return $record->forceDelete();
}
}

View File

@@ -4,5 +4,10 @@ namespace Bluesquare\Connect\Traits;
trait HasConnectTokens
{
//
public function fillConnectTokens(array $data)
{
$this->connect_access_token = $data['access_token'];
$this->connect_refresh_token = $data['refresh_token'];
$this->connect_expires_at = $data['expires_at'];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Bluesquare\Connect\Traits;
trait HasConnectWebhook
{
public function onConnectCreate(array $data)
{
$this->onConnectUpdate($data);
}
public function onConnectUpdate(array $data)
{
if (in_array(HasConnectData::class, class_uses(self::class))) {
$this->fillConnectData($data);
} else {
$this->email = $data['email'];
}
$this->save();
}
public function onConnectDelete(array $data)
{
$this->onConnectUpdate($data);
if (in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses(self::class))) {
$this->delete();
} elseif (array_key_exists('remember_token', $this->attributes)) {
$this->remember_token = null;
$this->save();
}
}
public function onConnectRestore(array $data)
{
$this->restore();
$this->onConnectUpdate($data);
}
}