From 35503f8d3ebb2d6d43b401cebf918a402affb301 Mon Sep 17 00:00:00 2001 From: Houssem Eddine BENABDALLAH Date: Tue, 3 Nov 2026 18:35:18 +0100 Subject: [PATCH 1/3] fix: sync a user's SSH keys from GitHub when they open the terminal page, so the CLI finds keys added after signing in --- app/Http/Controllers/TerminalController.php | 26 +++++++++++++ app/Http/Requests/CreateKeyLookupRequest.php | 2 +- routes/web.php | 3 +- .../Cli/KeyLookupControllerTest.php | 38 ++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/TerminalController.php diff --git a/app/Http/Controllers/TerminalController.php b/app/Http/Controllers/TerminalController.php new file mode 100644 index 0000000000..82af6bd929 --- /dev/null +++ b/app/Http/Controllers/TerminalController.php @@ -0,0 +1,26 @@ +id}", 1, fn () => dispatch(new SyncGitHubSshKeysJob($user))); + + return Inertia::render('terminal/show', ['url' => url('/')]); + } +} diff --git a/app/Http/Requests/CreateKeyLookupRequest.php b/app/Http/Requests/CreateKeyLookupRequest.php index a3f037ae8b..0fca09ee97 100644 --- a/app/Http/Requests/CreateKeyLookupRequest.php +++ b/app/Http/Requests/CreateKeyLookupRequest.php @@ -18,7 +18,7 @@ public function authorize(): Response { return $this->publicKey() instanceof SshPublicKey ? Response::allow() - : Response::denyAsNotFound('None of your SSH keys are on the GitHub account of a /dev/null user. Add your key on GitHub (Settings → SSH and GPG keys), then sign in on '.url('/').'.'); + : Response::denyAsNotFound('None of your SSH keys are on the GitHub account of a /dev/null user. Add your key on GitHub (Settings → SSH and GPG keys), then open '.route('terminal.show').' while signed in to sync it.'); } /** diff --git a/routes/web.php b/routes/web.php index 42cd4ddaec..80949c5536 100644 --- a/routes/web.php +++ b/routes/web.php @@ -121,6 +121,7 @@ use App\Http\Controllers\StarredCommitController; use App\Http\Controllers\SuggestionDismissalController; use App\Http\Controllers\SystemHealthController; +use App\Http\Controllers\TerminalController; use App\Http\Controllers\TermsController; use App\Http\Controllers\TestEmailController; use App\Http\Controllers\TrafficController; @@ -650,7 +651,7 @@ Route::get('settings/appearance', fn () => Inertia::render('appearance/update'))->name('appearance.edit'); // Terminal... - Route::get('settings/terminal', fn () => Inertia::render('terminal/show', ['url' => url('/')]))->name('terminal.show'); + Route::get('settings/terminal', [TerminalController::class, 'show'])->name('terminal.show'); // Safety... Route::get('settings/safety', [SafetyController::class, 'edit'])->name('safety.edit'); diff --git a/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php b/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php index 2bb5d1d7ca..7f2d2359a6 100644 --- a/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php +++ b/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php @@ -2,8 +2,12 @@ declare(strict_types=1); +use App\Contracts\Services\GitHubProvider; +use App\Jobs\SyncGitHubSshKeysJob; use App\Models\SshKey; +use App\Models\User; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Queue; use Tests\Support\SshKeyPair; it('picks the key that is on the github account of a user', function (): void { @@ -33,7 +37,39 @@ $response->assertNotFound() ->assertHeader('Content-Type', 'text/plain; charset=utf-8') - ->assertSee('None of your SSH keys are on the GitHub account of a /dev/null user.'); + ->assertSee('None of your SSH keys are on the GitHub account of a /dev/null user.') + ->assertSee('then open '.route('terminal.show').' while signed in to sync it.'); +}); + +it('finds a key added on github after signing in once the user opens the terminal page', function (): void { + $user = User::factory()->create(['github_id' => '42']); + $key = SshKeyPair::generate()->publicKey; + $github = $this->mock(GitHubProvider::class); + $github->shouldReceive('login')->with('42')->andReturn('taylor'); + $github->shouldReceive('sshKeys')->with('taylor')->andReturn([$key->toOpenSsh()]); + + $this->call('POST', route('cli-key-lookup.store'), content: $key->toOpenSsh())->assertNotFound(); + + $this->actingAs($user)->get(route('terminal.show'))->assertOk(); + $this->artisan('queue:work', ['--once' => true])->assertSuccessful(); + + $this->call('POST', route('cli-key-lookup.store'), content: $key->toOpenSsh()) + ->assertOk() + ->assertContent($key->toOpenSsh()."\n"); +}); + +it('syncs the ssh keys of a user opening the terminal page at most once a minute', function (): void { + Queue::fake([SyncGitHubSshKeysJob::class]); + $user = User::factory()->create(); + + $this->actingAs($user)->get(route('terminal.show'))->assertOk(); + $this->actingAs($user)->get(route('terminal.show'))->assertOk(); + Queue::assertPushedTimes(SyncGitHubSshKeysJob::class, 1); + + $this->travel(61)->seconds(); + $this->actingAs($user)->get(route('terminal.show'))->assertOk(); + Queue::assertPushedTimes(SyncGitHubSshKeysJob::class, 2); + Queue::assertPushed(SyncGitHubSshKeysJob::class, fn (SyncGitHubSshKeysJob $job): bool => $job->user->is($user)); }); it('only considers the first 20 keys', function (): void { From d65b379313309d038f29485c039fda6f7a7bd273 Mon Sep 17 00:00:00 2001 From: Houssem Eddine BENABDALLAH Date: Tue, 3 Nov 2026 18:46:53 +0100 Subject: [PATCH 2/3] fix: never prefetch the terminal settings tab, so opening it always syncs the SSH keys --- resources/js/components/page-tabs.tsx | 6 +++- resources/js/layouts/settings/tabs.ts | 3 +- tests/Unit/PageTabs.test.tsx | 48 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/PageTabs.test.tsx diff --git a/resources/js/components/page-tabs.tsx b/resources/js/components/page-tabs.tsx index ae67a386c6..d1036469ba 100644 --- a/resources/js/components/page-tabs.tsx +++ b/resources/js/components/page-tabs.tsx @@ -25,6 +25,10 @@ export function PageTabs({ shortTitle?: string; href: NonNullable; isActive: boolean; + /** + * Overrides the tabs' prefetch for this one, for a page that does something each time it opens. + */ + prefetch?: boolean; }[]; }) { const indicatorName = `tab-indicator-${label.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-')}`; @@ -35,7 +39,7 @@ export function PageTabs({ +import assert from 'node:assert/strict'; +import * as React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { test, vi } from 'vite-plus/test'; + +vi.doMock('@inertiajs/react', () => ({ + Link: ({ + href, + prefetch, + children, + }: { + href: { url: string }; + prefetch: boolean; + children: React.ReactNode; + }) => ( + + {children} + + ), +})); + +const { PageTabs } = await import('../../resources/js/components/page-tabs'); +const { settingsTabs } = + await import('../../resources/js/layouts/settings/tabs'); + +test('the terminal settings tab is never prefetched, so opening it always syncs the SSH keys', () => { + const html = renderToStaticMarkup( + ({ ...tab, isActive: false }))} + />, + ); + + const prefetched = Object.fromEntries( + [...html.matchAll(/href="([^"]+)" data-prefetch="(\w+)"/gu)].map( + ([, url, prefetch]) => [url, prefetch], + ), + ); + + assert.deepEqual(prefetched, { + '/settings/profile': 'true', + '/settings/appearance': 'true', + '/settings/terminal': 'false', + '/settings/safety': 'true', + '/settings/devices': 'true', + }); +}); From cb34873a77a49772f368bbdf162a92f60f1b4bd3 Mon Sep 17 00:00:00 2001 From: Houssem Eddine BENABDALLAH Date: Tue, 3 Nov 2026 20:03:14 +0100 Subject: [PATCH 3/3] fix: point the CLI to Sync from GitHub on the profile settings for newly added SSH keys, instead of syncing on the terminal page --- app/Http/Controllers/TerminalController.php | 26 ---------- app/Http/Requests/CreateKeyLookupRequest.php | 2 +- resources/js/components/page-tabs.tsx | 6 +-- resources/js/layouts/settings/tabs.ts | 3 +- routes/web.php | 3 +- .../Cli/KeyLookupControllerTest.php | 25 ++-------- tests/Unit/PageTabs.test.tsx | 48 ------------------- 7 files changed, 9 insertions(+), 104 deletions(-) delete mode 100644 app/Http/Controllers/TerminalController.php delete mode 100644 tests/Unit/PageTabs.test.tsx diff --git a/app/Http/Controllers/TerminalController.php b/app/Http/Controllers/TerminalController.php deleted file mode 100644 index 82af6bd929..0000000000 --- a/app/Http/Controllers/TerminalController.php +++ /dev/null @@ -1,26 +0,0 @@ -id}", 1, fn () => dispatch(new SyncGitHubSshKeysJob($user))); - - return Inertia::render('terminal/show', ['url' => url('/')]); - } -} diff --git a/app/Http/Requests/CreateKeyLookupRequest.php b/app/Http/Requests/CreateKeyLookupRequest.php index 0fca09ee97..58d4f32d95 100644 --- a/app/Http/Requests/CreateKeyLookupRequest.php +++ b/app/Http/Requests/CreateKeyLookupRequest.php @@ -18,7 +18,7 @@ public function authorize(): Response { return $this->publicKey() instanceof SshPublicKey ? Response::allow() - : Response::denyAsNotFound('None of your SSH keys are on the GitHub account of a /dev/null user. Add your key on GitHub (Settings → SSH and GPG keys), then open '.route('terminal.show').' while signed in to sync it.'); + : Response::denyAsNotFound('None of your SSH keys are on the GitHub account of a /dev/null user. Add your key on GitHub (Settings → SSH and GPG keys), then click Sync from GitHub on '.route('user-profile.edit').'.'); } /** diff --git a/resources/js/components/page-tabs.tsx b/resources/js/components/page-tabs.tsx index d1036469ba..ae67a386c6 100644 --- a/resources/js/components/page-tabs.tsx +++ b/resources/js/components/page-tabs.tsx @@ -25,10 +25,6 @@ export function PageTabs({ shortTitle?: string; href: NonNullable; isActive: boolean; - /** - * Overrides the tabs' prefetch for this one, for a page that does something each time it opens. - */ - prefetch?: boolean; }[]; }) { const indicatorName = `tab-indicator-${label.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-')}`; @@ -39,7 +35,7 @@ export function PageTabs({ Inertia::render('appearance/update'))->name('appearance.edit'); // Terminal... - Route::get('settings/terminal', [TerminalController::class, 'show'])->name('terminal.show'); + Route::get('settings/terminal', fn () => Inertia::render('terminal/show', ['url' => url('/')]))->name('terminal.show'); // Safety... Route::get('settings/safety', [SafetyController::class, 'edit'])->name('safety.edit'); diff --git a/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php b/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php index 7f2d2359a6..93c03a312e 100644 --- a/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php +++ b/tests/Feature/Controllers/Cli/KeyLookupControllerTest.php @@ -3,11 +3,9 @@ declare(strict_types=1); use App\Contracts\Services\GitHubProvider; -use App\Jobs\SyncGitHubSshKeysJob; use App\Models\SshKey; use App\Models\User; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Queue; use Tests\Support\SshKeyPair; it('picks the key that is on the github account of a user', function (): void { @@ -38,40 +36,27 @@ $response->assertNotFound() ->assertHeader('Content-Type', 'text/plain; charset=utf-8') ->assertSee('None of your SSH keys are on the GitHub account of a /dev/null user.') - ->assertSee('then open '.route('terminal.show').' while signed in to sync it.'); + ->assertSee('then click Sync from GitHub on '.route('user-profile.edit').'.'); }); -it('finds a key added on github after signing in once the user opens the terminal page', function (): void { +it('finds a key added on github after signing in once the user syncs from github', function (): void { $user = User::factory()->create(['github_id' => '42']); $key = SshKeyPair::generate()->publicKey; $github = $this->mock(GitHubProvider::class); + $github->shouldReceive('account')->with('42')->andReturn(['login' => 'taylor']); $github->shouldReceive('login')->with('42')->andReturn('taylor'); $github->shouldReceive('sshKeys')->with('taylor')->andReturn([$key->toOpenSsh()]); $this->call('POST', route('cli-key-lookup.store'), content: $key->toOpenSsh())->assertNotFound(); - $this->actingAs($user)->get(route('terminal.show'))->assertOk(); - $this->artisan('queue:work', ['--once' => true])->assertSuccessful(); + $this->actingAs($user)->put(route('user-profile.update')) + ->assertInertiaFlash('toast', ['type' => 'success', 'message' => 'Synced from GitHub.']); $this->call('POST', route('cli-key-lookup.store'), content: $key->toOpenSsh()) ->assertOk() ->assertContent($key->toOpenSsh()."\n"); }); -it('syncs the ssh keys of a user opening the terminal page at most once a minute', function (): void { - Queue::fake([SyncGitHubSshKeysJob::class]); - $user = User::factory()->create(); - - $this->actingAs($user)->get(route('terminal.show'))->assertOk(); - $this->actingAs($user)->get(route('terminal.show'))->assertOk(); - Queue::assertPushedTimes(SyncGitHubSshKeysJob::class, 1); - - $this->travel(61)->seconds(); - $this->actingAs($user)->get(route('terminal.show'))->assertOk(); - Queue::assertPushedTimes(SyncGitHubSshKeysJob::class, 2); - Queue::assertPushed(SyncGitHubSshKeysJob::class, fn (SyncGitHubSshKeysJob $job): bool => $job->user->is($user)); -}); - it('only considers the first 20 keys', function (): void { $known = SshKeyPair::generate()->publicKey; SshKey::factory()->withPublicKey($known)->create(); diff --git a/tests/Unit/PageTabs.test.tsx b/tests/Unit/PageTabs.test.tsx deleted file mode 100644 index 627c93478b..0000000000 --- a/tests/Unit/PageTabs.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/// -import assert from 'node:assert/strict'; -import * as React from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { test, vi } from 'vite-plus/test'; - -vi.doMock('@inertiajs/react', () => ({ - Link: ({ - href, - prefetch, - children, - }: { - href: { url: string }; - prefetch: boolean; - children: React.ReactNode; - }) => ( - - {children} - - ), -})); - -const { PageTabs } = await import('../../resources/js/components/page-tabs'); -const { settingsTabs } = - await import('../../resources/js/layouts/settings/tabs'); - -test('the terminal settings tab is never prefetched, so opening it always syncs the SSH keys', () => { - const html = renderToStaticMarkup( - ({ ...tab, isActive: false }))} - />, - ); - - const prefetched = Object.fromEntries( - [...html.matchAll(/href="([^"]+)" data-prefetch="(\w+)"/gu)].map( - ([, url, prefetch]) => [url, prefetch], - ), - ); - - assert.deepEqual(prefetched, { - '/settings/profile': 'true', - '/settings/appearance': 'true', - '/settings/terminal': 'false', - '/settings/safety': 'true', - '/settings/devices': 'true', - }); -});