From fc283aaf5510508f01c7803c0fad5a98c16c171e Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 31 Mar 2026 20:23:40 +0100 Subject: [PATCH] Fix Ultra subscribers getting 402 when installing official plugins Plugin access checks only looked at team membership (isUltraTeamMember), which requires creating a team first. Ultra subscribers who hadn't created a team yet were denied access to official plugins. Now also checks hasUltraAccess() (the subscription itself) in both hasPluginAccess() and the Satis API listing endpoint. Co-Authored-By: Claude Opus 4.6 --- .../Api/PluginAccessController.php | 4 +- app/Models/User.php | 4 +- package-lock.json | 2 +- tests/Feature/UltraPluginAccessTest.php | 93 +++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/PluginAccessController.php b/app/Http/Controllers/Api/PluginAccessController.php index e56d63ad..40ba873f 100644 --- a/app/Http/Controllers/Api/PluginAccessController.php +++ b/app/Http/Controllers/Api/PluginAccessController.php @@ -152,8 +152,8 @@ protected function getAccessiblePlugins(User $user): array } } - // Team members get access to official plugins and owner's purchased plugins - if ($user->isUltraTeamMember()) { + // Ultra subscribers and team members get access to official plugins + if ($user->hasUltraAccess() || $user->isUltraTeamMember()) { $officialPlugins = Plugin::query() ->where('type', PluginType::Paid) ->where('is_official', true) diff --git a/app/Models/User.php b/app/Models/User.php index 24942277..298de6bf 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -445,8 +445,8 @@ public function hasPluginAccess(Plugin $plugin): bool return true; } - // Ultra team members get access to all official (first-party) plugins - if ($plugin->isOfficial() && $this->isUltraTeamMember()) { + // Ultra subscribers and team members get access to all official (first-party) plugins + if ($plugin->isOfficial() && ($this->hasUltraAccess() || $this->isUltraTeamMember())) { return true; } diff --git a/package-lock.json b/package-lock.json index e60aeb67..aafe2d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "turbo-lobster", + "name": "arctic-condor", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/tests/Feature/UltraPluginAccessTest.php b/tests/Feature/UltraPluginAccessTest.php index 4e4509d2..cd1304d1 100644 --- a/tests/Feature/UltraPluginAccessTest.php +++ b/tests/Feature/UltraPluginAccessTest.php @@ -666,6 +666,99 @@ public function test_team_member_with_own_subscription_sees_regular_price_for_th $this->assertEquals(4900, $bestPrice->amount); } + // ---- Ultra subscribers without a team ---- + + public function test_ultra_subscriber_without_team_has_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + // User has Ultra subscription but no team created + $this->assertNull($user->ownedTeam); + $this->assertTrue($user->hasPluginAccess($plugin)); + } + + public function test_ultra_subscriber_without_team_does_not_have_access_to_third_party_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createThirdPartyPlugin(); + + $this->assertFalse($user->hasPluginAccess($plugin)); + } + + public function test_comped_ultra_subscriber_without_team_has_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $this->assertNull($user->ownedTeam); + $this->assertTrue($user->hasPluginAccess($plugin)); + } + + public function test_legacy_comped_max_without_team_does_not_have_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $this->assertFalse($user->hasPluginAccess($plugin)); + } + + public function test_satis_api_includes_official_plugins_for_ultra_subscriber_without_team(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'ultra-no-team-key', + ]); + $this->createPaidMaxSubscription($user); + + $plugin = Plugin::factory()->create([ + 'name' => 'nativephp/secure-storage', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"), + ])->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $pluginNames = array_column($response->json('plugins'), 'name'); + $this->assertContains('nativephp/secure-storage', $pluginNames); + } + + public function test_satis_check_access_returns_true_for_ultra_subscriber_without_team(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'ultra-no-team-key', + ]); + $this->createPaidMaxSubscription($user); + + Plugin::factory()->create([ + 'name' => 'nativephp/secure-storage', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"), + ])->getJson('/api/plugins/access/nativephp/secure-storage'); + + $response->assertStatus(200) + ->assertJson([ + 'has_access' => true, + ]); + } + // ---- Comped Ultra subscriptions ---- public function test_comped_ultra_user_has_active_ultra_subscription(): void