app = $app; } // User config public function setSynchronized($models) { $this->synchronized = []; 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); $this->synchronized[$resource] = $model; } } // API /** * @param $method * @param $uri * @param null $data * @return array * @throws ConnectException */ public function request($method, $uri, $data = null, $auth = true): array { $url = $this->getUrl(); $url = $url . '/' . trim($uri, '/'); $client = new Client(); $config = [ 'headers' => [ 'Accept' => 'application/json' ] ]; if ($auth === true) { $config['headers']['Authorization'] = 'Bearer ' . $this->getAccessToken(); } elseif ($auth !== false) { $config['headers']['Authorization'] = 'Bearer ' . $auth; } if (!is_null($data)) { $config['form_params'] = $data; } try { $response = $client->request($method, $url, $config); $body = (string) $response->getBody(); if ($response->getStatusCode() !== 200) throw new ConnectException($body); return json_decode($body, true); } 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'), 'redirect_uri' => config('bconnect.redirect_url'), 'response_type' => 'code', 'state' => $state ]); $url = $this->getUrl() . '/oauth/authorize?' . $query; return redirect()->to($url); } public function getAccessTokenFromAuhtorizationCode($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($model, $data) { $model->connect_access_token = $data['access_token']; $model->connect_refresh_token = $data['refresh_token']; $model->connect_expires_at = $data['expires_at']; $model->save(); } public function getUserAccessToken($model) { if (empty($model->connect_access_token) || empty($model->connect_refresh_token) || empty($model->connect_expires_at)) { throw new ConnectException("Missing Bluesquare Connect attributes on model: connect_access_token, connect_refresh_token, connect_expires_at"); } if ($model->connect_expires_at <= now()) { $connect_data = $this->getAccessTokenFromRefreshToken($model->connect_refresh_token); $this->updateUserConnectData($model, $connect_data); return $connect_data['access_token']; } return $model->connect_access_token; } public function loginFromCallback(Request $request) { // State check if (!session()->has('connect_states')) { Log::debug("Missing session states"); return redirect('/'); } $states = session()->get('connect_states'); if (!is_array($states)) { Log::debug("Invalid session state"); return redirect('/'); } if (!$request->has('state') || !in_array($request->state, $states)) { Log::debug("Missing valid state in request"); return redirect('/'); } unset($states[array_search($request->state, $states)]); session()->put('connect_states', $states); // Code check if (!$request->has('code')) { Log::debug("Missing authorization code"); return redirect('/'); } // Access token $expires_at = now(); $connect_data = $this->getAccessTokenFromAuhtorizationCode($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'); $user = $model::where('email', $model_data['email'])->first() ?? new $model; $user->fill($model_data); if (in_array($model, $this->synchronized)) $user->id = $model_data['id']; $user->save(); $this->updateUserConnectData($user, $connect_data); auth()->login($user, true); return redirect('/'); } // 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'); } // Webhooks 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']); if ($data['connectEventType'] != 'deleted') { try { $data = $this->get($data['connectResourceType'], $data['connectResourceData']['id']); } catch (\Exception $e) { abort(404, "Could not retrieve this resource."); } } else { $data = $data['connectResourceData']; } $model::$method($data['id'], $data); return true; } // Resources endpoints public function getAll($resourceType) { return $this->request('get', "api/resources/$resourceType"); } public function get($resourceType, $resourceId) { return $this->request('get', "api/resources/$resourceType/$resourceId"); } // Resources sync 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->id), $identifiers)) $item->delete(); } } } 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::find($resourceId); $method = $this->getEventMethod($item ? 'updated' : 'created'); $model::$method($resourceId, $resourceData); } // 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; } protected function getEventMethod($event) { return 'onConnectResource' . ucfirst($event); } protected function getUrl() { return config('bconnect.url') ?? 'https://connect.bluesquare.io'; } 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') ->prefix('api') ->group(function () { Route::post('connect/webhook', 'ConnectController@webhook'); }); } }