From d89f6ceedef001fb7ce653dc782976ee15847f14 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 4 May 2026 19:22:09 -0300 Subject: [PATCH 1/4] fix: let authenticated users connect Google/GitHub from Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Connect button on /settings/authentication pointed at the auth.{provider}.redirect routes that live behind `guest` middleware, so authenticated users were bounced to /app/home before reaching Socialite. The OAuth callback also needed to handle two flows (signup/login vs link to current user) but had no branch for the second case — meaning a different-email GitHub account would have been registered as a new user, logging the original session out. Splits the flows by intent: - New `app.authentication.connect-provider` route in the auth group, handled by the settings controller (where it sits next to disconnect-provider). Replaces the OAuth signup link as the Connect button's target. - Auth callbacks moved out of the guest group (still one URL per provider, since OAuth apps only register one) and gain a single Auth::check() branch that calls connectToCurrentUser(). - connectToCurrentUser() rejects if the provider id already belongs to a different user; otherwise sets it on the current user and redirects back to settings with a flash message. --- .../App/Settings/AuthenticationController.php | 11 ++ .../Controllers/Auth/GitHubController.php | 23 +++ .../Controllers/Auth/GoogleController.php | 23 +++ lang/en/settings.php | 2 + lang/es/settings.php | 2 + lang/pt-BR/settings.php | 2 + .../pages/settings/profile/Authentication.vue | 10 +- routes/app.php | 2 + routes/auth.php | 10 +- tests/Feature/Auth/ConnectProviderTest.php | 175 ++++++++++++++++++ 10 files changed, 249 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/Auth/ConnectProviderTest.php diff --git a/app/Http/Controllers/App/Settings/AuthenticationController.php b/app/Http/Controllers/App/Settings/AuthenticationController.php index e093d9a4..cb7d29be 100644 --- a/app/Http/Controllers/App/Settings/AuthenticationController.php +++ b/app/Http/Controllers/App/Settings/AuthenticationController.php @@ -14,6 +14,7 @@ use Illuminate\Validation\Rule; use Inertia\Inertia; use Inertia\Response; +use Laravel\Socialite\Facades\Socialite; class AuthenticationController extends Controller { @@ -63,6 +64,16 @@ public function destroyOtherSessions(Request $request): RedirectResponse return back()->with('flash.success', __('settings.authentication.sessions.flash_logged_out')); } + public function connectProvider(string $provider): RedirectResponse + { + abort_unless(in_array($provider, self::PROVIDERS, true), 404); + + return match ($provider) { + 'google' => Socialite::driver('google-auth')->redirect(), + 'github' => Socialite::driver('github')->scopes(['read:user', 'user:email'])->redirect(), + }; + } + public function disconnectProvider(Request $request, string $provider): RedirectResponse { abort_unless(in_array($provider, self::PROVIDERS, true), 404); diff --git a/app/Http/Controllers/Auth/GitHubController.php b/app/Http/Controllers/Auth/GitHubController.php index 05be8a6b..7e6e0014 100644 --- a/app/Http/Controllers/Auth/GitHubController.php +++ b/app/Http/Controllers/Auth/GitHubController.php @@ -35,6 +35,10 @@ public function callback(): RedirectResponse return redirect()->route('login'); } + if (Auth::check()) { + return $this->connectToCurrentUser(Auth::user(), (string) $githubUser->getId()); + } + $user = User::where('github_id', (string) $githubUser->getId()) ->when($githubUser->getEmail(), fn ($query, $email) => $query->orWhere('email', $email)) ->first(); @@ -52,6 +56,25 @@ public function callback(): RedirectResponse return $this->registerNewUser($githubUser); } + private function connectToCurrentUser(User $user, string $githubId): RedirectResponse + { + $existing = User::where('github_id', $githubId) + ->where('id', '!=', $user->id) + ->first(); + + if ($existing) { + return redirect()->route('app.authentication.edit') + ->with('flash.error', __('settings.authentication.providers.flash_already_linked', ['provider' => 'GitHub'])); + } + + if ($user->github_id !== $githubId) { + $user->update(['github_id' => $githubId]); + } + + return redirect()->route('app.authentication.edit') + ->with('flash.success', __('settings.authentication.providers.flash_connected', ['provider' => 'GitHub'])); + } + private function loginExistingUser(User $user, string $githubId): RedirectResponse { if (! $user->github_id) { diff --git a/app/Http/Controllers/Auth/GoogleController.php b/app/Http/Controllers/Auth/GoogleController.php index 822b7a8e..ba69067e 100644 --- a/app/Http/Controllers/Auth/GoogleController.php +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -33,6 +33,10 @@ public function callback(): RedirectResponse return redirect()->route('login'); } + if (Auth::check()) { + return $this->connectToCurrentUser(Auth::user(), $googleUser->getId()); + } + $user = User::where('google_id', $googleUser->getId()) ->orWhere('email', $googleUser->getEmail()) ->first(); @@ -44,6 +48,25 @@ public function callback(): RedirectResponse return $this->registerNewUser($googleUser); } + private function connectToCurrentUser(User $user, string $googleId): RedirectResponse + { + $existing = User::where('google_id', $googleId) + ->where('id', '!=', $user->id) + ->first(); + + if ($existing) { + return redirect()->route('app.authentication.edit') + ->with('flash.error', __('settings.authentication.providers.flash_already_linked', ['provider' => 'Google'])); + } + + if ($user->google_id !== $googleId) { + $user->update(['google_id' => $googleId]); + } + + return redirect()->route('app.authentication.edit') + ->with('flash.success', __('settings.authentication.providers.flash_connected', ['provider' => 'Google'])); + } + private function loginExistingUser(User $user, string $googleId): RedirectResponse { if (! $user->google_id) { diff --git a/lang/en/settings.php b/lang/en/settings.php index b7996938..d9de6064 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -100,6 +100,8 @@ 'connect' => 'Connect', 'disconnect' => 'Disconnect', 'flash_disconnected' => ':provider disconnected successfully.', + 'flash_connected' => ':provider connected successfully.', + 'flash_already_linked' => 'That :provider account is already linked to another user.', 'flash_cannot_disconnect' => 'You cannot disconnect your only sign-in method. Set a password or connect another provider first.', ], ], diff --git a/lang/es/settings.php b/lang/es/settings.php index 3de7dc26..86a1d58c 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -100,6 +100,8 @@ 'connect' => 'Conectar', 'disconnect' => 'Desconectar', 'flash_disconnected' => ':provider desconectada correctamente.', + 'flash_connected' => ':provider conectada correctamente.', + 'flash_already_linked' => 'Esa cuenta de :provider ya está vinculada a otro usuario.', 'flash_cannot_disconnect' => 'No puedes desconectar tu único método de inicio de sesión. Define una contraseña o conecta otro proveedor primero.', ], ], diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index db2421cf..d735b1ae 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -100,6 +100,8 @@ 'connect' => 'Conectar', 'disconnect' => 'Desconectar', 'flash_disconnected' => ':provider desconectada com sucesso.', + 'flash_connected' => ':provider conectada com sucesso.', + 'flash_already_linked' => 'Essa conta do :provider já está vinculada a outro usuário.', 'flash_cannot_disconnect' => 'Você não pode desconectar seu único método de login. Defina uma senha ou conecte outro provedor primeiro.', ], ], diff --git a/resources/js/pages/settings/profile/Authentication.vue b/resources/js/pages/settings/profile/Authentication.vue index a75f3822..60e7c30d 100644 --- a/resources/js/pages/settings/profile/Authentication.vue +++ b/resources/js/pages/settings/profile/Authentication.vue @@ -26,11 +26,9 @@ import { Separator } from '@/components/ui/separator'; import AppLayout from '@/layouts/AppLayout.vue'; import { isMobileDevice, parseBrowserName, parseOsName } from '@/lib/userAgent'; import { settings as settingsHub } from '@/routes/app'; -import { edit as editAuthentication } from '@/routes/app/authentication'; +import { connectProvider, edit as editAuthentication } from '@/routes/app/authentication'; import { preferences as notificationPreferences } from '@/routes/app/notifications'; import { edit as editProfile } from '@/routes/app/profile'; -import { redirect as githubRedirect } from '@/routes/auth/github'; -import { redirect as googleRedirect } from '@/routes/auth/google'; import type { BreadcrumbItem } from '@/types'; type Session = { @@ -66,10 +64,6 @@ const tabs = computed(() => [ { name: 'notifications', label: trans('settings.nav.notifications'), href: notificationPreferences().url }, ]); -const providerRedirects: Record { url: string }> = { - google: googleRedirect, - github: githubRedirect, -}; const passwordHeading = computed(() => props.hasPassword @@ -335,7 +329,7 @@ const logoutDialogOpen = ref(false);