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 \Psr\Http\Message\StreamInterface * @throws ConnectException */ public function request($method, $uri, $data = null, $auth = true): StreamInterface { $url = config('bconnect.url') ?? 'https://connect.bluesquare.io'; $url = $url . '/' . trim($uri, '/'); $client = new Client(); $config = [ 'headers' => [ 'Accept' => 'application/json' ] ]; if ($auth === true) { $config['Authorization'] = 'Bearer ' . $this->getAccessToken(); } elseif ($auth !== false) { $config['Authorization'] = 'Bearer ' . $auth; } if (!is_null($data)) { $config['form_params'] = $data; } try { return json_decode( $client->request($method, $url, $config)->getBody(), 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'), 'response_type' => 'code', 'state' => $state ]); $url = config('bconnect.url') . '/oauth/authorize?' . $query; return redirect()->to($url); } public function loginFromCallback(Request $request) { // State check if (!session()->has('connect_states')) abort(403, "Session expired"); $states = session()->get('connect_states'); if (!is_array($states)) abort(403, "Session expired"); if (!$request->has('state') || !in_array($request->state, $states)) abort(403, "Invalid state"); unset($states[array_search($request->state, $states)]); session()->put('connect_states', $states); // Code check if (!$request->has('code')) abort(403, "Missing authorization code"); $code = $request->code; // Access token $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'), 'authorization_code' => $code ], false); return $data; } // 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']); $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) { 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) { 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 getEventMethod($event) { return 'onConnectResource' . ucfirst($event); } }