Skip to content

Commit ab29b7e

Browse files
simonhampclaude
andcommitted
Fix webhook active check and improve admin plugin edit screen
Change webhook endpoint to only reject inactive plugins (is_active=false) instead of unapproved ones, allowing all active plugins to receive webhook data regardless of approval status. Admin plugin edit improvements: - Link composer package name to Packagist for free plugins - Hide repository URL for paid third-party plugins - Link license to license page for paid plugins, GitHub for free - Add 'Go to User' action on submitter field in Submission Info Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9ae13eb commit ab29b7e

6 files changed

Lines changed: 215 additions & 11 deletions

File tree

app/Filament/Resources/PluginResource.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,17 @@ public static function form(Schema $schema): Schema
6161

6262
Forms\Components\Placeholder::make('name')
6363
->label('Composer Package Name')
64-
->content(fn (?Plugin $record) => $record?->name ?? '-'),
64+
->content(function (?Plugin $record) {
65+
if (! $record?->name) {
66+
return '-';
67+
}
68+
69+
if ($record->isFree()) {
70+
return new HtmlString('<a href="'.e($record->getPackagistUrl()).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->name).' ↗</a>');
71+
}
72+
73+
return $record->name;
74+
}),
6575

6676
Forms\Components\Select::make('type')
6777
->options(PluginType::class),
@@ -75,20 +85,24 @@ public static function form(Schema $schema): Schema
7585
->label('Repository URL')
7686
->content(fn (?Plugin $record) => $record?->repository_url
7787
? new HtmlString('<a href="'.e($record->repository_url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($record->repository_url).' ↗</a>')
78-
: '-'),
88+
: '-')
89+
->visible(fn (?Plugin $record) => ! ($record?->isPaid() && ! $record?->isOfficial())),
7990

8091
Forms\Components\Placeholder::make('license_type')
8192
->label('License')
8293
->content(function (?Plugin $record) {
8394
$license = $record?->getLicense();
84-
$licenseUrl = $record?->getLicenseUrl();
8595

8696
if (! $license) {
8797
return '-';
8898
}
8999

90-
if ($licenseUrl) {
91-
return new HtmlString('<a href="'.e($licenseUrl).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
100+
$url = $record->isPaid()
101+
? route('plugins.license', $record->routeParams())
102+
: $record->getLicenseUrl();
103+
104+
if ($url) {
105+
return new HtmlString('<a href="'.e($url).'" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">'.e($license).' ↗</a>');
92106
}
93107

94108
return $license;
@@ -212,7 +226,17 @@ public static function form(Schema $schema): Schema
212226
Forms\Components\Select::make('user_id')
213227
->relationship('user', 'email')
214228
->searchable()
215-
->preload(),
229+
->preload()
230+
->suffixAction(
231+
Action::make('viewUser')
232+
->label('Go to User')
233+
->icon('heroicon-o-arrow-top-right-on-square')
234+
->url(fn (?Plugin $record) => $record?->user_id
235+
? UserResource::getUrl('edit', ['record' => $record->user_id])
236+
: null)
237+
->openUrlInNewTab()
238+
->visible(fn (?Plugin $record) => $record?->user_id !== null),
239+
),
216240

217241
Forms\Components\Placeholder::make('created_at')
218242
->label('Submitted At')

app/Http/Controllers/PluginWebhookController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public function __invoke(Request $request, string $secret, PluginSyncService $sy
2424
return response()->json(['success' => true, 'message' => 'pong']);
2525
}
2626

27-
if (! $plugin->isApproved()) {
28-
return response()->json(['error' => 'Plugin is not approved'], 403);
27+
if (! $plugin->isActive()) {
28+
return response()->json(['error' => 'Plugin is not active'], 403);
2929
}
3030

3131
if ($event === 'release') {

app/Models/Plugin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ public function isRejected(): bool
268268
return $this->status === PluginStatus::Rejected;
269269
}
270270

271+
public function isActive(): bool
272+
{
273+
return $this->is_active ?? true;
274+
}
275+
271276
public function isFree(): bool
272277
{
273278
return $this->type === PluginType::Free;

database/factories/PluginFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ public function featured(): static
176176
]);
177177
}
178178

179+
public function inactive(): static
180+
{
181+
return $this->state(fn (array $attributes) => [
182+
'is_active' => false,
183+
]);
184+
}
185+
179186
public function free(): static
180187
{
181188
return $this->state(fn (array $attributes) => [
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Tests\Feature\Filament;
4+
5+
use App\Filament\Resources\PluginResource;
6+
use App\Filament\Resources\PluginResource\Pages\EditPlugin;
7+
use App\Filament\Resources\UserResource;
8+
use App\Models\Plugin;
9+
use App\Models\User;
10+
use Illuminate\Foundation\Testing\RefreshDatabase;
11+
use Livewire\Livewire;
12+
use Tests\TestCase;
13+
14+
class PluginResourceTest extends TestCase
15+
{
16+
use RefreshDatabase;
17+
18+
private User $admin;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->admin = User::factory()->create(['email' => 'admin@test.com']);
25+
config(['filament.users' => ['admin@test.com']]);
26+
}
27+
28+
public function test_free_plugin_shows_packagist_link_for_composer_name(): void
29+
{
30+
$plugin = Plugin::factory()->free()->approved()->create([
31+
'name' => 'acme/camera-123',
32+
]);
33+
34+
$response = $this->actingAs($this->admin)->get(
35+
PluginResource::getUrl('edit', ['record' => $plugin])
36+
);
37+
38+
$response->assertOk();
39+
$response->assertSee('https://packagist.org/packages/acme/camera-123');
40+
}
41+
42+
public function test_paid_plugin_does_not_show_packagist_link(): void
43+
{
44+
$plugin = Plugin::factory()->paid()->approved()->create([
45+
'name' => 'acme/paid-plugin-123',
46+
]);
47+
48+
$response = $this->actingAs($this->admin)->get(
49+
PluginResource::getUrl('edit', ['record' => $plugin])
50+
);
51+
52+
$response->assertOk();
53+
$response->assertDontSee('https://packagist.org/packages/acme/paid-plugin-123');
54+
}
55+
56+
public function test_paid_third_party_plugin_hides_repository_url_placeholder(): void
57+
{
58+
$plugin = Plugin::factory()->paid()->approved()->create([
59+
'name' => 'acme/paid-plugin-456',
60+
'repository_url' => 'https://github.com/acme/paid-plugin-456',
61+
'is_official' => false,
62+
]);
63+
64+
Livewire::actingAs($this->admin)
65+
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
66+
->assertDontSee('Repository URL');
67+
}
68+
69+
public function test_paid_official_plugin_shows_repository_url(): void
70+
{
71+
$plugin = Plugin::factory()->paid()->approved()->create([
72+
'name' => 'nativephp/paid-official-789',
73+
'repository_url' => 'https://github.com/nativephp/paid-official-789',
74+
'is_official' => true,
75+
]);
76+
77+
$response = $this->actingAs($this->admin)->get(
78+
PluginResource::getUrl('edit', ['record' => $plugin])
79+
);
80+
81+
$response->assertOk();
82+
$response->assertSee('https://github.com/nativephp/paid-official-789');
83+
}
84+
85+
public function test_free_plugin_shows_repository_url(): void
86+
{
87+
$plugin = Plugin::factory()->free()->approved()->create([
88+
'name' => 'acme/free-plugin-321',
89+
'repository_url' => 'https://github.com/acme/free-plugin-321',
90+
'is_official' => false,
91+
]);
92+
93+
$response = $this->actingAs($this->admin)->get(
94+
PluginResource::getUrl('edit', ['record' => $plugin])
95+
);
96+
97+
$response->assertOk();
98+
$response->assertSee('https://github.com/acme/free-plugin-321');
99+
}
100+
101+
public function test_paid_plugin_license_links_to_license_page(): void
102+
{
103+
$plugin = Plugin::factory()->paid()->approved()->create([
104+
'name' => 'acme/paid-license-111',
105+
'composer_data' => ['license' => 'Commercial'],
106+
]);
107+
108+
$response = $this->actingAs($this->admin)->get(
109+
PluginResource::getUrl('edit', ['record' => $plugin])
110+
);
111+
112+
$response->assertOk();
113+
$response->assertSee(route('plugins.license', ['vendor' => 'acme', 'package' => 'paid-license-111']));
114+
}
115+
116+
public function test_free_plugin_license_links_to_github(): void
117+
{
118+
$plugin = Plugin::factory()->free()->approved()->create([
119+
'name' => 'acme/free-license-222',
120+
'repository_url' => 'https://github.com/acme/free-license-222',
121+
'composer_data' => ['license' => 'MIT'],
122+
]);
123+
124+
$response = $this->actingAs($this->admin)->get(
125+
PluginResource::getUrl('edit', ['record' => $plugin])
126+
);
127+
128+
$response->assertOk();
129+
$response->assertSee('https://github.com/acme/free-license-222/blob/main/LICENSE');
130+
}
131+
132+
public function test_submission_info_shows_go_to_user_action(): void
133+
{
134+
$user = User::factory()->create();
135+
$plugin = Plugin::factory()->for($user)->approved()->create();
136+
137+
$response = $this->actingAs($this->admin)->get(
138+
PluginResource::getUrl('edit', ['record' => $plugin])
139+
);
140+
141+
$response->assertOk();
142+
$expectedUrl = UserResource::getUrl('edit', ['record' => $user->id]);
143+
$response->assertSee($expectedUrl);
144+
}
145+
}

tests/Feature/PluginWebhookTest.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests\Feature;
44

55
use App\Models\Plugin;
6+
use App\Services\PluginSyncService;
67
use Illuminate\Foundation\Testing\RefreshDatabase;
78
use PHPUnit\Framework\Attributes\Test;
89
use Tests\TestCase;
@@ -42,9 +43,9 @@ public function ping_event_succeeds_for_approved_plugin(): void
4243
}
4344

4445
#[Test]
45-
public function non_ping_event_returns_403_for_unapproved_plugin(): void
46+
public function non_ping_event_returns_403_for_inactive_plugin(): void
4647
{
47-
$plugin = Plugin::factory()->create();
48+
$plugin = Plugin::factory()->inactive()->create();
4849

4950
$response = $this->postJson(
5051
route('webhooks.plugins', $plugin->webhook_secret),
@@ -53,7 +54,29 @@ public function non_ping_event_returns_403_for_unapproved_plugin(): void
5354
);
5455

5556
$response->assertForbidden()
56-
->assertJson(['error' => 'Plugin is not approved']);
57+
->assertJson(['error' => 'Plugin is not active']);
58+
}
59+
60+
#[Test]
61+
public function non_ping_event_succeeds_for_unapproved_but_active_plugin(): void
62+
{
63+
$plugin = Plugin::factory()->create([
64+
'is_active' => true,
65+
'last_synced_at' => now(),
66+
]);
67+
68+
$this->mock(PluginSyncService::class, function ($mock) {
69+
$mock->shouldReceive('sync')->once()->andReturn(true);
70+
});
71+
72+
$response = $this->postJson(
73+
route('webhooks.plugins', $plugin->webhook_secret),
74+
[],
75+
['X-GitHub-Event' => 'push']
76+
);
77+
78+
$response->assertOk()
79+
->assertJson(['success' => true]);
5780
}
5881

5982
#[Test]

0 commit comments

Comments
 (0)