diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2040b3f45..a534c6f3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: fail-fast: false matrix: php-version: - - "8.0" - "8.1" - "8.2" defaults: diff --git a/README.md b/README.md index c4a5a6b51..58e620ebe 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Open the URL generated in your console. Once you grant permission to the app, yo ### Application Storage -This template uses [Laravel's Eloquent framework](https://laravel.com/docs/9.x/eloquent) to store Shopify session data. +This template uses [Laravel's Eloquent framework](https://laravel.com/docs/10.x/eloquent) to store Shopify session data. It provides migrations to create the necessary tables in your database, and it stores and loads session data from them. The database that works best for you depends on the data your app needs and how it is queried. diff --git a/web/app/Http/Kernel.php b/web/app/Http/Kernel.php index 3eb42388d..707187c48 100644 --- a/web/app/Http/Kernel.php +++ b/web/app/Http/Kernel.php @@ -16,7 +16,7 @@ class Kernel extends HttpKernel protected $middleware = [ // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, - \Fruitcake\Cors\HandleCors::class, + \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, diff --git a/web/app/Http/Middleware/TrustProxies.php b/web/app/Http/Middleware/TrustProxies.php index a297111e7..c0617abea 100644 --- a/web/app/Http/Middleware/TrustProxies.php +++ b/web/app/Http/Middleware/TrustProxies.php @@ -2,7 +2,7 @@ namespace App\Http\Middleware; -use Fideloper\Proxy\TrustProxies as Middleware; +use Illuminate\Http\Middleware\TrustProxies as Middleware; use Illuminate\Http\Request; class TrustProxies extends Middleware diff --git a/web/app/Lib/ShopifyGuard.php b/web/app/Lib/ShopifyGuard.php new file mode 100644 index 000000000..077c1a610 --- /dev/null +++ b/web/app/Lib/ShopifyGuard.php @@ -0,0 +1,94 @@ +provider = $provider; + } + + /** + * Get the current Shopify session. + * + * @return \Illuminate\Contracts\Auth\Authenticatable|null + */ + public function user() + { + if (! is_null($this->session)) { + return $this->session; + } + + $request = request(); + + if (! $request->hasHeader('Authorization')) { + return; + } + + $shopifySession = Utils::loadCurrentSession( + $request->header(), + $request->cookie(), + false + ); + + if (is_null($shopifySession)) { + return; + } + + $this->session = $this->provider->retrieveById($shopifySession->getId()); + + return $this->session; + } + + /** + * Validate a user's credentials. + * + * Credentials are validated through Shopify sessions so we can bypass the + * need to validate. + * + * @param array $credentials + * @return bool + */ + public function validate(array $credentials = []) + { + return true; + } + + /** + * Via remember is disabled for Shopify sessions. + * + * @return bool + */ + public function viaRemember() + { + return false; + } +} diff --git a/web/app/Models/Session.php b/web/app/Models/Session.php index 4962fc8ad..8b0643d18 100644 --- a/web/app/Models/Session.php +++ b/web/app/Models/Session.php @@ -3,9 +3,49 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Auth\User as Authenticatable; -class Session extends Model +class Session extends Authenticatable { use HasFactory; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = ['access_token']; + + /** + * Disable "remember me" compatibility. + * + * @var null + */ + protected $rememberTokenName = null; + + /** + * The non-primary key value that is used to identify a unique session. + * + * Normally, for a user User model, this would be something like "email", + * however, the equivalent for sessions is the session_id column. + * + * @return string + */ + public function getAuthIdentifierName() + { + return 'session_id'; + } + + /** + * Sessions do not have passwords, they have access tokens instead. + * + * This is the functional equivalent of a password in the context of + * Shopify sessions. + * + * @return string + */ + public function getAuthPassword() + { + return 'access_token'; + } } diff --git a/web/app/Providers/AuthServiceProvider.php b/web/app/Providers/AuthServiceProvider.php index ce7449164..29a927953 100644 --- a/web/app/Providers/AuthServiceProvider.php +++ b/web/app/Providers/AuthServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use App\Lib\ShopifyGuard; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; class AuthServiceProvider extends ServiceProvider @@ -25,6 +28,8 @@ public function boot() { $this->registerPolicies(); - // + Auth::extend('shopify', function (Application $app, string $name, array $config) { + return new ShopifyGuard(Auth::createUserProvider($config['provider'])); + }); } } diff --git a/web/composer.json b/web/composer.json index dc6382c2d..6182013e4 100644 --- a/web/composer.json +++ b/web/composer.json @@ -2,31 +2,28 @@ "name": "laravel/laravel", "type": "project", "description": "The Laravel Framework.", - "keywords": [ - "framework", - "laravel" - ], + "keywords": ["framework", "laravel"], "license": "UNLICENSED", "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "~8.1.0 || ~8.2.0", "ext-xml": "*", "ext-zip": "*", - "doctrine/dbal": "^3.1", - "fideloper/proxy": "^4.4", - "fruitcake/laravel-cors": "^3.0", - "guzzlehttp/guzzle": "^7.0.1", - "laravel/framework": "^8.12", - "laravel/tinker": "^2.5", + "doctrine/dbal": "^3.0", + "guzzlehttp/guzzle": "^7.2", + "laravel/framework": "^10.0", + "laravel/sanctum": "^3.2", + "laravel/tinker": "^2.8", "shopify/shopify-api": "^5.0", "squizlabs/php_codesniffer": "^3.6" }, "require-dev": { - "facade/ignition": "^2.5", "fakerphp/faker": "^1.9.1", - "laravel/sail": "^1.0.1", - "mockery/mockery": "^1.4.2", - "nunomaduro/collision": "^5.0", - "phpunit/phpunit": "^9.3.3" + "laravel/pint": "^1.0", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^7.0", + "phpunit/phpunit": "^10.1", + "spatie/laravel-ignition": "^2.0" }, "autoload": { "psr-4": { @@ -68,8 +65,12 @@ "config": { "optimize-autoloader": true, "preferred-install": "dist", - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true } diff --git a/web/config/auth.php b/web/config/auth.php index 365d479c1..b357f4290 100644 --- a/web/config/auth.php +++ b/web/config/auth.php @@ -14,8 +14,7 @@ */ 'defaults' => [ -// 'guard' => 'web', -// 'passwords' => 'users', + 'guard' => 'web', ], /* @@ -36,12 +35,11 @@ */ 'guards' => [ -// 'web' => [ -// 'driver' => 'session', -// 'provider' => 'users', -// ], - - ], + 'web' => [ + 'driver' => 'shopify', + 'provider' => 'sessions', + ], + ], /* |-------------------------------------------------------------------------- @@ -61,52 +59,10 @@ */ 'providers' => [ -// 'users' => [ -// 'driver' => 'eloquent', -// 'model' => App\Models\User::class, -// ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], + 'sessions' => [ + 'driver' => 'eloquent', + 'model' => \App\Models\Session::class, + ], ], - /* - |-------------------------------------------------------------------------- - | Resetting Passwords - |-------------------------------------------------------------------------- - | - | You may specify multiple password reset configurations if you have more - | than one user table or model in the application and you want to have - | separate password reset settings based on the specific user types. - | - | The expire time is the number of minutes that the reset token should be - | considered valid. This security feature keeps tokens short-lived so - | they have less time to be guessed. You may change this as needed. - | - */ - -// 'passwords' => [ -// 'users' => [ -// 'provider' => 'users', -// 'table' => 'password_resets', -// 'expire' => 60, -// 'throttle' => 60, -// ], -// ], - - /* - |-------------------------------------------------------------------------- - | Password Confirmation Timeout - |-------------------------------------------------------------------------- - | - | Here you may define the amount of seconds before a password confirmation - | times out and the user is prompted to re-enter their password via the - | confirmation screen. By default, the timeout lasts for three hours. - | - */ - -// 'password_timeout' => 10800, - ]; diff --git a/web/config/services.php b/web/config/services.php index 2a1d616c7..97ba404ef 100644 --- a/web/config/services.php +++ b/web/config/services.php @@ -30,4 +30,9 @@ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'shopify' => [ + 'key' => env('SHOPIFY_API_KEY'), + 'secret' => env('SHOPIFY_API_SECRET'), + ] + ]; diff --git a/web/tests/CreatesApplication.php b/web/tests/CreatesApplication.php new file mode 100644 index 000000000..547152f6a --- /dev/null +++ b/web/tests/CreatesApplication.php @@ -0,0 +1,22 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/web/tests/TestCase.php b/web/tests/TestCase.php new file mode 100644 index 000000000..412278c20 --- /dev/null +++ b/web/tests/TestCase.php @@ -0,0 +1,40 @@ +withoutMiddleware([ + EnsureShopifyInstalled::class, + EnsureShopifySession::class + ]); + + $shopifySecret = Config::get('services.shopify.secret'); + + $token = JWT::encode(['dest' => $session->shop], $shopifySecret, 'HS256'); + + return parent::actingAs($session, $guard)->withToken($token); + } +}