Fall back when Parcel2Go profile scope is denied
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
This commit is contained in:
@@ -46,11 +46,11 @@ class DashboardController extends Controller
|
|||||||
$shippingConfig = $settings->for('parcel2go');
|
$shippingConfig = $settings->for('parcel2go');
|
||||||
if ($isAdmin && filled($shippingConfig['client_id'] ?? null) && filled($shippingConfig['client_secret'] ?? null) && filled($shippingConfig['auth_url'] ?? null)) {
|
if ($isAdmin && filled($shippingConfig['client_id'] ?? null) && filled($shippingConfig['client_secret'] ?? null) && filled($shippingConfig['auth_url'] ?? null)) {
|
||||||
try {
|
try {
|
||||||
|
$parcel2Go->verifyCredentials();
|
||||||
if ($parcel2Go->hasScope('my-profile')) {
|
if ($parcel2Go->hasScope('my-profile')) {
|
||||||
$shipments = array_slice($parcel2Go->fetch('me/orders/detail')['Orders'] ?? [], 0, 4);
|
$shipments = array_slice($parcel2Go->fetch('me/orders/detail')['Orders'] ?? [], 0, 4);
|
||||||
$shippingStatus = 'Live account';
|
$shippingStatus = 'Live account';
|
||||||
} else {
|
} else {
|
||||||
$parcel2Go->verifyCredentials();
|
|
||||||
$shippingStatus = 'Quotes available';
|
$shippingStatus = 'Quotes available';
|
||||||
}
|
}
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ShippingController extends Controller
|
|||||||
{
|
{
|
||||||
abort_unless($request->user()->role === 'admin', 403);
|
abort_unless($request->user()->role === 'admin', 403);
|
||||||
try {
|
try {
|
||||||
|
$parcel2Go->verifyCredentials();
|
||||||
$account = [];
|
$account = [];
|
||||||
$balance = [];
|
$balance = [];
|
||||||
$orders = [];
|
$orders = [];
|
||||||
@@ -31,8 +32,9 @@ class ShippingController extends Controller
|
|||||||
$profileNotice = 'Parcel2Go connected, but account history is unavailable. Check that this client has my-profile access.';
|
$profileNotice = 'Parcel2Go connected, but account history is unavailable. Check that this client has my-profile access.';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$parcel2Go->verifyCredentials();
|
$profileNotice = $parcel2Go->scopeWasReduced()
|
||||||
$profileNotice = 'Parcel2Go connected for quotes and collections. Add my-profile access in Parcel2Go to show account history and labels.';
|
? 'Parcel2Go rejected the selected API access. Basic shipping access works, but account history and labels need my-profile permission for this client.'
|
||||||
|
: 'Parcel2Go connected for quotes and collections. Add my-profile access in Parcel2Go to show account history and labels.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('shipping', compact('account', 'balance', 'orders', 'profileAvailable', 'profileNotice') + [
|
return view('shipping', compact('account', 'balance', 'orders', 'profileAvailable', 'profileNotice') + [
|
||||||
|
|||||||
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Http\Client\Response;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class Parcel2Go
|
class Parcel2Go
|
||||||
{
|
{
|
||||||
|
private ?string $token = null;
|
||||||
|
|
||||||
|
private ?string $activeScope = null;
|
||||||
|
|
||||||
|
private bool $reducedScope = false;
|
||||||
|
|
||||||
public function __construct(private IntegrationSettings $settings) {}
|
public function __construct(private IntegrationSettings $settings) {}
|
||||||
|
|
||||||
public function hasScope(string $scope): bool
|
public function hasScope(string $scope): bool
|
||||||
{
|
{
|
||||||
return in_array($scope, explode(' ', (string) ($this->settings->for('parcel2go')['scope'] ?? 'public-api')), true);
|
return in_array($scope, explode(' ', $this->activeScope ?? (string) ($this->settings->for('parcel2go')['scope'] ?? 'public-api')), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWasReduced(): bool
|
||||||
|
{
|
||||||
|
return $this->reducedScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function verifyCredentials(): void
|
public function verifyCredentials(): void
|
||||||
@@ -107,22 +119,41 @@ class Parcel2Go
|
|||||||
|
|
||||||
private function accessToken(): string
|
private function accessToken(): string
|
||||||
{
|
{
|
||||||
|
if ($this->token !== null) {
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
$settings = $this->settings->for('parcel2go');
|
$settings = $this->settings->for('parcel2go');
|
||||||
if (! $settings['client_id'] || ! $settings['client_secret'] || ! $settings['auth_url']) {
|
if (! $settings['client_id'] || ! $settings['client_secret'] || ! $settings['auth_url']) {
|
||||||
throw new RuntimeException('Parcel2Go credentials are not configured.');
|
throw new RuntimeException('Parcel2Go credentials are not configured.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = Http::asForm()->timeout(10)->post($settings['auth_url'], [
|
$scope = (string) ($settings['scope'] ?? 'public-api');
|
||||||
'grant_type' => 'client_credentials',
|
$response = $this->tokenResponse($settings, $scope);
|
||||||
'scope' => $settings['scope'] ?? 'public-api',
|
if ($scope !== 'public-api' && $response->status() === 400 && $response->json('error') === 'invalid_scope') {
|
||||||
'client_id' => $settings['client_id'],
|
$scope = 'public-api';
|
||||||
'client_secret' => $settings['client_secret'],
|
$response = $this->tokenResponse($settings, $scope);
|
||||||
])->throw()->json('access_token');
|
$this->reducedScope = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $response->throw()->json('access_token');
|
||||||
if (! $token) {
|
if (! $token) {
|
||||||
throw new RuntimeException('Parcel2Go did not return an access token.');
|
throw new RuntimeException('Parcel2Go did not return an access token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $token;
|
$this->activeScope = $scope;
|
||||||
|
|
||||||
|
return $this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{auth_url: string, client_id: string, client_secret: string} $settings */
|
||||||
|
private function tokenResponse(array $settings, string $scope): Response
|
||||||
|
{
|
||||||
|
return Http::asForm()->timeout(10)->post($settings['auth_url'], [
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
'scope' => $scope,
|
||||||
|
'client_id' => $settings['client_id'],
|
||||||
|
'client_secret' => $settings['client_secret'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class DashboardAccessTest extends TestCase
|
|||||||
$admin = User::factory()->create(['role' => 'admin']);
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
|
|
||||||
$this->actingAs($admin)->get(route('shipping'))->assertOk()->assertSee('Lee Hickman')->assertSee('12.50');
|
$this->actingAs($admin)->get(route('shipping'))->assertOk()->assertSee('Lee Hickman')->assertSee('12.50');
|
||||||
Http::assertSentCount(6);
|
Http::assertSentCount(4);
|
||||||
Http::assertSent(fn ($request) => $request->url() === 'https://auth.example.test/token'
|
Http::assertSent(fn ($request) => $request->url() === 'https://auth.example.test/token'
|
||||||
&& $request['scope'] === 'public-api my-profile');
|
&& $request['scope'] === 'public-api my-profile');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,28 @@ class IntegrationsTest extends TestCase
|
|||||||
Http::assertSentCount(1);
|
Http::assertSentCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_shipping_falls_back_to_basic_access_when_parcel2go_rejects_profile_scope(): void
|
||||||
|
{
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
Http::fake(['https://auth.example.test/token' => function ($request) {
|
||||||
|
return $request['scope'] === 'public-api'
|
||||||
|
? Http::response(['access_token' => 'test-token'])
|
||||||
|
: Http::response(['error' => 'invalid_scope'], 400);
|
||||||
|
}]);
|
||||||
|
config()->set('services.parcel2go', [
|
||||||
|
'client_id' => 'id', 'client_secret' => 'secret',
|
||||||
|
'auth_url' => 'https://auth.example.test/token',
|
||||||
|
'base_url' => 'https://shipping.example.test/api',
|
||||||
|
'scope' => 'public-api my-profile',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
|
||||||
|
->assertOk()->assertSee('Parcel2Go rejected the selected API access')
|
||||||
|
->assertDontSee('Parcel2Go connected, but account history is unavailable')
|
||||||
|
->assertDontSee('Parcel2Go is unavailable');
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_shipping_label_is_downloaded_for_an_order_in_account_history(): void
|
public function test_shipping_label_is_downloaded_for_an_order_in_account_history(): void
|
||||||
{
|
{
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
|
|||||||
Reference in New Issue
Block a user