Skip to content

Commit 9ddf998

Browse files
simonhampclaude
andauthored
Fix Ultra subscribers getting 402 when installing official plugins (#326)
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 <noreply@anthropic.com>
1 parent 1c8f192 commit 9ddf998

4 files changed

Lines changed: 98 additions & 5 deletions

File tree

app/Http/Controllers/Api/PluginAccessController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ protected function getAccessiblePlugins(User $user): array
152152
}
153153
}
154154

155-
// Team members get access to official plugins and owner's purchased plugins
156-
if ($user->isUltraTeamMember()) {
155+
// Ultra subscribers and team members get access to official plugins
156+
if ($user->hasUltraAccess() || $user->isUltraTeamMember()) {
157157
$officialPlugins = Plugin::query()
158158
->where('type', PluginType::Paid)
159159
->where('is_official', true)

app/Models/User.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,8 @@ public function hasPluginAccess(Plugin $plugin): bool
445445
return true;
446446
}
447447

448-
// Ultra team members get access to all official (first-party) plugins
449-
if ($plugin->isOfficial() && $this->isUltraTeamMember()) {
448+
// Ultra subscribers and team members get access to all official (first-party) plugins
449+
if ($plugin->isOfficial() && ($this->hasUltraAccess() || $this->isUltraTeamMember())) {
450450
return true;
451451
}
452452

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/Feature/UltraPluginAccessTest.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,99 @@ public function test_team_member_with_own_subscription_sees_regular_price_for_th
666666
$this->assertEquals(4900, $bestPrice->amount);
667667
}
668668

669+
// ---- Ultra subscribers without a team ----
670+
671+
public function test_ultra_subscriber_without_team_has_access_to_official_plugin(): void
672+
{
673+
$user = User::factory()->create();
674+
$this->createPaidMaxSubscription($user);
675+
$plugin = $this->createOfficialPlugin();
676+
677+
// User has Ultra subscription but no team created
678+
$this->assertNull($user->ownedTeam);
679+
$this->assertTrue($user->hasPluginAccess($plugin));
680+
}
681+
682+
public function test_ultra_subscriber_without_team_does_not_have_access_to_third_party_plugin(): void
683+
{
684+
$user = User::factory()->create();
685+
$this->createPaidMaxSubscription($user);
686+
$plugin = $this->createThirdPartyPlugin();
687+
688+
$this->assertFalse($user->hasPluginAccess($plugin));
689+
}
690+
691+
public function test_comped_ultra_subscriber_without_team_has_access_to_official_plugin(): void
692+
{
693+
$user = User::factory()->create();
694+
$this->createCompedUltraSubscription($user);
695+
$plugin = $this->createOfficialPlugin();
696+
697+
$this->assertNull($user->ownedTeam);
698+
$this->assertTrue($user->hasPluginAccess($plugin));
699+
}
700+
701+
public function test_legacy_comped_max_without_team_does_not_have_access_to_official_plugin(): void
702+
{
703+
$user = User::factory()->create();
704+
$this->createCompedMaxSubscription($user);
705+
$plugin = $this->createOfficialPlugin();
706+
707+
$this->assertFalse($user->hasPluginAccess($plugin));
708+
}
709+
710+
public function test_satis_api_includes_official_plugins_for_ultra_subscriber_without_team(): void
711+
{
712+
$user = User::factory()->create([
713+
'plugin_license_key' => 'ultra-no-team-key',
714+
]);
715+
$this->createPaidMaxSubscription($user);
716+
717+
$plugin = Plugin::factory()->create([
718+
'name' => 'nativephp/secure-storage',
719+
'type' => PluginType::Paid,
720+
'status' => PluginStatus::Approved,
721+
'is_active' => true,
722+
'is_official' => true,
723+
]);
724+
725+
$response = $this->withHeaders([
726+
'X-API-Key' => config('services.bifrost.api_key'),
727+
'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"),
728+
])->getJson('/api/plugins/access');
729+
730+
$response->assertStatus(200);
731+
732+
$pluginNames = array_column($response->json('plugins'), 'name');
733+
$this->assertContains('nativephp/secure-storage', $pluginNames);
734+
}
735+
736+
public function test_satis_check_access_returns_true_for_ultra_subscriber_without_team(): void
737+
{
738+
$user = User::factory()->create([
739+
'plugin_license_key' => 'ultra-no-team-key',
740+
]);
741+
$this->createPaidMaxSubscription($user);
742+
743+
Plugin::factory()->create([
744+
'name' => 'nativephp/secure-storage',
745+
'type' => PluginType::Paid,
746+
'status' => PluginStatus::Approved,
747+
'is_active' => true,
748+
'is_official' => true,
749+
]);
750+
751+
$response = $this->withHeaders([
752+
'X-API-Key' => config('services.bifrost.api_key'),
753+
'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"),
754+
])->getJson('/api/plugins/access/nativephp/secure-storage');
755+
756+
$response->assertStatus(200)
757+
->assertJson([
758+
'has_access' => true,
759+
]);
760+
}
761+
669762
// ---- Comped Ultra subscriptions ----
670763

671764
public function test_comped_ultra_user_has_active_ultra_subscription(): void

0 commit comments

Comments
 (0)