|
|
|
@ -2,6 +2,7 @@
|
|
|
|
|
|
|
|
|
|
namespace Bluesquare\Connect; |
|
|
|
|
|
|
|
|
|
use Bluesquare\Connect\Traits\HasConnectData; |
|
|
|
|
use Bluesquare\Connect\Traits\HasConnectSync; |
|
|
|
|
use Bluesquare\Connect\Traits\HasConnectTokens; |
|
|
|
|
use GuzzleHttp\Client; |
|
|
|
@ -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 ($access_token !== null) { |
|
|
|
|
$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,80 +93,59 @@ 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['expires_at'] = $expires_at->addSeconds($connect_data['expires_in']); |
|
|
|
|
|
|
|
|
|
$model_data = $this->getUserData($connect_data['access_token']); |
|
|
|
|
$model = config('bconnect.model'); |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
// User data |
|
|
|
|
|
|
|
|
|
if (in_array($model, $this->synchronized)) |
|
|
|
|
$user->{$model::$connectColumnId} = $model_data['id']; |
|
|
|
|
$user_data = $this->getUserData($connect_data['access_token']); |
|
|
|
|
$user = $this->resolveUser($user_data); |
|
|
|
|
|
|
|
|
|
$this->updateUserConnectData($user, $connect_data); |
|
|
|
|
$this->updateUserData($user, $user_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', [ |
|
|
|
@ -220,7 +155,7 @@ class Connect
|
|
|
|
|
'scope' => config('bconnect.user_scopes'), |
|
|
|
|
'redirect_uri' => config('bconnect.redirect_url'), |
|
|
|
|
'code' => $code |
|
|
|
|
], false); |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
return $data; |
|
|
|
|
} |
|
|
|
@ -234,7 +169,7 @@ class Connect
|
|
|
|
|
'scope' => config('bconnect.user_scopes'), |
|
|
|
|
'redirect_uri' => config('bconnect.redirect_url'), |
|
|
|
|
'refresh_token' => $refresh_token |
|
|
|
|
], false); |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
return $data; |
|
|
|
|
} |
|
|
|
@ -244,164 +179,65 @@ class Connect
|
|
|
|
|
return $this->request('get', 'api/user', null, $access_token); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public function updateUserConnectData($user, $data) |
|
|
|
|
{ |
|
|
|
|
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) |
|
|
|
|
public function getUserAccessToken($model) |
|
|
|
|
{ |
|
|
|
|
$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() |
|
|
|
|
{ |
|
|
|
|
$access_token = cache()->get('bconnect.access_token'); |
|
|
|
|
$access_token_expiration = cache()->get('bconnect.access_token_expiration'); |
|
|
|
|
|
|
|
|
|
if ($access_token && $access_token_expiration > time() + 60) { |
|
|
|
|
return $access_token; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$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']; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public function deleteAccessToken() |
|
|
|
|
{ |
|
|
|
|
cache()->delete('bconnect.access_token'); |
|
|
|
|
cache()->delete('bconnect.access_token_expiration'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Webhook handler |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* @param Request $request |
|
|
|
|
* @return bool |
|
|
|
|
*/ |
|
|
|
|
public function handleWebhook(Request $request) |
|
|
|
|
{ |
|
|
|
|
$data = $request->validate([ |
|
|
|
|
'connectEventType' => 'required|in:created,updated,deleted', |
|
|
|
|
'connectResourceType' => 'required', |
|
|
|
|
'connectResourceTable' => 'required', |
|
|
|
|
'connectResourceData' => 'required|array', |
|
|
|
|
'connectResourceData.id' => 'required' |
|
|
|
|
'event_type' => 'required|in:created,updated,deleted,restored', |
|
|
|
|
'connect_data' => 'required|array', |
|
|
|
|
'connect_data.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."); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$data = $this->convertForeignKeys($data); |
|
|
|
|
|
|
|
|
|
$model::$method($data['id'], $data); |
|
|
|
|
//@TODO |
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
return ['handled' => false]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Resources getters |
|
|
|
|
|
|
|
|
|
public function getAll($resourceType) |
|
|
|
|
public function updateUserConnectData($user, $data) |
|
|
|
|
{ |
|
|
|
|
return $this->request('get', "api/resources/$resourceType"); |
|
|
|
|
if (in_array(HasConnectTokens::class, class_uses(get_class($user)))) |
|
|
|
|
$user->fillConnectTokens($data); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public function get($resourceType, $resourceId) |
|
|
|
|
public function updateUserData($user, $data) |
|
|
|
|
{ |
|
|
|
|
return $this->request('get', "api/resources/$resourceType/$resourceId"); |
|
|
|
|
if (in_array(HasConnectData::class, class_uses(get_class($user)))) |
|
|
|
|
$user->fillConnectData($data); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Resources syncing |
|
|
|
|
|
|
|
|
|
public function syncAll($resourceTypes = null) |
|
|
|
|
protected function resolveUser($data) |
|
|
|
|
{ |
|
|
|
|
$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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
$model = config('bconnect.model'); |
|
|
|
|
|
|
|
|
|
public function sync($resourceType, $resourceId, $resourceData = null) |
|
|
|
|
{ |
|
|
|
|
$resourceType = $this->resolveResourceType($resourceType); |
|
|
|
|
if (in_array(HasConnectData::class, class_uses($model))) { |
|
|
|
|
$origin = is_array($model::$connectIdentifier) ? $model::$connectIdentifier[0] : $model::$connectIdentifier; |
|
|
|
|
$target = is_array($model::$connectIdentifier) ? $model::$connectIdentifier[1] : $model::$connectIdentifier; |
|
|
|
|
|
|
|
|
|
if (is_null($resourceData)) { |
|
|
|
|
$resourceData = $this->get($resourceType, $resourceId); |
|
|
|
|
$user = $model::where($target, $data[$origin])->first() ?? new $model; |
|
|
|
|
} else { |
|
|
|
|
$user = $model::where('email', $data['email'])->first() ?? new $model; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$model = $this->synchronized[$resourceType]; |
|
|
|
|
$item = $model::findConnectResource($resourceId); |
|
|
|
|
$method = $this->getEventMethod($item ? 'updated' : 'created'); |
|
|
|
|
|
|
|
|
|
$data = $this->convertForeignKeys($resourceData); |
|
|
|
|
$model::$method($resourceId, $data); |
|
|
|
|
return $user; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Routing |
|
|
|
@ -425,44 +261,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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|