@ -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,14 +44,11 @@ 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)) {
if (! is_null($data)) {
$config['form_params'] = $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))
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))
return redirect('/');
// Code check
if (!$request->has('code')) {
Log::debug("Missing authorization code");
if (! $this->checkState($request) || ! $request->has('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']);
$this->updateUserConnectData($user, $connect_data);
// Login
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)
{
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");
if (! $has_fields) {
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;
}
$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($event, ['update', 'delete'])) {
$model = $this->resolveUser($data);
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'
]);
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 (! $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 & & $model->trashed()) {
$event = 'restore';
} else {
$event = 'update';
}
}
}
$data = $this->convertForeignKeys($data);
$model::$method($data['id'], $data);
if (in_array(HasConnectWebhook::class, class_uses($class))) {
$method = 'onConnect' . ucfirst($event);
$model->$method($data);
} else {
$this->updateUserData($model, $data);
$model->save();
}
return true;
return $model;
}
// 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, $withTrashed = false)
{
$resourceTypes = $resourceTypes ?? $this->synchronized;
$class = config('bconnect.model');
$query = $class::query();
foreach ($resourceTypes as $resourceType)
{
$resourceType = $this->resolveResourceType($resourceType);
if ($withTrashed)
$query->withTrashed();
if (!array_key_exists($resourceType, $this->synchronized))
throw new ConnectException("Resource $resourceType not declared as synchronized.");
$model = new $class;
$resources = $this->getAll($resourceType);
$model = $this->synchronized[$resourceType];
$identifiers = [];
if (in_array(HasConnectData::class, class_uses($class))) {
$id = $model->getConnectIdentifier();
foreach ($resources as $data)
{
$identifiers[] = intval($data['id']);
$this->sync($resourceType, $data['id'], $data);
}
$origin = is_array($id) ? $id[0] : $id;
$target = is_array($id) ? $id[1] : $id;
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);
$model = $query->where($target, $data[$origin])->first() ?? $model;
$model->$target = $data[$origin];
} else {
$model = $query->where('email', $data['email'])->first() ?? $model;
if (is_null($resourceData)) {
$resourceData = $this->get($resourceType, $resourceId) ;
if (! $model->exists())
$model->email = $data['email'];
}
$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;
}
}