2020-05-11 16:26:34 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Bluesquare\Connect;
|
|
|
|
|
|
|
|
use Bluesquare\Connect\Traits\HasConnectSync;
|
2020-05-12 15:57:23 +02:00
|
|
|
use Bluesquare\Connect\Traits\HasConnectTokens;
|
2020-05-11 16:26:34 +02:00
|
|
|
use GuzzleHttp\Client;
|
|
|
|
use Illuminate\Http\Request;
|
2020-05-12 12:47:10 +02:00
|
|
|
use Illuminate\Routing\Router;
|
2020-05-11 17:54:18 +02:00
|
|
|
use Illuminate\Support\Facades\Log;
|
2020-05-12 13:00:14 +02:00
|
|
|
use Illuminate\Support\Facades\Route;
|
2020-05-11 16:26:34 +02:00
|
|
|
use Illuminate\Support\Str;
|
|
|
|
use Psr\Http\Message\StreamInterface;
|
|
|
|
|
|
|
|
class Connect
|
|
|
|
{
|
2020-05-12 16:02:34 +02:00
|
|
|
protected static $resources = [
|
|
|
|
'Role',
|
|
|
|
'Company',
|
|
|
|
'Team',
|
|
|
|
'User',
|
|
|
|
'UserTeam'
|
|
|
|
];
|
|
|
|
|
2020-05-13 12:38:26 +02:00
|
|
|
protected static $foreignKeys = [
|
|
|
|
'role_id' => 'Role',
|
|
|
|
'company_id' => 'Company',
|
|
|
|
'team_id' => 'Team',
|
|
|
|
'user_id' => 'User',
|
|
|
|
'user_teams_id' => 'UserTeam'
|
|
|
|
];
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
protected $app;
|
|
|
|
protected $synchronized = [];
|
|
|
|
|
2020-05-11 17:09:25 +02:00
|
|
|
public function __construct($app)
|
2020-05-11 16:26:34 +02:00
|
|
|
{
|
|
|
|
$this->app = $app;
|
|
|
|
}
|
|
|
|
|
|
|
|
// User config
|
|
|
|
|
|
|
|
public function setSynchronized($models)
|
|
|
|
{
|
2020-05-12 16:02:34 +02:00
|
|
|
$items = [];
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2020-05-12 16:02:34 +02:00
|
|
|
$items[$resource] = $model;
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
2020-05-12 16:02:34 +02:00
|
|
|
|
|
|
|
$synchronized = [];
|
|
|
|
|
|
|
|
foreach (self::$resources as $resourceType) { // Re-ordering
|
|
|
|
foreach ($items as $resource => $model) {
|
|
|
|
if ($resource == $resourceType)
|
|
|
|
$synchronized[$resource] = $model;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->synchronized = $synchronized;
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// API
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $method
|
|
|
|
* @param $uri
|
|
|
|
* @param null $data
|
2020-05-11 17:54:18 +02:00
|
|
|
* @return array
|
2020-05-11 16:26:34 +02:00
|
|
|
* @throws ConnectException
|
|
|
|
*/
|
2020-05-11 17:54:18 +02:00
|
|
|
public function request($method, $uri, $data = null, $auth = true): array
|
2020-05-11 16:26:34 +02:00
|
|
|
{
|
2020-05-11 17:13:58 +02:00
|
|
|
$url = $this->getUrl();
|
2020-05-11 16:26:34 +02:00
|
|
|
$url = $url . '/' . trim($uri, '/');
|
|
|
|
|
|
|
|
$client = new Client();
|
|
|
|
|
|
|
|
$config = [
|
|
|
|
'headers' => [
|
|
|
|
'Accept' => 'application/json'
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($auth === true) {
|
2020-05-11 18:19:43 +02:00
|
|
|
$config['headers']['Authorization'] = 'Bearer ' . $this->getAccessToken();
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
elseif ($auth !== false) {
|
2020-05-11 18:19:43 +02:00
|
|
|
$config['headers']['Authorization'] = 'Bearer ' . $auth;
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_null($data)) {
|
|
|
|
$config['form_params'] = $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2020-05-12 13:37:30 +02:00
|
|
|
$response = $client->request($method, $url, $config);
|
|
|
|
$body = (string) $response->getBody();
|
|
|
|
|
|
|
|
if ($response->getStatusCode() !== 200)
|
|
|
|
throw new ConnectException($body);
|
|
|
|
|
|
|
|
return json_decode($body, true);
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
} catch(\Exception $e) {
|
|
|
|
$this->deleteAccessToken();
|
|
|
|
throw new ConnectException($e->getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OAuth (user)
|
|
|
|
|
|
|
|
public function redirect($state = null)
|
|
|
|
{
|
|
|
|
if (is_null($state))
|
|
|
|
$state = Str::random();
|
|
|
|
|
|
|
|
$states = session()->get('connect_states');
|
|
|
|
if (!is_array($states))
|
|
|
|
$states = [];
|
|
|
|
$states[] = $state;
|
|
|
|
session()->put('connect_states', $states);
|
|
|
|
|
|
|
|
$query = http_build_query([
|
|
|
|
'client_id' => config('bconnect.client_id'),
|
|
|
|
'scope' => config('bconnect.scopes'),
|
2020-05-11 16:49:01 +02:00
|
|
|
'redirect_uri' => config('bconnect.redirect_url'),
|
2020-05-11 16:26:34 +02:00
|
|
|
'response_type' => 'code',
|
|
|
|
'state' => $state
|
|
|
|
]);
|
|
|
|
|
2020-05-11 17:13:58 +02:00
|
|
|
$url = $this->getUrl() . '/oauth/authorize?' . $query;
|
2020-05-11 16:26:34 +02:00
|
|
|
return redirect()->to($url);
|
|
|
|
}
|
|
|
|
|
2020-05-12 16:56:50 +02:00
|
|
|
public function checkState(Request $request)
|
2020-05-11 16:26:34 +02:00
|
|
|
{
|
2020-05-11 17:54:18 +02:00
|
|
|
if (!session()->has('connect_states')) {
|
|
|
|
Log::debug("Missing session states");
|
2020-05-12 16:56:50 +02:00
|
|
|
return false;
|
2020-05-11 17:54:18 +02:00
|
|
|
}
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
$states = session()->get('connect_states');
|
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
if (!is_array($states)) {
|
|
|
|
Log::debug("Invalid session state");
|
2020-05-12 16:56:50 +02:00
|
|
|
return false;
|
2020-05-11 17:54:18 +02:00
|
|
|
}
|
2020-05-11 16:26:34 +02:00
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
if (!$request->has('state') || !in_array($request->state, $states)) {
|
|
|
|
Log::debug("Missing valid state in request");
|
2020-05-12 16:56:50 +02:00
|
|
|
return false;
|
2020-05-11 17:54:18 +02:00
|
|
|
}
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
unset($states[array_search($request->state, $states)]);
|
|
|
|
|
|
|
|
session()->put('connect_states', $states);
|
2020-05-12 16:56:50 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loginFromCallback(Request $request, $redirect_to = '/')
|
|
|
|
{
|
|
|
|
if (!$this->checkState($request))
|
|
|
|
return redirect('/');
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
// Code check
|
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
if (!$request->has('code')) {
|
|
|
|
Log::debug("Missing authorization code");
|
|
|
|
return redirect('/');
|
|
|
|
}
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
// Access token
|
2020-05-11 17:54:18 +02:00
|
|
|
$expires_at = now();
|
2020-05-12 15:57:23 +02:00
|
|
|
$connect_data = $this->getAccessTokenFromAuthorizationCode($request->code);
|
2020-05-11 17:54:18 +02:00
|
|
|
$connect_data['expires_at'] = $expires_at->addSeconds($connect_data['expires_in']);
|
2020-05-11 16:26:34 +02:00
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
$model_data = $this->getUserData($connect_data['access_token']);
|
2020-05-11 18:28:00 +02:00
|
|
|
$model = config('bconnect.model');
|
2020-05-11 16:26:34 +02:00
|
|
|
|
2020-05-13 12:38:26 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-13 13:08:40 +02:00
|
|
|
$model_data = $this->convertForeignKeys($model_data);
|
|
|
|
|
|
|
|
$user->fill($model_data);
|
2020-05-11 17:54:18 +02:00
|
|
|
|
2020-05-11 18:28:00 +02:00
|
|
|
if (in_array($model, $this->synchronized))
|
2020-05-13 12:38:26 +02:00
|
|
|
$user->{$model::$connectColumnId} = $model_data['id'];
|
2020-05-11 18:28:00 +02:00
|
|
|
|
|
|
|
$user->save();
|
2020-05-11 17:54:18 +02:00
|
|
|
|
2020-05-12 18:49:09 +02:00
|
|
|
if (in_array($model, $this->synchronized))
|
2020-05-13 12:38:26 +02:00
|
|
|
$user = $model::findConnectResource($model_data['id']);
|
2020-05-12 18:49:09 +02:00
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
$this->updateUserConnectData($user, $connect_data);
|
|
|
|
|
2020-05-11 18:22:06 +02:00
|
|
|
auth()->login($user, true);
|
|
|
|
|
2020-05-12 16:56:50 +02:00
|
|
|
return redirect($redirect_to);
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
|
2020-05-12 15:57:23 +02:00
|
|
|
public function getAccessTokenFromAuthorizationCode($code)
|
|
|
|
{
|
|
|
|
$data = $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', [
|
|
|
|
'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)
|
|
|
|
{
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
$has_fields = in_array(HasConnectTokens::class, class_uses(get_class($user)));
|
|
|
|
|
|
|
|
if (!$has_fields) {
|
|
|
|
throw new ConnectException("User class does not implement HasConnectTokens");
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($user->connect_expires_at <= now()) {
|
|
|
|
$connect_data = $this->getAccessTokenFromRefreshToken($user->connect_refresh_token);
|
|
|
|
$this->updateUserConnectData($user, $connect_data);
|
|
|
|
return $connect_data['access_token'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $user->connect_access_token;
|
|
|
|
}
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
// OAuth (client)
|
|
|
|
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2020-05-12 15:57:23 +02:00
|
|
|
// Webhook handler
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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']);
|
2020-05-12 13:37:30 +02:00
|
|
|
|
2020-05-13 13:08:40 +02:00
|
|
|
$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') {
|
2020-05-12 13:37:30 +02:00
|
|
|
abort(404, "Could not retrieve this resource.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-13 12:38:26 +02:00
|
|
|
$data = $this->convertForeignKeys($data);
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
$model::$method($data['id'], $data);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-05-12 15:57:23 +02:00
|
|
|
// Resources getters
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
public function getAll($resourceType)
|
|
|
|
{
|
|
|
|
return $this->request('get', "api/resources/$resourceType");
|
|
|
|
}
|
|
|
|
|
|
|
|
public function get($resourceType, $resourceId)
|
|
|
|
{
|
|
|
|
return $this->request('get', "api/resources/$resourceType/$resourceId");
|
|
|
|
}
|
|
|
|
|
2020-05-12 15:57:23 +02:00
|
|
|
// Resources syncing
|
2020-05-11 16:26:34 +02:00
|
|
|
|
|
|
|
public function syncAll($resourceTypes = null)
|
|
|
|
{
|
|
|
|
$resourceTypes = $resourceTypes ?? $this->synchronized;
|
|
|
|
|
|
|
|
foreach ($resourceTypes as $resourceType)
|
|
|
|
{
|
2020-05-11 17:54:18 +02:00
|
|
|
$resourceType = $this->resolveResourceType($resourceType);
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
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)
|
|
|
|
{
|
2020-05-13 12:38:26 +02:00
|
|
|
if (!in_array(intval($item->{$model::$connectColumnId}), $identifiers))
|
2020-05-12 16:56:50 +02:00
|
|
|
$model::onConnectResourceDoesNotExist($item);
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function sync($resourceType, $resourceId, $resourceData = null)
|
|
|
|
{
|
2020-05-11 17:54:18 +02:00
|
|
|
$resourceType = $this->resolveResourceType($resourceType);
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
if (is_null($resourceData)) {
|
|
|
|
$resourceData = $this->get($resourceType, $resourceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
$model = $this->synchronized[$resourceType];
|
2020-05-13 12:38:26 +02:00
|
|
|
$item = $model::findConnectResource($resourceId);
|
2020-05-11 16:26:34 +02:00
|
|
|
$method = $this->getEventMethod($item ? 'updated' : 'created');
|
2020-05-13 12:38:26 +02:00
|
|
|
|
|
|
|
$data = $this->convertForeignKeys($resourceData);
|
|
|
|
$model::$method($resourceId, $data);
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|
|
|
|
|
2020-05-12 15:57:23 +02:00
|
|
|
// Routing
|
|
|
|
|
|
|
|
public function routes()
|
|
|
|
{
|
|
|
|
Route::namespace('\Bluesquare\Connect\Controllers')
|
|
|
|
->group(function () {
|
|
|
|
Route::get('connect/authorize', 'ConnectController@authorize');
|
|
|
|
Route::get('connect/callback', 'ConnectController@callback');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public function apiRoutes()
|
|
|
|
{
|
|
|
|
Route::namespace('\Bluesquare\Connect\Controllers')
|
|
|
|
->group(function () {
|
|
|
|
Route::post('connect/webhook', 'ConnectController@webhook');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Misc
|
2020-05-11 16:26:34 +02:00
|
|
|
|
2020-05-11 17:54:18 +02:00
|
|
|
protected function resolveResourceType($class)
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-11 16:26:34 +02:00
|
|
|
protected function getEventMethod($event)
|
|
|
|
{
|
|
|
|
return 'onConnectResource' . ucfirst($event);
|
|
|
|
}
|
2020-05-11 17:13:58 +02:00
|
|
|
|
|
|
|
protected function getUrl()
|
|
|
|
{
|
|
|
|
return config('bconnect.url') ?? 'https://connect.bluesquare.io';
|
|
|
|
}
|
2020-05-13 12:38:26 +02:00
|
|
|
|
|
|
|
protected function convertForeignKeys($data)
|
|
|
|
{
|
|
|
|
foreach (self::$foreignKeys as $key => $resourceType)
|
|
|
|
{
|
|
|
|
if (!array_key_exists($key, $data)) continue;
|
|
|
|
|
|
|
|
$model = $this->resolveResourceModel($resourceType);
|
|
|
|
if (!in_array(HasConnectSync::class, class_uses($model))) continue;
|
|
|
|
|
|
|
|
$record = $model::findConnectResource($data[$key]);
|
|
|
|
$data[$key] = $record ? $record->id : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
2020-05-11 16:26:34 +02:00
|
|
|
}
|