From 830ebbcdd34649427febd33a406d9c339a53241f Mon Sep 17 00:00:00 2001 From: Maxime Renou Date: Thu, 19 May 2022 13:24:54 +0200 Subject: [PATCH] feat: sync, webhook, tokens refresh, fillable, key config --- src/Commands/RefreshTokens.php | 4 +- src/Commands/Sync.php | 4 +- src/Connect.php | 96 ++++++++++++++++++++++------------- src/ConnectServiceProvider.php | 6 +-- src/Controllers/ConnectController.php | 15 +++++- src/Traits/HasConnectData.php | 44 ++++++++++++---- src/Traits/HasConnectWebhook.php | 41 +++++++++++++++ 7 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 src/Traits/HasConnectWebhook.php diff --git a/src/Commands/RefreshTokens.php b/src/Commands/RefreshTokens.php index 8b3415e..0de27dc 100644 --- a/src/Commands/RefreshTokens.php +++ b/src/Commands/RefreshTokens.php @@ -19,13 +19,13 @@ class RefreshTokens extends Command $has_fields = in_array(HasConnectTokens::class, class_uses($class)); - if (!$has_fields) { + if (! $has_fields) { throw new ConnectException("$class does not implement HasConnectTokens"); } $class::query()->chunks(10, function ($models) use ($connect) { $models->each(function ($model) use ($connect) { - if (!empty($model->connect_refresh_token) && $model->connect_expires_at <= now()->addHour()) { + if (! empty($model->connect_refresh_token) && $model->connect_expires_at <= now()->addHour()) { try { $tokens = $connect->getAccessTokenFromRefreshToken($model->connect_refresh_token); $connect->updateUserConnectData($model, $tokens); diff --git a/src/Commands/Sync.php b/src/Commands/Sync.php index 2b9915e..2adefc8 100644 --- a/src/Commands/Sync.php +++ b/src/Commands/Sync.php @@ -21,14 +21,14 @@ class Sync extends Command $has_fields = in_array(HasConnectTokens::class, class_uses($class)); - if (!$has_fields) { + if (! $has_fields) { throw new ConnectException("$class does not implement HasConnectTokens"); } $class::query()->chunks(10, function ($models) use ($connect) { $models->each(function ($model) use ($connect) { try { - if (!empty($model->connect_access_token)) { + if (! empty($model->connect_access_token)) { $data = $connect->getUserData($model->connect_access_token); $connect->updateUserData($model, $data); $model->save(); diff --git a/src/Connect.php b/src/Connect.php index ea18f2c..35709d9 100644 --- a/src/Connect.php +++ b/src/Connect.php @@ -3,8 +3,8 @@ namespace Bluesquare\Connect; use Bluesquare\Connect\Traits\HasConnectData; -use Bluesquare\Connect\Traits\HasConnectSync; use Bluesquare\Connect\Traits\HasConnectTokens; +use Bluesquare\Connect\Traits\HasConnectWebhook; use GuzzleHttp\Client; use Illuminate\Http\Request; use Illuminate\Routing\Router; @@ -44,11 +44,11 @@ class Connect ] ]; - if ($access_token !== null) { + if (! is_null($access_token)) { $config['headers']['Authorization'] = 'Bearer ' . $access_token; } - if (!is_null($data)) { + if (! is_null($data)) { $config['form_params'] = $data; } @@ -77,7 +77,7 @@ class Connect $states = session()->get('connect_states'); - if (!is_array($states)) + if (! is_array($states)) $states = []; $states[] = $state; @@ -99,15 +99,15 @@ class Connect public function checkState(Request $request) { - if (!session()->has('connect_states')) + if (! session()->has('connect_states')) return false; $states = session()->get('connect_states'); - if (!is_array($states)) + if (! is_array($states)) return false; - if (!$request->has('state') || !in_array($request->state, $states)) + if (! $request->has('state') || ! in_array($request->state, $states)) return false; unset($states[array_search($request->state, $states)]); @@ -119,22 +119,21 @@ class Connect public function loginFromCallback(Request $request, $redirect_to = '/') { - if (!$this->checkState($request) || !$request->has('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']); // User data $user_data = $this->getUserData($connect_data['access_token']); - $user = $this->resolveUser($user_data); + $user = $this->sync('create', $user_data); $this->updateUserConnectData($user, $connect_data); - $this->updateUserData($user, $user_data); $user->save(); // Login @@ -148,7 +147,7 @@ class Connect 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'), @@ -156,13 +155,11 @@ class Connect 'redirect_uri' => config('bconnect.redirect_url'), 'code' => $code ]); - - 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'), @@ -170,8 +167,6 @@ class Connect 'redirect_uri' => config('bconnect.redirect_url'), 'refresh_token' => $refresh_token ]); - - return $data; } public function getUserData($access_token) @@ -184,7 +179,7 @@ class Connect $class = get_class($model); $has_fields = in_array(HasConnectTokens::class, class_uses($class)); - if (!$has_fields) { + if (! $has_fields) { throw new ConnectException("$class does not implement HasConnectTokens"); } @@ -199,17 +194,38 @@ class Connect // Sync - public function handleWebhook(Request $request) + public function sync(string $event, array $data) { - $data = $request->validate([ - 'event_type' => 'required|in:created,updated,deleted,restored', - 'connect_data' => 'required|array', - 'connect_data.id' => 'required' - ]); + $class = config('bconnect.model'); - //@TODO + if (in_array($event, ['update', 'delete'])) { + $model = $this->resolveUser($data); - return ['handled' => false]; + 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) { + $event = 'restore'; + } else { + $event = 'update'; + } + } + } + + if (in_array(HasConnectWebhook::class, class_uses($class))) { + $method = 'onConnect' . ucfirst($event); + $model->$method($data); + } else { + $this->updateUserData($model, $data); + $model->save(); + } + + return $model; } public function updateUserConnectData($user, $data) @@ -224,20 +240,32 @@ class Connect $user->fillConnectData($data); } - protected function resolveUser($data) + protected function resolveUser($data, $withTrashed = false) { - $model = config('bconnect.model'); + $class = config('bconnect.model'); + $query = $class::query(); - 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 ($withTrashed) + $query->withTrashed(); - $user = $model::where($target, $data[$origin])->first() ?? new $model; + $model = new $class; + + if (in_array(HasConnectData::class, class_uses($class))) { + $id = $model->getConnectIdentifier(); + + $origin = is_array($id) ? $id[0] : $id; + $target = is_array($id) ? $id[1] : $id; + + $model = $query->where($target, $data[$origin])->first() ?? $model; + $model->$target = $data[$origin]; } else { - $user = $model::where('email', $data['email'])->first() ?? new $model; + $model = $query->where('email', $data['email'])->first() ?? $model; + + if (! $model->exists()) + $model->email = $data['email']; } - return $user; + return $model; } // Routing diff --git a/src/ConnectServiceProvider.php b/src/ConnectServiceProvider.php index acb2d7b..4d2137b 100644 --- a/src/ConnectServiceProvider.php +++ b/src/ConnectServiceProvider.php @@ -10,7 +10,7 @@ class ConnectServiceProvider extends ServiceProvider { public function register() { - $this->mergeConfigFrom($this->path('src/config/bconnect.php'), 'bconnect'); + $this->mergeConfigFrom($this->path('config/bconnect.php'), 'bconnect'); $this->app->singleton(Connect::class, function ($app) { return new Connect($app); @@ -19,7 +19,7 @@ class ConnectServiceProvider extends ServiceProvider public function boot() { - $config_path = $this->path('src/config/bconnect.php'); + $config_path = $this->path('config/bconnect.php'); $views_path = $this->path('resources/views/connect'); $this->publishes([ @@ -50,6 +50,6 @@ class ConnectServiceProvider extends ServiceProvider private function path($path = '') { - return __DIR__ . "/../../$path"; + return __DIR__ . "/../$path"; } } diff --git a/src/Controllers/ConnectController.php b/src/Controllers/ConnectController.php index eb6bd89..81a9ff3 100644 --- a/src/Controllers/ConnectController.php +++ b/src/Controllers/ConnectController.php @@ -20,6 +20,19 @@ class ConnectController extends Controller public function webhook(Request $request, Connect $connect) { - return $connect->handleWebhook($request); + $hash = sha1(config('bconnect.client_secret') . date('Y-m-d')); + + if ($request->header('x-connect-hash') !== $hash) + abort(403); + + $data = $request->validate([ + 'event_type' => 'required|in:create,update,delete', + 'connect_data' => 'required|array', + 'connect_data.*' => 'nullable', + 'connect_data.id' => 'required', + 'connect_data.email' => 'required_if:event_type,create|required_if:event_type,update', + ]); + + $connect->sync($data['event_type'], $data['connect_data']); } } diff --git a/src/Traits/HasConnectData.php b/src/Traits/HasConnectData.php index d7c5452..c949cf9 100644 --- a/src/Traits/HasConnectData.php +++ b/src/Traits/HasConnectData.php @@ -1,23 +1,47 @@ connectIdentifier ?? 'connect_id'; + } public function fillConnectData(array $data) { - foreach ($this->connectFillable as $origin => $target) { - if (is_string($origin)) { - $this->$target = $data[$origin] ?? null; - } else { - $this->$target = $data[$target] ?? null; + $touched = []; + + $fillable = $this->connectFillable ?? []; + + foreach ($fillable as $origin => $targets) { + $value = is_string($origin) ? $data[$origin] : $data[$targets]; + $targets = is_string($origin) && is_array($targets) ? $targets : [$targets]; + + foreach ($targets as $target) { + $parts = explode('|', $target); + $target = $parts[0]; + $currentValue = $value ?? ($parts[1] ?? null); + $target_model = $this; + $parts = explode('.', $target); + + foreach ($parts as $i => $property) { + if ($i < count($parts) - 1) { + $target_model = $target_model->$property; + continue; + } + + if ($target_model !== $this) + $touched[] = $target_model; + + $target_model->$property = $currentValue; + } } } - } - abstract public function fill(array $attributes); + foreach ($touched as $model) + $model->save(); + } } diff --git a/src/Traits/HasConnectWebhook.php b/src/Traits/HasConnectWebhook.php new file mode 100644 index 0000000..d9e124f --- /dev/null +++ b/src/Traits/HasConnectWebhook.php @@ -0,0 +1,41 @@ +onConnectUpdate($data); + } + + public function onConnectUpdate(array $data) + { + if (in_array(HasConnectData::class, class_uses(self::class))) { + $this->fillConnectData($data); + } else { + $this->email = $data['email']; + } + + $this->save(); + } + + public function onConnectDelete(array $data) + { + $this->onConnectUpdate($data); + + if (in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, class_uses(self::class))) { + $this->delete(); + } elseif (array_key_exists('remember_token', $this->attributes)) { + $this->remember_token = null; + $this->save(); + } + } + + public function onConnectRestore(array $data) + { + $this->restore(); + + $this->onConnectUpdate($data); + } +}