diff --git a/app-modules/identity/src/Auth/Http/Controllers/OAuthController.php b/app-modules/identity/src/Auth/Http/Controllers/OAuthController.php index a3f694c4..a55cd9f8 100644 --- a/app-modules/identity/src/Auth/Http/Controllers/OAuthController.php +++ b/app-modules/identity/src/Auth/Http/Controllers/OAuthController.php @@ -76,11 +76,19 @@ public function getAuthenticate(string $provider, HandleOAuthCallbackAction $act return redirect()->to($result->redirectUrl); } + $redirectUrl = $result->redirectUrl; + if ($result->intent === OAuthIntent::Login) { Auth::login($result->user, remember: true); filament()->setCurrentPanel(filament()->getPanel($state->panel)); + + if ($state->panel === 'app') { + $redirectUrl = url()->query($redirectUrl, [ + 'oauth_provider' => $identityProvider->value, + ]); + } } - return redirect()->to($result->redirectUrl); + return redirect()->to($redirectUrl); } } diff --git a/app-modules/identity/tests/Feature/Auth/OAuthControllerTest.php b/app-modules/identity/tests/Feature/Auth/OAuthControllerTest.php new file mode 100644 index 00000000..5ba8e129 --- /dev/null +++ b/app-modules/identity/tests/Feature/Auth/OAuthControllerTest.php @@ -0,0 +1,121 @@ +instance(GitHubOAuthClient::class, new readonly class($access, $user) implements OAuthClientContract + { + public function __construct( + private OAuthAccessDTO $access, + private OAuthUserDTO $user, + ) {} + + public function redirectUrl(?OAuthStateDTO $state = null): string + { + return 'https://github.test/oauth'; + } + + public function auth(string $code): OAuthAccessDTO + { + return $this->access; + } + + public function getAuthenticatedUser(OAuthAccessDTO $credentials): OAuthUserDTO + { + return $this->user; + } + }); +} + +function callGithubCallback(OAuthStateDTO $state): string +{ + request()->merge([ + 'state' => (string) $state, + 'code' => 'auth-code', + ]); + + return resolve(OAuthController::class) + ->getAuthenticate('github', resolve(HandleOAuthCallbackAction::class)) + ->getTargetUrl(); +} + +test('successful app oauth login marks the provider in the redirect URL', function (): void { + Filament::setCurrentPanel(Filament::getPanel('app')); + bindControllerGithubClient(); + + $targetUrl = callGithubCallback(new OAuthStateDTO( + intent: OAuthIntent::Login, + provider: IdentityProvider::GitHub, + panel: 'app', + returnUrl: '/app?source=oauth', + )); + + expect($targetUrl) + ->toContain('source=oauth') + ->toContain('oauth_provider=github') + ->and(Auth::check())->toBeTrue(); +}); + +test('denied app oauth login does not mark a provider in the redirect URL', function (): void { + $state = new OAuthStateDTO( + intent: OAuthIntent::Login, + provider: IdentityProvider::GitHub, + panel: 'app', + returnUrl: '/app/login', + ); + + request()->merge([ + 'state' => (string) $state, + 'error' => 'access_denied', + ]); + + $targetUrl = resolve(OAuthController::class) + ->getAuthenticate('github', resolve(HandleOAuthCallbackAction::class)) + ->getTargetUrl(); + + expect($targetUrl) + ->toContain('/app/login') + ->not->toContain('oauth_provider='); +}); diff --git a/app-modules/panel-app/resources/js/last-auth-provider.js b/app-modules/panel-app/resources/js/last-auth-provider.js new file mode 100644 index 00000000..8780852c --- /dev/null +++ b/app-modules/panel-app/resources/js/last-auth-provider.js @@ -0,0 +1,18 @@ +(() => { + const url = new URL(window.location.href); + const provider = url.searchParams.get('oauth_provider'); + const supportedProviders = ['discord', 'github', 'twitch']; + + if (!supportedProviders.includes(provider)) { + return; + } + + try { + window.localStorage.setItem('lastAuthProvider', provider); + } catch (error) { + // Ignore browsers that block localStorage access. + } + + url.searchParams.delete('oauth_provider'); + window.history.replaceState({}, document.title, url); +})(); diff --git a/app-modules/panel-app/resources/views/auth/login.blade.php b/app-modules/panel-app/resources/views/auth/login.blade.php index 7db1b862..8ecddb08 100644 --- a/app-modules/panel-app/resources/views/auth/login.blade.php +++ b/app-modules/panel-app/resources/views/auth/login.blade.php @@ -80,29 +80,63 @@ class="h-12 w-auto text-purple-500"

Acesse sua conta He4rt Developers

{{-- OAuth --}} -
+
@svg ('fab-discord', 'h-5 w-5') Continuar com Discord + + Último acesso + @svg ('fab-github', 'h-5 w-5') Continuar com GitHub + + Último acesso + @svg ('fab-twitch', 'h-5 w-5') Continuar com Twitch + + Último acesso +
diff --git a/app-modules/panel-app/resources/views/dashboard.blade.php b/app-modules/panel-app/resources/views/dashboard.blade.php index 7076f2e5..0875024b 100644 --- a/app-modules/panel-app/resources/views/dashboard.blade.php +++ b/app-modules/panel-app/resources/views/dashboard.blade.php @@ -1,4 +1,13 @@ + {{-- OAuth callbacks land here; persist the provider for the next login page render. --}} + @if (in_array(request()->query('oauth_provider'), ['discord', 'github', 'twitch'], strict: true)) + + @endif +
diff --git a/app-modules/panel-app/src/PanelAppServiceProvider.php b/app-modules/panel-app/src/PanelAppServiceProvider.php index c64ef513..a7e0fc93 100644 --- a/app-modules/panel-app/src/PanelAppServiceProvider.php +++ b/app-modules/panel-app/src/PanelAppServiceProvider.php @@ -4,6 +4,8 @@ namespace He4rt\PanelApp; +use Filament\Support\Assets\Js; +use Filament\Support\Facades\FilamentAsset; use He4rt\PanelApp\Livewire\Timeline\Composer; use He4rt\PanelApp\Livewire\Timeline\Feed; use He4rt\PanelApp\Livewire\Timeline\PostShow; @@ -21,6 +23,11 @@ public function boot(): void $this->loadViewsFrom(__DIR__.'/../resources/views', 'panel-app'); $this->loadTranslationsFrom(__DIR__.'/../lang', 'panel-app'); + FilamentAsset::register([ + Js::make('last-auth-provider', __DIR__.'/../resources/js/last-auth-provider.js') + ->loadedOnRequest(), + ], package: 'he4rt/panel-app'); + Livewire::component('timeline-composer', Composer::class); Livewire::component('timeline-feed', Feed::class); Livewire::component('timeline-post-show', PostShow::class); diff --git a/app-modules/panel-app/tests/Feature/Auth/LastAuthProviderTest.php b/app-modules/panel-app/tests/Feature/Auth/LastAuthProviderTest.php new file mode 100644 index 00000000..d5de5173 --- /dev/null +++ b/app-modules/panel-app/tests/Feature/Auth/LastAuthProviderTest.php @@ -0,0 +1,39 @@ +get(Filament::getPanel('app')->getLoginUrl()) + ->assertSuccessful() + ->assertSee("window.localStorage.getItem('lastAuthProvider')", escape: false) + ->assertDontSee('last-auth-provider', escape: false); +}); + +test('oauth callback landing loads the writer asset for a supported provider', function (): void { + $this->actingAs(User::factory()->create()); + + $this->get(url()->query(TimelinePage::getUrl(), [ + 'oauth_provider' => 'github', + ])) + ->assertSuccessful() + ->assertSee('last-auth-provider', escape: false); +}); + +test('dashboard does not load the writer asset without a supported oauth marker', function (array $query): void { + $this->actingAs(User::factory()->create()); + + $this->get(url()->query(TimelinePage::getUrl(), $query)) + ->assertSuccessful() + ->assertDontSee('last-auth-provider', escape: false); +})->with([ + 'without marker' => [[]], + 'unsupported provider' => [['oauth_provider' => 'devto']], +]);