-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Log in using GitHub #680
base: main
Are you sure you want to change the base?
Log in using GitHub #680
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Auth; | ||
|
||
use App\Models\User; | ||
use App\Services\GitHub; | ||
use Illuminate\Http\RedirectResponse; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\Auth; | ||
use Laravel\Socialite\Facades\Socialite; | ||
use Laravel\Socialite\Two\GithubProvider; | ||
|
||
final readonly class GitHubLoginController | ||
{ | ||
/** | ||
* Handles the GitHub login redirect. | ||
*/ | ||
public function index(): RedirectResponse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the endpoint called from the "Log in using github" in the login page, it redirects you to GitHub's authentication page, setting the right callback url |
||
{ | ||
/** @var GithubProvider $driver */ | ||
$driver = Socialite::driver('github'); | ||
|
||
return $driver->redirectUrl(route('profile.connect.github.login.callback'))->redirect(); | ||
} | ||
|
||
/** | ||
* Handles the GitHub login callback. | ||
*/ | ||
public function update(Request $request, GitHub $github): RedirectResponse | ||
{ | ||
$githubUser = Socialite::driver('github')->user(); | ||
|
||
$user = User::where('email', $githubUser->getEmail())->first(); | ||
if (! $user instanceof User) { | ||
session([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is no user with the email sent back by GitHub, we redirect the user to the "register" page, and we save in the session the github email and the username so that they can be used in that page |
||
'github_email' => $githubUser->getEmail(), | ||
'github_username' => $githubUser->getNickname(), | ||
]); | ||
|
||
return to_route('register'); | ||
} | ||
if ($user->github_username === null) { | ||
$errors = $github->linkGitHubUser($githubUser->getNickname(), $user, $request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user with the GitHub email is not yet linked to this GitHub account, we try to link it |
||
|
||
if ($errors !== []) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error will only happen in a very unusual situation: if there is already a user linked to this GitHub account but the email in the Pinkary account does not match the email in the GitHub account. In this case we cannot link the user or log them in, so we redirect back to login with an error |
||
return to_route('login')->withErrors($errors, 'github'); | ||
} | ||
} | ||
Auth::login($user); | ||
|
||
return to_route('home.feed'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,9 +9,11 @@ | |
use App\Rules\Recaptcha; | ||
use App\Rules\UnauthorizedEmailProviders; | ||
use App\Rules\Username; | ||
use App\Services\GitHub; | ||
use Illuminate\Auth\Events\Registered; | ||
use Illuminate\Http\RedirectResponse; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Carbon; | ||
use Illuminate\Support\Facades\Auth; | ||
use Illuminate\Support\Facades\Hash; | ||
use Illuminate\Validation\Rules; | ||
|
@@ -24,16 +26,27 @@ | |
*/ | ||
public function create(): View | ||
{ | ||
return view('auth.register'); | ||
$githubEmail = session()->pull('github_email'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two fields may have been set by the GitHub login process and are used in the register template |
||
$githubUsername = session()->pull('github_username'); | ||
|
||
return view('auth.register')->with('githubEmail', $githubEmail)->with('githubUsername', $githubUsername); | ||
} | ||
|
||
/** | ||
* Handle an incoming registration request. | ||
* | ||
* @throws \Illuminate\Validation\ValidationException | ||
*/ | ||
public function store(Request $request): RedirectResponse | ||
public function store(Request $request, GitHub $gitHub): RedirectResponse | ||
{ | ||
$githubUsername = $request->github_username; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we receive a GitHub username, we save again these two fields in the session in case there is a validation error and the user is redirected back to the register page |
||
if ($githubUsername) { | ||
session([ | ||
'github_email' => $request->email, | ||
'github_username' => $request->github_username, | ||
]); | ||
} | ||
|
||
$request->validate([ | ||
'name' => ['required', 'string', 'max:255'], | ||
'username' => ['required', 'string', 'min:4', 'max:50', 'unique:'.User::class, new Username], | ||
|
@@ -43,13 +56,26 @@ public function store(Request $request): RedirectResponse | |
'g-recaptcha-response' => app()->environment('production') ? ['required', new Recaptcha($request->ip())] : [], | ||
]); | ||
|
||
$request->session()->forget(['github_email', 'github_username']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the validation was successful, we don't need these two fields in the session any longer |
||
|
||
$user = User::create([ | ||
'name' => $request->name, | ||
'email' => $request->email, | ||
'username' => $request->username, | ||
'password' => Hash::make($request->string('password')->value()), | ||
]); | ||
|
||
if (is_string($githubUsername) && $githubUsername !== '') { | ||
$errors = $gitHub->linkGitHubUser($githubUsername, $user, $request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We try to link the new user to their GitHub account |
||
if ($errors !== []) { | ||
$user->delete(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will happen if there is already a user linked to the GitHub account but their email does not match the email in GitHub. In this case, we cannot register the user linked to the GitHub account, so we remove the recently created user and redirect them back to the register page so that they can try to register again without linking to GitHub |
||
|
||
return to_route('register')->withErrors($errors, 'github'); | ||
} | ||
$user->email_verified_at = Carbon::now(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user registered using a GitHub account, we can mark the email as verified as GitHub will already have taken care of verifying this email |
||
$user->save(); | ||
} | ||
|
||
event(new Registered($user)); | ||
|
||
Auth::login($user); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,12 @@ | |
namespace App\Http\Controllers; | ||
|
||
use App\Jobs\SyncVerifiedUser; | ||
use App\Jobs\UpdateUserAvatar; | ||
use App\Models\User; | ||
use App\Services\GitHub; | ||
use Illuminate\Http\RedirectResponse; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\Validator; | ||
use Illuminate\Validation\ValidationException; | ||
use Laravel\Socialite\Facades\Socialite; | ||
use Laravel\Socialite\Two\GithubProvider; | ||
|
||
final readonly class UserGitHubUsernameController | ||
{ | ||
|
@@ -20,51 +19,31 @@ | |
*/ | ||
public function index(): RedirectResponse | ||
{ | ||
return type(Socialite::driver('github')->redirect())->as(RedirectResponse::class); | ||
/** @var GithubProvider $driver */ | ||
$driver = Socialite::driver('github'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case we also set the URL dynamically |
||
|
||
return $driver->redirectUrl(route('profile.connect.github.update'))->redirect(); | ||
} | ||
|
||
/** | ||
* Handles the GitHub connection update. | ||
*/ | ||
public function update(Request $request): RedirectResponse | ||
public function update(Request $request, GitHub $github): RedirectResponse | ||
{ | ||
$githubUser = Socialite::driver('github')->user(); | ||
|
||
$user = type($request->user())->as(User::class); | ||
|
||
try { | ||
$validated = Validator::validate([ | ||
'github_username' => $githubUser->getNickname(), | ||
], [ | ||
'github_username' => ['required', 'string', 'max:255', 'unique:users,github_username'], | ||
], [ | ||
'github_username.unique' => 'This GitHub username is already connected to another account.', | ||
]); | ||
} catch (ValidationException $e) { | ||
if ($githubUser->getNickname() === $user->github_username) { | ||
session()->flash('flash-message', 'The same GitHub account has been connected.'); | ||
|
||
return to_route('profile.edit'); | ||
} | ||
|
||
return to_route('profile.edit')->withErrors($e->errors(), 'verified'); | ||
} | ||
$githubUsername = $githubUser->getNickname(); | ||
|
||
$user->update($validated); | ||
$errors = $github->linkGitHubUser($githubUsername, $user, $request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We moved all this code to a service so that it can be reused in the other two cases where we want to link a user to their GitHub account |
||
|
||
SyncVerifiedUser::dispatchSync($user); | ||
|
||
$user = type($user->fresh())->as(User::class); | ||
|
||
$user->is_verified | ||
? session()->flash('flash-message', 'Your GitHub account has been connected and you are now verified.') | ||
: session()->flash('flash-message', 'Your GitHub account has been connected.'); | ||
if ($errors !== []) { | ||
if ($githubUsername === $user->github_username) { | ||
session()->flash('flash-message', 'The same GitHub account has been connected.'); | ||
} | ||
|
||
if (! $user->is_uploaded_avatar) { | ||
UpdateUserAvatar::dispatch( | ||
user: $user, | ||
service: 'github', | ||
); | ||
return to_route('profile.edit')->withErrors($errors, 'verified'); | ||
} | ||
|
||
return to_route('profile.edit'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,13 @@ | |
namespace App\Services; | ||
|
||
use App\Exceptions\GitHubException; | ||
use App\Jobs\SyncVerifiedUser; | ||
use App\Jobs\UpdateUserAvatar; | ||
use App\Models\User; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\Http; | ||
use Illuminate\Support\Facades\Validator; | ||
use Illuminate\Validation\ValidationException; | ||
|
||
final readonly class GitHub | ||
{ | ||
|
@@ -41,6 +47,55 @@ public function isCompanySponsor(string $username): bool | |
)->values()->isNotEmpty(); | ||
} | ||
|
||
/** | ||
* Validates the user received from Github and links our user to it | ||
* | ||
* @return array<string, mixed> list of validation errors | ||
*/ | ||
public function linkGitHubUser(?string $githubUsername, User $user, Request $request): array | ||
{ | ||
try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the code that we extracted from the |
||
$validated = $this->validateGitHubUsername($githubUsername); | ||
} catch (ValidationException $e) { | ||
return $e->errors(); | ||
} | ||
|
||
$user->update($validated); | ||
|
||
SyncVerifiedUser::dispatchSync($user); | ||
|
||
$user = type($user->fresh())->as(User::class); | ||
|
||
$user->is_verified | ||
? $request->session()->flash('flash-message', 'Your GitHub account has been connected and you are now verified.') | ||
: $request->session()->flash('flash-message', 'Your GitHub account has been connected.'); | ||
|
||
if (! $user->is_uploaded_avatar) { | ||
UpdateUserAvatar::dispatch( | ||
user: $user, | ||
service: 'github', | ||
); | ||
} | ||
|
||
return []; | ||
} | ||
|
||
/** | ||
* @return array<string, mixed> | ||
* | ||
* @throws ValidationException | ||
*/ | ||
private function validateGitHubUsername(?string $githubUsername): array | ||
{ | ||
return Validator::validate([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I extracted this function because PhpStan seemed to be unable to guess that this piece of code could throw a ValidationException |
||
'github_username' => $githubUsername, | ||
], [ | ||
'github_username' => ['required', 'string', 'max:255', 'unique:users,github_username'], | ||
], [ | ||
'github_username.unique' => 'This GitHub username is already connected to another account.', | ||
]); | ||
} | ||
|
||
/** | ||
* Get the content from the GitHub API. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,7 +41,7 @@ | |
'github' => [ | ||
'client_id' => env('GITHUB_CLIENT_ID'), | ||
'client_secret' => env('GITHUB_CLIENT_SECRET'), | ||
'redirect' => env('GITHUB_REDIRECT_URI'), | ||
'redirect' => '', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is now set dynamically |
||
'token' => env('GITHUB_PERSONAL_ACCESS_TOKEN'), | ||
], | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
<x-guest-layout> | ||
<x-input-error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We display the errors related to the GitHub login at the top of the page |
||
:messages="$errors->github->get('github_username')" | ||
class="mb-5" | ||
/> | ||
|
||
<form | ||
method="POST" | ||
action="{{ route('login') }}" | ||
|
@@ -91,6 +96,17 @@ class="text-sm dark:text-slate-200 text-slate-800 underline hover:no-underline" | |
</div> | ||
</form> | ||
|
||
<div class="mt-4 text-center"> | ||
<a | ||
href="{{ route('profile.connect.github.login') }}" | ||
> | ||
<span class="inline-flex items-center gap-x-2 text-sm dark:text-slate-200 text-slate-800 underline hover:no-underline"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the new "Log in using GitHub" link |
||
<x-icons.github class="inline-block h-6 w-6 mr-100" /> | ||
{{ __('Log in using GitHub') }} | ||
</span> | ||
</a> | ||
</div> | ||
|
||
<x-section-border /> | ||
|
||
<div class="mt-4 text-center text-sm text-slate-500"> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,17 @@ | |
></script> | ||
@endsection | ||
|
||
<x-input-error | ||
:messages="$errors->github->get('github_username')" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Errors related to the GithHub registrfation |
||
class="mb-5" | ||
/> | ||
|
||
@if ($githubUsername) | ||
<div class="mb-5"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have received the email from GitHub, we invite the user to fill in the rest of their details |
||
{{ __('To complete your registration using GitHub, please fill in these additional fields:') }} | ||
</div> | ||
@endif | ||
|
||
<form | ||
method="POST" | ||
action="{{ route('register') }}" | ||
|
@@ -61,11 +72,12 @@ class="mt-2" | |
/> | ||
<x-text-input | ||
id="email" | ||
class="mt-1 block w-full" | ||
class="mt-1 block w-full {{ $githubEmail !== null ? 'text-slate-500' : '' }}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we received the email from GitHub we mark it as readonly and with a lighter colour |
||
type="email" | ||
name="email" | ||
:value="old('email')" | ||
:value="old('email', $githubEmail)" | ||
required | ||
:readonly="$githubEmail !== null" | ||
autocomplete="email" | ||
/> | ||
<x-input-error | ||
|
@@ -162,6 +174,15 @@ class="mt-2" | |
/> | ||
@endif | ||
|
||
@if ($githubUsername) | ||
<x-text-input | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We add a hidden field to be able to send back the GitHub username back to the registration controller |
||
id="github_username" | ||
name="github_username" | ||
type="hidden" | ||
:value="$githubUsername" | ||
/> | ||
@endif | ||
|
||
<div class="mt-4 flex items-center justify-end space-x-3.5 text-sm"> | ||
<div> | ||
<span class="text-slate-500">Already have an account?</span> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
use App\Http\Controllers\Auth\ConfirmablePasswordController; | ||
use App\Http\Controllers\Auth\EmailVerificationNotificationController; | ||
use App\Http\Controllers\Auth\EmailVerificationPromptController; | ||
use App\Http\Controllers\Auth\GitHubLoginController; | ||
use App\Http\Controllers\Auth\NewPasswordController; | ||
use App\Http\Controllers\Auth\PasswordResetLinkController; | ||
use App\Http\Controllers\Auth\RegisteredUserController; | ||
|
@@ -47,6 +48,15 @@ | |
->middleware('throttle:two-factor'); | ||
}); | ||
|
||
Route::prefix('/profile/connect/github')->group(function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new routes for the GitHub login. They don't need authorization |
||
Route::get('/login', [GitHubLoginController::class, 'index']) | ||
->name('profile.connect.github.login'); | ||
|
||
Route::get('/login/callback', [ | ||
GitHubLoginController::class, 'update', | ||
])->name('profile.connect.github.login.callback'); | ||
}); | ||
|
||
Route::middleware('auth')->group(function () { | ||
Route::get('verify-email', EmailVerificationPromptController::class) | ||
->name('verification.notice'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that there are now two different callbacks, the redirect URL is not loaded from the config, it is set up dynamically in each of the redirecting endpoints