Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions app/Http/Controllers/GitHubIntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ protected function redirectAfterLogin(User $user, GitHubAuthType $authType, stri
->with('success', $message);
}

/**
* GitHub redirects here after the app is installed and sends the installation webhook at the same
* time, so the webhook can record the installation while this request is still checking it.
*/
public function handleSetup(Request $request): RedirectResponse
{
$installationId = (int) $request->query('installation_id');
Expand All @@ -213,21 +217,21 @@ public function handleSetup(Request $request): RedirectResponse
$appService = app(GitHubAppService::class);
$installation = GitHubInstallation::where('installation_id', $installationId)->first();

if ($installation && $installation->user_id !== $user->id) {
return to_route('customer.integrations')
->with('error', 'That GitHub App installation is already linked to another NativePHP account.');
}

if (! $installation) {
if (! $user->isUsingGitHubApp() || ! $appService->userCanAccessInstallation($user, $installationId)) {
return to_route('customer.integrations')
->with('error', "We couldn't confirm that GitHub App installation belongs to your GitHub account. Please connect GitHub and try again.");
}

$installation = $user->githubInstallations()->create([
'installation_id' => $installationId,
'account_login' => $user->github_username ?? 'unknown',
]);
$installation = GitHubInstallation::createOrFirst(
['installation_id' => $installationId],
['user_id' => $user->id, 'account_login' => $user->github_username ?? 'unknown'],
);
}

if ($installation->user_id !== $user->id) {
return to_route('customer.integrations')
->with('error', 'That GitHub App installation is already linked to another NativePHP account.');
}

if (! $appService->syncInstallation($installation) && ! $installation->exists) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

@if($needsMigration || $missingAccess->isNotEmpty())
<div @class([
'mb-6 rounded-lg border p-6',
'mb-6 rounded-xl border p-6',
'border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20' => $urgent,
'border-blue-200 bg-blue-50 dark:border-blue-900/50 dark:bg-blue-900/20' => ! $urgent,
])>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@if($hasLicense)
<div @class(['max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' => !$inline])>
<div class="bg-gradient-to-r from-purple-50 to-indigo-100 dark:from-purple-900/20 dark:to-indigo-900/20 border border-purple-300 dark:border-purple-600 rounded-lg p-6 h-full">
<div class="bg-gradient-to-r from-purple-50 to-indigo-100 dark:from-purple-900/20 dark:to-indigo-900/20 border border-purple-300 dark:border-purple-600 rounded-xl p-6 h-full">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div class="flex items-start">
<div class="flex-shrink-0">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/livewire/discord-access-banner.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div>
<div @class(['max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' => !$inline])>
<div class="bg-gradient-to-r from-indigo-50 to-purple-100 dark:from-indigo-900 dark:to-purple-900 border border-indigo-300 dark:border-indigo-600 rounded-lg p-6 h-full">
<div class="bg-gradient-to-r from-indigo-50 to-purple-100 dark:from-indigo-900 dark:to-purple-900 border border-indigo-300 dark:border-indigo-600 rounded-xl p-6 h-full">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div class="flex items-start">
<div class="flex-shrink-0">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/livewire/git-hub-access-banner.blade.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div>
@if(auth()->user()->hasMobileRepoAccess())
<div @class(['max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' => !$inline])>
<div class="bg-gradient-to-r from-gray-50 to-slate-100 dark:from-gray-800 dark:to-slate-900 border border-gray-300 dark:border-gray-600 rounded-lg p-6 h-full">
<div class="bg-gradient-to-r from-gray-50 to-slate-100 dark:from-gray-800 dark:to-slate-900 border border-gray-300 dark:border-gray-600 rounded-xl p-6 h-full">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div class="flex items-start">
<div class="flex-shrink-0">
Expand Down
23 changes: 8 additions & 15 deletions resources/views/livewire/git-hub-app-status.blade.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
<div>
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div class="flex items-center justify-between">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">GitHub App Installations</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Manage which accounts and repositories the NativePHP app can access.
</p>
<flux:heading>GitHub App Installations</flux:heading>
<flux:text class="mt-1">Manage which accounts and repositories the NativePHP app can access.</flux:text>
</div>
@if($installUrl)
<a href="{{ $installUrl }}" target="_blank" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
Add Account
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="ml-1.5 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</a>
<flux:button href="{{ $installUrl }}" target="_blank" icon:trailing="plus">Add Account</flux:button>
@endif
</div>

@if($installations->isEmpty())
<div class="mt-4 rounded-md bg-gray-50 p-4 dark:bg-gray-700/50">
<div class="mt-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
<p class="text-sm text-gray-600 dark:text-gray-400">
No GitHub App installations found. Install the app on your GitHub account to grant repository access.
</p>
Expand All @@ -34,7 +27,7 @@
@else
<div class="mt-4 space-y-3">
@foreach($installations as $installation)
<div class="flex items-center justify-between rounded-md border border-gray-200 p-3 dark:border-gray-600">
<div class="flex items-center justify-between rounded-lg border border-gray-200 p-3 dark:border-gray-600">
<div class="flex items-center gap-3">
<div class="flex size-8 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700">
@if($installation->account_type === 'Organization')
Expand Down Expand Up @@ -91,5 +84,5 @@
</div>
</div>
@endif
</div>
</flux:card>
</div>
59 changes: 59 additions & 0 deletions tests/Feature/GitHubAppSetupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ private function fakeGitHub(array $userInstallationIds, string $selection = 'sel
]);
}

/**
* Fake GitHub so the installation webhook records installation 555 for the given user while
* the setup request is still asking GitHub whether the installation is theirs.
*/
private function fakeGitHubWhileTheWebhookRecordsTheInstallationFor(User $webhookUser): void
{
Http::fake([
'api.github.com/user/installations*' => function () use ($webhookUser) {
GitHubInstallation::factory()->create([
'user_id' => $webhookUser->id,
'installation_id' => 555,
]);

return Http::response(['installations' => [['id' => 555]]]);
},
'api.github.com/app/installations/555' => Http::response([
'id' => 555,
'account' => ['login' => 'acme', 'type' => 'Organization', 'id' => 99],
'repository_selection' => 'all',
'suspended_at' => null,
]),
]);
}

public function test_setup_records_an_installation_the_user_can_see_on_github(): void
{
$this->fakeGitHub([555], 'selected', ['acme/one', 'acme/two']);
Expand Down Expand Up @@ -153,6 +177,41 @@ public function test_setup_refreshes_an_installation_the_webhook_already_recorde
$this->assertSame(['acme/new-repo'], $installation->fresh()->repository_selection);
}

public function test_setup_uses_the_installation_the_webhook_records_while_it_checks_github(): void
{
$user = User::factory()->withGitHubApp()->create();
$this->fakeGitHubWhileTheWebhookRecordsTheInstallationFor($user);

$this->actingAs($user)
->get(route('github.setup', ['installation_id' => 555]))
->assertRedirect(route('customer.integrations'))
->assertSessionHas('success');

$installation = $user->githubInstallations()->sole();

$this->assertSame('acme', $installation->account_login);
$this->assertSame('Organization', $installation->account_type);
}

public function test_setup_will_not_hand_over_an_installation_the_webhook_links_to_someone_else_while_it_checks_github(): void
{
$owner = User::factory()->withGitHubApp()->create();
$this->fakeGitHubWhileTheWebhookRecordsTheInstallationFor($owner);

$otherUser = User::factory()->withGitHubApp()->create();

$this->actingAs($otherUser)
->get(route('github.setup', ['installation_id' => 555]))
->assertRedirect(route('customer.integrations'))
->assertSessionHas('error', 'That GitHub App installation is already linked to another NativePHP account.');

$this->assertDatabaseCount('github_installations', 1);
$this->assertDatabaseHas('github_installations', [
'installation_id' => 555,
'user_id' => $owner->id,
]);
}

public function test_sync_command_updates_installations_and_removes_ones_github_no_longer_has(): void
{
$this->fakeGitHub([], 'selected', ['acme/one']);
Expand Down
Loading